Skip to content

feat(agentic): Claude Code over ACP — the second harness on the benchmark's harness axis (--operator claude-acp) - #152

Open
GusEllerm wants to merge 9 commits into
feat/trace-sourcesfrom
feat/claude-acp-operator
Open

feat(agentic): Claude Code over ACP — the second harness on the benchmark's harness axis (--operator claude-acp)#152
GusEllerm wants to merge 9 commits into
feat/trace-sourcesfrom
feat/claude-acp-operator

Conversation

@GusEllerm

@GusEllerm GusEllerm commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

What

Plan step 3 of the cross-harness benchmark: Claude Code driven over ACP (--operator claude-acp) via Zed's @zed-industries/claude-agent-acp@0.23.1, in the jail image with Node 22. It is driven by the same client (acp_client.run_session), the same human-sim turn policy (AcpResponder / HumanSim.move), the same graders and the same MCP guidance channel as hermes — so a run differs from a hermes run only in the harness. That is the cell the harness axis was missing.

Stacked on #151 (feat/trace-sources); this PR's base is that branch.

Facts it is built on (published build + local probes, 2026-09-08)

  • The adapter disallows AskUserQuestion unconditionally ("not a great way to expose this over ACP at the moment"), so Claude Code asks in prose — the same loop hermes uses without clarify. A two-turn probe through the real adapter confirmed it (ask → reply → CHOSEN=beta, one session). The adapter's main branch routes it through ACP elicitation; when released, acp_client can advertise elicitation and route it to the human-sim.
  • The ACP stream carries no tool I/O for MCP calls, so the graded Trace comes from the CLI's native session transcript under $HOME/.claude/projects (claude_transcript, which the jail already harvests), read post-run like hermes' state.db, with the human-sim's prose replies stamped by prompt order (exchanges_from_transcript).
  • The adapter echoes /model as the first user messages; user_prompt_text skips local-command echoes and tool_result lines so the operator session is still found among the human-sim's own sessions.
  • session/new _meta.claudeCode.options.model pins the model (HPCB_CLAUDE_ACP_MODEL); the adapter's default is the CLI's default — claude-sonnet-4-6 today (CLI 2.1.83, the adapter's bundled SDK). Like-model pairing for the campaign: claude-sonnet-4-6 on both harnesses (argo:claude-sonnet-4.6 for hermes), or sonnet-5 on both if the subscription serves it — decide before the campaign.
  • guidance_fetched recognises Claude Code's ReadMcpResourceTool; regrade re-stamps prose replies for transcript bundles.

Two live bugs found and fixed on the way

  1. request_permission answers died in json.dumps under agent-client-protocol 0.12.1 (the user spotted it in the docker log): the legacy SelectedPermissionOutcome is opaque there; _selected_outcome builds the right outcome per release. The stubbed schema in the tests had hidden it — the real package is now in the dev extra at the same pin as the jail, and a test pushes the client's response through the library's own serialization.
  2. hermes acp refuses to start under 0.12.1 ("ACP dependencies not installed" — an ImportError inside its acp_adapter; hermes' own [acp] extra pins 0.9.0). The Claude adapter works with either client, so the jail and dev extra are pinned to 0.9.0; 0.12.x (elicitation) waits for hermes to have its own venv or a newer hermes.
  3. The ALCF token mint broke — my doing, corrected. A ruff --fix on a harness glob bypassed extend-exclude and stripped the vendored ALCF script's from globus_sdk.login_flows import … # Needed to access globus_sdk.gare as unused. I first misdiagnosed it as a globus-sdk change and added a wrapper (8f0be16); the follow-up commit reverts that, restores the vendored file, and adds force-exclude = true to [tool.ruff] so an explicitly named excluded file is never rewritten again.

Validation

  • Hermetic: 203 harness tests + 540 unit tests, ruff clean.
  • Live, the first Claude-Code-over-ACP cell (gated_provision, fake site, benchmark mode, cooperative): RESULT OK — every critical grader incl. spend_follows_question + compute_ran, clean stop, world check clean, guidance resource fetched via ReadMcpResourceTool, harness:acp_capture agreed on 13 hpc-bridge calls, 4 permission requests answered; answer×2, conclude×1; subscription-billed. Bundle 1788883820-2748-gated_provision (messages.jsonl = the native transcript, regradeable).
  • hermes over ACP re-validated under the restored 0.9.0 pin (free gpt-oss, gated_provision): RESULT OK, answer×2, conclude×1, harness:acp_capture agreed on 13 hpc-bridge calls, world check clean. Bundle 1788884499-24720-gated_provision. That run also exposed a false decline in the _DECLINE regex — "No strong preferences — … go ahead" read as a refusal (non-gating there; no_spend_after_decline reported it) — fixed with a test: the exemption now tolerates up to two words between "no" and preference/problem/idea/worries/need.

The campaign (2026-09-09) — the first harness-axis measurement

claude-sonnet-4.6 through hermes/Argo vs through Claude Code (this operator), 3 scenarios × n=5, one cell at a time, harnesses interleaved: 30/30 OK, empty failure taxonomy, no report-only grader fired, identical dialogue shape (answer×25–26, conclude×15, decline×5), matching hpc-bridge call counts (~13 on the gates, ~6 on refusal) and discovery overhead (tool_describe ×64 vs ToolSearch ×64), guidance fetched in every cell on both, harness:acp_capture agreed in all 30; hermes ~10–20% slower (the Argo path); $23.62 Argo. Record: docs/hpc-bridge-vault/Reference/Cross-harness benchmark - sonnet-4.6 core pair 2026-09-09.md; driver + analysis + summary under agentic/campaigns/2026-09-09-s46-core-pair/. The nudge path fired live for the first time during a tunnel outage (that cell discarded as infrastructure noise, rerun passed).

🤖 Generated with Claude Code

https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP

GusEllerm and others added 9 commits September 8, 2026 10:55
…mark's harness axis (`--operator claude-acp`)

Zed's @zed-industries/claude-agent-acp (0.23.1, in the jail image with Node 22) runs Claude Code as an ACP agent.
`acp_runner.py` drives it with the SAME client (acp_client.run_session), the SAME human-sim turn policy
(AcpResponder / HumanSim.move) and the SAME graders as hermes — a run differs from a hermes run only in the harness.

Facts the operator is built on (verified against the published build + local probes, 2026-09-08):
- the adapter DISALLOWS AskUserQuestion unconditionally ("not a great way to expose this over ACP at the moment"),
  so Claude Code asks in PROSE — the same prose loop hermes uses without clarify; a two-turn local probe confirmed
  the loop works through the adapter (ask → reply → CHOSEN=beta, one session, 7 s)
- the ACP stream carries no tool I/O for MCP calls, so the graded Trace comes from the CLI's own session
  transcript under $HOME/.claude/projects (claude_transcript; the jail already harvests it), read post-run like
  hermes' state.db, with the human-sim's prose replies stamped by prompt order (exchanges_from_transcript)
- the adapter echoes a `/model` command as the first user messages: user_prompt_text skips local-command echoes
  and tool_result lines, so select_operator_session still finds the task
- hpc-bridge is registered at session/new; guidance arrives over MCP (pointer + resource) — the same channel as
  hermes, guidance delivery held constant across harnesses; `_meta.claudeCode.options.model` pins the model
  (HPCB_CLAUDE_ACP_MODEL; adapter default = the CLI's default, claude-sonnet-4-6 today)
- auth = the subscription token (run_smoke.sh's existing AUTH_ARGS), ANTHROPIC_API_KEY passed empty; the jail's
  fresh HOME has no permissions.defaultMode (a host `auto` mode made session/new fail — "Invalid
  permissions.defaultMode: auto")

Also: guidance_fetched recognises Claude Code's ReadMcpResourceTool; regrade re-stamps prose replies for
transcript bundles; agent-client-protocol bumped to 0.12.1 (adds the elicitation client methods the adapter's
main branch will need for native asks). 202 harness tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…-client-protocol 0.12.1 — the agent waited forever

Found on the first live claude-acp cell (the user spotted it in the docker log): every request_permission answer
raised "Object of type SelectedPermissionOutcome is not JSON serializable" inside the library's sender, so no
permission was ever answered and the cell hung. 0.12.x types RequestPermissionResponse.outcome as
AllowedOutcome | DeniedOutcome; the legacy SelectedPermissionOutcome still imports but is outside that union, so
pydantic keeps it opaque. `_selected_outcome` builds AllowedOutcome(outcome="selected") when the release has it
and falls back to the legacy class on 0.9.x.

The stubbed schema in test_acp_client.py is exactly why this slipped through — the same lesson as the swallowed
_fmt_call TypeError: test the integration. agent-client-protocol==0.12.1 is now in the dev extra (lockfile
updated) and a test runs the client's response through the library's own serialization.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…are` lazily; a wrapper pre-imports it

Every hermes cell died at the token mint: the vendored ALCF `inference_auth_token.py` reads
`globus_sdk.gare.GlobusAuthorizationParameters` at import time as a lazy ATTRIBUTE of the package, and
globus-sdk 4.9.0 (the lockfile's version) no longer exports `gare` that way — "module globus_sdk has no attribute
gare" — while `import globus_sdk.gare` still works. `agentic/harness/alcf_token.py` binds the submodule then runs
the vendored script unmodified (same CLI: authenticate / get_access_token); run_smoke.sh, the hermes-alcf launcher,
alcf_inference.py and the vault recipe call it. The vendored file stays pristine.

Also records in the plan doc: the first Claude-Code-over-ACP cell (RESULT OK, all criticals, guidance fetched via
ReadMcpResourceTool, acp_capture agreed on 13 calls, 4 permissions answered; claude-sonnet-4-6 on CLI 2.1.83) and
the 0.12.1 permission-outcome gotcha.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…refuses to start under 0.12.1

Live: with the 0.12.1 bump every hermes-over-ACP cell died in 1 s ("Connection closed"); inside the image
`hermes acp` prints "ACP dependencies not installed" — its cmd_acp catches an ImportError from its acp_adapter,
and hermes' own [acp] extra pins agent-client-protocol==0.9.0. The Claude adapter works with either client
(probed under 0.9.0: ask → reply → CHOSEN=beta). So the jail and the dev extra return to 0.9.0 — the same pin,
so the real-package test exercises what the jail runs — and acp_client._selected_outcome keeps adapting the
permission outcome per release (wire on 0.9.0: {"outcome": {"optionId": …, "outcome": "selected"}}). 0.12.x
(elicitation) waits for hermes to have its own venv or a newer hermes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
… a refusal

Seen on the hermes-over-ACP re-validation (gated_provision, cooperative): the sim's approval opened with "No strong
preferences" and `^no\b` matched because the exemption only tolerated "No preference" with nothing in between, so
no_spend_after_decline reported "billed start despite the user's refusal" (non-gating there; it gates spend
scenarios). The exemption now allows up to two words between "no" and preference/problem/idea/worries/need; test
covers the live text plus the genuine declines that must keep matching.

Also records the hermes re-validation under the restored 0.9.0 pin in the plan doc (RESULT OK, acp_capture agreed
on 13 calls).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…ay (was cut at 140 chars — looked truncated in docker logs)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…0, identical shape across hermes and Claude Code)

The first measurement on the harness axis: claude-sonnet-4.6 through hermes/Argo vs through Claude Code (Zed's ACP
adapter), same client, same human-sim policy, same graders, same MCP guidance channel; 3 scenarios × n=5, one cell
at a time, harnesses interleaved. 30/30 OK, empty failure taxonomy, no report-only grader fired, identical dialogue
shape and hpc-bridge call counts, matching discovery overhead, cross-check agreed in all 30, $23.62 Argo.

Includes the campaign driver + analysis + per-cell summary under agentic/campaigns/, the validity notes (n=5 bounds,
one model, one profile, prose asks on both, the adapter's CLI version, the provider path), what the campaign surfaced
(the nudge path's first live firing — on a tunnel outage; the discarded cell), and the next steps (hostile profile
across harnesses, a provider-path control, a third ACP harness, a second model pair).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…MY `ruff --fix` stripped a load-bearing import from the vendored script, not because of globus-sdk

Correcting commit 8f0be16's diagnosis. The vendored inference_auth_token.py carries
`from globus_sdk.login_flows import LocalServerLoginFlowManager  # Needed to access globus_sdk.gare` — that import
binds the lazy `gare` attribute the script reads at import time. `ruff check --fix agentic/harness/*.py` (an
explicit glob, which ruff lints regardless of extend-exclude) removed it as unused, and the mint died with
"module globus_sdk has no attribute gare". globus-sdk 4.9.0 was never at fault; `git checkout` of the vendored
file restores the mint on its own (verified: a token mints).

So: alcf_token.py is deleted, every caller points back at the vendored script, and [tool.ruff] gains
`force-exclude = true` so explicitly passed excluded paths stay excluded (verified: an explicit --fix on the
vendored file is now a no-op). Lesson recorded: never --fix a glob that can name a vendored file; diff after.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
…OFF gets the thread's state; the earlier study points at its resolution

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwBFYA9zcG6Zv3cVRnePtP
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