Skip to content

feat(pi): add first-class Pi provider support - #5688

Closed
darjss wants to merge 8 commits into
pingdotgg:mainfrom
darjss:feat/pi-provider
Closed

feat(pi): add first-class Pi provider support#5688
darjss wants to merge 8 commits into
pingdotgg:mainfrom
darjss:feat/pi-provider

Conversation

@darjss

@darjss darjss commented Aug 8, 2026

Copy link
Copy Markdown

Closes #402.

What Changed

Adds Pi as a first-class provider through Pi's RPC mode. T3 now discovers the installed Pi binary and its real models, starts and resumes Pi session files, maps Pi output and tools into the existing provider event model, and exposes Pi on web, desktop, and mobile.

The provider also reads Pi's command and skill catalog, bridges RPC-safe extension questions into T3's user-input panel, and projects Pi subagents and workflows into the existing Agents panel. Pi keeps ownership of its models, accounts, tools, extensions, skills, config, and session files.

This does not add an Executor or MCP path.

Why

#402 asks for a provider that fits T3's current provider and orchestration layers without a Pi-only client flow. This implementation keeps the process and protocol details at the Pi adapter boundary and reuses the current model picker, composer, task lifecycle, Agents panel, and provider settings.

The full vertical slice is large because a provider needs contracts, health checks, model discovery, session lifecycle, event mapping, clients, and tests to work safely. Splitting those pieces would leave branches that register a provider which cannot complete a turn.

UI Changes

No new UI system or provider-only screen was added. Pi uses the current provider picker, model and reasoning controls, composer command and skill menus, user-input panel, and Agents panel. Before this change Pi is absent from those existing surfaces; after it is shown when its configured binary passes the health check.

Verification

  • 65 focused Pi provider tests pass
  • Server, contracts, web, and mobile typechecks pass
  • Tested against Pi 0.84.0 in the real web app:
    • dynamic model and reasoning selection
    • / extension commands and $ skills
    • extension Ask User request and response
    • extension slash-command settlement
    • background subagent completion and parent follow-up
    • workflow phase, model, token, and duration data in the Agents panel

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Built with GPT-5.4 in Pi.


Note

Medium Risk
Large new provider path with child-process RPC, durable session files, and complex turn/settlement concurrency; mistakes could break threads or leak sessions, but it does not change auth or billing.

Overview
Pi is registered as a built-in provider driver that spawns the Pi CLI in RPC mode, probes models/commands/skills for snapshots, and wires adapter + text generation like other agents.

The Pi adapter owns durable session files (allocate, resume cursor validation, per-file leasing), full-access-only threads, and maps Pi RPC stream events into T3 turns, tool items (bash/edit/agents), user-input for extension UI, and task lifecycle for subagents and workflows—including steer prompts, settlement on agent_settled, and interrupt/close edge cases.

Supporting pieces include an NDJSON RPC client, URL-encoded model slugs with thinking-level options, PiTextGeneration for commit/PR/branch/title helpers, plus a Pi icon and "Pi" display label on mobile.

Reviewed by Cursor Bugbot for commit f3eb5d0. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add first-class Pi provider support with RPC client, session management, and text generation

  • Introduces PiDriver, PiAdapter, and makePiTextGeneration to register Pi as a full built-in provider with adapter, text generation, and managed snapshot capabilities.
  • Adds makePiRpcTransport and makePiRpcClient in PiRpcClient.ts for NDJSON-framed RPC communication with the Pi binary, including request correlation, timeouts, and graceful shutdown.
  • Implements PiSessionFile utilities for secure per-instance session file allocation with strict containment, permission, and symlink validation.
  • Adds checkPiProviderStatus in PiProvider.ts to probe the Pi binary, discover models and commands, and build provider snapshots distinguishing missing-binary from other errors.
  • Exposes Pi in server settings (PiSettings/PiSettingsPatch), the session provider picker (marked 'new'), and web/mobile UI with the Pi icon and display label.
📊 Macroscope summarized f3eb5d0. 20 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

coderabbitai Bot commented Aug 8, 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 Plus

Run ID: b93860e5-8e64-450a-b144-7bc340d6f1c7

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

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 Aug 8, 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.

Reviewed the new Pi provider modules (PiDriver, PiAdapter, PiProvider, PiRpcClient, PiSessionFile, PiTextGeneration) against the Effect service conventions. Dependency acquisition, namespace imports, and layer/factory shapes look consistent with the existing provider modules. Three error-modeling/handling violations are noted inline.

Posted via Macroscope — Effect Service Conventions

Comment on lines +207 to +214
Effect.catchTag(
"TimeoutError",
() =>
new TextGenerationError({
operation: input.operation,
detail: "Pi text generation timed out.",
}),
),

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.

Statically known tagged failures should be recovered with Effect.catchTags({ ... }), including when only one tag is handled; Effect.catchTag is not used by this convention.

Suggested change
Effect.catchTag(
"TimeoutError",
() =>
new TextGenerationError({
operation: input.operation,
detail: "Pi text generation timed out.",
}),
),
Effect.catchTags({
TimeoutError: () =>
new TextGenerationError({
operation: input.operation,
detail: "Pi text generation timed out.",
}),
}),

Posted via Macroscope — Effect Service Conventions

new TextGenerationError({
operation: input.operation,
detail: "Failed to start Pi RPC text generation.",
cause: String(cause),

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.

String(cause) erases the underlying PiRpcError (tag, fields, stack) so the error chain is no longer inspectable. TextGenerationError.cause is Schema.Defect(), so the failure itself can be preserved directly — consider passing it through as sibling text-generation modules do (OpenCode2TextGeneration uses cause).

Suggested change
cause: String(cause),
cause,

Posted via Macroscope — Effect Service Conventions

Comment on lines +224 to +225
onFailure: (cause) =>
end(new PiRpcProcessExitedError({ detail: `Pi RPC stdout failed: ${String(cause)}` })),

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.

This stringifies the stdout failure into detail and drops the failure itself: PiRpcProcessExitedError (lines 40-43) has no cause field, so the immediate underlying error and its stack are lost, and the caller-visible message becomes arbitrary defect text. Consider adding cause: Schema.optional(Schema.Defect()) to the error class and constructing it with a stable detail (for example "Pi RPC stdout failed") plus cause, keeping the exact value only in cause.

Posted via Macroscope — Effect Service Conventions

@darjss darjss closed this Aug 8, 2026
cause: info.type,
});
yield* fs.access(sessionFile, { readable: true });
const firstLine = (yield* fs.readFileString(sessionFile)).split(/\r?\n/, 1)[0] ?? "";

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 pi/PiSessionFile.ts:79

validatePiResumeSessionFile calls fs.readFileString(sessionFile) and only then takes the first line, so resuming a long-running session loads the entire JSONL transcript into memory even though only the header is needed. A large session file can cause a major memory spike or OOM. Consider reading a bounded prefix of the file (for example via a stream or a fixed-size read) and parsing the first line from that instead.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/pi/PiSessionFile.ts around line 79:

`validatePiResumeSessionFile` calls `fs.readFileString(sessionFile)` and only then takes the first line, so resuming a long-running session loads the entire JSONL transcript into memory even though only the header is needed. A large session file can cause a major memory spike or OOM. Consider reading a bounded prefix of the file (for example via a stream or a fixed-size read) and parsing the first line from that instead.

| { readonly confirmed: boolean }
| { readonly cancelled: true },
) {
if (ctx.pendingUserInputs.get(requestId) !== pending) return false;

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:341

resolveExtensionInput deletes the pending request from ctx.pendingUserInputs and emits user-input.resolved before calling ctx.client.respondToExtensionUi. If that RPC call fails (e.g., the Pi process exited), the error propagates to the caller, but the UI has already been told the request was resolved and the map entry is gone — so the user cannot retry and Pi never receives the answer. The pending entry should be retained (or restored) and the resolved event published only after respondToExtensionUi succeeds.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/PiAdapter.ts around line 341:

`resolveExtensionInput` deletes the pending request from `ctx.pendingUserInputs` and emits `user-input.resolved` *before* calling `ctx.client.respondToExtensionUi`. If that RPC call fails (e.g., the Pi process exited), the error propagates to the caller, but the UI has already been told the request was resolved and the map entry is gone — so the user cannot retry and Pi never receives the answer. The pending entry should be retained (or restored) and the resolved event published only after `respondToExtensionUi` succeeds.

!available.models.some((m) => m.provider === parsed.provider && m.id === parsed.modelId)
)
return yield* validation("sendTurn", "Selected Pi model is not currently available.");
yield* ctx.client

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 Layers/PiAdapter.ts:1354

After setModel succeeds, ctx.session.model is never updated, so listSessions() keeps returning the model from startSession even when a later turn selects a different one. This means the advertised in-session model switch is not reflected in session state. Consider assigning ctx.session.model (and any related fields) from the parsed selection after the setModel call, as other in-session adapters do.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/PiAdapter.ts around line 1354:

After `setModel` succeeds, `ctx.session.model` is never updated, so `listSessions()` keeps returning the model from `startSession` even when a later turn selects a different one. This means the advertised in-session model switch is not reflected in session state. Consider assigning `ctx.session.model` (and any related fields) from the parsed selection after the `setModel` call, as other in-session adapters do.

}
if (state.value.isStreaming === true) return;
const terminalEvents: ProviderRuntimeEvent[] = [];
if (turn.assistantText.trim().length > 0) {

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 Layers/PiAdapter.ts:1022

When a Pi response contains only whitespace deltas, the assistant message item is left permanently inProgress even after the turn completes. turn.assistantStarted is set to true on the first delta, but the settlement logic at agent_settled only emits item.completed when turn.assistantText.trim().length > 0, so a whitespace-only response never closes the item. The reasoning branch correctly uses turn.reasoningStarted for this decision — the assistant branch should use turn.assistantStarted instead of re-checking the trimmed text length.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/PiAdapter.ts around line 1022:

When a Pi response contains only whitespace deltas, the assistant message item is left permanently `inProgress` even after the turn completes. `turn.assistantStarted` is set to `true` on the first delta, but the settlement logic at `agent_settled` only emits `item.completed` when `turn.assistantText.trim().length > 0`, so a whitespace-only response never closes the item. The reasoning branch correctly uses `turn.reasoningStarted` for this decision — the assistant branch should use `turn.assistantStarted` instead of re-checking the trimmed text length.

Effect.map((value) => `t3-pi-${String(value)}`),
);
const waiter = yield* Deferred.make<PiRpcResponse, PiRpcError>();
yield* Ref.update(pending, (current) => new Map(current).set(id, waiter));

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 pi/PiRpcClient.ts:257

After the transport has ended (stdout exits or client.close() runs), new request calls still register a waiter and block until the 120-second timeout instead of failing immediately with PiRpcProcessExitedError. The end function clears the pending map once, so any request added afterward is stranded with no response or future end to complete it.

This happens because request checks neither the closed ref nor whether the transport is still alive before inserting into pending. Consider failing the request up front when closed is true, or having end also reject late additions via a shared flag.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/pi/PiRpcClient.ts around line 257:

After the transport has ended (stdout exits or `client.close()` runs), new `request` calls still register a waiter and block until the 120-second timeout instead of failing immediately with `PiRpcProcessExitedError`. The `end` function clears the pending map once, so any request added afterward is stranded with no response or future `end` to complete it.

This happens because `request` checks neither the `closed` ref nor whether the transport is still alive before inserting into `pending`. Consider failing the request up front when `closed` is `true`, or having `end` also reject late additions via a shared flag.

);
};

const interruptTurn: ProviderAdapterShape<ProviderAdapterError>["interruptTurn"] = (

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:1432

interruptTurn calls withThreadLock, but sendTurn already holds that lock while ctx.client.prompt can be blocked waiting for extension UI input. The interrupt request deadlocks on the lock and the abort RPC is never sent, so the turn cannot be cancelled while it is waiting on user input. The same deadlock affects stopSession (and stopAll during finalization): it blocks on the lock held by sendTurn and hangs indefinitely when the extension UI has no timeout. Consider not serializing interruptTurn and stopSession behind the same lock, or having sendTurn release the lock while awaiting extension UI, so abort/close can proceed.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/PiAdapter.ts around line 1432:

`interruptTurn` calls `withThreadLock`, but `sendTurn` already holds that lock while `ctx.client.prompt` can be blocked waiting for extension UI input. The interrupt request deadlocks on the lock and the abort RPC is never sent, so the turn cannot be cancelled while it is waiting on user input. The same deadlock affects `stopSession` (and `stopAll` during finalization): it blocks on the lock held by `sendTurn` and hangs indefinitely when the extension UI has no timeout. Consider not serializing `interruptTurn` and `stopSession` behind the same lock, or having `sendTurn` release the lock while awaiting extension UI, so abort/close can proceed.

@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 2 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 f3eb5d0. Configure here.

if (Exit.isFailure(state) || !piStateMatchesCursor(state.value, ctx.cursor)) {
yield* failActive(ctx, "Pi session identity drifted during settlement.", native);
yield* close(ctx);
return;

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.

Settlement errors misread as drift

Medium Severity

During agent_settled handling, a failed get_state Exit is treated the same as a cursor mismatch. That fails the turn with an identity-drift message and closes the session, so a transient RPC/get_state error tears down an otherwise healthy Pi session.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f3eb5d0. Configure here.

},
raw: raw(native),
});
}

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.

Whitespace leaves assistant item open

Medium Severity

Assistant text_delta events start an assistant item whenever the delta is non-empty, including whitespace. Settlement only emits item.completed when trimmed assistant text is non-empty, so whitespace-only replies leave an in-progress assistant item after turn.completed.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f3eb5d0. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3eb5d0f67

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +959 to +961
if (type === "message_end") {
yield* handleSubagentResultMessage(ctx, turn, event, native);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat failed Pi messages as failed turns

When Pi ends an assistant message with stopReason: "error" or "aborted", this branch ignores that status and the later agent_settled handler publishes a successful turn.completed. API, authentication, and model failures can therefore appear to users as successful blank turns; handle terminal assistant-message stop reasons here, as PiTextGeneration already does.

AGENTS.md reference: AGENTS.md:L139-L142

Useful? React with 👍 / 👎.

Comment on lines +107 to +111
const client = yield* makeRpcClient({
command: settings.binaryPath,
args: DETERMINISTIC_ARGS,
env: environment,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Discover Pi commands from the project working directory

The health probe launches Pi without a cwd, so getCommands() loads commands, extensions, and skills relative to the T3 server process rather than the user's project. For projects with project-scoped Pi configuration, those commands will be absent from the composer catalog, while commands from the server's own directory may be advertised instead; command discovery needs project/session context or must avoid presenting this process-global snapshot as the project catalog.

Useful? React with 👍 / 👎.

cause: info.type,
});
yield* fs.access(sessionFile, { readable: true });
const firstLine = (yield* fs.readFileString(sessionFile)).split(/\r?\n/, 1)[0] ?? "";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Read only the session header during resume validation

For every resumed Pi thread, this reads the entire append-only JSONL session merely to inspect its first line. Long-running sessions can contain large tool outputs and attachments, so resume cost and peak memory grow with the complete conversation and can stall or exhaust the server; stream or bound the read to the first newline instead.

AGENTS.md reference: AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

Comment on lines +1314 to +1316
const parsed = selection ? decodePiModelSlug(selection.model) : undefined;
if (!parsed)
return yield* validation("sendTurn", "A valid Pi model selection is required.");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fall back to the session model when a turn omits selection

ThreadTurnStartCommand.modelSelection is optional, and the orchestration layer deliberately passes it through as absent for in-session-switching providers, but Pi rejects every such turn even when ctx.session.model already identifies the active model. Compatible clients or API callers that omit the redundant selection on a follow-up therefore fail with “A valid Pi model selection is required”; use the stored session model and keep it updated after model switches.

AGENTS.md reference: AGENTS.md:L67-L74

Useful? React with 👍 / 👎.

@macroscopeapp

macroscopeapp Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

6 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.

@darjss
darjss deleted the feat/pi-provider branch August 8, 2026 08:27
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

1 participant