Skip to content

feat: Add Pi Coding agent as a provider - #3947

Closed
1337hero wants to merge 9 commits into
pingdotgg:mainfrom
1337hero:feat/pi-provider
Closed

feat: Add Pi Coding agent as a provider#3947
1337hero wants to merge 9 commits into
pingdotgg:mainfrom
1337hero:feat/pi-provider

Conversation

@1337hero

@1337hero 1337hero commented Jul 13, 2026

Copy link
Copy Markdown

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 dunno

07-pi-rail-selected

Why 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.select dialogs 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)

  • RPC connection over JSONL stdio with request/response correlation via Deferred + pending map, timed at 30s by default. Startup noise and partial writes are ignored (strict framing).
  • spawnPiRpcSession - spawns pi --mode rpc with 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.
  • Approval extension is an injected .ts file Pi loads via --extension. It intercepts tool_call events for gated tools (bash always, edit/write unless the mode auto-accepts edits) and presents a ctx.ui.select dialog whose title carries a JSON marker payload.
    The adapter maps these to request.opened / respondToRequest. Modes are driven by the T3CODE_PI_RUNTIME_MODE env var.

Session adapter (apps/server/src/provider/Layers/PiAdapter.ts)

  • Full lifecycle: startSession (spawns Pi, validates readiness with a get_state round trip), sendTurn (prompt + images, supports steer), interruptTurn, stopSession, stopAll.
  • Event stream: translates Pi's RPC event vocabulary (message_*, tool_execution_*, agent_end, extension_ui_request, compaction_start/end, auto_retry_start, extension_error) into canonical ProviderRuntimeEvent types.
  • Approvals: handleExtensionUiRequest parses the JSON marker from extension dialog titles into request.opened events. respondToRequest maps T3 Code decisions (accept/acceptForSession/decline/cancel) back to Pi's selection
    vocabulary.
  • Dialogs: unsupported UI methods (input, editor) auto-cancel to avoid stalling the turn. confirm and select dialogs from the extension are surfaced as user-input.requested events.
  • Token usage: best-effort get_session_stats after each completed turn, not a functional signal, so failure stays silent.
  • readThread — rebuilds the turn snapshot from a fresh get_messages RPC.

Provider wiring (apps/server/src/provider/Layers/PiProvider.ts, apps/server/src/provider/Drivers/PiDriver.ts)

  • Driver and provider layers register the Pi adapter in the provider registry.
  • Model selection is in-session (switch model between turns without
    reconnecting).
  • Built-in driver listing includes Pi when the binary is available.

Text generation (apps/server/src/textGeneration/PiTextGeneration.ts)

  • One-shot pi --print for commit messages, branch names, PR content, and
    thread titles. Sessions, tools, extensions, skills, prompt templates, and
    context files are all disabled, each call is a pure prompt in, text out exchange.
  • Prompt goes in over stdin, so no argv length worries.
  • Model selection is forwarded to Pi; thinking is forced off for these calls.

Schemas (packages/contracts/src/settings.ts, model.ts, providerRuntime.ts)

  • Pi-specific settings shape: binary path, enabled flag, custom model list.
  • "pi" added to ProviderDriverKind.
  • Model selection for Pi uses the same provider/modelId slug 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.

02-settings 03-pi-expanded 05-model-picker-open 06-model-picker-pi 07-pi-rail-selected

Testing

Pi runtime:

  • RPC session spawn/kill, request correlation, timeout, and error mapping through piRuntime.test.ts.
  • Full server suite: 1442 passed, 7 skipped (166 test files).
  • Typecheck: vp run --filter t3 typecheck — clean.

Verification notes:

  • Built and tested against Pi v0.80.x. The approval extension protocol depends on Pi's tool_call blocking hook, which upstream documents as stable in RPC mode.
  • Pi runs against the user's configured LLM provider; T3 Code does not manage Pi's dependencies.

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 --print text generation, registered alongside existing built-in drivers with PiRuntimeLive at server boot.

Pi runtime and sessions spawn pi --mode rpc over 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, and readThread snapshots.

Refactors move OpenCode (and Pi) onto shared parseProviderModelSlug and toToolLifecycleItemType instead 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

  • Adds a complete Pi provider integration: driver registration, provider/model picker UI, icon, settings schema, and diagnostics classification
  • Implements PiAdapter for session management, event streaming, approval handling, and tool lifecycle events via the Pi CLI
  • Implements PiTextGeneration for commit messages, PR content, branch names, and thread titles via Pi CLI JSON mode
  • Adds PiProvider with health checks (version probe + model discovery RPC), thinking capability mapping, and snapshot streaming
  • Moves shared utilities (parseProviderModelSlug, titleCaseSlug, toToolLifecycleItemType) into shared packages and removes duplicated local implementations from the OpenCode adapter
  • Composer send actions are disabled and a thread error is shown when no model is selected; the Pi provider defaults to an empty model string rather than the global default
  • Risk: renaming PiAgentIcon to PiIcon in Icons.tsx is a breaking change for any existing import of PiAgentIcon
📊 Macroscope summarized f7b1880. 25 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

1337hero added 9 commits July 3, 2026 19:05
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
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: dd009d08-98f7-4789-85bd-4ae108d7338c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 13, 2026
}
return itemId;
}
if (explicit) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 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({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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"] = () =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 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`.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.

Fix All in Cursor

❌ 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f7b1880. Configure here.

nextThinking = thinkingLevel;
}
context.currentModelSlug = nextModelSlug;
context.currentThinking = nextThinking;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f7b1880. Configure here.

: { state: "completed" },
});
yield* emitTokenUsage(context);
break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f7b1880. Configure here.

);

const emit = (event: ProviderRuntimeEvent) =>
Queue.offer(runtimeEvents, event).pipe(Effect.asVoid);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f7b1880. Configure here.

@macroscopeapp

macroscopeapp Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@1337hero 1337hero closed this Jul 13, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Effect service conventions review: one finding on the new PiRuntime service definition.

Posted via Macroscope — Effect Service Conventions

Comment on lines +590 to +601
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>;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@Jbollenbacher

Copy link
Copy Markdown

Why was this closed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add Pi provider integration via RPC

2 participants