co-mono/packages/coding-agent/docs/RPC.md

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 AgentToolResult object with content and details fields
  • A string error message if isError is 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:

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

  1. message_end with AssistantMessage containing ToolCall items in content[]
  2. tool_execution_start with toolCallId, toolName, and args
  3. tool_execution_end with toolCallId, result, and isError
  4. message_end with ToolResultMessage containing toolCallId and content[]

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