feat: Add Pi Coding agent as a provider - #3947
Conversation
Adds a ProviderDriver for Pi (https://github.com/earendil-works/pi-mono), becuase I like Pi. - piRuntime: spawns `pi --mode rpc` per thread and speaks Pi's strict JSONL protocol over stdio (request/response correlation, event queue) - PiAdapter: maps Pi agent events onto canonical runtime events (streaming text/thinking deltas, tool lifecycle, token usage, compaction, steering while a turn is active, in-session model and thinking-level switching) - Approvals: Pi has no built-in permission system, so non-full-access sessions load an embedded Pi extension that blocks gated tool_calls behind ctx.ui.select dialogs, which surface through Pi's RPC extension UI protocol and map onto T3 Code approval requests, honoring approval-required and auto-accept-edits modes - PiProvider: probes `pi --version` / `pi --list-models` for status and the model catalog (provider/modelId slugs, thinking capability) - PiTextGeneration: commit/PR/branch/title generation via `pi --print`with sessions, tools, extensions, and context files disabled - Web: provider metadata, icon, and picker entries
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| } | ||
| return itemId; | ||
| } | ||
| if (explicit) { |
There was a problem hiding this comment.
🟠 High Layers/PiAdapter.ts:225
When a tool_execution_start omits toolCallId, fallbackToolCallItemId mints an ID and queues it, but a later tool_execution_update or tool_execution_end for the same invocation that includes an explicit toolCallId returns that explicit ID instead. This means the item-started event and the item-completed event emit under different IDs, so the started item is never completed and remains permanently in progress. The minted ID also stays queued forever because it can never match the explicit ID on the end event. Consider resolving explicit IDs seen on updates/ends back to the queued minted ID for the same tool invocation, so the lifecycle events share one canonical item ID.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/PiAdapter.ts around line 225:
When a `tool_execution_start` omits `toolCallId`, `fallbackToolCallItemId` mints an ID and queues it, but a later `tool_execution_update` or `tool_execution_end` for the same invocation that includes an explicit `toolCallId` returns that explicit ID instead. This means the item-started event and the item-completed event emit under different IDs, so the started item is never completed and remains permanently in progress. The minted ID also stays queued forever because it can never match the explicit ID on the end event. Consider resolving explicit IDs seen on updates/ends back to the queued minted ID for the same tool invocation, so the lifecycle events share one canonical item ID.
| data: Schema.optionalKey(Schema.Unknown), | ||
| }); | ||
|
|
||
| const PiContentBlock = Schema.Struct({ |
There was a problem hiding this comment.
🟡 Medium provider/piRuntime.ts:159
PiContentBlock only declares type and optional text, so decoding a valid content block that carries non-text fields (e.g. an image block with data/mimeType) silently strips them. Because PiContentBlock is used in PiMessageContent, PiThreadMessage, and PiToolResult, both readThread snapshots and emitted tool-event data.result lose all non-text content. Consider including the additional expected fields (or falling back to Schema.Unknown for the payload) so non-text blocks survive decoding.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/piRuntime.ts around line 159:
`PiContentBlock` only declares `type` and optional `text`, so decoding a valid content block that carries non-text fields (e.g. an image block with `data`/`mimeType`) silently strips them. Because `PiContentBlock` is used in `PiMessageContent`, `PiThreadMessage`, and `PiToolResult`, both `readThread` snapshots and emitted tool-event `data.result` lose all non-text content. Consider including the additional expected fields (or falling back to `Schema.Unknown` for the payload) so non-text blocks survive decoding.
| }), | ||
| ); | ||
|
|
||
| const stopAll: PiAdapterShape["stopAll"] = () => |
There was a problem hiding this comment.
🟠 High Layers/PiAdapter.ts:1384
stopAll closes every Pi session without calling settlePendingRequestsAsCancelled, so any open approval or user-input dialogs are never resolved. Consumers are left with a permanently pending request.opened or user-input.requested event even though the Pi process backing it has been terminated. stopSession and the adapter finalizer both call settlePendingRequestsAsCancelled before closing, but stopAll skips it. Consider calling settlePendingRequestsAsCancelled(context) for each context before stopPiContext.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/PiAdapter.ts around line 1384:
`stopAll` closes every Pi session without calling `settlePendingRequestsAsCancelled`, so any open approval or user-input dialogs are never resolved. Consumers are left with a permanently pending `request.opened` or `user-input.requested` event even though the Pi process backing it has been terminated. `stopSession` and the adapter finalizer both call `settlePendingRequestsAsCancelled` before closing, but `stopAll` skips it. Consider calling `settlePendingRequestsAsCancelled(context)` for each context before `stopPiContext`.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f7b1880. Configure here.
| stopped: yield* Ref.make(false), | ||
| sessionScope: started.sessionScope, | ||
| }; | ||
| sessions.set(input.threadId, context); |
There was a problem hiding this comment.
Concurrent start races session map
High Severity
Concurrent startSession calls for the same thread can race past the sessions map check, causing multiple Pi processes to be spawned. Only one process is registered, leaving others orphaned and leaking resources, while their event streams may continue to emit incorrect or duplicated runtime events.
Reviewed by Cursor Bugbot for commit f7b1880. Configure here.
| nextThinking = thinkingLevel; | ||
| } | ||
| context.currentModelSlug = nextModelSlug; | ||
| context.currentThinking = nextThinking; |
There was a problem hiding this comment.
Model switch not rolled back
Medium Severity
When sendTurn updates both the model and thinking level, the set_model RPC runs before set_thinking_level. If thinking level setup fails, the adapter's internal model state isn't updated, but Pi may have already switched models. This desynchronizes T3's cached model with Pi, potentially leading to later turns using the wrong model.
Reviewed by Cursor Bugbot for commit f7b1880. Configure here.
| : { state: "completed" }, | ||
| }); | ||
| yield* emitTokenUsage(context); | ||
| break; |
There was a problem hiding this comment.
Interrupt may emit completed
Medium Severity
handlePiEvent snapshots activeTurnId at handler entry, and interruptTurn clears that id and emits turn.aborted. A concurrent agent_end handler can still use the captured id and emit turn.completed for the same turn, leaving orchestration with conflicting terminal turn events.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f7b1880. Configure here.
| if (MODE === "full-access") return; | ||
| const tool = event.toolName; | ||
| const isEditTool = tool === "edit" || tool === "write"; | ||
| const gated = tool === "bash" || (isEditTool && MODE !== "auto-accept-edits"); |
There was a problem hiding this comment.
Patch tools skip approval gate
High Severity
In approval-required mode, the injected Pi approval extension only gates bash, edit, and write. Tools such as multiedit and patch still run without ctx.ui.select, while the adapter classifies them as file changes—so file edits can execute without the approval UI T3 Code expects.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f7b1880. Configure here.
| ); | ||
|
|
||
| const emit = (event: ProviderRuntimeEvent) => | ||
| Queue.offer(runtimeEvents, event).pipe(Effect.asVoid); |
There was a problem hiding this comment.
Bounded queues unlike other adapters
Medium Severity
Pi uses Queue.bounded(1024) for canonical runtime events and inbound RPC events, while Codex, Claude, and OpenCode use unbounded queues. When the downstream consumer lags, emit and the JSONL reader block on Queue.offer, which can stall Pi event processing and leave turns appearing stuck without turn.completed or approvals.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f7b1880. Configure here.
ApprovabilityVerdict: Needs human review 3 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
Effect service conventions review: one finding on the new PiRuntime service definition.
Posted via Macroscope — Effect Service Conventions
| export interface PiRuntimeShape { | ||
| readonly runCommand: (input: { | ||
| readonly binaryPath: string; | ||
| readonly args: ReadonlyArray<string>; | ||
| readonly environment?: NodeJS.ProcessEnv; | ||
| readonly cwd?: string; | ||
| readonly stdin?: string; | ||
| }) => Effect.Effect<PiCommandResult, PiRuntimeError>; | ||
| readonly spawnSession: ( | ||
| input: SpawnPiRpcInput, | ||
| ) => Effect.Effect<PiRpcHandle, PiRuntimeError, Scope.Scope>; | ||
| } |
There was a problem hiding this comment.
PiRuntime is a new Effect service, but its interface is declared as a standalone PiRuntimeShape. Per the service-definition convention, define the interface inline in the Context.Service declaration and reference the inferred shape as PiRuntime["Service"] (for the internal runCommand/spawnSession typings, the satisfies at the bottom of make, and the test doubles that currently import PiRuntimeShape), rather than retaining a separate PiRuntimeShape type. The TextGeneration.TextGeneration["Service"] usage already in this codebase is the pattern to follow.
Posted via Macroscope — Effect Service Conventions
|
Why was this closed? |


Why
I use Pi, I like Pi - full stop. This adds Pi. That's my why.
T3 Code already wraps every major coding agent. [Pi](https://github.com/earendil-works/pi/tree/main) is open source and already well known, runs locally or against any LLM provider, and fills a gap: self-hosted, no license gate, full control over your model stack. This PR adds the provider driver, the RPC session plumbing, and all the UI wiring in the same house pattern as OpenCode, Claude, and the rest.
Closes #402.
Note* I can't get around this being a gigantic PR. So...
¯\_(ツ)_/¯given the contributions note i just dunnoWhy the existing provider abstraction falls short
Pi's RPC protocol (
pi --mode rpc) speaks JSONL over stdio, not the JSON-RPC protocol the Codex adapter was built for. Pi has no built-in permission system, so T3 Code's runtime modes (approval-required, auto-accept-edits, full-access)need a custom extension that maps
ctx.ui.selectdialogs onto the existing approval UI. And Pi hosts exactly one session per process, not one process per session, the session lifecycle maps differently.None of the existing adapters could extend to cover these differences without bending their own contract. Pi gets its own adapter layer.
What changed
Provider runtime (
apps/server/src/provider/piRuntime.ts)spawnPiRpcSession- spawnspi --mode rpcwith model/provider selection, thinking level, and the T3 Code approval extension. Process lifetime is scoped to the caller Effect scope; closing it kills the child, shuts queues, and fails in-flight requests.runCommand- one-shot invocation for version checks and model listing..tsfile Pi loads via--extension. It interceptstool_callevents for gated tools (bash always, edit/write unless the mode auto-accepts edits) and presents actx.ui.selectdialog whose title carries a JSON marker payload.The adapter maps these to
request.opened/respondToRequest. Modes are driven by theT3CODE_PI_RUNTIME_MODEenv var.Session adapter (
apps/server/src/provider/Layers/PiAdapter.ts)startSession(spawns Pi, validates readiness with aget_stateround trip),sendTurn(prompt + images, supports steer),interruptTurn,stopSession,stopAll.message_*,tool_execution_*,agent_end,extension_ui_request,compaction_start/end,auto_retry_start,extension_error) into canonicalProviderRuntimeEventtypes.handleExtensionUiRequestparses the JSON marker from extension dialog titles intorequest.openedevents.respondToRequestmaps T3 Code decisions (accept/acceptForSession/decline/cancel) back to Pi's selectionvocabulary.
confirmandselectdialogs from the extension are surfaced asuser-input.requestedevents.get_session_statsafter each completed turn, not a functional signal, so failure stays silent.readThread— rebuilds the turn snapshot from a freshget_messagesRPC.Provider wiring (
apps/server/src/provider/Layers/PiProvider.ts,apps/server/src/provider/Drivers/PiDriver.ts)in-session(switch model between turns withoutreconnecting).
Text generation (
apps/server/src/textGeneration/PiTextGeneration.ts)pi --printfor commit messages, branch names, PR content, andthread titles. Sessions, tools, extensions, skills, prompt templates, and
context files are all disabled, each call is a pure prompt in, text out exchange.
Schemas (
packages/contracts/src/settings.ts,model.ts,providerRuntime.ts)"pi"added toProviderDriverKind.provider/modelIdslug format as the CLI.UI Changes
Provider picker Pi now appears as a selectable provider alongside Codex, Claude, Grok, Cursor, and OpenCode.
Settings → Diagnostics Pi model listing displayed when the binary is available.
No motion or interaction changes. This is a static addition to existing UI patterns. Should conform.
Testing
Pi runtime:
piRuntime.test.ts.vp run --filter t3 typecheck— clean.Verification notes:
tool_callblocking hook, which upstream documents as stable in RPC mode.Note
High Risk
Large new surface area for subprocess RPC, approvals, and MCP bridging in core provider orchestration; mistakes could affect session stability or tool execution across threads.
Overview
Adds Pi as a first-class provider: driver, health/model discovery, RPC runtime, session adapter, and
pi --printtext generation, registered alongside existing built-in drivers withPiRuntimeLiveat server boot.Pi runtime and sessions spawn
pi --mode rpcover JSONL stdio with correlated requests, an injected approval extension mapped to T3 approval UI, and optional T3 MCP bridge config (with retry/degrade when MCP setup fails). The adapter covers full thread lifecycle—turns with steer/model/thinking switches, tool events, dialogs, token usage, andreadThreadsnapshots.Refactors move OpenCode (and Pi) onto shared
parseProviderModelSlugandtoToolLifecycleItemTypeinstead of local helpers. Extensive adapter/runtime/provider tests are included.Reviewed by Cursor Bugbot for commit f7b1880. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add Pi Coding Agent as a provider with full adapter, text generation, and settings support
PiAdapterfor session management, event streaming, approval handling, and tool lifecycle events via the Pi CLIPiTextGenerationfor commit messages, PR content, branch names, and thread titles via Pi CLI JSON modePiProviderwith health checks (version probe + model discovery RPC), thinking capability mapping, and snapshot streamingparseProviderModelSlug,titleCaseSlug,toToolLifecycleItemType) into shared packages and removes duplicated local implementations from the OpenCode adapterPiAgentIcontoPiIconinIcons.tsxis a breaking change for any existing import ofPiAgentIcon📊 Macroscope summarized f7b1880. 25 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.