mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Fixes #11692. ## Problem In RPC mode the startup catalog refresh runs in the background and also refreshes an expired OAuth credential. If pi exits after the token request went out but before the new token is written, the provider has already spent the old single-use refresh token and auth.json keeps only that one. The next request fails with `invalid_grant`. Trace: `pi --mode rpc` starts, `get_state` is answered, catalog refresh sends the token request, the client sends SIGTERM (or closes stdin), `shutdown()` runs `runtimeHost.dispose()`, which does not know about the refresh, and the process exits. Same for print and interactive mode, and for a prompt-time refresh cut off by an exit. ## Fix - `RuntimeCredentials.modify()` tracks every pending modification (same idiom as the MCP `track()` / `McpAuthProvider.settled()`). - `RuntimeCredentials.settled()` / `ModelRuntime.settleCredentialWrites()` wait for them. - `AgentSessionRuntime.dispose()` waits for them. All modes exit through `dispose()`. No extra timeout: the OAuth refresh (15 s) and the auth.json lock wait (30 s) are already bounded, and a second signal still exits at once. Not changed: the catalog refresh still refreshes expired OAuth credentials, also for providers whose catalog does not use them (the pi.dev catalog for `openai`, `anthropic`, ...). Skipping that would need a per-provider "catalog needs auth" signal in pi-ai; it is optional now that exits wait. `docs/rpc.md` now says that pi waits for pending work before exiting. ## Testing - `test/runtime-credentials.test.ts`: `settled()` waits until a pending modification is written, and resolves after a failed one. - `test/suite/regressions/11692-dispose-waits-for-oauth-refresh.test.ts`: `dispose()` waits until the token from a background catalog refresh is saved. Fails without the fix. - Real process: `pi --mode rpc` with an extension OAuth provider (expired credential, 400 ms refresh), SIGTERM about 100 ms after the token request started. Without the wait: 3 of 3 runs kept the spent refresh token. With the fix: 3 of 3 saved the new one (exit 143). Closing stdin instead of SIGTERM: new token saved, exit 0. - `npm run check`; `test/rpc*`, `test/print-mode`, `test/model-runtime*`, runtime and regression suites pass.
195 lines
9.2 KiB
Markdown
195 lines
9.2 KiB
Markdown
# RPC Mode
|
|
|
|
RPC mode runs Pi as a long-lived subprocess controlled through JSON records on stdin and stdout. Use it for language-independent integrations, process isolation, IDEs, and custom user interfaces.
|
|
|
|
For an in-process Node.js or Bun integration, prefer the [SDK](sdk.md). For a subprocess-based TypeScript integration, prefer the exported `RpcClient`, which starts Pi, correlates responses, exposes typed command methods, and delivers events to listeners.
|
|
|
|
| Interface | Process boundary | Control model | Best fit |
|
|
|---|---|---|---|
|
|
| [SDK](sdk.md) | In process | Direct TypeScript methods and events | Node.js or Bun hosts that want complete API access |
|
|
| RPC | Child process | JSONL commands, responses, and events | Other languages, isolated processes, IDEs, or custom clients |
|
|
|
|
## Start RPC mode
|
|
|
|
```bash
|
|
pi --mode rpc --no-session
|
|
```
|
|
|
|
Normal CLI options still select the working folder, model, tools, resources, and session behavior. Common choices include `--provider`, `--model`, `--name`, `--no-session`, and `--session-dir`. See [Command Line](cli.md) for the complete, version-specific interface; `pi --help` is authoritative for the installed version.
|
|
|
|
RPC mode rejects `@file` prompt arguments. Send prompts through the [`prompt`](rpc-commands.md#prompt) command instead.
|
|
|
|
## Protocol records
|
|
|
|
The protocol has four record families:
|
|
|
|
| Direction | Record | Purpose |
|
|
|---|---|---|
|
|
| stdin | Command | Ask Pi to prompt, inspect state, change configuration, or manage the session |
|
|
| stdout | `response` | Report whether one command succeeded and return any command data |
|
|
| stdout | Session event | Stream run, message, tool, queue, compaction, and retry activity |
|
|
| Both | Extension UI record | Forward supported extension interactions between Pi and the client |
|
|
|
|
See [RPC Commands](rpc-commands.md), [JSON Event Stream](json.md), and [RPC Extension UI](rpc-extension-ui.md) for the canonical record definitions.
|
|
|
|
### Correlate commands and responses
|
|
|
|
Every command accepts an optional string `id`. A matching response repeats it:
|
|
|
|
```json
|
|
{"id":"req-1","type":"get_state"}
|
|
{"id":"req-1","type":"response","command":"get_state","success":true,"data":{"...":"..."}}
|
|
```
|
|
|
|
Use unique IDs whenever more than one command can be outstanding. Command handling is asynchronous, so clients should correlate by ID rather than response order.
|
|
|
|
Session events generally have no command ID because they describe session activity. `bash_execution_update` is the exception: when the originating [`bash`](rpc-commands.md#bash) command has an ID, its output events repeat that ID.
|
|
|
|
An `extension_ui_response` uses the ID supplied by its `extension_ui_request`. It does not produce a normal command response.
|
|
|
|
## Framing
|
|
|
|
RPC uses strict JSONL framing. Write one complete JSON object per record and terminate it with LF (`\n`). Read stdout as a byte or UTF-8 stream and split records only on LF. Strip an optional preceding carriage return to accept CRLF input.
|
|
|
|
Do not use a generic line reader that treats Unicode line or paragraph separators as record boundaries. In particular, Node.js `readline` also splits on `U+2028` and `U+2029`, which are valid inside JSON strings.
|
|
|
|
Read stdout continuously. Pi honors stdout backpressure, but a client that stops reading can stall the process. Honor stdin backpressure when writing commands. Stdout is reserved for protocol records; diagnostics and application logging go to stderr.
|
|
|
|
## Run lifecycle
|
|
|
|
A successful `prompt` response means the prompt was accepted, queued, or handled. It does not mean model work completed:
|
|
|
|
```json
|
|
{"id":"req-2","type":"prompt","message":"Review this repository"}
|
|
{"id":"req-2","type":"response","command":"prompt","success":true,"data":{"disposition":"started"}}
|
|
```
|
|
|
|
`data.disposition` reports what happened to the prompt. If it is `"handled"`, no run started for this prompt, so don't wait for `agent_settled`. See [RPC Commands](rpc-commands.md#prompt) for all values.
|
|
|
|
Continue consuming [events](json.md) after that response. `agent_end` marks the end of one low-level agent run, but retries, overflow recovery, compaction, steering, or follow-up work can still follow. Wait for `agent_settled` when the client needs to know Pi will not continue automatically.
|
|
|
|
Subscribe before sending a prompt to avoid missing a fast completion. `RpcClient.promptAndWait()` does this internally. If using separate `RpcClient` calls, install the event listener before `prompt()` and call `waitForIdle()` only while a run is active.
|
|
|
|
## Errors
|
|
|
|
A failed command returns one response with `success: false`:
|
|
|
|
```json
|
|
{"id":"req-3","type":"response","command":"set_model","success":false,"error":"Model not found: invalid/model"}
|
|
```
|
|
|
|
Malformed JSON produces a parse response without a request ID:
|
|
|
|
```json
|
|
{"type":"response","command":"parse","success":false,"error":"Failed to parse command: Unexpected token..."}
|
|
```
|
|
|
|
A success response only covers command handling. Provider failures and aborts after a prompt is accepted appear in the message and event stream.
|
|
|
|
Clients must also handle child-process startup failures, unexpected exits, stderr diagnostics, cancellation, and their own deadlines. Do not parse stderr as protocol data.
|
|
|
|
## Shutdown
|
|
|
|
Close the child's stdin to request an orderly shutdown. Pi disposes the active runtime and waits for pending work to finish before exiting, so give it time to exit before killing it. Clients should still handle process signals and unexpected exits.
|
|
|
|
SIGTERM and SIGHUP also start an orderly shutdown. Once a shutdown has started, from either stdin EOF or a signal, a stdin EOF or a single signal does not interrupt it; a second signal exits at once. Closing stdin and sending SIGTERM together is safe.
|
|
|
|
An extension can also request shutdown with `ctx.shutdown()`. If the session is idle and no command is being handled, Pi shuts down right away. Otherwise it shuts down once the current command finishes (for `prompt`, once the prompt settles) or the active run emits `agent_settled`. (A request made while an extension-started compaction is running waits for the next command or run to finish.)
|
|
|
|
## Minimal client
|
|
|
|
This Python example uses a binary pipe reader, which splits on LF without treating Unicode separators as protocol boundaries:
|
|
|
|
```python
|
|
import json
|
|
import subprocess
|
|
|
|
process = subprocess.Popen(
|
|
["pi", "--mode", "rpc", "--no-session"],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
|
|
assert process.stdin is not None
|
|
assert process.stdout is not None
|
|
|
|
command = {"id": "prompt-1", "type": "prompt", "message": "Hello"}
|
|
process.stdin.write(json.dumps(command).encode("utf-8") + b"\n")
|
|
process.stdin.flush()
|
|
|
|
while line := process.stdout.readline():
|
|
record = json.loads(line)
|
|
if record.get("type") == "message_update":
|
|
update = record["assistantMessageEvent"]
|
|
if update["type"] == "text_delta":
|
|
print(update["delta"], end="", flush=True)
|
|
elif record.get("type") == "agent_settled":
|
|
print()
|
|
break
|
|
|
|
process.stdin.close()
|
|
process.wait()
|
|
```
|
|
|
|
For maintained TypeScript clients, use the checked [RPC client example](../examples/rpc-client.ts). It requires a built Pi CLI because the repository example points to `dist/cli.js`.
|
|
|
|
## Reference
|
|
|
|
- [RPC Commands](rpc-commands.md): every stdin command and response
|
|
- [JSON Event Stream](json.md): shared stdout session events and streaming reconstruction
|
|
- [RPC Extension UI](rpc-extension-ui.md): dialogs, notifications, responses, and limitations
|
|
- [Message Types](message-types.md): messages and content blocks used by responses and events
|
|
- [Session File Format](session-format.md): entries returned by session commands
|
|
- [`rpc-types.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/rpc/rpc-types.ts): exported TypeScript protocol definitions
|
|
- [`RpcClient`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/rpc/rpc-client.ts): subprocess client implementation
|
|
|
|
## Moved reference anchors
|
|
|
|
The detailed references formerly on this page now have dedicated pages. These anchors preserve existing links.
|
|
|
|
<a id="prompt"></a>
|
|
<a id="steer"></a>
|
|
<a id="follow_up"></a>
|
|
<a id="abort"></a>
|
|
<a id="clear_queue"></a>
|
|
<a id="new_session"></a>
|
|
<a id="get_state"></a>
|
|
<a id="get_messages"></a>
|
|
<a id="set_model"></a>
|
|
<a id="cycle_model"></a>
|
|
<a id="get_available_models"></a>
|
|
<a id="set_thinking_level"></a>
|
|
<a id="cycle_thinking_level"></a>
|
|
<a id="get_available_thinking_levels"></a>
|
|
<a id="set_steering_mode"></a>
|
|
<a id="set_follow_up_mode"></a>
|
|
<a id="compact"></a>
|
|
<a id="set_auto_compaction"></a>
|
|
<a id="set_auto_retry"></a>
|
|
<a id="abort_retry"></a>
|
|
<a id="bash"></a>
|
|
<a id="abort_bash"></a>
|
|
<a id="get_session_stats"></a>
|
|
<a id="export_html"></a>
|
|
<a id="switch_session"></a>
|
|
<a id="fork"></a>
|
|
<a id="clone"></a>
|
|
<a id="get_fork_messages"></a>
|
|
<a id="get_entries"></a>
|
|
<a id="get_tree"></a>
|
|
<a id="get_last_assistant_text"></a>
|
|
<a id="set_session_name"></a>
|
|
<a id="get_commands"></a>
|
|
|
|
Command details moved to [RPC Commands](rpc-commands.md).
|
|
|
|
<a id="message_update-streaming"></a>
|
|
<a id="bash_execution_update"></a>
|
|
<a id="compaction_start--compaction_end"></a>
|
|
<a id="summarization_retry_scheduled--summarization_retry_attempt_start--summarization_retry_finished"></a>
|
|
|
|
Event details moved to [JSON Event Stream](json.md).
|
|
|
|
<a id="extension-ui-protocol"></a>
|
|
|
|
Extension interaction details moved to [RPC Extension UI](rpc-extension-ui.md).
|