#334

harnesses: add Pi

Openkajogo777wants to merge intofrom

Stacked on #318. Adds Pi as a third harness: harnesses.pi(), over pi --mode rpc.

What it is

  • One pi --mode rpc process per conversation. The SDK mints the id (--session-id), so pi --session <id> opens the same conversation in pi's own TUI.
  • Same surface as claude_code() / codex(): run, stream, steer, stop, resume, fork, history() / sessions() (read from pi's session files), tui(), handoff (history=), the session lock, gateway, MCP servers, images, structured output. pi() takes every claude_code() argument; max_turns raises UnsupportedError.
  • Approvals: pi has no permission prompt, so the SDK loads a small extension (_adapters/pi_extension.js) whose tool_call handler asks the hook through an RPC dialog. Coverage is "all", Allow(input=) rewrites the call, and Deny(reason) reaches the agent. Startup refuses to continue unless the extension proves it loaded, so the gate can't fail open.
  • Gateway: pi's gateway provider sends x-api-key, which sandbox egress injection doesn't rewrite. The extension sends Authorization: Bearer instead. Measured: the gateway accepts it, and a wrong key comes back as a 401.
  • No model given: pi chooses exactly as it does on its own.
  • A pi conversation handed to pi keeps its model. pi restores a session's model from its last reply's author, so handed-off replies keep theirs (provider_metadata["pi"]). Measured: without this, a pushed conversation ran on pi's default model, which the gateway refused.

Also in this PR:

  • afk drives pi: it lists pi conversations and handles push, attach, peek, pull, stop and rm.
  • New example: examples/harnesses/pi.py.
  • Docs, README and skill updated.

Tested

  • Offline: 1243 tests pass, 40 of them new in test_pi_adapter.py. Format, lint, mypy and ty are clean.
  • Live pi suite, local: 92 passed, 3 skipped.
  • Live pi suite, Vercel Sandbox: 86 passed, 1 skipped. Gateway and brokered-credential tests included.
  • afk, for real on pi in a sandbox:
    • push --bg, then wait, peek and pull --files
    • TUI push, Ctrl-] detach, attach
    • stop, then attach (resumes from the snapshot)
    • attach to a --bg one (ends the agent, opens the TUI), then rm
    • The pushed agent remembered the codeword from the handed-off history in each case.
  • Example: harnesses/pi.py passes in run-examples.py.
  • Review: four rounds of Codex /review (gpt-6.1-sol). Every P1 is fixed.

Known gaps

  • A pi started in a terminal isn't shown as running: pi hides its arguments from ps.
  • Sandbox TUI test: pi's ready prompt in a sandbox isn't measured yet, so that check is skipped.
  • Extension-injected context mid-turn is in history() but not in the live session.messages. The SDK has no event for it.
  • A failed retry's partial text stays in session.messages; it is excluded from result.text.
  • Not from pi, seen while testing: RuntimeError: aclose(): asynchronous generator is already running is sometimes printed when a turn is abandoned (afk push --bg). It also happens with codex, so it comes from the shared Turn close path in #318, not from pi.

Activity

13