16 KiB
RPC Mode Protocol
The coding agent supports an RPC (Remote Procedure Call) mode for programmatic integration. This document describes the protocol for communicating with the agent over stdin/stdout using JSON messages.
Starting RPC Mode
pi --mode rpc [--no-session]
--mode rpc: Enables RPC mode (JSON over stdin/stdout)--no-session: Optional flag to disable session persistence
Input Protocol
Send JSON messages to stdin, one per line. Each message must be a complete JSON object followed by a newline.
Input Message Types
Prompt Message
Send a user prompt to the agent:
{
"type": "prompt",
"message": "Your prompt text here",
"attachments": [] // Optional array of Attachment objects
}
The attachments field is optional and supports images and documents. See Attachment for the schema.
Abort Message
Abort the current agent operation:
{
"type": "abort"
}
Output Protocol
The agent emits JSON events to stdout, one per line. Events follow the AgentEvent type hierarchy.
Event Types Overview
| Event Type | Description |
|---|---|
agent_start |
Agent begins processing a prompt |
agent_end |
Agent completes all processing |
turn_start |
A new turn begins (assistant response + tool calls) |
turn_end |
A turn completes |
message_start |
A message begins (user, assistant, or tool result) |
message_update |
Streaming update for assistant messages |
message_end |
A message completes |
tool_execution_start |
Tool execution begins |
tool_execution_end |
Tool execution completes |
error |
An error occurred |
Event Schemas
agent_start
Emitted when the agent begins processing a prompt.
{
"type": "agent_start"
}
agent_end
Emitted when the agent completes all processing. Contains all messages generated during this prompt.
{
"type": "agent_end",
"messages": [...] // Array of AppMessage objects
}
turn_start
Emitted when a new turn begins. A turn consists of an optional user message, an assistant response, and any resulting tool calls/results.
{
"type": "turn_start"
}
turn_end
Emitted when a turn completes.
{
"type": "turn_end",
"message": {...}, // AssistantMessage
"toolResults": [...] // Array of ToolResultMessage objects
}
message_start
Emitted when a message begins. The message can be a user message, assistant message, or tool result.
{
"type": "message_start",
"message": {...} // AppMessage (UserMessage, AssistantMessage, or ToolResultMessage)
}
message_update
Emitted during streaming of assistant messages. Contains both the partial message and the specific streaming event.
{
"type": "message_update",
"message": {...}, // Partial AssistantMessage
"assistantMessageEvent": {...} // AssistantMessageEvent with delta
}
The assistantMessageEvent contains streaming deltas:
text_delta: New text content{ "type": "text_delta", "contentIndex": 0, "delta": "text chunk", "partial": {...} }thinking_delta: New thinking content{ "type": "thinking_delta", "contentIndex": 0, "delta": "thinking chunk", "partial": {...} }toolcall_delta: Tool call argument streaming{ "type": "toolcall_delta", "contentIndex": 0, "delta": "json chunk", "partial": {...} }
See AssistantMessageEvent for all event types.
message_end
Emitted when a message is complete.
{
"type": "message_end",
"message": {...} // Complete AppMessage
}
tool_execution_start
Emitted when a tool begins execution.
{
"type": "tool_execution_start",
"toolCallId": "call_abc123",
"toolName": "bash",
"args": { "command": "ls -la" }
}
tool_execution_end
Emitted when a tool completes execution.
{
"type": "tool_execution_end",
"toolCallId": "call_abc123",
"toolName": "bash",
"result": {...}, // AgentToolResult or error string
"isError": false
}
The result field contains either:
- An
AgentToolResultobject withcontentanddetailsfields - A string error message if
isErroris true
error
Emitted when an error occurs during input processing.
{
"type": "error",
"error": "Error message"
}
Type Definitions
All types are defined in the following source files:
- Agent types:
packages/agent/src/types.ts - AI types:
packages/ai/src/types.ts - Agent loop types:
packages/ai/src/agent/types.ts
Message Types
UserMessage
Defined in packages/ai/src/types.ts
interface UserMessage {
role: "user";
content: string | (TextContent | ImageContent)[];
timestamp: number; // Unix timestamp in milliseconds
}
UserMessageWithAttachments
Defined in packages/agent/src/types.ts
Extends UserMessage with optional attachments for the agent layer:
type UserMessageWithAttachments = UserMessage & {
attachments?: Attachment[];
}
AssistantMessage
Defined in packages/ai/src/types.ts
interface AssistantMessage {
role: "assistant";
content: (TextContent | ThinkingContent | ToolCall)[];
api: Api;
provider: Provider;
model: string;
usage: Usage;
stopReason: StopReason;
errorMessage?: string;
timestamp: number; // Unix timestamp in milliseconds
}
ToolResultMessage
Defined in packages/ai/src/types.ts
interface ToolResultMessage<TDetails = any> {
role: "toolResult";
toolCallId: string;
toolName: string;
content: (TextContent | ImageContent)[];
details?: TDetails;
isError: boolean;
timestamp: number; // Unix timestamp in milliseconds
}
AppMessage
Defined in packages/agent/src/types.ts
Union type of all message types including custom app messages:
type AppMessage =
| AssistantMessage
| UserMessageWithAttachments
| Message // Includes ToolResultMessage
| CustomMessages[keyof CustomMessages];
Content Types
TextContent
interface TextContent {
type: "text";
text: string;
textSignature?: string;
}
ThinkingContent
interface ThinkingContent {
type: "thinking";
thinking: string;
thinkingSignature?: string;
}
ImageContent
interface ImageContent {
type: "image";
data: string; // base64 encoded
mimeType: string; // e.g., "image/jpeg", "image/png"
}
ToolCall
interface ToolCall {
type: "toolCall";
id: string;
name: string;
arguments: Record<string, any>;
thoughtSignature?: string;
}
Attachment
Defined in packages/agent/src/types.ts
interface Attachment {
id: string;
type: "image" | "document";
fileName: string;
mimeType: string;
size: number;
content: string; // base64 encoded (without data URL prefix)
extractedText?: string; // For documents
preview?: string; // base64 image preview
}
Usage
Defined in packages/ai/src/types.ts
interface Usage {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
cost: {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
total: number;
};
}
StopReason
type StopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
AssistantMessageEvent
Defined in packages/ai/src/types.ts
Streaming events for assistant message generation:
type AssistantMessageEvent =
| { type: "start"; partial: AssistantMessage }
| { type: "text_start"; contentIndex: number; partial: AssistantMessage }
| { type: "text_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
| { type: "text_end"; contentIndex: number; content: string; partial: AssistantMessage }
| { type: "thinking_start"; contentIndex: number; partial: AssistantMessage }
| { type: "thinking_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
| { type: "thinking_end"; contentIndex: number; content: string; partial: AssistantMessage }
| { type: "toolcall_start"; contentIndex: number; partial: AssistantMessage }
| { type: "toolcall_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
| { type: "toolcall_end"; contentIndex: number; toolCall: ToolCall; partial: AssistantMessage }
| { type: "done"; reason: "stop" | "length" | "toolUse"; message: AssistantMessage }
| { type: "error"; reason: "aborted" | "error"; error: AssistantMessage };
AgentToolResult
Defined in packages/ai/src/agent/types.ts
interface AgentToolResult<T> {
content: (TextContent | ImageContent)[];
details: T;
}
Correlating Tool Calls with Results
When the assistant invokes tools, you'll receive separate events for the tool call (in the AssistantMessage) and the result (in a ToolResultMessage). To display them together, correlate them using the toolCallId.
Event Flow
message_endwithAssistantMessagecontainingToolCallitems incontent[]tool_execution_startwithtoolCallId,toolName, andargstool_execution_endwithtoolCallId,result, andisErrormessage_endwithToolResultMessagecontainingtoolCallIdandcontent[]
Correlation Strategy
Track pending tool calls by toolCallId, then merge with results:
// Track pending tool calls
const pendingTools = new Map<string, { name: string; args: any }>();
function handleEvent(event: any) {
if (event.type === "tool_execution_start") {
// Store tool call info
pendingTools.set(event.toolCallId, {
name: event.toolName,
args: event.args
});
}
if (event.type === "tool_execution_end") {
const toolCall = pendingTools.get(event.toolCallId);
if (toolCall) {
// Now you have both the call and result
const merged = {
name: toolCall.name,
args: toolCall.args,
result: event.result,
isError: event.isError
};
// Format for display
displayToolExecution(merged);
pendingTools.delete(event.toolCallId);
}
}
}
Display Formatting Example
Format tool executions for a chat interface (e.g., WhatsApp):
function displayToolExecution(tool: {
name: string;
args: any;
result: { content: Array<{ type: string; text?: string }> } | string;
isError: boolean;
}): string {
const resultText = typeof tool.result === "string"
? tool.result
: tool.result.content
.filter(c => c.type === "text")
.map(c => c.text)
.join("\n");
switch (tool.name) {
case "bash":
return `$ ${tool.args.command}\n${resultText}`;
case "read":
return `📄 ${tool.args.path}\n${resultText.slice(0, 500)}...`;
case "write":
return `✏️ Wrote ${tool.args.path}`;
case "edit":
return `✏️ Edited ${tool.args.path}`;
default:
return `🔧 ${tool.name}: ${resultText.slice(0, 200)}`;
}
}
Alternative: Using turn_end
The turn_end event provides the assistant message and all tool results together:
if (event.type === "turn_end") {
const { message, toolResults } = event;
// Extract tool calls from assistant message
const toolCalls = message.content.filter(c => c.type === "toolCall");
// Match each tool call with its result by toolCallId
for (const call of toolCalls) {
const result = toolResults.find(r => r.toolCallId === call.id);
if (result) {
// Display merged tool call + result
}
}
}
Example Session
Input
{"type": "prompt", "message": "List files in the current directory"}
Output Stream
{"type":"agent_start"}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"user","content":"List files in the current directory","timestamp":1733234567890}}
{"type":"message_end","message":{"role":"user","content":"List files in the current directory","timestamp":1733234567890}}
{"type":"message_start","message":{"role":"assistant","content":[],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1733234567891}}
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"I'll list","partial":{...}}}
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":" the files","partial":{...}}}
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"toolcall_start","contentIndex":1,"partial":{...}}}
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"toolcall_end","contentIndex":1,"toolCall":{"type":"toolCall","id":"call_123","name":"bash","arguments":{"command":"ls -la"}},"partial":{...}}}
{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"I'll list the files for you."},{"type":"toolCall","id":"call_123","name":"bash","arguments":{"command":"ls -la"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"toolUse","timestamp":1733234567891}}
{"type":"tool_execution_start","toolCallId":"call_123","toolName":"bash","args":{"command":"ls -la"}}
{"type":"tool_execution_end","toolCallId":"call_123","toolName":"bash","result":{"content":[{"type":"text","text":"total 48\ndrwxr-xr-x 12 user staff 384 Dec 3 14:00 .\n..."}],"details":undefined},"isError":false}
{"type":"message_start","message":{"role":"toolResult","toolCallId":"call_123","toolName":"bash","content":[{"type":"text","text":"total 48\n..."}],"isError":false,"timestamp":1733234567900}}
{"type":"message_end","message":{"role":"toolResult","toolCallId":"call_123","toolName":"bash","content":[{"type":"text","text":"total 48\n..."}],"isError":false,"timestamp":1733234567900}}
{"type":"turn_end","message":{...},"toolResults":[{...}]}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"assistant","content":[],...}}
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Here are the files","partial":{...}}}
{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"Here are the files in the current directory:\n..."}],...,"stopReason":"stop",...}}
{"type":"turn_end","message":{...},"toolResults":[]}
{"type":"agent_end","messages":[...]}
Example Client
See test/rpc-example.ts for a complete example of an interactive RPC client.
import { spawn } from "node:child_process";
import * as readline from "readline";
// Spawn agent in RPC mode
const agent = spawn("pi", ["--mode", "rpc", "--no-session"]);
// Parse output events
readline.createInterface({ input: agent.stdout }).on("line", (line) => {
const event = JSON.parse(line);
if (event.type === "message_update") {
const { assistantMessageEvent } = event;
if (assistantMessageEvent.type === "text_delta") {
process.stdout.write(assistantMessageEvent.delta);
}
}
if (event.type === "tool_execution_start") {
console.log(`\n[Tool: ${event.toolName}]`);
}
});
// Send prompt
agent.stdin.write(JSON.stringify({ type: "prompt", message: "Hello" }) + "\n");
// Abort on Ctrl+C
process.on("SIGINT", () => {
agent.stdin.write(JSON.stringify({ type: "abort" }) + "\n");
});