Documentation Index Fetch the complete documentation index at: https://mintlify.com/earendil-works/pi/llms.txt
Use this file to discover all available pages before exploring further.
RPC mode lets you drive Pi’s coding agent from any programming language or environment by communicating over stdin/stdout using a JSON protocol. It is the right choice when you need process isolation or are building a non-Node.js client.
If you’re building a Node.js application, use AgentSession directly from the SDK instead of spawning a subprocess. See Pi SDK for the type-safe API.
Starting RPC mode
Common options:
Option Description --provider <name>LLM provider (anthropic, openai, google, etc.) --model <pattern>Model pattern or ID, supports provider/id and optional :<thinking> --no-sessionDisable session persistence --session-dir <path>Custom session storage directory
For a stateless session useful during development or testing:
pi --mode rpc --no-session
Protocol overview
Commands : JSON objects sent to stdin, one per line
Responses : JSON objects with type: "response" indicating success or failure
Events : Agent events streamed to stdout as JSON lines, asynchronously during operation
All commands accept an optional id field. When provided, the matching response includes the same id for request/response correlation.
Framing
RPC mode uses strict JSONL semantics. Records are delimited by LF (\n) only.
Do not use Node.js readline or any reader that splits on Unicode line separators (U+2028, U+2029). Those characters are valid inside JSON strings. Split on \n only, and strip a trailing \r if present.
Commands
Prompting
Send a user prompt to the agent. The response is emitted once the prompt is accepted, queued, or handled. Events continue streaming asynchronously after acceptance. { "id" : "req-1" , "type" : "prompt" , "message" : "Hello, world!" }
With images: {
"type" : "prompt" ,
"message" : "What's in this image?" ,
"images" : [{ "type" : "image" , "data" : "base64-encoded-data" , "mimeType" : "image/png" }]
}
If the agent is already streaming, specify streamingBehavior to queue the message: { "type" : "prompt" , "message" : "New instruction" , "streamingBehavior" : "steer" }
"steer": Delivered after the current assistant turn finishes its tool calls, before the next LLM call.
"followUp": Delivered only when the agent fully stops.
If the agent is streaming and streamingBehavior is omitted, the command returns an error. Response: { "id" : "req-1" , "type" : "response" , "command" : "prompt" , "success" : true }
success: true means the prompt was accepted, queued, or handled. success: false means it was rejected before acceptance. Failures after acceptance appear in the normal event stream.
Queue a steering message while the agent is running. Delivered after the current assistant turn finishes its tool calls, before the next LLM call. Skill commands and prompt templates are expanded. Extension commands are not allowed here — use prompt instead. { "type" : "steer" , "message" : "Stop and do this instead" }
With images: { "type" : "steer" , "message" : "Look at this instead" , "images" : [ ... ]}
Response: { "type" : "response" , "command" : "steer" , "success" : true }
Queue a follow-up message to be delivered only when the agent fully stops (no more tool calls or steering messages). Skill commands and prompt templates are expanded. Extension commands are not allowed — use prompt instead. { "type" : "follow_up" , "message" : "After you're done, also do this" }
Response: { "type" : "response" , "command" : "follow_up" , "success" : true }
Abort the current agent operation. Response: { "type" : "response" , "command" : "abort" , "success" : true }
Start a fresh session. Can be cancelled by a session_before_switch extension handler. With optional parent session tracking: { "type" : "new_session" , "parentSession" : "/path/to/parent.jsonl" }
Response: { "type" : "response" , "command" : "new_session" , "success" : true , "data" : { "cancelled" : false }}
State
Get current session state. Response: {
"type" : "response" ,
"command" : "get_state" ,
"success" : true ,
"data" : {
"model" : { ... },
"thinkingLevel" : "medium" ,
"isStreaming" : false ,
"isCompacting" : false ,
"steeringMode" : "all" ,
"followUpMode" : "one-at-a-time" ,
"sessionFile" : "/path/to/session.jsonl" ,
"sessionId" : "abc123" ,
"sessionName" : "my-feature-work" ,
"autoCompactionEnabled" : true ,
"messageCount" : 5 ,
"pendingMessageCount" : 0
}
}
sessionName is omitted when not set via set_session_name.
Get all messages in the current conversation. Response: {
"type" : "response" ,
"command" : "get_messages" ,
"success" : true ,
"data" : { "messages" : [ ... ]}
}
Model
Switch to a specific model. { "type" : "set_model" , "provider" : "anthropic" , "modelId" : "claude-sonnet-4-20250514" }
Response includes the full model object: { "type" : "response" , "command" : "set_model" , "success" : true , "data" : { ... }}
Cycle to the next available model. Returns null data when only one model is available. Response: {
"type" : "response" ,
"command" : "cycle_model" ,
"success" : true ,
"data" : { "model" : { ... }, "thinkingLevel" : "medium" , "isScoped" : false }
}
List all configured models with valid API keys. { "type" : "get_available_models" }
Response: { "type" : "response" , "command" : "get_available_models" , "success" : true , "data" : { "models" : [ ... ]}}
Thinking
Set the reasoning level for models that support it. Levels: "off", "minimal", "low", "medium", "high", "xhigh" { "type" : "set_thinking_level" , "level" : "high" }
"xhigh" is only supported by OpenAI codex-max models.
Response: { "type" : "response" , "command" : "set_thinking_level" , "success" : true }
Cycle through available thinking levels. { "type" : "cycle_thinking_level" }
Response: { "type" : "response" , "command" : "cycle_thinking_level" , "success" : true , "data" : { "level" : "high" }}
Returns null data when the model doesn’t support thinking.
Queue modes
Control how steering messages from steer are delivered. { "type" : "set_steering_mode" , "mode" : "one-at-a-time" }
Modes:
"all": Deliver all queued steering messages after the current turn
"one-at-a-time": Deliver one per completed assistant turn (default)
Response: { "type" : "response" , "command" : "set_steering_mode" , "success" : true }
Control how follow-up messages from follow_up are delivered. { "type" : "set_follow_up_mode" , "mode" : "one-at-a-time" }
Modes:
"all": Deliver all follow-up messages when agent finishes
"one-at-a-time": Deliver one per agent completion (default)
Response: { "type" : "response" , "command" : "set_follow_up_mode" , "success" : true }
Compaction
Manually compact conversation context to reduce token usage. With custom instructions: { "type" : "compact" , "customInstructions" : "Focus on code changes" }
Response: {
"type" : "response" ,
"command" : "compact" ,
"success" : true ,
"data" : {
"summary" : "Summary of conversation..." ,
"firstKeptEntryId" : "abc123" ,
"tokensBefore" : 150000 ,
"details" : {}
}
}
Enable or disable automatic compaction when context is nearly full. { "type" : "set_auto_compaction" , "enabled" : true }
Response: { "type" : "response" , "command" : "set_auto_compaction" , "success" : true }
Retry
Enable or disable automatic retry on transient errors (overloaded, rate limit, 5xx). { "type" : "set_auto_retry" , "enabled" : true }
Response: { "type" : "response" , "command" : "set_auto_retry" , "success" : true }
Abort an in-progress retry — cancels the delay and stops retrying. Response: { "type" : "response" , "command" : "abort_retry" , "success" : true }
Bash
Execute a shell command and add its output to the conversation context. { "type" : "bash" , "command" : "ls -la" }
Response: {
"type" : "response" ,
"command" : "bash" ,
"success" : true ,
"data" : {
"output" : "total 48 \n drwxr-xr-x ..." ,
"exitCode" : 0 ,
"cancelled" : false ,
"truncated" : false
}
}
When output is truncated, fullOutputPath is included in the response data. How bash output reaches the LLM: The bash command runs immediately and its result is stored as a BashExecutionMessage. On the next prompt command, all pending bash results are converted to user messages and sent to the LLM. Multiple bash commands can be queued before a prompt; all outputs are included.
Abort a currently running bash command. Response: { "type" : "response" , "command" : "abort_bash" , "success" : true }
Session
Get token usage, cost statistics, and current context window usage. { "type" : "get_session_stats" }
Response: {
"type" : "response" ,
"command" : "get_session_stats" ,
"success" : true ,
"data" : {
"sessionFile" : "/path/to/session.jsonl" ,
"sessionId" : "abc123" ,
"userMessages" : 5 ,
"assistantMessages" : 5 ,
"toolCalls" : 12 ,
"toolResults" : 12 ,
"totalMessages" : 22 ,
"tokens" : {
"input" : 50000 ,
"output" : 10000 ,
"cacheRead" : 40000 ,
"cacheWrite" : 5000 ,
"total" : 105000
},
"cost" : 0.45 ,
"contextUsage" : {
"tokens" : 60000 ,
"contextWindow" : 200000 ,
"percent" : 30
}
}
}
contextUsage is omitted when no model is available. contextUsage.tokens and contextUsage.percent are null immediately after compaction until the next fresh assistant response.
Load a different session file. Can be cancelled by a session_before_switch extension handler. { "type" : "switch_session" , "sessionPath" : "/path/to/session.jsonl" }
Response: { "type" : "response" , "command" : "switch_session" , "success" : true , "data" : { "cancelled" : false }}
Create a fork from a previous user message on the active branch. Returns the text of the message being forked from. { "type" : "fork" , "entryId" : "abc123" }
Response: {
"type" : "response" ,
"command" : "fork" ,
"success" : true ,
"data" : { "text" : "The original prompt text..." , "cancelled" : false }
}
Duplicate the current active branch into a new session at the current position. Response: { "type" : "response" , "command" : "clone" , "success" : true , "data" : { "cancelled" : false }}
Get user messages available for forking. { "type" : "get_fork_messages" }
Response: {
"type" : "response" ,
"command" : "get_fork_messages" ,
"success" : true ,
"data" : {
"messages" : [
{ "entryId" : "abc123" , "text" : "First prompt..." },
{ "entryId" : "def456" , "text" : "Second prompt..." }
]
}
}
Get the text content of the last assistant message. Returns {"text": null} when no assistant messages exist. { "type" : "get_last_assistant_text" }
Response: { "type" : "response" , "command" : "get_last_assistant_text" , "success" : true , "data" : { "text" : "..." }}
Set a display name for the current session. Appears in session listings and get_state. { "type" : "set_session_name" , "name" : "my-feature-work" }
Response: { "type" : "response" , "command" : "set_session_name" , "success" : true }
Export the session to an HTML file. With a custom output path: { "type" : "export_html" , "outputPath" : "/tmp/session.html" }
Response: { "type" : "response" , "command" : "export_html" , "success" : true , "data" : { "path" : "/tmp/session.html" }}
Commands
get_commands
Get available commands: extension commands, prompt templates, and skills. These can be invoked via prompt by prefixing with /.
Response:
{
"type" : "response" ,
"command" : "get_commands" ,
"success" : true ,
"data" : {
"commands" : [
{
"name" : "session-name" ,
"description" : "Set or clear session name" ,
"source" : "extension" ,
"path" : "/home/user/.pi/agent/extensions/session.ts"
},
{
"name" : "fix-tests" ,
"description" : "Fix failing tests" ,
"source" : "prompt" ,
"location" : "project" ,
"path" : "/home/user/project/.pi/agent/prompts/fix-tests.md"
},
{
"name" : "skill:brave-search" ,
"description" : "Web search via Brave API" ,
"source" : "skill" ,
"location" : "user" ,
"path" : "/home/user/.pi/agent/skills/brave-search/SKILL.md"
}
]
}
}
source values: "extension", "prompt", "skill". location values: "user", "project", "path". Built-in TUI commands like /settings are not included.
Events
Events stream to stdout as JSON lines during agent operation. Events do not include an id field — only responses do.
Event Description agent_startAgent begins processing agent_endAgent completes; includes all generated messages turn_startNew turn begins turn_endTurn completes; includes assistant message and tool results message_startMessage begins message_updateStreaming update (text, thinking, or toolcall deltas) message_endMessage completes tool_execution_startTool begins execution tool_execution_updateTool execution progress (streaming output) tool_execution_endTool completes queue_updatePending steering/follow-up queue changed compaction_startCompaction begins compaction_endCompaction completes auto_retry_startAuto-retry begins after a transient error auto_retry_endAuto-retry completes (success or final failure) extension_errorAn extension threw an error
message_update streaming
message_update events carry a message (the partial assistant message) and an assistantMessageEvent delta:
{
"type" : "message_update" ,
"message" : { ... },
"assistantMessageEvent" : {
"type" : "text_delta" ,
"contentIndex" : 0 ,
"delta" : "Hello " ,
"partial" : { ... }
}
}
Example stream for a text response:
{ "type" : "message_update" , "message" :{ ... }, "assistantMessageEvent" :{ "type" : "text_start" , "contentIndex" : 0 , "partial" :{ ... }}}
{ "type" : "message_update" , "message" :{ ... }, "assistantMessageEvent" :{ "type" : "text_delta" , "contentIndex" : 0 , "delta" : "Hello" , "partial" :{ ... }}}
{ "type" : "message_update" , "message" :{ ... }, "assistantMessageEvent" :{ "type" : "text_delta" , "contentIndex" : 0 , "delta" : " world" , "partial" :{ ... }}}
{ "type" : "message_update" , "message" :{ ... }, "assistantMessageEvent" :{ "type" : "text_end" , "contentIndex" : 0 , "content" : "Hello world" , "partial" :{ ... }}}
Use toolCallId to correlate start, update, and end events. The partialResult in tool_execution_update contains accumulated output so far — replace your display on each update rather than appending.
{ "type" : "tool_execution_start" , "toolCallId" : "call_abc123" , "toolName" : "bash" , "args" : { "command" : "ls -la" }}
{ "type" : "tool_execution_update" , "toolCallId" : "call_abc123" , "toolName" : "bash" , "args" : { "command" : "ls -la" }, "partialResult" : { "content" : [{ "type" : "text" , "text" : "partial output..." }], "details" : {}}}
{ "type" : "tool_execution_end" , "toolCallId" : "call_abc123" , "toolName" : "bash" , "result" : { "content" : [{ "type" : "text" , "text" : "total 48 \n ..." }], "details" : {}}, "isError" : false }
compaction_end
{
"type" : "compaction_end" ,
"reason" : "threshold" ,
"result" : {
"summary" : "..." ,
"firstKeptEntryId" : "abc123" ,
"tokensBefore" : 150000 ,
"details" : {}
},
"aborted" : false ,
"willRetry" : false
}
reason is "manual", "threshold", or "overflow". When reason is "overflow" and compaction succeeds, willRetry is true and the agent automatically retries the prompt.
Extension UI sub-protocol
Extensions can request user interaction in RPC mode via a request/response sub-protocol layered on top of the base command/event flow.
Dialog methods (select, confirm, input, editor) emit an extension_ui_request on stdout and block until the client sends back an extension_ui_response with the matching id. If the request includes a timeout field, the agent auto-resolves with a default value when it expires — the client does not need to track timeouts.
Fire-and-forget methods (notify, setStatus, setWidget, setTitle, set_editor_text) emit an extension_ui_request on stdout but expect no response.
Requests (stdout)
{
"type" : "extension_ui_request" ,
"id" : "uuid-1" ,
"method" : "select" ,
"title" : "Allow dangerous command?" ,
"options" : [ "Allow" , "Block" ],
"timeout" : 10000
}
Expected response: extension_ui_response with value (selected option string) or cancelled: true.
{
"type" : "extension_ui_request" ,
"id" : "uuid-2" ,
"method" : "confirm" ,
"title" : "Clear session?" ,
"message" : "All messages will be lost." ,
"timeout" : 5000
}
Expected response: extension_ui_response with confirmed: true/false or cancelled: true.
{
"type" : "extension_ui_request" ,
"id" : "uuid-4" ,
"method" : "editor" ,
"title" : "Edit some text" ,
"prefill" : "Line 1 \n Line 2 \n Line 3"
}
Expected response: extension_ui_response with value (edited text) or cancelled: true.
{
"type" : "extension_ui_request" ,
"id" : "uuid-5" ,
"method" : "notify" ,
"message" : "Command blocked by user" ,
"notifyType" : "warning"
}
notifyType is "info", "warning", or "error". Defaults to "info".
setStatus (fire-and-forget)
Set or clear a status entry. Omit or set statusText to undefined to clear. {
"type" : "extension_ui_request" ,
"id" : "uuid-6" ,
"method" : "setStatus" ,
"statusKey" : "my-ext" ,
"statusText" : "Turn 3 running..."
}
setWidget (fire-and-forget)
setTitle (fire-and-forget)
{
"type" : "extension_ui_request" ,
"id" : "uuid-8" ,
"method" : "setTitle" ,
"title" : "pi - my project"
}
set_editor_text (fire-and-forget)
{
"type" : "extension_ui_request" ,
"id" : "uuid-9" ,
"method" : "set_editor_text" ,
"text" : "prefilled text for the user"
}
Responses (stdin)
Send responses only for dialog methods. The id must match the request.
{ "type" : "extension_ui_response" , "id" : "uuid-1" , "value" : "Allow" }
{ "type" : "extension_ui_response" , "id" : "uuid-2" , "confirmed" : true }
To dismiss any dialog:
{ "type" : "extension_ui_response" , "id" : "uuid-3" , "cancelled" : true }
Error handling
Failed commands return success: false:
{
"type" : "response" ,
"command" : "set_model" ,
"success" : false ,
"error" : "Model not found: invalid/model"
}
Parse errors:
{
"type" : "response" ,
"command" : "parse" ,
"success" : false ,
"error" : "Failed to parse command: Unexpected token..."
}
Client examples
import subprocess
import json
proc = subprocess.Popen(
[ "pi" , "--mode" , "rpc" , "--no-session" ],
stdin = subprocess. PIPE ,
stdout = subprocess. PIPE ,
text = True
)
def send ( cmd ):
proc.stdin.write(json.dumps(cmd) + " \n " )
proc.stdin.flush()
def read_events ():
for line in proc.stdout:
yield json.loads(line)
send({ "type" : "prompt" , "message" : "Hello!" })
for event in read_events():
if event.get( "type" ) == "message_update" :
delta = event.get( "assistantMessageEvent" , {})
if delta.get( "type" ) == "text_delta" :
print (delta[ "delta" ], end = "" , flush = True )
if event.get( "type" ) == "agent_end" :
print ()
break
This example uses StringDecoder and manual buffer splitting on \n to comply with the framing rules. const { spawn } = require ( "child_process" );
const { StringDecoder } = require ( "string_decoder" );
const agent = spawn ( "pi" , [ "--mode" , "rpc" , "--no-session" ]);
function attachJsonlReader ( stream , onLine ) {
const decoder = new StringDecoder ( "utf8" );
let buffer = "" ;
stream . on ( "data" , ( chunk ) => {
buffer += typeof chunk === "string" ? chunk : decoder . write ( chunk );
while ( true ) {
const newlineIndex = buffer . indexOf ( " \n " );
if ( newlineIndex === - 1 ) break ;
let line = buffer . slice ( 0 , newlineIndex );
buffer = buffer . slice ( newlineIndex + 1 );
if ( line . endsWith ( " \r " )) line = line . slice ( 0 , - 1 );
onLine ( line );
}
});
stream . on ( "end" , () => {
buffer += decoder . end ();
if ( buffer . length > 0 ) {
onLine ( buffer . endsWith ( " \r " ) ? buffer . slice ( 0 , - 1 ) : buffer );
}
});
}
attachJsonlReader ( agent . stdout , ( line ) => {
const event = JSON . parse ( line );
if ( event . type === "message_update" ) {
const { assistantMessageEvent } = event ;
if ( assistantMessageEvent . type === "text_delta" ) {
process . stdout . write ( assistantMessageEvent . delta );
}
}
});
agent . stdin . write ( JSON . stringify ({ type: "prompt" , message: "Hello" }) + " \n " );
process . on ( "SIGINT" , () => {
agent . stdin . write ( JSON . stringify ({ type: "abort" }) + " \n " );
});
RPC vs SDK
Use RPC mode when
Integrating from Python, Go, Rust, or another language
You want process isolation between your app and the agent
You’re building a language-agnostic client
Use the SDK when
You’re in a Node.js process and want type safety
You need direct access to agent state and messages
You want to customize tools or extensions programmatically
See Pi SDK for the Node.js API.