Why
Pi has an established user base with subscription configurations (ChatGPT Plus/Pro through Codex, Claude Pro/Max, GitHub Copilot) plus a multi-provider API-key surface (Anthropic, OpenAI, DeepSeek, Google Gemini, Mistral, Groq, Cerebras, NVIDIA, Together, Xiaomi, OpenRouter-like custom setups). Adding Pi as a Kai backend lowers adoption friction for users already in Pi's ecosystem: they bring their existing subscription and familiar model choices to Kai instead of standing up a separate backend just to onboard. Same logic that brought Claude, Codex, Goose, and OpenCode in.
Scope (this issue)
Pi as a full first-class backend on parity with Claude / Codex / Goose / OpenCode. That means both the conversational transport AND the one-shot reasoner ship together so a user selecting Pi gets memory extraction, PR review, triage, and behavioral judge routed through Pi without needing a different backend configured for those roles. The two surfaces share the same JSONL protocol parser, so splitting them into separate issues would mostly defer config and dispatch wiring without reducing real implementation risk.
The seven things this issue ships:
- Conversational backend over
pi --mode rpc with JSONL framing on stdin and stdout, JSON command objects in, JSON response and event objects out.
PiOneShotReasoner over pi --mode rpc --no-session --no-tools, terminating after the final event. Shares the protocol parser with the conversational backend; the difference is launch flags, session lifecycle, and termination logic.
- Per-user auth/config home isolation: Pi state lives under each target OS user's
~/.pi/agent/ via sudo -H -u <os_user>, mirroring the existing per-user pattern for Claude/Codex/Goose/OpenCode.
- Provider/model config:
pi added to VALID_BACKENDS and BACKEND_PROVIDERS; structural model validation accepts <provider>/<model> plus optional :<thinking> suffix; small curated tier defaults in _BACKEND_PROVIDER_TIER_MODELS; user can type Pi-native model strings without a registry edit.
- Safe default launch flags:
--no-approve always, no implicit project-local .pi/ extension trust. A Telegram-driven session does not load arbitrary TypeScript extensions even when the operator has set defaultProjectTrust in their Pi config. The opt-in extension trust posture is a separate future issue, not this one.
- Streaming: convert Pi
message_update text deltas into StreamEvent.text_so_far; treat the documented terminal event as final completion; surface tool-execution events as logs only, never mixed into assistant text.
- Backend discovery: missing Pi binary is a deferred failure (only when the backend is selected), not an import-time crash. The wizard / installer must work without Pi installed.
Implementation shape
New module src/kai/pi.py with PiBackend(AgentBackend). Pi documents its own RPC protocol, not ACP, so this is not an AcpBackend subclass. The shape is closer to Codex: a custom JSONL protocol adapter, simpler than Codex because the event schema is more linear.
Transport:
- Spawn
pi --mode rpc --no-approve with cwd=self.workspace.
- Pass
--provider, --model, --thinking, --session-dir, and optionally --no-session from Kai config.
- Launch under
sudo -H -u <os_user> following the existing isolation pattern.
- Read stdout as strict byte-
readline() split on LF (strip optional trailing CR); do NOT use generic text readline that reinterprets Unicode line separators.
- Write JSON command objects to stdin, one per line.
- Correlate command responses by
id.
- Treat agent events as a separate stream from command responses.
Streaming:
message_update text deltas → StreamEvent.text_so_far.
- Terminal event (
turn_end / agent_end per documented schema) → final completion after a prompt.
- Tool execution events (
tool_start, tool_update, tool_end) → logs or optional future user-facing progress; never assistant text.
- Error handling:
extension_error, failed command responses, EOF, invalid JSON, and timeout all surface as backend errors.
Session:
- Decision point in the implementation PR: let Pi own
~/.pi/agent/sessions/ for conversational continuity, or point Pi at a Kai-controlled --session-dir under the user's Kai home for tighter cleanup / export control. The research note leans toward the former.
- Use RPC
get_state after startup to capture sessionId and the session file path.
- Workspace change → kill and respawn (matches existing backends).
- Restart → kill and respawn.
- Shutdown: no Claude-style prompt-saving needed; Pi sessions auto-save.
Provider and model:
BACKEND_PROVIDERS["pi"] row populated with the providers Kai already supports where Pi names align (Anthropic, OpenAI, DeepSeek, Gemini, GitHub Copilot, plus subscription providers Pi documents).
- Tier defaults: small curated set per provider (one or two model strings each) so
/model has sane defaults without trying to mirror Pi's full release-driven catalog.
validate_model_for_backend(..., "pi", ...): structural; accept <provider>/<model>[:<thinking>] and bare <model> only when the provider is set explicitly.
Environment:
PI_BIN env var to pin the binary path (mirrors CODEX_BIN, GOOSE_BIN, OPENCODE_BIN).
- Preserve provider API key env vars through
sudo: ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, DEEPSEEK_API_KEY, plus any other provider vars enabled in BACKEND_PROVIDERS["pi"].
- Preserve
TMPDIR, PATH, and any Pi-specific config override variables the installed Pi version documents.
- Prefer per-user
~/.pi/agent/auth.json for subscription login and durable API-key auth; env-var auth is acceptable when the user has configured it that way.
Configuration flow (mirrors existing backends):
Config dataclass field for Pi-specific knobs (whatever the wizard collects).
- Validated parsing in
load_config() with SystemExit on bad input.
- Pass-through in
pool.py to PiBackend initialization.
- Wizard prompt in
install.py _cmd_config() with validation loop, gated on the operator choosing to enable Pi.
.env.example documents PI_BIN and the provider key vars Pi cares about.
- New
_CONFIG_ENV_VARS entries in tests/test_config.py.
One-shot reasoner (src/kai/oneshot.py addition):
- New
PiOneShotReasoner alongside the existing Claude/Codex/Goose/OpenCode reasoners.
- Launch:
pi --mode rpc --no-session --no-tools (sessionless, no tool use) under sudo -H -u <os_user>. The same protocol parser the conversational backend uses ingests stdout; the one-shot path drives a single prompt command and waits for the terminal event before tearing down the subprocess.
- No tools by default for memory extraction, PR review, triage, and behavioral judge; callers that need tools must opt in explicitly.
ONESHOT_REASONER_BACKENDS includes pi so role routing in get_model_for(role, ...) can dispatch one-shot calls through Pi when the operator has set it as their reasoner backend.
BACKEND_PROVIDERS["pi"] provider list is reused; one-shot does not need its own provider matrix.
- Memory extraction, PR review, triage, and behavioral judge call sites pick up Pi automatically through the existing
_run_reasoner / get_model_for indirection; no per-caller changes beyond the dispatch table.
Trust posture
Hard rules for this issue:
- Pi launches with
--no-approve always.
- No code path passes
--approve from a Telegram-driven session in any form.
defaultProjectTrust from the operator's ~/.pi/ config is ignored for Kai-driven launches; Kai sets trust per-invocation, not by inheriting Pi's default.
- The extension hook surface is Powerful + TypeScript-native. Kai does not load project-local
.pi/ extensions from a chat-driven session under any circumstance covered by this issue. Opt-in extension trust is explicitly deferred to a separate future issue, not a config knob shipped here.
Pi itself does not provide a built-in filesystem / process / network / credential sandbox; it runs with the launching user's permissions. Kai's existing per-OS-user isolation remains load-bearing. Stronger isolation, if ever wanted, belongs in containerization or macOS sandboxing, not in prompt wording.
Decisions to make in the implementation PR (or in a quick design comment before it opens)
- Open-ended Pi model strings vs curated registry. Recommendation: open-ended
/model entry with small curated tier defaults; Pi's model surface is release-driven and a rigid registry will go stale quickly.
- Session directory ownership: Pi-owned
~/.pi/agent/sessions/ vs Kai-controlled --session-dir.
- How aggressive to be about preserving env vars across
sudo -H (broad allowlist vs minimal per-provider set).
Tests
Conversational backend:
- JSONL framing and CR/LF tolerance.
- Prompt acceptance: a single
prompt command produces streaming message_update events, a terminal event, and a command response correlated by id.
- Streaming delta accumulation reaches
StreamEvent.text_so_far in order.
- Final completion logic on the terminal event.
- Tool events do NOT bleed into assistant text.
extension_error, failed command responses, EOF on stdout, invalid JSON on stdout, and timeout all surface as backend errors with the right shape.
One-shot reasoner:
- Launch flags assertion: every
PiOneShotReasoner invocation passes --mode rpc --no-session --no-tools --no-approve; the test suite fails if --approve or tool flags leak in.
- Single-prompt happy path: drive one
prompt command, observe streaming deltas, return the final text on the terminal event.
- Subprocess teardown: the reasoner kills the Pi process after the terminal event; no orphaned children across successive calls.
- Error surfaces: extension errors, failed command responses, EOF, invalid JSON, and timeout all surface with the right structured error from the reasoner contract.
- Routing:
get_model_for(role, ...) returns the configured Pi model for each reasoner role (memory extraction, PR review, triage, behavioral judge) when pi is the operator's reasoner backend.
Config and discovery:
VALID_BACKENDS and ONESHOT_REASONER_BACKENDS include pi.
BACKEND_PROVIDERS["pi"] populated.
- Model validation accepts/rejects the documented patterns.
- Registry completeness across all roles.
- Per-user overrides.
- Pool dispatch routes
pi to PiBackend.
- Smoke test that detects a missing Pi binary without requiring Pi to be installed for the Kai test suite to pass.
Out of scope (this issue)
- Loading
.pi/ project extensions or supporting the --approve flag from a chat-driven session.
- Stronger filesystem / process / network sandbox beyond per-OS-user isolation.
- Multi-backend model-overlap policy: when a model is reachable via Pi AND via a direct backend Kai already has, the user picks; Kai does not silently route.
- Curated Pi model matrix mirroring Pi's full provider catalog.
- One-shot calls that need Pi tools enabled (read/write/edit/bash/grep/find/ls). All Kai one-shot roles ship with
--no-tools; tool-enabled one-shot is a future decision per caller.
Acceptance
Conversational backend:
pi --mode rpc --no-approve subprocess launches under sudo -H -u <os_user> with cwd=workspace.
- Streaming assistant deltas reach
StreamEvent.text_so_far in real time during a conversation.
/backend pi works in chat; session continuity holds across messages within a workspace.
/model accepts a structurally valid Pi model string and rejects malformed ones with a clear error.
One-shot reasoner:
pi --mode rpc --no-session --no-tools --no-approve is the launch shape; the test suite asserts no other flag combination ships in production code paths.
- Memory extraction, PR review, triage, and behavioral judge run end-to-end through Pi when the operator has set
pi as the reasoner backend.
- Subprocess teardown is clean after every one-shot call: no orphans, no leaked stdin/stdout handles.
- Reasoner error contract matches the existing Claude/Codex/Goose/OpenCode reasoners.
Shared:
- Selecting
pi when the binary is missing produces a deferred error message, not an import-time crash.
- Tests cover JSONL framing, prompt/response correlation, streaming text, tool-event isolation, EOF, invalid JSON, timeout, and the one-shot launch-flag assertion above.
- Wizard prompts for
PI_BIN when the operator enables Pi.
.env.example documents PI_BIN and the provider key vars Pi consumes.
- Pi launches always pass
--no-approve and never --approve; the test suite asserts this.
Notes
Research note prepared 2026-06-16 enumerates the Pi protocol surface, RPC command families (prompt, get_state, get_messages, set_model, cycle_model, queue controls, compaction, retry, bash, session ops, abort), the event schema, the provider model, and the trust posture. Primary sources: pi.dev docs (/latest/rpc, /latest/sdk, /latest/sessions, /latest/providers, /latest/settings, /latest/custom-provider, /latest/extensions), the earendil-works/pi GitHub repo, and the @earendil-works/pi-coding-agent npm package. Implementation work should re-verify the protocol surface against the installed Pi version before locking schema-level assumptions in code.
Why
Pi has an established user base with subscription configurations (ChatGPT Plus/Pro through Codex, Claude Pro/Max, GitHub Copilot) plus a multi-provider API-key surface (Anthropic, OpenAI, DeepSeek, Google Gemini, Mistral, Groq, Cerebras, NVIDIA, Together, Xiaomi, OpenRouter-like custom setups). Adding Pi as a Kai backend lowers adoption friction for users already in Pi's ecosystem: they bring their existing subscription and familiar model choices to Kai instead of standing up a separate backend just to onboard. Same logic that brought Claude, Codex, Goose, and OpenCode in.
Scope (this issue)
Pi as a full first-class backend on parity with Claude / Codex / Goose / OpenCode. That means both the conversational transport AND the one-shot reasoner ship together so a user selecting Pi gets memory extraction, PR review, triage, and behavioral judge routed through Pi without needing a different backend configured for those roles. The two surfaces share the same JSONL protocol parser, so splitting them into separate issues would mostly defer config and dispatch wiring without reducing real implementation risk.
The seven things this issue ships:
pi --mode rpcwith JSONL framing on stdin and stdout, JSON command objects in, JSON response and event objects out.PiOneShotReasoneroverpi --mode rpc --no-session --no-tools, terminating after the final event. Shares the protocol parser with the conversational backend; the difference is launch flags, session lifecycle, and termination logic.~/.pi/agent/viasudo -H -u <os_user>, mirroring the existing per-user pattern for Claude/Codex/Goose/OpenCode.piadded toVALID_BACKENDSandBACKEND_PROVIDERS; structural model validation accepts<provider>/<model>plus optional:<thinking>suffix; small curated tier defaults in_BACKEND_PROVIDER_TIER_MODELS; user can type Pi-native model strings without a registry edit.--no-approvealways, no implicit project-local.pi/extension trust. A Telegram-driven session does not load arbitrary TypeScript extensions even when the operator has setdefaultProjectTrustin their Pi config. The opt-in extension trust posture is a separate future issue, not this one.message_updatetext deltas intoStreamEvent.text_so_far; treat the documented terminal event as final completion; surface tool-execution events as logs only, never mixed into assistant text.Implementation shape
New module
src/kai/pi.pywithPiBackend(AgentBackend). Pi documents its own RPC protocol, not ACP, so this is not anAcpBackendsubclass. The shape is closer to Codex: a custom JSONL protocol adapter, simpler than Codex because the event schema is more linear.Transport:
pi --mode rpc --no-approvewithcwd=self.workspace.--provider,--model,--thinking,--session-dir, and optionally--no-sessionfrom Kai config.sudo -H -u <os_user>following the existing isolation pattern.readline()split on LF (strip optional trailing CR); do NOT use generic text readline that reinterprets Unicode line separators.id.Streaming:
message_updatetext deltas →StreamEvent.text_so_far.turn_end/agent_endper documented schema) → final completion after a prompt.tool_start,tool_update,tool_end) → logs or optional future user-facing progress; never assistant text.extension_error, failed command responses, EOF, invalid JSON, and timeout all surface as backend errors.Session:
~/.pi/agent/sessions/for conversational continuity, or point Pi at a Kai-controlled--session-dirunder the user's Kai home for tighter cleanup / export control. The research note leans toward the former.get_stateafter startup to capturesessionIdand the session file path.Provider and model:
BACKEND_PROVIDERS["pi"]row populated with the providers Kai already supports where Pi names align (Anthropic, OpenAI, DeepSeek, Gemini, GitHub Copilot, plus subscription providers Pi documents)./modelhas sane defaults without trying to mirror Pi's full release-driven catalog.validate_model_for_backend(..., "pi", ...): structural; accept<provider>/<model>[:<thinking>]and bare<model>only when the provider is set explicitly.Environment:
PI_BINenv var to pin the binary path (mirrorsCODEX_BIN,GOOSE_BIN,OPENCODE_BIN).sudo:ANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEY,DEEPSEEK_API_KEY, plus any other provider vars enabled inBACKEND_PROVIDERS["pi"].TMPDIR,PATH, and any Pi-specific config override variables the installed Pi version documents.~/.pi/agent/auth.jsonfor subscription login and durable API-key auth; env-var auth is acceptable when the user has configured it that way.Configuration flow (mirrors existing backends):
Configdataclass field for Pi-specific knobs (whatever the wizard collects).load_config()withSystemExiton bad input.pool.pytoPiBackendinitialization.install.py_cmd_config()with validation loop, gated on the operator choosing to enable Pi..env.exampledocumentsPI_BINand the provider key vars Pi cares about._CONFIG_ENV_VARSentries intests/test_config.py.One-shot reasoner (
src/kai/oneshot.pyaddition):PiOneShotReasoneralongside the existing Claude/Codex/Goose/OpenCode reasoners.pi --mode rpc --no-session --no-tools(sessionless, no tool use) undersudo -H -u <os_user>. The same protocol parser the conversational backend uses ingests stdout; the one-shot path drives a singlepromptcommand and waits for the terminal event before tearing down the subprocess.ONESHOT_REASONER_BACKENDSincludespiso role routing inget_model_for(role, ...)can dispatch one-shot calls through Pi when the operator has set it as their reasoner backend.BACKEND_PROVIDERS["pi"]provider list is reused; one-shot does not need its own provider matrix._run_reasoner/get_model_forindirection; no per-caller changes beyond the dispatch table.Trust posture
Hard rules for this issue:
--no-approvealways.--approvefrom a Telegram-driven session in any form.defaultProjectTrustfrom the operator's~/.pi/config is ignored for Kai-driven launches; Kai sets trust per-invocation, not by inheriting Pi's default..pi/extensions from a chat-driven session under any circumstance covered by this issue. Opt-in extension trust is explicitly deferred to a separate future issue, not a config knob shipped here.Pi itself does not provide a built-in filesystem / process / network / credential sandbox; it runs with the launching user's permissions. Kai's existing per-OS-user isolation remains load-bearing. Stronger isolation, if ever wanted, belongs in containerization or macOS sandboxing, not in prompt wording.
Decisions to make in the implementation PR (or in a quick design comment before it opens)
/modelentry with small curated tier defaults; Pi's model surface is release-driven and a rigid registry will go stale quickly.~/.pi/agent/sessions/vs Kai-controlled--session-dir.sudo -H(broad allowlist vs minimal per-provider set).Tests
Conversational backend:
promptcommand produces streamingmessage_updateevents, a terminal event, and a command response correlated byid.StreamEvent.text_so_farin order.extension_error, failed command responses, EOF on stdout, invalid JSON on stdout, and timeout all surface as backend errors with the right shape.One-shot reasoner:
PiOneShotReasonerinvocation passes--mode rpc --no-session --no-tools --no-approve; the test suite fails if--approveor tool flags leak in.promptcommand, observe streaming deltas, return the final text on the terminal event.get_model_for(role, ...)returns the configured Pi model for each reasoner role (memory extraction, PR review, triage, behavioral judge) whenpiis the operator's reasoner backend.Config and discovery:
VALID_BACKENDSandONESHOT_REASONER_BACKENDSincludepi.BACKEND_PROVIDERS["pi"]populated.pitoPiBackend.Out of scope (this issue)
.pi/project extensions or supporting the--approveflag from a chat-driven session.--no-tools; tool-enabled one-shot is a future decision per caller.Acceptance
Conversational backend:
pi --mode rpc --no-approvesubprocess launches undersudo -H -u <os_user>withcwd=workspace.StreamEvent.text_so_farin real time during a conversation./backend piworks in chat; session continuity holds across messages within a workspace./modelaccepts a structurally valid Pi model string and rejects malformed ones with a clear error.One-shot reasoner:
pi --mode rpc --no-session --no-tools --no-approveis the launch shape; the test suite asserts no other flag combination ships in production code paths.pias the reasoner backend.Shared:
piwhen the binary is missing produces a deferred error message, not an import-time crash.PI_BINwhen the operator enables Pi..env.exampledocumentsPI_BINand the provider key vars Pi consumes.--no-approveand never--approve; the test suite asserts this.Notes
Research note prepared 2026-06-16 enumerates the Pi protocol surface, RPC command families (
prompt,get_state,get_messages,set_model,cycle_model, queue controls, compaction, retry, bash, session ops, abort), the event schema, the provider model, and the trust posture. Primary sources: pi.dev docs (/latest/rpc,/latest/sdk,/latest/sessions,/latest/providers,/latest/settings,/latest/custom-provider,/latest/extensions), theearendil-works/piGitHub repo, and the@earendil-works/pi-coding-agentnpm package. Implementation work should re-verify the protocol surface against the installed Pi version before locking schema-level assumptions in code.