AHE Adapter#
AHE, Agentic Harness Engineering (Lin et al., 2026, code), evolves a coding agent’s harness in a closed loop: k rollouts per task, an Agent Debugger that distils each task’s traces into a report, and an Evolve Agent that edits the harness and declares every change with the fixes and regressions it predicts, which the next iteration’s rollouts confirm or refute; changes judged harmful are reverted. AHE edits the whole NexAU harness, its tool implementations and its middleware classes included. AHE Adapter is AHE’s loop confined to an adapter: the acting agent (Codex or Claude Code), its agent loop and its tools are fixed, and only the adapter’s files change: the system prompt, scripts the shell runs, a middleware module run as the host’s hooks, skills, sub-agents and a long-term memory, the same files on either host. Choose it when the acting agent runs hooks, each task can be run k times per iteration, and you want the learner to change more than one text file under an attributable, falsifiable record of what each change was for.
Setup#
template: ahe@1.0
evolver:
name: ahe_adapter
params: {task_field: uid, k: 2, rollback: auto} # the k rows of a task share task_field
base: codex # the Agent Debugger and the Evolve Agent
model: {name: gpt-5.6-luna, login: chatgpt, effort: medium}
data:
train: rows/train_k2.txt # every training task k times: rows <task>#1 ... <task>#k
step_size: 100 # one step = one AHE iteration over every trial row
trainer: {epochs: 10, allow_early_stop: true} # the paper's ten iterations
sandbox: {enabled: true, network: restricted, allow: [chatgpt.com, auth.openai.com]}
Both learners are sessions of the codex Base Agent: the Agent Debugger one session per task and attempt, the Evolve
Agent one session per step with check_spec as its only tool. A task’s k rollouts are k rows of the training split
that share task_field (a row without it is grouped by its id without the #n); the example’s make_rows.py writes
them. The learners are Codex sessions whatever the acting agent is. The acting agent must deploy the adapter as
hermit deploy does for its host, so that the host’s hooks run the middleware (Codex: .codex/hooks.json; Claude
Code: hooks in .claude/settings.json). Pi runs no hooks: its deployment lists the hook commands for the agent to run
by hand, and run so, without an event’s payload, only session_start has something to do (it hands the memory over).
The template#
ahe@1.0 (hermit/adapter/templates/ahe/) maps AHE’s harness components onto files, the same for every host:
AHE component |
File |
Learnable |
|---|---|---|
System prompt and tool guidance |
|
yes |
Tool implementations |
|
yes |
Middleware |
|
yes |
Skills |
|
yes |
Sub-agents |
|
yes |
Long-term memory |
|
yes |
The hook runner |
|
fixed |
hooks.json starts hook_runner.py from the adapter’s deployed copy on every lifecycle event of the host; it
registers the events of both hosts (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure,
PostToolBatch, Stop, PreModelRequest), and each host fires those it has and skips the others (Codex says nothing;
Claude Code writes an “Unknown hook event” line to its debug log). The runner reads the host from the deployment’s record
(.hermit-deploy.json, written by hermit.hosts.codex or hermit.hosts.claude_code), so the template names no host.
It hands each call to that host’s wire in hook_hosts.py, which turns the host’s payload into the one HookInput
the middleware reads and its HookResult into the host’s output (context to add, a tool call to deny, a stop to
block). The runner calls the middleware’s function for the event with the task text (the workspace’s task.md,
else the session’s first prompt), fails open on any error, and logs every call to .hermit-hooks/hook-calls.jsonl
in the workspace, which the Debugger reads beside the session’s trajectory. The hosts differ where this table, the
one .hermit/EVOLVER.md gives the Evolve Agent (hook_hosts.CAPABILITIES), says:
Codex |
Claude Code |
|
|---|---|---|
|
SessionStart, before the prompt (again on a resumed session: |
SessionStart, before the prompt ( |
|
PreToolUse (none for a |
PreToolUse (none for a call whose input the CLI refused): |
|
PostToolUse after a call that ran, a failed command too (no exit code: |
PostToolUse, or PostToolUseFailure after a call that failed ( |
|
Stop: |
Stop: |
|
PreModelRequest, before every model request; its output is ignored: only its |
no such event: the runner calls it after the prompt, after every model message’s tool calls (PostToolBatch) and after a blocked stop, once per model request (none before a sub-agent’s first); only its |
queued context |
handed over with the next prompt or tool result, or a blocked stop; a stop that goes through leaves it for the session’s next prompt (a resumed session) |
handed over with the agent’s next prompt or tool result (a failed one too), or a blocked stop; a stop that goes through leaves it for the next prompt; a sub-agent’s queue is its own |
|
ends the session at |
ends the session at |
|
shown nowhere |
shown to the person watching, not to the model |
|
|
|
|
the session’s directory ( |
the shell’s current directory, which follows the agent’s |
The rows were measured on the hosts’ own CLIs, Codex 0.164.0 and Claude Code 2.1.317 (2.1.318 alike), each run on a
scripted local model with a probe hook on every event; tests/fixtures/ahe_hosts/ keeps the payloads they sent, which
tests/test_ahe_hosts.py replays through the wires, and the same file runs both CLIs on a scripted model when they
are installed: the memory, a note after a call (on Claude Code a failed one too, after a cd), the queued
before_model context, a deny, a blocked stop with the queue, a context given with allow, a new input, a block with a
system message, and stop_session after a call; the other cells rest on the probe and its payloads. .hermit/check.py
deploys a copy of the adapter to each host as the framework does, measures the context file each reads (Codex reads
32,768 bytes), runs one session of synthetic events in each host’s own shape and order through the hooks the
deployment installed, as the host runs them, holding every output to the fields the host reads (and to the one drop it
knows of: a context Codex drops when it comes with allow), and requires AGENTS.md to keep the paper’s three seed
rules; MEMORY.md its header; middleware.py to define the five functions, import only the standard library or
hermit_hooks, keep reading MEMORY.md at session_start and hand an entry of it to the model on each host; skills
and sub-agents their front matter.
One framework step#
Step N is AHE iteration N: step 1 scores the seed, and step N’s edits are scored by step N+1’s rollouts.
Classify every task by its k rollouts (pass, fail, exception), diff against step N−1 (flipped, regressed, rollout moves, infrastructure buckets), and score step N−1’s change manifest with upstream’s rules (EFFECTIVE … HARMFUL). The best-ever record keeps the commit the rollouts ran when their pass@1 is strictly higher.
Stop when pass@1 reaches
target_pass_rate(withtrainer.allow_early_stop).rollback: auto: the files of every change judged HARMFUL are restored from before the change (the paper’s Algorithm 1), undone again ifcheck_specthen fails;rollback: agentonly reports, as the released code.The Agent Debugger: one Codex session per task over its rollouts’ trajectories, hook audits and the judge’s verdicts, with upstream’s query; a failed attempt is retried, and after
retriesupstream’s fallback text stands in. The reports and an overview go tostate/analysis/step_N/.The Evolve Agent: one Codex session reads the overview, the verdicts and the history (
START.mdand the evolution query on stdin), edits the adapter, and writesstate/change_manifest.jsonfor iteration N.The manifest is validated and archived, and the iteration’s scores and history are updated.
flowchart TD
R["Trainer: k rollouts of every task on H_(N-1)"]
S["1. classify, diff against N-1, score manifest N-1,<br/>best-ever (strict >, the commit the rollouts ran)"]
E{"pass@1 >= target_pass_rate?"}
B["3. roll back the HARMFUL changes of manifest N-1"]
D["4. Agent Debugger: one codex session per task<br/>over its trajectories, hook audits and verdicts"]
V["5. Evolve Agent: one codex session edits the adapter<br/>(check_spec), writes manifest N"]
P["6. archive the manifest, scores, history"]
F["finalize: final = best_ever (git archive of its commit) or last"]
R --> S --> E
E -- yes --> F
E -- no --> B --> D --> V --> P --> R
finalize hands back the best-ever commit (final: best_ever) or the working copy (final: last). With no
validation split, best-ever is chosen on the training rollouts, so its training pass rate overstates what the same
adapter scores on held-out tasks; evaluate it on a test split.
Parameters#
description: >-
AHE Adapter: Agentic Harness Engineering (Lin et al., arXiv:2604.25850; china-qijizhifeng/agentic-harness-engineering @
8b2a55d, evolve.py) on the `ahe` template, confined to the adapter (the acting agent is fixed). Each step it classifies
the step's k rollouts per task (pass / fail / exception), diffs them against the previous step (flipped / regressed / rollout
moves / infra buckets), scores the previous step's change manifest (EFFECTIVE ... HARMFUL, upstream's rules verbatim), rolls
HARMFUL changes back file by file (Algorithm 1; `rollback: agent` leaves it to the agent as the released code does), runs the
Agent Debugger as one session of the codex Base Agent per task over the rollouts' trajectories and verdicts (upstream's
queries verbatim), writes the deterministic analysis overview, and runs the Evolve Agent (AgentEvolver's session of the codex
Base Agent on upstream's evolve prompt) that edits the adapter and writes the step's change manifest. No Explore Agent; no
Best-of-N; finalize hands back the best-ever evaluated snapshot; tokens/trial is recorded, which upstream never implemented.
class: evolver.py:AHEAdapterEvolver
needs: []
params:
backend: codex # the Evolve Agent's and the Agent Debugger's Base Agent: codex only
model: null # the Evolve Agent's model (the example sets it); with login: chatgpt, a Codex model name
login: null # chatgpt: the user's ChatGPT login, its shared copy and its plan (hermit.agent.base.login)
effort: null # Codex's model_reasoning_effort (the example sets it)
bin: null # the codex program; null: codex on PATH
args: [] # extra codex arguments
prompt: START.md
resume_prompt: null # a fresh session every step (evolve.py:3055)
price: null # USD per million tokens {input, cached_input, output}; null: cost unknown (ChatGPT login)
home: null # without a login, the files of the user's Codex home copied for each call; null: auth.json
tools: [check_spec] # the Evolve Agent's only Trainer tool (upstream: validate_agent.py)
web_search: false # web search, set explicitly: Codex's hosted search runs at the provider, past the sandbox
hide: null # paths hidden from the Evolve Agent besides examples/tests/docs/.git; null: nothing hidden
task_field: task # the row field naming the benchmark task, which its k rows <task>#1..#k share
# (OfficeQA: uid); a row without it: its id without the #n
k: null # rollouts per task; null: inferred from the rows sharing task_field
evolve_timeout_s: 7200 # the Evolve session's wall clock (upstream: 500 turns, no clock)
timeout_as: exception # exception (released code) | fail: a timed-out trial in the diff/attribution; pass@1 counts it 0 either way
target_pass_rate: 0.95 # base.yaml:10; needs trainer.allow_early_stop
stability_min_iterations: 3
rollback: auto # auto: revert file-wise (paper Alg. 1 l.8) | agent: report only (released code)
rollback_verdicts: [HARMFUL] # [HARMFUL, INEFFECTIVE] is Appendix C's reading
final: best_ever # best_ever | last
explore: false # the Explore Agent is not ported
debugger: # model, effort, bin, args: null = the Evolve Agent's
enabled: true
model: null # another model only on the CLI's own provider (a login) with no price: the
# Debugger takes the Evolve Agent's binding and price, so setup refuses one
effort: null
bin: null
args: null
concurrency: 16
timeout_s: 600 # one attempt's wall clock
retries: 3
backoff_s: 2.0
max_tasks: 90
verifier_output: true
verifier_tail_chars: 4000
verifier_tail_lines: 60
explode_traces: true
Records#
Under <run>/workspace/state/ (and each step’s copy, step_NNNN/state/): stats/, analysis/step_NNNN/ (the
Debugger’s overview, one report per task, its jobs and usage), queries/ (the Evolve Agent’s input), manifests/,
evaluations/ (the verdicts on the previous step’s changes), rollbacks/, best_ever.json, iteration_scores.md
and evolution_history.md; <run>/workspace/output/final_selection.json names the iteration finalize handed back.
Each rollout’s log holds its hook-calls.jsonl and hook-state/ beside the session’s trajectory.
Result#
On OfficeQA’s GPT-5.6-Luna column, one training: 0.616 on the 172 test questions, +25.0 points [+16.9, +33.1] over its starting adapter (example, recipe).
What it is not#
The Evolve Agent cannot change the host’s own tools or loop: a tool is a script the shell runs, and middleware acts only through what the host’s hooks allow (add context, deny a tool call, block a stop; the table above). The Explore Agent is not ported. Claude Code as the acting agent is checked hook by hook, not run on a benchmark here. The paper evolves and reports on the same benchmark; on OfficeQA the adapter is trained on the training split and reported on the held-out test split.