On this page
RPC Mode
RPC Mode
#RPC mode runs pig 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 PHP integration, prefer the PHP SDK. For cross-language integration (Python, Go, Node.js, Rust), launch pig in RPC mode over standard subprocess streams.
| Interface | Process boundary | Control model | Best fit |
|---|---|---|---|
| SDK | In process (PHP) | Direct PHP class methods and events | PHP hosts that want complete API access |
| RPC | Child process (Cross-language) | JSONL commands, responses, and events | Python, Go, Node.js, Rust, IDEs, or custom clients |
Start RPC mode
#pig --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 for the complete, version-specific interface; pig --help is authoritative for the installed version.
RPC mode rejects @file prompt arguments. Send prompts through the prompt command instead.
Protocol records
#The protocol has four record families:
| Direction | Record | Purpose |
|---|---|---|
| stdin | Command | Ask pig 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 pig and the client |
See RPC Commands, JSON Event Stream, and RPC Extension UI for the canonical record definitions.
Correlate commands and responses
#Every command accepts an optional string id. A matching response repeats it:
{"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 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. pig 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:
{"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 for all values.
Continue consuming events 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 pig 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 with onEvent() before prompt() and call waitForIdle() only while a run is active; waitForIdle(), collectEvents(), and promptAndWait() all wait for agent_settled.
Errors
#A failed command returns one response with success: false:
{"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 (a trailing carriage return is stripped before parsing):
{"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. pig disposes the active runtime before exiting. Clients should still handle process signals and unexpected exits.
An extension can also request shutdown through its extension context. pig completes shutdown after the current command or after the active run emits agent_settled.
Minimal client
#This Python example uses a binary pipe reader, which splits on LF without treating Unicode separators as protocol boundaries:
import json
import subprocess
process = subprocess.Popen(
["pig", "--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 PHP hosts, pig ships a maintained client, Pig\CodingAgent\Rpc\RpcClient, which starts bin/pig --mode rpc and has one method per command (getState(), getAvailableModels(), exportHtml(), getTree(), getEntries(), getCommands(), navigateTree(), clone(), and so on):
use Pig\Async\Async;
use Pig\CodingAgent\Rpc\RpcClient;
$client = new RpcClient(cwd: '/some/project');
Async::run(function () use ($client): void {
$client->start();
$client->onEvent(static fn (array $event) => print $event['type'] . "\n");
$client->promptAndWait('what does bin/pig do?');
$client->stop();
});
Reference
#- RPC Commands: every stdin command and response
- JSON Event Stream: shared stdout session events and streaming reconstruction
- RPC Extension UI: dialogs, notifications, responses, and limitations
- Message Types: messages and content blocks used by responses and events
- Session File Format: entries returned by session commands
RpcMode.phpandRpcEvents.php: command dispatch and event serializationRpcClient.php: PHP subprocess client implementation
Moved reference anchors
#The detailed references formerly on this page now have dedicated pages. These anchors preserve existing links.
Command details moved to RPC Commands.
Event details moved to JSON Event Stream.
Extension interaction details moved to RPC Extension UI.