Skip to content

feat(v1): add prime-agent harness over native ACP - #2254

Open
sethkarten wants to merge 12 commits into
mainfrom
feat/prime-agent-harness
Open

feat(v1): add prime-agent harness over native ACP#2254
sethkarten wants to merge 12 commits into
mainfrom
feat/prime-agent-harness

Conversation

@sethkarten

@sethkarten sethkarten commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Problem

Verifiers v1 has no native Prime Agent harness. Routing through the pi adapter loses Prime Agent-specific behavior and does not preserve its live ACP session state.

Change

  • Add and register a prime-agent v1 harness using Prime Agent’s native ACP mode.
  • Route model calls through the verifiers interception endpoint.
  • Isolate Prime Agent state per trace.
  • Support skills, resumed interaction turns, and autonomous gates.
  • Reject MCP task pairing because Prime Agent’s ACP mode does not accept client-provided MCP servers.
  • Install a pinned Prime Agent release and provision Node when the runtime image does not contain it.

Verification

  • Harness resolves through the v1 loader and config model.
  • Docker e2e confirms Prime Agent and IPython state persist across interaction turns and trace state is cleaned afterward.
  • Ruff, format, and ty pass.

This PR is the base of #2260, which adds the production hardening needed for long-lived concurrent sessions.


Note

Medium Risk
New harness depends on external npm tarballs and Node bootstrap in containers; API key handling and concurrent install locking are security-sensitive but scoped per trace with explicit MCP rejection for tool tasksets.

Overview
Adds a prime-agent v1 harness that runs Prime Agent in native ACP mode via the shared ACP helper, instead of going through the pi adapter.

PrimeAgentHarness bootstraps Node (≥22.8) and installs a pinned npm release (default 0.6.0) under a locked shared path, wires model calls through the interception endpoint as an OpenAI-compatible provider, and launches prime-agent --mode acp with per-trace HOME (session + IPython kernel isolation). It sets SUPPORTS_MCP = false so tool-bearing tasksets fail pairing validation, supports resume/skills/autonomous gates, injects API keys at exec time through a wrapper script, and removes .vf-prime-agent-{trace.id} on cleanup.

Tests: new prime-agent-persistence-v1 fixture and docker e2e test_prime_agent_persists_native_acp_session check IPython state across two interaction turns, rollout dir cleanup, and native (non–transcript-replay) message shape; pytest prime_agent mark and harness list docs are updated.

Reviewed by Cursor Bugbot for commit f9513af. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add prime-agent harness over native ACP for v1 e2e evaluation

  • Introduces PrimeAgentHarness in harness.py, which installs the prime-agent npm tarball into /tmp/vf-prime-agent on first use and launches it in ACP mode with per-trace isolated working directories.
  • _prepare writes a models.json routing through an interception endpoint and generates a wrapper script that injects the API key at launch time, with restricted file permissions (700/600) to limit secret exposure.
  • Harness supports resume (SUPPORTS_RESUME=True), skipping the system prompt on resumed segments, and optional autonomous mode with configurable gates.
  • Adds a PrimeAgentPersistenceEnv and matching taskset in prime_agent_persistence_v1.py that validates IPython kernel state persists across two interaction turns without reloading from conversation text.
  • Risk: the harness shells out to flock/lockf and a Node one-liner during setup and launch; failures in those steps will surface as non-zero exit codes with no structured error wrapping.
📊 Macroscope summarized a0681ae. 1 file reviewed, 5 issues evaluated, 5 issues filtered, 0 comments posted

🗂️ Filtered Issues

verifiers/v1/harnesses/prime_agent/harness.py — 0 comments posted, 5 evaluated, 5 filtered
  • line 187: This suppression drops the system prompt on every resumed turn. Harness.resume removes existing system messages from the branch whenever data.system_prompt is set (line 203 of harness.py), then calls launch; because that branch is nonempty, this line sets the otherwise re-emitted system_prompt to None. The ACP runner consequently receives neither a system message in the replayed prompt nor its separate system prompt, so resumed segments run without the task instructions. [ Already posted ]
  • line 189: launch creates per-trace agent directories containing ACP sessions, kernel state, wrapper, and (after execution) the bearer-key models.json, but PrimeAgentHarness never overrides cleanup. The base Harness.cleanup is a no-op, so on a reused runtime every completed rollout leaves this state behind indefinitely, consuming disk and retaining sensitive conversation/session data. [ Already posted ]
  • line 284: The chmod 700/chmod 600 calls do not isolate concurrent rollouts: every agent and its model-executed IPython code runs as the same runtime user, so it can list another trace's .vf-prime-agent-* directory and read that directory's models.json after its wrapper has replaced the placeholder with PRIME_AGENT_INTERCEPT_KEY. This exposes another rollout's bearer credential despite the intended per-trace isolation. [ Already posted ]
  • line 284: chmod 700 on each agent_dir does not isolate concurrent rollouts because all agent subprocesses in a shared runtime run as the same Unix user. After the wrapper replaces the placeholder, any model-executed code can enumerate sibling .vf-prime-agent-* directories and read their models.json (mode 600 is also readable by that same user), exposing another rollout's interception credential. Per-rollout OS users or another isolation mechanism are needed; permissions alone do not provide the claimed credential isolation. [ Already posted ]
  • line 287: The call to PRIME_AGENT_ACP.run omits session_path. Consequently ACP.run supplies None, and its runner treats every segment as new and calls connection.new_session rather than resume_session/load_session. Resumed interactions therefore replay the transcript instead of continuing the native ACP session, losing any session state that is not representable in the messages. [ Previously rejected ]

Comment thread verifiers/v1/harnesses/prime_agent/harness.py
Comment thread verifiers/v1/harnesses/prime_agent/harness.py
Comment thread verifiers/v1/harnesses/prime_agent/harness.py
Comment thread verifiers/v1/harnesses/prime_agent/harness.py
Comment thread verifiers/v1/harnesses/prime_agent/harness.py
Comment thread verifiers/v1/harnesses/prime_agent/harness.py
@macroscopeapp

macroscopeapp Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new prime-agent harness with substantial new runtime logic (~450+ lines of new code). New features of this scope require human review. Additionally, an unresolved review comment identifies a potential bug in the resume path where the system prompt may be incorrectly dropped.

You can customize Macroscope's approvability policy. Learn more.

@sethkarten

Copy link
Copy Markdown
Contributor Author

Pushed b59b806 with four fixes found by running a real eval in the docker runtime. Each was verified in a container, not inferred.

The eval now completes end to end

1/1 · 55s · reward 0.00 · err 0.00
[success] task idx=0 · docker · reward=0.00
time  boot 0.3s · setup 18s · agent 32s (model 20s + harness 12s)

From the trace: ok: true, 0 errors, 4 model calls, 19.5s of model time, reward {alphabet_sort: {score: 0.0, weight: 1.0}}. The 0.0 score is the model getting the task wrong, which is a legitimate eval outcome; the pipeline itself works.

What was broken

Progress was measurable at each step, which is how I knew each fix was real:

symptom cause
npm: not found containers ship no Node; prime-agent needs >=22.8
Unknown provider "intercept" wrong env prefix, so models.json was never read
401 unauthorized $VAR in models.json is not expanded
silent end_turn, no output a prime-agent ACP bug, fixed separately

Two are worth calling out because they come from copying the pi harness without checking that prime-agent had diverged:

Env prefix. The published release derives its prefix from its own package piConfig, so it reads PRIME_AGENT_CODING_AGENT_DIR. Verified by running prime-agent model list in the container: with the correct variable the intercept provider appears, with PI_CODING_AGENT_DIR it does not.

The $VAR indirection does not work. pi uses "apiKey": "$PI_INTERCEPT_KEY", so I did the same. prime-agent sends the literal string as the bearer token. I pointed it at a capturing HTTP server and got:

AUTH HEADER SEEN: Bearer $PRIME_AGENT_INTERCEPT_KEY

Now inlined.

Dependency

The silent-end_turn bug is fixed in prime-agent#624, not here. Without it a failed turn reports success with no output, so the harness cannot tell a broken run from an empty one — that is how the 401 stayed hidden.

I verified this harness against a locally patched 0.6.0 tarball containing that fix, using the tarball_url override. Once #624 merges and a release is cut, the pinned version needs bumping to it. Until then the default pin (0.6.0) will hit the silent-failure path.

uv run ruff check, ruff format --check, and ty check verifiers all pass; the loader resolves prime-agent correctly.

Comment thread verifiers/v1/harnesses/prime_agent/harness.py Outdated
Comment thread verifiers/v1/harnesses/prime_agent/harness.py
@sethkarten
sethkarten requested a review from alexzhang13 August 5, 2026 05:52
Comment thread verifiers/v1/harnesses/prime_agent/harness.py
Comment thread verifiers/v1/harnesses/prime_agent/harness.py Outdated
Comment thread verifiers/v1/harnesses/prime_agent/harness.py Outdated
Comment thread verifiers/v1/harnesses/prime_agent/harness.py Outdated
sethkarten and others added 12 commits August 5, 2026 21:59
Drives prime-agent's own ACP mode through the existing ACP helper rather than
the pi harness's third-party pi-acp adapter. That adapter spawns `pi --mode rpc`
and hard-codes pi's RPC command and event union, so prime-agent's IPython-only
tool model, subagents, autonomous gates, goals, and heartbeats either degrade to
a generic tool call or disappear.

prime-agent speaks ACP natively as of its ACP mode, and carries the concepts ACP
has no field for in a namespaced `ai.primeintellect.prime-agent` `_meta`
envelope, so a rollout can observe them without the harness parsing a private
protocol.

Targets the current launch() contract. HOME is pinned per trace because
prime-agent writes session and kernel state beneath it and concurrent rollouts
must not share either.
0.6.0 is the first Prime Agent release that ships native ACP mode, so the
harness can now install a published tarball that actually has --mode acp.
Verified the derived tarball URL returns 200.
… key

Four fixes found by running an actual eval in the docker runtime, each verified
in a container rather than inferred:

- Containers ship no Node and prime-agent requires >=22.8, so install failed with
  "npm: not found". Bootstraps Node the way the pi harness does, and exports it on
  PATH for the launch wrapper too, since the bundled Node is not on the container
  PATH.
- The published release derives its env prefix from its own package piConfig, so
  it reads PRIME_AGENT_CODING_AGENT_DIR, not the upstream PI_ prefix. With the
  wrong name models.json was ignored and the run failed with
  "Unknown provider intercept".
- prime-agent does not expand "$VAR" in models.json: it sends the literal string
  as the bearer token, which produced "401 unauthorized". Confirmed against a
  capturing HTTP server, which received `Bearer $PRIME_AGENT_INTERCEPT_KEY`.
  Inlines the secret instead of the pi-style indirection.

With these, an eval completes end to end: 4 model calls, err 0.00, and a scored
alphabet_sort reward.
…he prompt

Review follow-up on two real problems.

The bearer token was written in plaintext to the per-trace models.json. Concurrent
rollouts share a runtime, so model-executed code in one rollout could read another
rollout's credential and issue authenticated requests against its interception
endpoint. This was a consequence of inlining the secret to work around prime-agent
not expanding "$VAR": the pi harness's indirection had kept it out of the file.
models.json now carries a placeholder, and the launch wrapper substitutes the real
value from the environment at exec time under a 0700 dir and a 0600 file.

The system prompt was applied twice: once via --append-system-prompt and again by
the ACP runner, which seeds it into the conversation for a new session. Dropped
the flag and left a note so it does not come back.

Verified by rerunning the eval: ok=true, 3 model calls, 0 errors, and a scored
alphabet_sort reward.
…Alpine

The install guard short-circuited on the binary alone, so a runtime that
already held one build reused it after `version` or `tarball_url` changed.
Key the guard on the requested tarball like the pi harness keys on its
versions, and give the Alpine branch the same repo-bump retry, since the
official Node build is glibc-only and an older Alpine's own nodejs-current
is below the 22.8 prime-agent needs.
…have

prime-agent's ACP mode ignores the `mcpServers` of `session/new` entirely, and
its own MCP integrations are authored Python skills the model imports in its
kernel, so tool servers handed to the harness never reached the model: the
repo's own echo-acp-resume-v1 fixture ran to completion with the agent
reporting the tool did not exist and the reward at 0. Declare SUPPORTS_MCP
false so `validate_pairing` rejects that pairing instead of degrading it.
…t segment

A resumed segment replays the accreted conversation, and the ACP runner renders
that transcript into the prompt with the `[system]` block the first segment
already rendered into it, so passing `system_prompt` again delivered the task
instructions twice. Observed on echo-user-sim-v1: the second segment's prompt
carried two copies of the system prompt, one after this change.
…arly

Three install/launch edges from review, none reachable on the images this runs
on today but all of them failing obscurely when they are hit:

- the wrapper substituted the bearer token with `sed`, so a token containing
  `|`, `&`, or a backslash would corrupt the key or abort the wrapper before
  exec; node now rewrites the parsed models.json instead. Today's secret is
  `secrets.token_urlsafe(16)`, which cannot contain those, so this is about not
  depending on the generator's alphabet.
- a curl-less non-Debian image ran `apt-get` regardless and failed three steps
  later inside `tar` ("tar: invalid magic"); it now says what it needs.
- an unrecognized machine fell back to the x64 Node archive and failed as
  "prime-agent requires Node.js 22.8 or newer"; it is now rejected by name,
  like the OS check already does.
The bucket URL reads like a stale internal endpoint next to the user-facing
installer at app.primeintellect.ai/prime-agent/install.sh, so note why the harness
uses it. That script is a thin front end: it defaults its own prime_agent_base_url
to this bucket and downloads $base/releases/v$version/$package-$version.tgz, the
same shape _tarball() builds. The value is also the prime-agent repo's
R2_PUBLIC_BASE_URL variable, which its release workflow publishes to. This harness
installs the npm tarball directly instead of running the script, so it needs the
artifact base rather than the installer URL.

Comment only; no behavior change. Verified the derived URL for the pinned version
returns 200, and the docker eval still reports ok=true with 4 model calls, no
errors, and a scored reward.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f9513af. Configure here.

# it rendered on the first segment. Re-emitting the system prompt here
# would hand the model the same instructions twice.
if trace.branches and trace.branches[-1].messages:
system_prompt = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resume drops system prompt

Medium Severity

On the non-live fallback path, launch clears system_prompt whenever the branch already has messages. Default resume already strips system messages from the replayed transcript so resolve_prompt can re-emit them, so those instructions never reach the new ACP session.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f9513af. Configure here.

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.

2 participants