Skip to content

feat(agentic): ACP interactive driver — one persistent session, human-sim in the loop - #148

Merged
GusEllerm merged 7 commits into
mainfrom
feat/acp-interactive-driver
Sep 7, 2026
Merged

feat(agentic): ACP interactive driver — one persistent session, human-sim in the loop#148
GusEllerm merged 7 commits into
mainfrom
feat/acp-interactive-driver

Conversation

@GusEllerm

@GusEllerm GusEllerm commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

What

An ACP (Agent Client Protocol) driver for INTERACTIVE cross-harness benchmarks — the harness-compatibility axis (Planned/ACP interactive benchmark driver.md). Replaces the token-heavy per-turn hermes -z transcript-replay with one persistent session: no conversation re-send, clean turn boundaries, a structured update stream, and the persona'd human-sim in the loop.

Contents (all committed here)

  • acp_client — agent-agnostic ACP client (agent-client-protocol): run_session(..., respond, max_turns) drives one session; BenchClient auto-approves permissions and records the update stream.
  • hermes_runner._run_acp (gated behind HPCB_HERMES_ACP=1) — autonomous = one prompt; interactive = a prompt↔human-sim loop. Trace from hermes' state.db → the graders apply unchanged. Transcript-replay stays the default (unset the env).
  • Live tool-call logging — the hermes/ACP docker log now streams the operator's play-by-play (→ connect_facility(...), → [execute] run_shell(...)), matching the Claude-SDK operator. Hermetic regression test in CI.
  • Gate stamping fixhermes_trace.exchanges_from_messages stamps the prose Q&A post-run from the flushed state.db, correlated by message order (trace index = tool-calls-before-the-reply, question = the preceding clean assistant message). Fixes intermittent spend_follows_question/choice_respected false-fails caused by grading off the ACP capture (count skew + merged-chunk text). Hermetic regression test reproduces the exact failure.
  • run_smoke.sh / run_suite.py forward HPCB_HERMES_ACP + HPCB_BENCHMARK_MODE.

Validation

Live (fake site, benchmark mode): gpt-oss-120b and claude-sonnet-5 (via Argo) both drive gated_provision over ACP; spend_follows_question is correct in both (passes when spend is gated, "no billed start" when it isn't). The stamping fix is proven hermetically and live. 118 harness tests + mypy + ruff green.

Known limitation (deferred, tracked)

A pre-existing turn-continuation gap: the loop replies only when the operator ends a turn with a question, so a decisive operator that pauses mid-task with a plan (no question) can end the session before provisioning → noisy interactive compute_ran. Fix is a persona-aware human-sim "continue vs conclude" nudge (must not nudge a legitimate decline into spending), landing separately with its own tests. Until then the paid interactive campaign is on hold. Driver mechanics, logging, and gate stamping are unaffected.

🤖 Generated with Claude Code

GusEllerm and others added 7 commits September 6, 2026 17:47
…-sim in the loop

Replaces the fragile per-turn `hermes -z` transcript-replay for INTERACTIVE cross-harness
runs with an ACP-based driver (harness-compatibility axis; `Planned/ACP interactive
benchmark driver.md`). One persistent session: no per-turn conversation re-send (cheap),
`prompt()` returning IS the turn boundary (no ends_with_question guessing to detect turn
end — only to decide whether to reply), and a structured update stream.

- acp_client: `run_session(..., respond, max_turns)` drives one session up to N turns; the
  async `respond(AcpTurn) -> str|None` hook returns the user's next message (sent as the next
  prompt in the SAME session) or None to stop. `run_once` now delegates to it.
- hermes_runner: `_run_acp` (gated behind HPCB_HERMES_ACP=1) — autonomous = one prompt;
  interactive = a prompt↔persona'd human-sim loop, capped at MAX_PROSE_FOLLOWUPS. Strips the
  config's mcp block (ACP new_session registers hpc-bridge). Reuses stamp_exchanges +
  trace_from_messages/state.db, so the graders apply unchanged. Transcript-replay stays the
  default (unset the env) for side-by-side comparison until a capable control validates ACP.
- run_smoke.sh / run_suite.py: forward HPCB_HERMES_ACP and HPCB_BENCHMARK_MODE into the jail
  / keep them across suite cells.

Live-validated (2026-09-06): gpt-oss-120b over ACP on gated_provision (fake `site`, benchmark
mode) = RESULT OK — every critical grader green (spend_follows_question, compute_ran, safety
floor), 2 clean human-sim exchanges (answer×2), clean teardown, no_raw_ssh correctly demoted
to report-only. n=1 — validates the driver mechanically, not a pass rate. Go/no-go capable-
agent control (405B free, then claude-sonnet-5 via Argo) is the next step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EuENLwMGBonfaQJrg5jFY
…he Claude operator)

The hermes/ACP path's docker log showed only the human-sim's replies — the operator's own
tool calls (list_facilities/connect/run_shell/stop_endpoint) went to hermes' state.db and were
visible only in the post-hoc Trace, so a live run looked sparse and "different" from the
Claude-SDK operator's play-by-play. BenchClient.session_update already captures every tool call;
now it also prints `  → tool(args)` to stderr as each arrives, mirroring runner.py's
`  → {logical_name}({inp})`. `_fmt_call` strips MCP/server name prefixes and truncates args so
one call is one readable line. Best-effort (suppressed) so logging never breaks a run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EuENLwMGBonfaQJrg5jFY
… labels

Live-verification (free gpt-oss run) showed the first cut mangled path- and glob-shaped titles:
`→ src()`, `→ *.py()`, `→ hpc-bridge()` — because `_fmt_call` split the title on ':'/'__' and
took the last segment. The ACP `title` is ALREADY the tool name for MCP calls (`connect_facility`)
and a short label for the agent's own file/search/terminal tools, so print it verbatim. Add the
`kind` (read/execute/search/…) as a `[kind]` prefix so terse titles read as `[read] src` /
`[search] *.py`, and drop the empty `()` when there are no args. MCP calls now log cleanly, e.g.
`→ connect_facility(facility=…, ssh_host=login)`, `→ [execute] run_shell(command=hostname, shape=compute)`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EuENLwMGBonfaQJrg5jFY
The formatter fix widened `_fmt_call` to (title, kind, raw_input) but left the session_update call
site at `_fmt_call(title, raw_input)` — a 2-vs-3-arg TypeError on every tool call, silently swallowed
by the `contextlib.suppress(Exception)` guarding the log line. Net effect: the live `→ tool(args)`
play-by-play printed NOTHING (caught by inspecting a real cell log, not the unit test — which called
the 3-arg function directly). Pass `call_kind` through.

Adds test_acp_client.py (stubs the jail-only `acp` like conftest stubs the agent SDK, so `pytest -q`
covers it): `test_session_update_logs_and_captures` drives session_update end-to-end and asserts the
`→` lines are actually printed — the integration the suppress hid — plus `_fmt_call` shape cases.
Wired into the CI harness-test list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EuENLwMGBonfaQJrg5jFY
…ect fix

The paid campaign surfaced that spend_follows_question false-fails on the ACP path: _run_acp stamps
prose Q&A from the ACP capture (count + joined chunks) while the graded trace comes from state.db, so
the stamp index and text misalign. A mid-session state.db read fixed the source but lags hermes' flush
(ended turns early → compute_ran false-fail); reverted. Documents the correct fix (post-run stamping
from the flushed state.db, correlated by the human-sim's user messages) to be built with hermetic tests.
Driver mechanics + the live → logging are unaffected and stand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EuENLwMGBonfaQJrg5jFY
…ACP capture

The ACP interactive gate grading (spend_follows_question) false-failed because _run_acp stamped each
prose exchange using the ACP capture: the call_index came from len(capture.tool_calls) (which counts the
operator's file/search exploration, misaligning vs the state.db-derived trace the graders read) and the
question text was " ".join(all chunks) (merging setup narration — "installing…interface…scratch" — into
the ask, so _is_spend_question's setup-veto misfired on an otherwise-clean spend question). A first patch
read state.db MID-SESSION, but that lags hermes' flush and ended turns early (compute_ran false-fail).

Fix: correlate POST-RUN from the fully-flushed state.db. hermes_trace.exchanges_from_messages walks the
messages in order — each human-sim reply is a `user` row after the first (the task), pairs with the Nth
recorded {answer,kind}, takes the preceding assistant prose as the (clean, single-message) question, and
counts tool-calls-before-it EXACTLY as trace_from_messages does → the synthetic AskUserQuestion lands at
the right index with the right text. _run_acp now records only {answer,kind} during the run (turn
continuation still keyed on the reliable ACP capture) and stamps post-run. The -z path is unchanged.

Hermetic regression test reproduces the exact failure (setup narration + a clean spend ask in the same
turn) and asserts is_spend + spend_follows_question pass. 118 harness tests + mypy green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EuENLwMGBonfaQJrg5jFY
…ation gap

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EuENLwMGBonfaQJrg5jFY
@GusEllerm
GusEllerm merged commit d307276 into main Sep 7, 2026
7 checks passed
@GusEllerm
GusEllerm deleted the feat/acp-interactive-driver branch September 7, 2026 00:19
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