Repository navigation
Add Pi as a CLI Base Agent (acting agent and pi Evolver) - #89
Conversation
…-Lab#77 Infini-AI-Lab#77 (Claude subscription) reworked CliAgent's result: a CLI's `outcome()` reports `failed` for a session that exited 0 without a result, and every caller reads it. Pi now implements `outcome()` (a last request that failed or was aborted is `failed`) instead of the shared `exit_code` hook this branch had added, and takes its home from `last_file`'s directory as Claude Code does, so the shared runner's `command()` keeps main's signature. Pi takes no `login` (acting agent, isolate check, the Evolver's sign-in check); `isolation: container`, now Codex's and Claude Code's, stays refused for Pi. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sume, RRSI, docs) - A bound model's cost follows its price: Pi's own figure counted a model with no cost in models.json as free (an Anthropic reference recorded $0) and ignored a given `price`. - A session that did not settle, aborted, or whose reply was cut at its output limit is `failed`, as Pi's text mode exits 1 for each. - The MCP servers set Pi's per-request `timeout` (1800 s for the call's tools, 120 s for deployed ones, as Codex's tool_timeout_sec) instead of Pi's 60 s, which cut off Trainer tools that run rollouts. - A compaction's summary request (`compaction_end.result.usage`) counts as a call. - An inherited PI_CODING_AGENT_SESSION_DIR is emptied, so sessions stay in the home where a resume finds them. - RRSI reads a log found below a trajectory with that host's reader (a nested agent-pi.jsonl gave no steps). - Docs: the recorded cost, `evolver.base: pi` beside `evolver.model`, `pi` in config-fields, and that Pi speaks chat completions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inside a run (HA_ADAPTER_SANDBOX=1, every task-sandboxed acting run) the stdio tool server a deployed workspace starts (`.codex/config.toml`, `.mcp.json`, `.pi/mcp.json`) builds a sandboxed Session, which loads no adapter in the server's process; the server read `session.adapter.manifest` and crashed before MCP started, so the adapter's tools never reached Codex, Claude Code or Pi. It now reads `session.manifest`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pi offers a model it does not know text only and drops a tool's image for it (a screenshot, a page read as an image). `image_input: true` (agent.pi, evolver.pi) writes the bound model's `input: ["text", "image"]` into models.json; the default stays text only, since a text-only model refuses images. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
haizhongzheng
left a comment
There was a problem hiding this comment.
Thanks for adding Pi. We reviewed the PR (two independent reviews per round, each finding checked against the code
and, for Pi's behaviour, against Pi 1.5.0's source) and pushed four commits to this branch. All four are in the PR now.
2361862: merge main (#77) into the branch
#77 (Claude subscription) reworked how CliAgent reports a session: each CLI now implements outcome(), and
callers treat failed (exit 0 without a result) as a failure. So the merge:
- Replaces the shared
exit_codehook withPi.outcome(). A failed Pi session now reads as exit code 0 with
extra["failed"], not exit code 1 withcli_exit_code. - Has
Pi.command()take its home fromlast_file's directory, as Claude Code does. The sharedcommand()signature
stays as on main, without thehomekeyword. - Makes Pi refuse
loginin the acting agent, theisolate.pycheck and the Evolver's sign-in check.
isolation: container, which now runs Codex and Claude Code, stays refused for Pi.
47fa33c: review fixes
Each fix has a regression test that fails without it.
- Cost. For a bound model, Pi's own cost figure was kept. Pi prices a model with no
costinmodels.jsonat
0, so a model bound to Anthropic recorded $0, and apricegiven in the config was ignored. Now the cost of a
bound model is computed from its price (Pi.adjust_usage). - Failed sessions. A session that did not settle, aborted, or whose reply was cut at its output limit
(stopReason: length) was reported as done. It is nowfailed, matching Pi's text mode, which exits 1 for each
of these. - Tool timeout. Pi's MCP servers default to a 60 s per-request timeout, which cut off Trainer tools that run
rollouts. Both servers now settimeout, matching Codex'stool_timeout_sec: 1800 s for the call's tools and
120 s for tools deployed withhermit deploy --host pi. - Compaction. The summary request's usage (
compaction_end.result.usage) is now counted as a call. - Resume. An inherited
PI_CODING_AGENT_SESSION_DIRmoved sessions out of the private home, so a resume
silently started over. It is now emptied. - RRSI. A nested
agent-pi.jsonlwas read with the Codex parser and gave 0 steps. It is now read by its host's
reader. - Docs. Four corrections: how the recorded cost is computed;
evolver.base: pialongsideevolver.model(without
it the config is refused);piinconfig-fields.md; and the backend guide's "the CLI agents do not [speak chat
completions]".
68b42b2: tool server in a sandboxed run (also affects Codex and Claude Code)
In a task-sandboxed acting run, the stdio tool server a deployed workspace starts crashed at startup:
mcp_server.py read session.adapter.manifest, but a sandboxed Session has adapter = None. As a result the
adapter's tools never loaded. This predates the PR and hit Codex and Claude Code as well, but it also broke this
PR's Pi acting path, so it is fixed here (session.manifest), with an end-to-end test that starts the server
sandboxed and calls a tool.
1f207fd: image_input
Pi treats a model it does not know as text-only, so a bound vision model never received images that tools returned.
image_input: true (in agent.pi or evolver.pi) declares the model's input: ["text", "image"]. The default
stays text-only, because a text-only model rejects images.
We also updated the PR description to match: it no longer describes the exit_code hook and the home keyword,
and it has a short "After review" section.
CI passes on 1f207fd, the head with all four commits.
Noted, no change in this PR
- Lost usage. A compaction that fails loses its usage, because Pi reports none for it.
- Same on main. These also happen with Codex and Claude, so they are not specific to this PR:
- the acting agent's retry loop keeps only the last attempt's usage;
extra_bodyis not passed to the CLI.
- Rare cases. These need unusual inputs or setups:
- a prompt starting with
@; - system instructions longer than 120 KB in a sandbox;
- session ids containing
_or.; - a workspace
.pi/settings.jsonthat setssessionDir.
- a prompt starting with
🤖 Generated with Claude Code
…Evolver) DeepSeek Harness (`dsh`, npm @deepseek-ai/dsh 0.2.2-rc.1) beside codex, claude and pi, in the same three layers as Pi (#89), on the runner's outcome() of #77. - Layer 1: a <backend>/<model> reference becomes a route of dsh's pi-ai adapter (Binding.dsh_provider): the backend's URL in the first dialect it serves, the key by its variable's name (apiKeyEnv), the context window. dsh reports no cost: the backend's price applies. - Layer 2: Dsh(CliAgent) runs `dsh --profile headless --json` in a private DSH_HOME, HOME and DSH_AGENTS_HOME, telemetry off, the caller's own DSH_* variables dropped. The session's settings are a patch layer of dsh's own rows (--patch): the bound model as the default model (effort as its reasoningEffort, image_input as Pi's), the call's tools as an MCP server offered directly (1800 s per call), web search and fetch off unless asked for, no title request, session log, plugin list, sub-agents or workflows. A server that needs no key gets the placeholder key through HERMIT_DSH_NO_KEY (a route that names none is refused by dsh). Approvals are never asked (--approval-policy never); permission_mode picks dsh's own sandbox. Resume by --session-id when the home keeps the session; the workspace's .dsh/skills are also the session's own (dsh reads project skills only at the nearest folder with a .git). outcome(): a run without a completed `final` has failed, whatever dsh's exit code (SIGTERM exits 0). Usage from step_end events; the trajectory from the stream, a command's exit code from dsh's end markers (a timeout or a stop: unknown; "(no output)": empty); every unfinished end is an error step. dsh's outage codes (RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT, EMPTY_RESPONSE, STREAM_CLOSED) join UNAVAILABLE. Roles, a login and container isolation are refused. - Layer 3: hermit/hosts/dsh.py (`hermit deploy --host dsh`): AGENTS.md, .dsh/skills and the adapter's tool server (120 s per call) as an insert row of .dsh/hermit.patch.yml, which the acting agent passes with --patch. The AgentEvolver, the loader and the config accept dsh; methods/dsh/ is the new Evolver (dsh's own sandbox off in the evolver sandbox, as Codex's is). - Tests: tests/core/test_dsh_agent.py over streams recorded from dsh 0.2.2-rc.1 (tests/fixtures/dsh) and, marked integration, the installed dsh: a session unsandboxed and in bubblewrap, the acting agent inside a git repository (the deployed skill offered, the adapter's own tool server answering), a keyless server, and --dump-config holding every row the session patches; the deploy host; selection entries. Checked against dsh's source at the tag dsh-v0.2.2-rc.1 and with the installed dsh. First built on #89's earlier head; rebuilt on main after #89 merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Change
Adds Pi (
pi, v1.5.0) as a third CLI Base Agent besidecodexandclaude, as oneCliAgentsubclass. It can act (agent.base: pi) and learn (the newpiEvolver).<backend>/<model>reference becomes Pi'smodels.jsonprovider entry (Binding.pi_provider): the key written as$VARIABLE(never the value), the backend's price, and the first dialect the backend serves (chat completions, then Messages, then Responses).Pi(CliAgent)runspi --mode jsonin a private home (PI_CODING_AGENT_DIR,HOME), offline.exposure: direct; Pi's default would hide them behind its Code Mode.records.parse_pi,trajectory.pi_steps).Unsupported.Pi.outcome()reportsfailed(main's mechanism since Claude Code subscriptions as a shared model backend for acting and learning (#70) #77: exit 0 without a result) for a session that did not settle, aborted, or whose last request failed, was aborted or was cut at its output limit. No change to the shared runner's interface.hosts/pi.pyand passes--approve, without which Pi skips the workspace's.pi/tools and skills.AgentEvolver, the loader and the config acceptpi. The newmethods/pi/Evolver copies no login (home: []).hermit deploy --host pi: the adapter's MCP server is now declaredexposure: direct. This is a visible change for existing deploy users.~/.pi/agent/auth.json) are left for later.hermit/isolate.py, one word:agent_home.mkdir(parents=True). The task sandbox could not create Pi's two-level user folder (.pi/agent), so every Pi acting call crashed. Under the sandbox checklist this needs core-team agreement (Please confirm here @haizhongzheng )agent-layers.md,model-backends.md,config-fields.md,task.md,evolver.md,methods/agent.md,custom-evolver.md,compatibility.md,installation.md, CHANGELOG.After review
Four commits were added on review (details in the review comment):
outcome()instead of its ownexit_codehook, and takes its home fromlast_file's directory instead of ahomekeyword oncommand(). Pi refusesloginandisolation: container.failed;timeoutis 1800 s / 120 s instead of Pi's 60 s;PI_CODING_AGENT_SESSION_DIRis emptied;session.adapteris None in a sandboxed Session), so adapter tools never loaded. It now readssession.manifest.image_input:image_input: true(inagent.piorevolver.pi) declares a bound model that takes images (Pi's default for an unknown model is text only).The table below is from before these commits.
Validation
Run on Linux with bubblewrap 0.9.0, Python 3.12 and Pi 1.5.0 (release binary, checked against
SHA256SUMS). No real provider was used: a local mock chat-completions server stands in for the model.python -m pytest --integration, pristineorigin/main(6b258fb)python -m pytest --integration, this branch (with6b258fbmerged)tests/core/test_pi_agent.py(11 tests, about 5 s)pi, unsandboxed and in bubblewrapCLI_HOMES,STREAMSand the unknown-agent messagehermit train+hermit eval, sandbox enforced, Pi acting and learning (docs' toy task)examples/clbench_database(golden parity), which already fail onmain.test_an_installed_cli_starts_with_only_its_own_paths.tests/fixtures/pi/) are real Pi output; only two paths are changed, as their README says.HTTPS_PROXYis confirmed from its source (anEnvHttpProxyAgentset up inmain.ts), not from a run.Review
agent.base: pi,evolver.base: pi, api:settings section, theprovidersetting. One visible change:hermit deploy --host pinow writesexposure: direct.models.jsonholds the key's variable name, never its value.UNAVAILABLE),isolate.py(mkdir(parents=True); Pi refuseslogin) andlogin.evolver_problems(the same for thepiEvolver).unavailable, so the acting agent retries.make("pi").run, the acting agent'srun,hermit trainandhermit eval. The stand-ins are only at the edges (the CLI, the model).tests/core/; the fixtures are registered intests/selection.py(CORE) andtests/README.md.integrationtests.test_selection.py'sBUILT_INgainspi.CliAgentinside the task and evolver sandboxes. All of its variables come fromhome_env(). Run records:rollout-1.json / learner-1.json (excerpt)
isolate.pychanges. Confirmed by post-pr.