# How to Use the JSONL/RPC Protocol for Headless Automation with Prime Agent

> Unlock headless automation with Prime Agent's JSONL/RPC protocol. Drive coding agents externally via stdin/stdout without the interactive TUI. Learn how today.

- Repository: [Prime Intellect/prime-agent](https://github.com/PrimeIntellect-ai/prime-agent)
- Tags: how-to-guide
- Published: 2026-08-12

---

**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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/src/core/agent-session.ts) |
| **Extension UI sub-protocol** | Enables dialog requests (select, confirm, input, editor) over RPC | [`rpc-types.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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:

```python
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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/src/modes/rpc/jsonl.ts):

```javascript
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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/rpc-client.ts), [`jsonl.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/jsonl.ts), and [`rpc-types.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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.