Thank you, Tim, for open-webui and now for computer. Both have been a joy to use and to build for.
Opening as an issue because PR creation is limited to collaborators. The change is complete and ready on my fork: kelvinq/computer:feat/pi-coding-agent-pr (rebased on v0.8.8, 101 tests passing). Happy to open it as a PR if you enable it, or feel free to pull the branch.
Add Pi coding agent as a first-class agent profile
Rebased on origin/main @ v0.8.8. 22 files, +4862 / −1767 (the large delta is uv.lock: a uv format-revision rewrite, not new runtime deps). No runtime dependency: Pi is invoked as a CLI subprocess, so the only new packages are the dev-group test tools. 102 tests, 101 passing.
Adds Pi (@earendil-works/pi-coding-agent) as a 7th first-class coding agent, in the same one-file-per-agent pattern as codex and cline. Admins add it from the agents panel like any other agent. Users pick a Pi model in the model picker and chat with it through the existing task runner. Unlike the single-model agents, Pi exposes a whole roster of models across providers through one profile (agent:pi · 30 models on this box).
Summary
Pi speaks JSON-RPC over stdio behind a --mode rpc flag. This change adds a PiRpcClient stdio transport and a run_pi_agent async generator that adapts Pi's event stream onto cptr's existing AgentEvent types, then wires Pi into registration, dispatch, detection, and the admin UI. It sits alongside codex, claude_code, cursor, grok, opencode, and cline.
The integration lives entirely in one module, cptr/utils/agents/pi.py (626 lines), following the single-file-per-agent convention. Pi is a CLI peer of codex, claude, and cline, not a Python package, so this adds no runtime dependency.
Changes
22 files against origin/main (+4862 / −1767; the lockfile accounts for the bulk, see Dev deps below):
cptr/utils/agents/pi.py (new, 626 lines) — PiRpcClient (stdio JSONL transport), event-translation helpers mapping Pi RPC events to cptr AgentEvents, and the public run_pi_agent async generator.
- Agent wiring —
models.py ("pi" in _VALID_AGENTS, a default profile, a default row, a launch_args-trimming normaliser branch), chat_task.py (runners["pi"] = run_pi_agent), model_targets.py ("pi" in the AgentModelTarget.agent Literal), detection.py (a pi branch: pi --mode rpc --help probe then RPC model listing, with a pi --list-models fallback).
- Admin UI —
admin.ts ("pi" in the AgentType union), AgentProfileModal.svelte (Pi in the type dropdown + a launch_args field), Agents.svelte (label/command maps).
- Tests —
tests/agents/ (7 files, 99 unit tests, incl. a fake_pi_rpc.py JSONL double that drives tool_execution_update with accumulated partialResult), tests/integration/test_pi_e2e.py (3 real-binary tests).
- Docs —
README.md (Pi listed with the other coding agents), CHANGELOG.md (Unreleased entry, matching the Cline precedent).
- Dev deps —
pyproject.toml adds pytest + pytest-asyncio to the dev group with a [tool.pytest.ini_options] block. The uv.lock diff is large (+4862 / −1767) but its substance is small: it adds five packages (iniconfig, packaging, pluggy, pytest, pytest-asyncio) and changes no existing version. The line churn is a uv lockfile format-revision rewrite (the local resolver emits the newer format), not a dependency change. If the project pins a specific uv version for lockfile generation, re-running it under that version collapses the diff to a handful of lines.
How it works
Pi's --mode rpc starts a JSON-RPC server on stdio that splits records on \n only and rejects Unicode line separators. PiRpcClient enforces that contract with manual buffer splitting and a small async read task. (One spec detail worth noting: under --mode rpc, help text goes to stderr because stdout is reserved for JSONL. The detection probe handles this.)
The runner is a thin adapter: build a client, start it, call prompt, forward each event through translate_event. Pi message_update events map to AgentTextDelta and AgentReasoningDelta (reasoning blocks close implicitly, the same path every other agent uses). The tool_execution_start and tool_execution_end events map to AgentToolUpdate. The bash tool name is renamed to run_command so it renders like every other agent's shell tool.
Models come from get_available_models over RPC. The detection layer probes RPC first and falls back to pi --list-models. Resume is supported via session_id stored in chat.meta.agent_sessions and proven by the e2e TestPiE2EResume round-trip.
Streaming tool output
Pi's RPC spec carries the accumulated output in tool_execution_update.partialResult, not the new chunk. cptr's chat_task appends AgentToolOutputDelta.delta to existing output on the consumer side, so a naive forwarder concatenates "a" + "a b" + "a b c" into "aa ba b c". Output grows quadratically with each update and is scrambled. This is the integration's load-bearing correctness problem.
The adapter handles it in run_pi_agent: it tracks the last-forwarded text per call_id and emits only the new suffix as the delta. Because Pi models updates as replace-your-display, if a later partialResult is not a prefix-extension of the previous one the adapter re-emits the full new text and resets the per-call counter (so a truncation can't silently go mute or slice at a stale offset). The consumer still appends, so a genuine truncation renders as old + new concatenated rather than a clean replace. A true display-replace would need a consumer-side change and is out of scope here.
Covered by:
tests/agents/test_pi_runner.py::TestStreamingToolOutput (monotonic deltas concatenate exactly once)
tests/agents/test_pi_runner.py::TestStreamingToolOutputTruncationSafety (non-monotonic and shorter-prefix resets)
tests/integration/test_pi_e2e.py::TestPiE2EStreamingToolOutput (a 12-line bash printf against the real binary: the test whose absence would have let the bug ship)
Tests
$ uv run pytest tests/ -q
.......s.................................................................. [ 70%]
.............................. [100%]
101 passed, 1 skipped in 48.03s
102 collected (99 in tests/agents/, 3 in tests/integration/). The single skip is a deliberately slow timeout test (test_request_timeout, off by default, run with -k test_request_timeout).
Real-RPC integration tests against the pi binary on PATH:
$ uv run pytest tests/integration/test_pi_e2e.py -v
TestPiE2EBasic::test_simple_prompt_yields_events PASSED
TestPiE2EResume::test_session_resume_across_turns PASSED
TestPiE2EStreamingToolOutput::test_bash_streaming_no_duplicated_lines PASSED
3 passed
The integration module uses pytestmark = pytest.mark.skipif(not _AUTH_OK, ...), where _AUTH_OK runs a tiny pi -p probe against an authed provider, so CI without secrets stays green. Registration is asserted at three layers by tests/agents/test_pi_registration.py (callable runner, AgentModelTarget.agent Literal membership, and the "pi": run_pi_agent dispatch entry).
Compatibility
The change adds a new agent kind and renames or removes nothing. Existing agents.profiles entries parse unchanged through normalize_agent_profiles. The default profile list grows from six to seven. The RPC client is gated behind profile["agent"] == "pi", so other code paths never import PiRpcClient. The dispatch change is one new key in one map inside _run_agent_target, and no conditionals changed elsewhere.
Trade-offs
- Permission model. Pi uses its own permission model and does not consume cptr's
tool_approval_mode, sandbox_mode, or permission_mode flags (those translate into agent-specific CLI args, and Pi's RPC interface has no equivalent). This is intentional. The admin modal therefore does not surface those fields for Pi: admins configure Pi's approval behaviour through Pi's config and launch_args. The flags are still accepted and ignored by the normaliser, so existing profiles carrying them stay forward-compatible.
- Framing. Pi's RPC mandates
\n-framed JSONL and rejects Unicode separators, and the client splits manually rather than calling readline (covered by test_pi_client.py). Detection has a 5-second RPC timeout with a --list-models fallback when RPC is unavailable. Bad provider keys or a missing binary surface immediately: PiRpcClient ring-buffers the last 20 stderr lines and folds them into the AgentError on prompt failure, timeout, or start failure (covered by TestStderrCapture).
Visuals
Agents panel — Pi registered (agent:pi · 30 models, Ready, green dot) alongside Claude Code, rendering identically to the other agent rows. The footer reads 2 profiles.
Add-profile modal — the agent-type dropdown offers Pi. Selecting it fills Name=Pi, Command placeholder=pi, and surfaces the Launch arguments field, so Pi profiles are addable and editable from the panel like any other agent.
Checklist
Thank you, Tim, for open-webui and now for
computer. Both have been a joy to use and to build for.Opening as an issue because PR creation is limited to collaborators. The change is complete and ready on my fork: kelvinq/computer:feat/pi-coding-agent-pr (rebased on v0.8.8, 101 tests passing). Happy to open it as a PR if you enable it, or feel free to pull the branch.
Add Pi coding agent as a first-class agent profile
Rebased on
origin/main@v0.8.8. 22 files, +4862 / −1767 (the large delta isuv.lock: auvformat-revision rewrite, not new runtime deps). No runtime dependency: Pi is invoked as a CLI subprocess, so the only new packages are the dev-group test tools. 102 tests, 101 passing.Adds Pi (
@earendil-works/pi-coding-agent) as a 7th first-class coding agent, in the same one-file-per-agent pattern as codex and cline. Admins add it from the agents panel like any other agent. Users pick a Pi model in the model picker and chat with it through the existing task runner. Unlike the single-model agents, Pi exposes a whole roster of models across providers through one profile (agent:pi · 30 modelson this box).Summary
Pi speaks JSON-RPC over stdio behind a
--mode rpcflag. This change adds aPiRpcClientstdio transport and arun_pi_agentasync generator that adapts Pi's event stream onto cptr's existingAgentEventtypes, then wires Pi into registration, dispatch, detection, and the admin UI. It sits alongside codex, claude_code, cursor, grok, opencode, and cline.The integration lives entirely in one module,
cptr/utils/agents/pi.py(626 lines), following the single-file-per-agent convention. Pi is a CLI peer ofcodex,claude, andcline, not a Python package, so this adds no runtime dependency.Changes
22 files against
origin/main(+4862 / −1767; the lockfile accounts for the bulk, see Dev deps below):cptr/utils/agents/pi.py(new, 626 lines) —PiRpcClient(stdio JSONL transport), event-translation helpers mapping Pi RPC events to cptrAgentEvents, and the publicrun_pi_agentasync generator.models.py("pi"in_VALID_AGENTS, a default profile, a default row, alaunch_args-trimming normaliser branch),chat_task.py(runners["pi"] = run_pi_agent),model_targets.py("pi"in theAgentModelTarget.agentLiteral),detection.py(apibranch:pi --mode rpc --helpprobe then RPC model listing, with api --list-modelsfallback).admin.ts("pi"in theAgentTypeunion),AgentProfileModal.svelte(Pi in the type dropdown + alaunch_argsfield),Agents.svelte(label/command maps).tests/agents/(7 files, 99 unit tests, incl. afake_pi_rpc.pyJSONL double that drivestool_execution_updatewith accumulatedpartialResult),tests/integration/test_pi_e2e.py(3 real-binary tests).README.md(Pi listed with the other coding agents),CHANGELOG.md(Unreleased entry, matching the Cline precedent).pyproject.tomladdspytest+pytest-asyncioto thedevgroup with a[tool.pytest.ini_options]block. Theuv.lockdiff is large (+4862 / −1767) but its substance is small: it adds five packages (iniconfig,packaging,pluggy,pytest,pytest-asyncio) and changes no existing version. The line churn is auvlockfile format-revision rewrite (the local resolver emits the newer format), not a dependency change. If the project pins a specificuvversion for lockfile generation, re-running it under that version collapses the diff to a handful of lines.How it works
Pi's
--mode rpcstarts a JSON-RPC server on stdio that splits records on\nonly and rejects Unicode line separators.PiRpcClientenforces that contract with manual buffer splitting and a small async read task. (One spec detail worth noting: under--mode rpc, help text goes to stderr because stdout is reserved for JSONL. The detection probe handles this.)The runner is a thin adapter: build a client, start it, call
prompt, forward each event throughtranslate_event. Pimessage_updateevents map toAgentTextDeltaandAgentReasoningDelta(reasoning blocks close implicitly, the same path every other agent uses). Thetool_execution_startandtool_execution_endevents map toAgentToolUpdate. Thebashtool name is renamed torun_commandso it renders like every other agent's shell tool.Models come from
get_available_modelsover RPC. The detection layer probes RPC first and falls back topi --list-models. Resume is supported viasession_idstored inchat.meta.agent_sessionsand proven by the e2eTestPiE2EResumeround-trip.Streaming tool output
Pi's RPC spec carries the accumulated output in
tool_execution_update.partialResult, not the new chunk. cptr'schat_taskappendsAgentToolOutputDelta.deltato existing output on the consumer side, so a naive forwarder concatenates"a" + "a b" + "a b c"into"aa ba b c". Output grows quadratically with each update and is scrambled. This is the integration's load-bearing correctness problem.The adapter handles it in
run_pi_agent: it tracks the last-forwarded text percall_idand emits only the new suffix as the delta. Because Pi models updates as replace-your-display, if a laterpartialResultis not a prefix-extension of the previous one the adapter re-emits the full new text and resets the per-call counter (so a truncation can't silently go mute or slice at a stale offset). The consumer still appends, so a genuine truncation renders asold + newconcatenated rather than a clean replace. A true display-replace would need a consumer-side change and is out of scope here.Covered by:
tests/agents/test_pi_runner.py::TestStreamingToolOutput(monotonic deltas concatenate exactly once)tests/agents/test_pi_runner.py::TestStreamingToolOutputTruncationSafety(non-monotonic and shorter-prefix resets)tests/integration/test_pi_e2e.py::TestPiE2EStreamingToolOutput(a 12-linebash printfagainst the real binary: the test whose absence would have let the bug ship)Tests
102 collected (99 in
tests/agents/, 3 intests/integration/). The single skip is a deliberately slow timeout test (test_request_timeout, off by default, run with-k test_request_timeout).Real-RPC integration tests against the
pibinary on PATH:The integration module uses
pytestmark = pytest.mark.skipif(not _AUTH_OK, ...), where_AUTH_OKruns a tinypi -pprobe against an authed provider, so CI without secrets stays green. Registration is asserted at three layers bytests/agents/test_pi_registration.py(callable runner,AgentModelTarget.agentLiteral membership, and the"pi": run_pi_agentdispatch entry).Compatibility
The change adds a new agent kind and renames or removes nothing. Existing
agents.profilesentries parse unchanged throughnormalize_agent_profiles. The default profile list grows from six to seven. The RPC client is gated behindprofile["agent"] == "pi", so other code paths never importPiRpcClient. The dispatch change is one new key in one map inside_run_agent_target, and no conditionals changed elsewhere.Trade-offs
tool_approval_mode,sandbox_mode, orpermission_modeflags (those translate into agent-specific CLI args, and Pi's RPC interface has no equivalent). This is intentional. The admin modal therefore does not surface those fields for Pi: admins configure Pi's approval behaviour through Pi's config andlaunch_args. The flags are still accepted and ignored by the normaliser, so existing profiles carrying them stay forward-compatible.\n-framed JSONL and rejects Unicode separators, and the client splits manually rather than callingreadline(covered bytest_pi_client.py). Detection has a 5-second RPC timeout with a--list-modelsfallback when RPC is unavailable. Bad provider keys or a missing binary surface immediately:PiRpcClientring-buffers the last 20 stderr lines and folds them into theAgentErroron prompt failure, timeout, or start failure (covered byTestStderrCapture).Visuals
Agents panel — Pi registered (
agent:pi · 30 models,Ready, green dot) alongside Claude Code, rendering identically to the other agent rows. The footer reads2 profiles.Add-profile modal — the agent-type dropdown offers
Pi. Selecting it fills Name=Pi, Command placeholder=pi, and surfaces theLaunch argumentsfield, so Pi profiles are addable and editable from the panel like any other agent.Checklist
origin/mainat v0.8.8