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 \r for 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 cycles
  • turn_start / turn_end — Delineate single LLM turns
  • message_start / message_update / message_end — Stream incremental deltas:
    • text_delta — Generated text chunks
    • thinking_delta — Reasoning tokens
    • toolcall_delta — Tool call construction
  • tool_execution_start / tool_execution_update / tool_execution_end — Report long-running tool progress
  • compaction_start / compaction_end — Context compression events
  • auto_retry_start / auto_retry_end — API retry attempts
  • extension_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 \n only
  • read_events() splits on \n and parses immediately
  • Client handles message_update for streaming and agent_end for 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:

  1. Agent emits: {"type":"extension_ui_request","id":"uuid-1","method":"select","options":[...]}
  2. Client replies with matching ID: {"type":"extension_ui_response","id":"uuid-1","result":...}
  3. 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_update events incrementally rather than buffering complete responses
  • Session files: Use --no-session for ephemeral automation, or specify sessionPath for resumable long-running tasks stored as JSONL files via packages/coding-agent/src/core/session-manager.ts
  • Compaction: Monitor compaction_start/compaction_end events; 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, and rpc-types.ts demonstrate 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →