Skip to content

feat(miracle): add replay-driven LLM harness loop - #19

Open
gaoxiaobei wants to merge 28 commits into
SAST-agent:mainfrom
gaoxiaobei:feature/miracle
Open

feat(miracle): add replay-driven LLM harness loop#19
gaoxiaobei wants to merge 28 commits into
SAST-agent:mainfrom
gaoxiaobei:feature/miracle

Conversation

@gaoxiaobei

@gaoxiaobei gaoxiaobei commented Aug 2, 2026

Copy link
Copy Markdown

Summary

This PR adds a runnable, replay-driven LLM harness for the 24th Miracle game:

  • ports the official Miracle game logic and exposes one battle/replay CLI;
  • defines the strategy interface, observation/action space, action mask, termination rules, and strict deterministic KL status;
  • adds an OpenAI-compatible LLM iteration loop with SSE streaming enabled by default;
  • saves immutable strategy versions, battles, official replays, readable traces, failures, budgets, score/IG curves, and AgentBenchResults-compatible Run metadata;
  • adds formal Harness and Replay Reader Skills plus runnable examples.

Quick start

Run all commands from the AgentBenchFramework repository root. The only Miracle entry point is:

uv run python -m agentbench_frame.miracle <match|replay|ig|loop>

Show help with:

uv run python -m agentbench_frame.miracle --help
uv run python -m agentbench_frame.miracle match --help
uv run python -m agentbench_frame.miracle replay --help
uv run python -m agentbench_frame.miracle ig --help
uv run python -m agentbench_frame.miracle loop --help

1. Strategy interface

A Miracle strategy implements MiracleAgent:

from agentbench_frame.miracle.agent_bridge import MiracleAgent


class CandidateAgent(MiracleAgent):
    def choose_cards(self, camp):
        return {
            "artifacts": ["HolyLight"],
            "creatures": ["Archer", "Swordsman", "BlackBat"],
        }

    def act(self, obs):
        return {
            "operation_type": "endround",
            "operation_parameters": {},
        }

choose_cards(camp) selects one artifact and three creatures. act(obs) returns one official operation:

  • summon: {type, level, position};
  • move: {mover, position};
  • attack: {attacker, target};
  • endround: {};
  • surrender: {};
  • artifact actions supported by the official logic.

For manual match/ig commands, register a named strategy once in AGENTS at the bottom of src/agentbench_frame/miracle/agent_bridge.py:

AGENTS = {
    "endround": EndRoundAgent,
    "sample": SampleAgent,
    "my_agent": MyAgent,
}

The LLM loop does not register v0/v1/v2 globally. It loads the configured initial strategy and every generated CandidateAgent from immutable per-iteration source files.

2. Run a battle and save replay

uv run python -m agentbench_frame.miracle match \
  --agent0 sample \
  --agent1 endround \
  --seed 11 \
  --output-dir agentbench_data/replays/24_miracle \
  --tag sample-vs-endround
  • agent0 is first player/camp 0; agent1 is second player/camp 1.
  • For a seat-balanced comparison, swap the agents and run a second battle with the same seed.
  • seed fixes official-logic randomness such as map type and day/night values.
  • stdout is JSON containing winner, scores, rounds, terminated_by, errors, replay, and trace.
  • A fully valid battle has terminated_by="normal" and errors=[].

Each battle saves:

  • *.mrc: official binary replay;
  • *.mrc.trace.jsonl: readable host/logic traffic, including observations and actions.

The same interface is callable from Python without registration:

from agentbench_frame.miracle import run_match
from my_agent import CandidateAgent, OpponentAgent

result = run_match(
    CandidateAgent(),
    OpponentAgent(),
    seed=11,
    replay_dir="agentbench_data/replays/24_miracle",
    tag="candidate-vs-opponent",
)
print(result.winner, result.scores, result.replay_path, result.trace_path)

3. Read a replay

Parse the official .mrc into an event timeline:

uv run python -m agentbench_frame.miracle replay \
  --path agentbench_data/replays/24_miracle/<match>.mrc \
  --jsonl agentbench_data/replays/24_miracle/<match>.events.jsonl

The command prints the winner, final round, and event counts. --jsonl is optional.

Use the paired .mrc.trace.jsonl to inspect the exact observations sent to an agent and the operations returned by it. Important replay semantics:

  • .mrc is binary, not JSON;
  • trace payload.content[0] has a six-character length prefix before its JSON body;
  • outer trace state is not the game round; use the decoded observation's round;
  • score 30000 is an official win/loss marker, not 30,000 ordinary points;
  • units[i] is an 18-field array documented in skills/miracle-replay-reader/SKILL.md;
  • CAN_MOVE/CAN_ATK = 0 means unavailable this turn, often because the unit was just summoned or already acted;
  • attack target=camp means attack that camp's Miracle;
  • obstacles and the complete boundary table are not included in obs; official logic remains the final legality arbiter.

The full event-code, unit-field, creature/artifact-code, damage-type, and buff-type tables are in skills/miracle-replay-reader/SKILL.md.

4. Compare old and new deterministic policies (strict KL status)

First generate a real trace with match, then replay the same observation sequence through two registered strategies:

uv run python -m agentbench_frame.miracle ig \
  --trace agentbench_data/replays/24_miracle/<match>.mrc.trace.jsonl \
  --old old_agent \
  --new new_agent \
  --camp 0 \
  --iteration 1 \
  --output-dir agentbench_data/ig/24_miracle

Run again with --camp 1 to compare decisions for the other seat. Outputs include per-decision records and an aggregated ig_curve.json.

Strict deterministic KL reporting is intentionally not replaced by a proxy metric:

  • unchanged_ratio: old/new legal actions match, so strict KL is 0;
  • infinite_ratio: deterministic actions differ, so strict KL diverges;
  • missing_ratio: an action is invalid, unparsable, or outside the finite action support;
  • finite_kl_mean: mean of genuinely finite KL values only; otherwise null.

The finite support is generated from each observation and includes end-round, surrender, legal summons, moves, attacks, and artifact uses. Official logic still performs final adjudication.

5. Run the OpenAI-compatible LLM harness loop

Copy and edit examples/miracle-loop.toml:

agent = "tested_llm"
initial_strategy = "miracle-initial-strategy.py"
opponent = "sample"

[llm]
base_url = "https://api.openai.com"
api_key_env = "OPENAI_API_KEY"
model = "your-model-name"
temperature = 0.0
timeout_seconds = 120
reasoning_effort = "low" # optional; omit if unsupported
stream = true
max_context_tokens = 1000000

[evaluation]
seeds = [11]
seats = [0, 1]

[budget]
max_iterations = 3
max_rollouts = 8
max_episode_reads = 3
max_decision_reads = 500
max_total_tokens = 3000000
max_wall_seconds = 1800

initial_strategy is resolved relative to the TOML file. It must contain a complete CandidateAgent implementation.

Set the key only through the configured environment variable and start the Run:

export OPENAI_API_KEY='<API key>'

uv run python -m agentbench_frame.miracle loop \
  --config examples/miracle-loop.toml \
  --data-dir ../AgentBenchResults

The API contract is OpenAI-compatible POST /v1/chat/completions. SSE streaming is enabled by default to survive long generations and gateway idle timeouts. Set stream=false only for a provider that does not support streaming.

max_tokens is omitted by default and is sent only when explicitly configured. max_context_tokens defaults to 1,000,000 and is checked against authoritative prompt + completion usage returned by the provider; the harness does not pretend that character counts are token counts.

What the LLM sees

Every update request contains:

  1. a system contract requiring exactly one JSON object with complete source;
  2. the complete miracle-harness Skill;
  3. the complete miracle-replay-reader Skill;
  4. the current accepted strategy source;
  5. the immediately previous iteration's metrics/status;
  6. selected structured replay evidence (episode outcome plus budget-limited observations);
  7. cumulative budget consumption.

The LLM must return:

{
  "analysis": "problem found and reason for this update",
  "strategy_code": "complete Python source defining CandidateAgent"
}

Patches, shell commands, or partial methods are rejected. The harness validates, saves, loads, battles, and compares the returned source itself.

Budget semantics

All limits are cumulative across one Run:

  • max_iterations: LLM update attempts, excluding baseline iteration 0;
  • max_rollouts: battles actually started;
  • max_episode_reads: replay episodes exposed to the LLM;
  • max_decision_reads: individual structured observations exposed to the LLM;
  • max_total_tokens: provider-reported total tokens;
  • max_context_tokens: provider-reported tokens in any single request;
  • max_wall_seconds: total Run wall-clock time.

A Run is one complete benchmark execution; an iteration is one strategy-update attempt; an episode is one battle; a round is one in-game round.

Invalid model output, invalid strategies, API errors, battle timeouts, incomplete evaluations, and missing IG are retained with explicit status/reason. They are never silently removed from curves.

6. Output layout and interpretation

--data-dir ../AgentBenchResults produces:

AgentBenchResults/
└── runs/24_miracle/<agent>/<run_id>/
    ├── run.toml
    ├── config.json
    ├── summary.json
    ├── events.jsonl
    ├── score_curve.json
    ├── ig_curve.json
    ├── skills/
    └── iterations/
        ├── iteration-0000/
        │   ├── strategy.py
        │   └── episodes/
        └── iteration-0001/
            ├── llm_request.json
            ├── llm_response.json
            ├── candidate.py
            ├── strategy.py
            ├── iteration.json
            ├── episodes/
            └── ig/
  • iteration-0000 is the raw baseline.
  • strategy.py is the immutable accepted strategy for that iteration.
  • candidate.py preserves generated source before/while validation.
  • episodes/ contains result JSON, official .mrc, and readable .trace.jsonl.
  • events.jsonl is the ordered audit log for API calls, battles, failures, and iteration transitions.
  • summary.json contains raw/evolved score, gain, best iteration, AUC, failure counts, and cumulative budgets.
  • score_curve.json keeps version-aligned raw, evo, gain, win/completion rates, rollouts, token, episode-read, and time axes.
  • ig_curve.json keeps version-aligned strict-KL status ratios. Missing/incomplete points remain visible.

API keys are read from the configured environment variable and are not saved in Run files.

Validate exported Results with:

uv run agentbench data check --data-dir ../AgentBenchResults

7. Verification

PYTHONPATH=src python -m pytest tests/miracle -q
  • 61 Miracle tests passed.
  • A real streamed three-iteration Run completed.
  • Export validation reported 6 valid, 0 invalid across retained Runs.

Artifact policy

Generated Runs, .mrc replays, traces, temporary API configuration, caches, credentials, and AgentBenchResults are intentionally excluded from this PR. They are reproducible runtime artifacts, not source files.

Canonical detailed references:

  • skills/miracle-harness/SKILL.md
  • skills/miracle-replay-reader/SKILL.md
  • examples/miracle-loop.toml
  • examples/miracle-initial-strategy.py

- official_logic/: 24_miracle 官方对战逻辑原样移植(子进程运行,零改动)
- protocol.py/logic_runner.py: 官方线协议(4 字节长度头 + json)与子进程运行器
- host.py: 评测机——逐帧驱动官方逻辑、agent 决策桥接、全量 trace 记录、
  决策超时与防卡死(相同 obs 连续 3 次 → 官方超时帧判负)
- agent_bridge.py: MiracleAgent 接口 + EndRoundAgent/SampleAgent(自写规则策略,
  含召唤容量感知、被拒感知、立方坐标移动)
- match.py: run_match 高层对局运行器(replay/trace 落盘、seed 注入)
- tests/miracle/test_smoke.py: 协议往返、双 endround 100 回合平局、
  sample 对局正常终局、可复现性(5 passed)
- decision_space.py: Obs schema(UNIT/PLAYER 字段索引)、macro-action 枚举与编码、
  action mask(召唤点/容量/mana/移动/攻击,含官方障碍与边界 MAPBORDER/ABYSS、
  飞行地面同格)、终止条件、信息增益用动作支持集
- 官方 Parser 仲裁一致性测试: mask 全量候选被官方接受、非法动作被拒且不在 mask
- 单位属性(UNIT_STATS)与官方 Data.json 核对,决策空间自包含不依赖官方目录
- ig.py: 统一口径 KL(new‖old) 逐决策点→episode→iteration 聚合落盘
- 缺失原因显式记录不冒充: support_expansion(严格 KL 发散)/state_mismatch/degenerate
- 数据契约对接 AgentBenchResults: agentbench_data/ig/24_miracle/<iter>/<ep>.json
  + ig_index.json(iteration 汇总)
- 单测 8 项:手算 KL 精确值、外扩缺失、全缺失、归一化、落盘往返
- .reasonix/skills/miracle-replay-reader/SKILL.md: 游戏规则、trace 帧格式、
  obs 字段数字含义、关键事件、10 条常见误读、解析工作流
- tests/miracle/test_replay_read.py: 用真实 trace 验证 skill 格式定义
  (长度前缀/字段索引/操作对齐/终局分类),7 项全过
- replay.py: 按官方 get_media_info 编码解码(7-int 块、round 单调守卫、
  ActivateArtifact 变长参数),GameStart/GameEnd/操作事件全量还原
- 与真实对局交叉核对 100% 一致:Summon 12/Move 128/Attack 26/TurnEnd 44
  vs trace 操作分布;GameEnd winner=0 round=44 与 MatchResult 一致
- test_replay.py 6 项:事件表、操作对齐、终局、单调性、无 UNSYNC、JSON 往返
- iterate.py: 评测→决策点收集(ε-soft 分布)→IG→导出→曲线 全管线
- SampleV2Agent: 一次受控策略更新(召唤优先级反转),AGENTS 注册即保存新版本
- AgentBenchResults 数据契约: runs/24_miracle/<族>/<iterN_ver_seedN>/
  run.toml + summary.json + ig.json(aggregate.py 可直接扫描)
- 演示(如实保留失败): iter0 sample 胜(30000/44轮)、iter1 sample_v2 负(0/99轮)
  IG=1.9474(172 决策点全可比);score/IG 曲线版本对齐
- test_iterate.py 8 项:决策点提取、ε-soft、导出契约、曲线
- versions.py: 策略源码+git commit+评测摘要+时间戳的版本快照归档
- loop.py: 改策略→存版本→再评测编排,全程 events.jsonl 可追溯
- cli.py/__main__.py: match/evaluate/iterate/replay/versions/curves 子命令
- 端到端验证: iterate sample,sample_v2 → events 5 条 + 版本快照 2 份
  + IG 1.947356(172 决策点全可比)
- test_loop_versions.py 6 项
- curves.py: build_curves/curves_to_svg(无依赖 SVG)/save_curves/curves_ascii
- 从 runs/ 契约目录汇总 score/IG per iteration;无数据时 no_data=true 如实标注
- 真实产物: agentbench_data/curves/24_miracle/sample_curves.{json,svg}
- test_curves.py 5 项:版本对齐、无数据报告、SVG 渲染、落盘往返
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant