How to Use the JSONL/RPC Protocol for Headless Automation with Prime Agent
Prime Agent's JSONL/RPC protocol enables fully headless interaction by exchanging JSON Lines records over stdin/stdout, letting external scripts and services drive the coding agent without launching the interactive TUI.
The PrimeIntellect-ai/prime-agent repository provides a robust RPC mode designed specifically for automation. This mode treats the agent as a subprocess that communicates via strict JSONL framing—one JSON object per line, delimited only by LF (\n). This architecture makes it straightforward to integrate the agent into CI/CD pipelines, IDE extensions, or custom orchestration tools.
Architecture of the JSONL/RPC Protocol
The protocol consists of five key components working together:
| Component | Role | Source Location |
|---|---|---|
| RPC client | Spawns prime-agent --mode rpc and handles bidirectional I/O |
packages/coding-agent/src/modes/rpc/rpc-client.ts |
| RPC types | TypeScript definitions for all commands, responses, and UI messages | packages/coding-agent/src/modes/rpc/rpc-types.ts |
| JSONL framing layer | Parses input streams line-by-line using only LF as delimiter | packages/coding-agent/src/modes/rpc/jsonl.ts |
| Core agent engine | Executes prompts, tool calls, and session management | packages/coding-agent/src/core/agent-session.ts |
| Extension UI sub-protocol | Enables dialog requests (select, confirm, input, editor) over RPC | rpc-types.ts (Extension UI section) |
Critical Framing Rule
The JSONL/RPC protocol for headless automation uses LF-only delimiters (\n). The framing layer in jsonl.ts explicitly:
- Splits records only on
\n - Strips optional trailing
\rfor Windows compatibility - Ignores Unicode line separators (U+2028/U+2029) that may appear inside JSON strings
This design prevents corruption of JSON payloads containing multi-line strings. Do not use Node's readline module or similar utilities—they split on Unicode separators and will break the protocol.
Core Command Set for Headless Automation
All commands are JSON objects sent one per line to stdin. Responses echo the request id (if provided) with "type": "response".
Prompt and Message Commands
| Command | Payload | Purpose |
|---|---|---|
prompt |
{type:"prompt", message:"...", images?:[...]} |
Submit a user prompt to the agent |
steer |
{type:"steer", message:"..."} |
Queue a steering message during active processing |
follow_up |
{type:"follow_up", message:"..."} |
Queue a message to process after current turn completes |
Session and State Management
| Command | Payload | Purpose |
|---|---|---|
new_session |
{sessionPath?: "..."} |
Create fresh session (optional persistence path) |
switch_session |
{sessionPath: "..."} |
Load existing session from JSONL file |
fork |
{parentSession: "...", entryId?: ...} |
Branch session at specific point |
clone |
Similar to fork |
Duplicate session with shared history |
get_state |
{} |
Retrieve current session metadata |
get_messages |
{} |
Fetch complete message history |
export_html |
{} |
Export conversation as HTML artifact |
Model and Configuration Control
| Command | Payload | Purpose |
|---|---|---|
set_model |
{model: "..."} |
Switch to specific LLM |
cycle_model |
{} |
Rotate through configured models |
get_available_models |
{} |
List available models |
set_thinking_level |
{level: "high" | "medium" | "low"} |
Adjust reasoning depth |
compact |
{customInstructions?: "..."} |
Force context compaction |
set_auto_compaction |
{enabled: boolean} |
Toggle automatic compaction |
Execution and Control
| Command | Payload | Purpose |
|---|---|---|
bash |
{command: "ls -la"} |
Execute shell command |
abort_bash |
{} |
Cancel running shell command |
abort |
{} |
Cancel current agent operation |
All commands accept an optional "id" field for request-response correlation.
Event Stream for Monitoring Agent State
The agent emits asynchronous events to stdout as a continuous JSONL stream. These events have no id field and represent the internal state machine:
agent_start/agent_end— Wrap complete processing cyclesturn_start/turn_end— Delineate single LLM turnsmessage_start/message_update/message_end— Stream incremental deltas:text_delta— Generated text chunksthinking_delta— Reasoning tokenstoolcall_delta— Tool call construction
tool_execution_start/tool_execution_update/tool_execution_end— Report long-running tool progresscompaction_start/compaction_end— Context compression eventsauto_retry_start/auto_retry_end— API retry attemptsextension_error— Extension failure propagation
Each line is a parseable JSON object. Clients dispatch on the "type" field.
Python Implementation for Headless Automation
This minimal client demonstrates the complete lifecycle:
import subprocess
import json
import sys
# Spawn agent in RPC mode without session persistence
proc = subprocess.Popen(
["prime-agent", "--mode", "rpc", "--no-session"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
)
def send(cmd: dict) -> None:
"""Send a command with proper JSONL framing."""
proc.stdin.write(json.dumps(cmd) + "\n")
proc.stdin.flush()
def read_events():
"""Yield parsed events from stdout."""
for line in proc.stdout:
yield json.loads(line)
# Submit prompt
send({"type": "prompt", "message": "Hello, world!"})
# Process streaming response
for ev in read_events():
if ev.get("type") == "message_update":
delta = ev.get("assistantMessageEvent", {})
if delta.get("type") == "text_delta":
sys.stdout.write(delta["delta"])
sys.stdout.flush()
if ev.get("type") == "agent_end":
print("\n--- Agent finished ---")
break
Key implementation details:
- Commands terminate with
\nonly read_events()splits on\nand parses immediately- Client handles
message_updatefor streaming andagent_endfor termination
Node.js Implementation for Headless Automation
This reference implementation follows the exact framing rules from packages/coding-agent/src/modes/rpc/jsonl.ts:
const { spawn } = require("child_process");
const { StringDecoder } = require("string_decoder");
const agent = spawn("prime-agent", ["--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 nl = buffer.indexOf("\n");
if (nl === -1) break;
let line = buffer.slice(0, nl);
buffer = buffer.slice(nl + 1);
// Strip optional CRLF
if (line.endsWith("\r")) line = line.slice(0, -1);
onLine(line);
}
});
stream.on("end", () => {
buffer += decoder.end();
if (buffer.length) {
onLine(buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer);
}
});
}
attachJsonlReader(agent.stdout, (line) => {
const ev = JSON.parse(line);
if (ev.type === "message_update") {
const { assistantMessageEvent } = ev;
if (assistantMessageEvent.type === "text_delta") {
process.stdout.write(assistantMessageEvent.delta);
}
}
if (ev.type === "agent_end") {
console.log("\n[agent finished]");
process.exit(0);
}
});
// Send initial prompt
agent.stdin.write(
JSON.stringify({ type: "prompt", message: "Hello from Node!" }) + "\n"
);
// Graceful abort handling
process.on("SIGINT", () => {
agent.stdin.write(JSON.stringify({ type: "abort" }) + "\n");
});
The attachJsonlReader function implements the critical LF-only rule—never use readline or generic line readers that split on Unicode separators.
Extension UI Sub-Protocol for Dialog Simulation
When extensions call UI methods (ctx.ui.select(), ctx.ui.confirm(), ctx.ui.input(), ctx.ui.editor()), the protocol enables headless responses:
- Agent emits:
{"type":"extension_ui_request","id":"uuid-1","method":"select","options":[...]} - Client replies with matching ID:
{"type":"extension_ui_response","id":"uuid-1","result":...} - Agent resumes extension execution
This mechanism allows UI-dependent extensions to function in fully automated environments without graphical components.
Error Handling in JSONL/RPC Headless Mode
The JSONL/RPC protocol for headless automation provides structured error reporting:
| Error Type | Response Format | Handling Strategy |
|---|---|---|
| Command failure | {"type":"response","id":"...","success":false,"error":"..."} |
Check success field before proceeding |
| JSON parse error | Same as above with parse-specific error | Validate JSON before sending |
| Transient API failure | auto_retry_start / auto_retry_end events |
Monitor events; abort if retries exceed threshold |
| Extension failure | extension_error event |
Log and optionally restart affected extension |
Performance Considerations for Automation
- Buffering: The protocol supports streaming; clients should process
message_updateevents incrementally rather than buffering complete responses - Session files: Use
--no-sessionfor ephemeral automation, or specifysessionPathfor resumable long-running tasks stored as JSONL files viapackages/coding-agent/src/core/session-manager.ts - Compaction: Monitor
compaction_start/compaction_endevents; automatic compaction prevents context window overflow in multi-turn automation
Summary
- Prime Agent's JSONL/RPC protocol enables complete headless automation via stdin/stdout JSONL streams
- LF-only framing (
\n) is mandatory—Unicode line separators inside JSON strings must not trigger splits - Core commands cover prompts, session management, model switching, and shell execution
- Event streaming provides real-time visibility into agent state, tool execution, and text generation
- Extension UI sub-protocol allows programmatic responses to dialog requests
- Reference implementations in
rpc-client.ts,jsonl.ts, andrpc-types.tsdemonstrate production-ready patterns
Frequently Asked Questions
What makes Prime Agent's JSONL/RPC protocol different from standard JSON-RPC?
The protocol uses JSON Lines rather than HTTP or framed binary messages. Each command and event is a single line of JSON terminated by LF (\n), read from stdin and written to stdout. This design eliminates HTTP overhead and enables simple subprocess-based integration, but requires strict LF-only parsing—unlike standard JSON-RPC which typically runs over HTTP with length-prefixed or delimiter-free framing.
Can I use Node's readline module to parse the RPC output?
No. The implementation in packages/coding-agent/src/modes/rpc/jsonl.ts explicitly avoids standard line readers because they split on Unicode line separators (U+2028/U+2029). These characters are valid inside JSON strings and would corrupt parsed payloads. Use a custom LF-only parser as shown in the Node.js example above.
How do I handle long-running tool execution in headless mode?
Subscribe to tool_execution_start, tool_execution_update, and tool_execution_end events. The tool_execution_update event contains progress information for commands like bash. You can send abort_bash or abort commands to cancel stalled operations. The event flow mirrors the internal state machine in packages/coding-agent/src/core/agent-session.ts.
Is it possible to resume an interrupted automation session?
Yes. The session persistence system in packages/coding-agent/src/core/session-manager.ts stores sessions as JSONL files. Use switch_session with a sessionPath to resume, or fork/clone to branch from specific history points. Each session file is itself valid JSONL containing the complete conversation and state.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →