Skip to content

Add Pi as a CLI Base Agent (acting agent and pi Evolver) - #89

Merged
haizhongzheng merged 7 commits into
Infini-AI-Lab:mainfrom
veoery:feat/pi
Nov 9, 2026
Merged

haizhongzheng merged 7 commits into
Infini-AI-Lab:mainfrom
veoery:feat/pi

Conversation

@veoery

@veoery veoery commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Change

Adds Pi (pi, v1.5.0) as a third CLI Base Agent beside codex and claude, as one CliAgent subclass. It can act (agent.base: pi) and learn (the new pi Evolver).

  • Layer 1: a <backend>/<model> reference becomes Pi's models.json provider 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).
  • Layer 2: Pi(CliAgent) runs pi --mode json in a private home (PI_CODING_AGENT_DIR, HOME), offline.
    • The call's tools are an MCP server with exposure: direct; Pi's default would hide them behind its Code Mode.
    • Resume uses the session file. Usage and the trajectory are read from Pi's events (records.parse_pi, trajectory.pi_steps).
    • Roles, web search and container isolation are refused with Unsupported.
    • Failed sessions: Pi's JSON mode exits 0 however a session ended, so Pi.outcome() reports failed (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.
  • Layer 3:
    • The acting agent deploys with hosts/pi.py and passes --approve, without which Pi skips the workspace's .pi/ tools and skills.
    • AgentEvolver, the loader and the config accept pi. The new methods/pi/ Evolver copies no login (home: []).
  • hermit deploy --host pi: the adapter's MCP server is now declared exposure: direct. This is a visible change for existing deploy users.
  • Login: a model reference only. Pi's own logins (~/.pi/agent/auth.json) are left for later.
  • Core change: 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 )
  • Docs: 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):

  • 2361862 merges main (Claude Code subscriptions as a shared model backend for acting and learning (#70) #77). Pi moves onto main's outcome() instead of its own exit_code hook, and takes its home from last_file's directory instead of a home keyword on command(). Pi refuses login and isolation: container.
  • 47fa33c review fixes:
    • a bound model's cost now follows its price (an Anthropic-bound Pi recorded $0);
    • cut, aborted or unsettled sessions are failed;
    • the MCP servers' timeout is 1800 s / 120 s instead of Pi's 60 s;
    • a compaction's request usage is counted;
    • an inherited PI_CODING_AGENT_SESSION_DIR is emptied;
    • RRSI reads a nested Pi log;
    • docs fixes.
  • 68b42b2 tool server fix (also affects Codex and Claude Code): in a task-sandboxed acting run, a deployed workspace's stdio tool server crashed at startup (session.adapter is None in a sandboxed Session), so adapter tools never loaded. It now reads session.manifest.
  • 1f207fd image_input: image_input: true (in agent.pi or evolver.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.

Check Result
python -m pytest --integration, pristine origin/main (6b258fb) 3 failed, 940 passed, 57 skipped
python -m pytest --integration, this branch (with 6b258fb merged) 3 failed, 952 passed, 57 skipped
New tests/core/test_pi_agent.py (11 tests, about 5 s) All pass, including 2 integration tests that run the real pi, unsandboxed and in bubblewrap
Codex and Claude unchanged (scratch before/after script, 79 cases) Byte-identical, apart from Pi's additions to CLI_HOMES, STREAMS and the unknown-agent message
hermit train + hermit eval, sandbox enforced, Pi acting and learning (docs' toy task) Test split: untrained 0.0, trained 1.0
  • Failures: the same 3 on both trees, in examples/clbench_database (golden parity), which already fail on main.
  • The +12 passes: the new Pi tests, plus Pi added to test_an_installed_cli_starts_with_only_its_own_paths.
  • Fixtures (tests/fixtures/pi/) are real Pi output; only two paths are changed, as their README says.
  • Not verified: reaching a real provider through the sandbox's allowlist proxy. Pi honoring HTTPS_PROXY is confirmed from its source (an EnvHttpProxyAgent set up in main.ts), not from a run.

Review

  • Tests and documentation cover the affected behavior: see Validation and the docs list.
  • Commands, defaults, and public interfaces are consistent. New: agent.base: pi, evolver.base: pi, a pi: settings section, the provider setting. One visible change: hermit deploy --host pi now writes exposure: direct.
  • Shared logs and examples contain no credentials or private data. The fixtures were checked; models.json holds the key's variable name, never its value.
  • Documentation-only: N/A.
  • Core changes: shared Layer 2 (Pi's outage wording added to UNAVAILABLE), isolate.py (mkdir(parents=True); Pi refuses login) and login.evolver_problems (the same for the pi Evolver).
  • Failures reported accurately: a failed request, an abort, and refused roles, web search or container are covered by tests. A provider outage sets unavailable, so the acting agent retries.
  • Real entry points: make("pi").run, the acting agent's run, hermit train and hermit eval. The stand-ins are only at the edges (the CLI, the model).
  • Test checklist:
    • Tests are in tests/core/; the fixtures are registered in tests/selection.py (CORE) and tests/README.md.
    • The stand-ins are built from recorded Pi 1.5.0 output.
    • The tests use no network, no sign-in, and no installed CLI outside integration tests.
    • test_selection.py's BUILT_IN gains pi.
  • Sandbox checklist, reuse: Pi runs through CliAgent inside the task and evolver sandboxes. All of its variables come from home_env(). Run records:
    rollout-1.json / learner-1.json (excerpt)
  rollout-1: role=rollout enforced=True network=isolated probe=True forwarded=[<model>]
     env: HA_ADAPTER_SANDBOX_ENFORCE, HA_RESULT_FD, HOME, LANG, MOCK_KEY, PATH, PI_CODING_AGENT_DIR, PYTHONPATH
  learner-1: role=learner enforced=True network=isolated probe=True forwarded=[<model>, <tool server>]
     env: HOME, LANG, MOCK_KEY, PATH, PI_CODING_AGENT_DIR, PI_OFFLINE, PI_TELEMETRY, PYTHONPATH
     program: <pi install>/pi   binds: /w/adapter, /w/material, /w/task-info, /w/history, /w/state, /w/scratch
  • Sandbox checklist, core change: isolate.py changes. Confirmed by post-pr.
  • Independent review (post-PR audit): one defect worth discussion.
    • Outage signals from pi are not fully recognized by UNAVAILABLE in cli.py. My suggestion is giving each CLI its own outage check inside its own subclass, and a new PR may fit this modification better and clearer.

veoery and others added 7 commits November 8, 2026 12:26
…-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 haizhongzheng left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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_code hook with Pi.outcome(). A failed Pi session now reads as exit code 0 with
    extra["failed"], not exit code 1 with cli_exit_code.
  • Has Pi.command() take its home from last_file's directory, as Claude Code does. The shared command() signature
    stays as on main, without the home keyword.
  • Makes Pi refuse login in the acting agent, the isolate.py check 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 cost in models.json at
    0, so a model bound to Anthropic recorded $0, and a price given 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 now failed, 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 set timeout, matching Codex's tool_timeout_sec: 1800 s for the call's tools and
    120 s for tools deployed with hermit 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_DIR moved sessions out of the private home, so a resume
    silently started over. It is now emptied.
  • RRSI. A nested agent-pi.jsonl was 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: pi alongside evolver.model (without
    it the config is refused); pi in config-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_body is 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.json that sets sessionDir.

🤖 Generated with Claude Code

@haizhongzheng
haizhongzheng merged commit d5f0a64 into Infini-AI-Lab:main Nov 9, 2026
1 check passed
iamziyuzhao added a commit that referenced this pull request Nov 9, 2026
…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>
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