aionrs JSON Stream Protocol Spec

August 13, 2026 · View on GitHub

This protocol defines the communication between aionrs (Rust CLI) and a host client (e.g., AionUi Electron app) via stdin/stdout JSON Lines.

Overview

┌──────────────┐   stdin (JSON Lines)     ┌──────────────────┐
│              │ ◄─────────────────────── │                  │
│    aionrs    │                          │   Host Client    │
│  (Rust CLI)  │ ──────────────────────►  │   (AionUi etc.)  │
│              │   stdout (JSON Lines)    │                  │
└──────────────┘                          └──────────────────┘
     stderr → diagnostic logs (not part of protocol)
  • Transport: stdin/stdout, one JSON object per line (JSON Lines / NDJSON)
  • Encoding: UTF-8
  • Activation: aionrs --json-stream [other flags]
  • Lifecycle: One process per conversation; process stays alive for multi-turn

1. Agent → Client Events (stdout)

Every line is a JSON object with a type field.

1.1 ready

Emitted once after initialization completes. Client MUST wait for this before sending messages.

{
  "type": "ready",
  "version": "0.2.0",
  "session_id": "a1b2c3",
  "capabilities": {
    "tool_approval": true,
    "image_input": "supported",
    "thinking": true,
    "effort": false,
    "effort_levels": [],
    "modes": ["default", "auto_edit", "yolo"],
    "current_mode": "default",
    "mcp": true
  }
}
FieldTypeDescription
versionstringProtocol version (semver)
session_idstring?Session ID (omitted when sessions are disabled in config)
capabilities.tool_approvalboolWhether agent supports pause-and-wait tool approval
capabilities.image_inputstringResolved image-input capability: supported, unsupported, or unknown
capabilities.thinkingboolWhether current provider supports extended thinking
capabilities.effortboolWhether current provider supports reasoning_effort
capabilities.effort_levelsstring[]Valid effort values (e.g., ["low", "medium", "high"]). Empty when effort is false
capabilities.modesstring[]Available approval modes for set_mode command
capabilities.current_modestringCurrently active approval mode
capabilities.mcpboolWhether MCP tools are available

1.2 stream_start

A new response turn has started.

{
  "type": "stream_start",
  "msg_id": "abc-123"
}

1.3 text_delta

Incremental text output (streaming).

{
  "type": "text_delta",
  "text": "Hello, ",
  "msg_id": "abc-123"
}

1.4 thinking

Model's internal reasoning (if extended thinking is enabled).

{
  "type": "thinking",
  "text": "Let me analyze the code structure...",
  "msg_id": "abc-123"
}

1.5 tool_request

Agent wants to invoke a tool and needs client approval. Agent PAUSES execution until it receives tool_approve or tool_deny.

{
  "type": "tool_request",
  "msg_id": "abc-123",
  "call_id": "tool-call-001",
  "tool": {
    "name": "Write",
    "category": "edit",
    "args": {
      "file_path": "/src/main.rs",
      "content": "fn main() { ... }"
    },
    "description": "Write to /src/main.rs"
  }
}
FieldTypeDescription
call_idstringUnique ID for this tool invocation
tool.namestringTool name: Read, Write, Edit, ExecCommand, Glob, Grep, Spawn, or MCP tool name
tool.categorystring"info" (read-only), "edit" (file mutation), "exec" (shell), "mcp" (MCP tool)
tool.argsobjectTool arguments
tool.descriptionstringHuman-readable one-line description

Category mapping for built-in tools:

ToolCategoryRationale
ReadinfoRead-only file access
GlobinfoRead-only file search
GrepinfoRead-only content search
WriteeditCreates or overwrites files
EditeditModifies file content
ExecCommandexecExecutes shell commands
SpawnexecSpawns sub-agent
MCP toolsmcpExternal MCP server tools

Note: When auto_approve = true (yolo mode) or when a tool is in the allow_list, the agent executes immediately and emits tool_running directly, skipping tool_request.

1.6 tool_running

Tool execution has started (after approval or auto-approve).

{
  "type": "tool_running",
  "msg_id": "abc-123",
  "call_id": "tool-call-001",
  "tool_name": "Write"
}

1.7 tool_result

Tool execution completed.

{
  "type": "tool_result",
  "msg_id": "abc-123",
  "call_id": "tool-call-001",
  "tool_name": "Write",
  "status": "success",
  "output": "File written successfully",
  "output_type": "text"
}
FieldTypeDescription
statusstring"success" or "error"
outputstringTool output (truncated if exceeds limit)
output_typestring"text" (default), "diff" (for Edit tool), "image" (base64)

Special output for Edit tool (output_type: "diff"):

{
  "type": "tool_result",
  "msg_id": "abc-123",
  "call_id": "tool-call-002",
  "tool_name": "Edit",
  "status": "success",
  "output": "--- a/src/main.rs\n+++ b/src/main.rs\n@@ -1,3 +1,3 @@\n-old line\n+new line",
  "output_type": "diff",
  "metadata": {
    "file_path": "/src/main.rs"
  }
}

1.8 tool_cancelled

Tool was denied by client or cancelled.

{
  "type": "tool_cancelled",
  "msg_id": "abc-123",
  "call_id": "tool-call-001",
  "reason": "User denied"
}

1.9 stream_end

Current response turn finished.

{
  "type": "stream_end",
  "msg_id": "abc-123",
  "usage": {
    "input_tokens": 1500,
    "output_tokens": 320,
    "cache_read_tokens": 800,
    "cache_write_tokens": 200
  }
}

1.10 error

An error occurred. The agent may or may not continue depending on severity.

{
  "type": "error",
  "msg_id": "abc-123",
  "error": {
    "code": "provider_error",
    "message": "Rate limit exceeded",
    "retryable": true
  }
}
Error CodeDescription
provider_errorLLM API error (rate limit, auth, etc.)
tool_errorBuilt-in tool execution error
config_errorConfiguration or initialization error
protocol_errorInvalid command from client
internal_errorUnexpected internal error

1.11 info

Informational message (non-critical, for display only).

{
  "type": "info",
  "msg_id": "abc-123",
  "message": "Stream interrupted, retrying... (1/2)"
}

1.12 config_changed

Emitted after a set_config command is processed. Contains the updated capabilities snapshot reflecting the current provider/model configuration.

{
  "type": "config_changed",
  "capabilities": {
    "tool_approval": true,
    "image_input": "unsupported",
    "thinking": false,
    "effort": true,
    "effort_levels": ["low", "medium", "high"],
    "modes": ["default", "auto_edit", "yolo"],
    "current_mode": "default",
    "mcp": true
  }
}

Clients should update their UI controls (e.g., enable/disable thinking toggle, populate effort dropdown) based on the new capabilities.

1.13 mcp_ready

Emitted after a dynamically injected MCP server has connected and its tools are registered.

{
  "type": "mcp_ready",
  "name": "my-tools",
  "tools": ["tool_a", "tool_b"]
}
FieldTypeDescription
namestringServer name (as provided in add_mcp_server)
toolsstring[]List of tool names registered from this server

1.14 pong

Response to a ping command from the client. Used for heartbeat/liveness detection.

{
  "type": "pong"
}

No additional fields. The agent emits pong immediately upon receiving a ping command, regardless of whether a message turn is active.

2. Client → Agent Commands (stdin)

Every line is a JSON object with a type field.

2.1 message

Send a user message. Agent responds with a stream of events.

{
  "type": "message",
  "msg_id": "abc-123",
  "content": "Read the file src/main.rs and explain the code",
  "files": ["/path/to/attached/file.png"]
}
FieldTypeRequiredDescription
msg_idstringyesClient-generated unique message ID
contentstringyesUser's message text
filesstring[]noAttached file paths (images, documents)

2.2 stop

Abort the current response stream.

{
  "type": "stop"
}

Agent MUST:

  1. Cancel any in-flight LLM request
  2. Cancel any running tool (if possible)
  3. Emit stream_end for the current msg_id

2.3 tool_approve

Approve a pending tool execution.

{
  "type": "tool_approve",
  "call_id": "tool-call-001",
  "scope": "once"
}
FieldTypeDescription
call_idstringMust match a pending tool_request
scopestring"once" = this call only; "always" = auto-approve this tool+category for the session

When scope = "always", the agent adds the tool's category to the session allow-list, so future calls of the same category skip approval.

2.4 tool_deny

Deny a pending tool execution.

{
  "type": "tool_deny",
  "call_id": "tool-call-001",
  "reason": "Not allowed to write this file"
}

Agent MUST:

  1. Emit tool_cancelled event
  2. Feed the denial reason back to the LLM as tool result
  3. Continue the conversation (LLM decides next action)

2.5 init_history

Inject prior conversation context (for conversation resume).

{
  "type": "init_history",
  "text": "Previous conversation summary:\nUser asked about X...\nAssistant replied with Y..."
}

Must be sent BEFORE the first message command. Agent incorporates this as conversation context.

2.6 set_mode

Change the agent's approval mode for the session.

{
  "type": "set_mode",
  "mode": "yolo"
}
ModeBehavior
"default"All tools need approval (except allow-listed)
"auto_edit"info and edit auto-approved; exec and mcp need approval
"yolo"All tools auto-approved

2.7 set_config

Update model and its host-resolved capabilities or request configuration at runtime.

{
  "type": "set_config",
  "model": "claude-opus-4",
  "image_input": "supported",
  "thinking": "enabled",
  "thinking_budget": 16000,
  "effort": "high",
  "compaction": "safe"
}
FieldTypeRequiredDescription
modelstringnoSwitch to a different model
image_inputstringnoHost-resolved capability for the selected model: supported, unsupported, or unknown
thinkingstringno"enabled" or "disabled"
thinking_budgetnumbernoToken budget for enabled thinking (default: 10000); sent on Anthropic requests and ignored by OpenAI-compatible requests
effortstringnoReasoning effort level (e.g., "low", "medium", "high")
compactionstringnoOutput compaction level: "off", "safe", "full"

All fields are optional. Only provided fields are updated.

When changing model, clients SHOULD send the matching image_input value in the same command. If model is provided without image_input, the agent resets image support to unknown rather than retaining the previous model's capability.

Validation: The agent validates effort values against the current provider's capabilities. Explicit thinking updates are applied as request intent; if the provider rejects the wire field, that provider error is surfaced during the model request. After processing, a config_changed event is always emitted with the updated capabilities.

2.8 add_mcp_server

Dynamically inject an MCP server before the conversation starts. This command is only accepted during the pre-message phase — after the ready event and before the first message command. Any add_mcp_server sent after the first message is rejected with an error.

{
  "type": "add_mcp_server",
  "name": "my-tools",
  "transport": "stdio",
  "command": "node",
  "args": ["bridge.js", "--port", "9000"],
  "env": {"TOKEN": "abc123"}
}
FieldTypeRequiredDescription
namestringyesUnique server name
transportstringyes"stdio", "sse", or "streamable-http"
commandstringstdio onlyExecutable to launch
argsstring[]noCommand arguments
envobjectnoEnvironment variables for the subprocess
urlstringsse/http onlyServer URL
headersobjectnoHTTP headers (for sse/http)

Dynamic MCP servers use the default startup timeout of 30000ms. To customize startup timeout, define the server in config with startup_timeout_ms.

Lifecycle:

Agent  → stdout: {"type":"ready",...}
Client → stdin:  {"type":"add_mcp_server","name":"tools","transport":"stdio","command":"node","args":["bridge.js"]}
Agent  → stdout: {"type":"mcp_ready","name":"tools","tools":["tool_a","tool_b"]}
Client → stdin:  {"type":"message","msg_id":"m1","content":"Hello"}
                  ↑ first message ends the injection window

2.9 ping

Heartbeat probe. The agent responds immediately with a pong event.

{
  "type": "ping"
}

Can be sent at any time — during idle, during message processing, or during tool execution. The agent always responds with {"type":"pong"}.

After the first message, any further add_mcp_server commands are rejected:

{
  "type": "error",
  "error": {
    "code": "protocol_error",
    "message": "AddMcpServer 'name': rejected — only allowed before first Message",
    "retryable": false
  }
}

3. Lifecycle

3.1 Startup

Client spawns:
  aionrs --json-stream \
    --provider anthropic \
    --model claude-sonnet-4-20250514 \
    --max-tokens 8192 \
    --max-turns 30

Environment variables set by client:
  ANTHROPIC_API_KEY=sk-...
  # or OPENAI_API_KEY, AWS_REGION, etc.

Agent initializes → stdout: {"type":"ready","session_id":"a1b2c3",...}

Pre-message phase (optional):

Between receiving ready and sending the first message, the client may inject MCP servers via add_mcp_server commands. The agent connects each server and emits mcp_ready when ready. This phase ends when the first message is sent.

Session lifecycle flags (mutually exclusive):

FlagDescription
--session-id <ID>Use a specific session ID instead of auto-generating one. Errors if the ID already exists.
--resume <ID>Resume a previous session (loads conversation history). Use latest to resume the most recent.
--fork-sessionWith --resume: fork the session into a NEW session id instead of continuing the original. The ready event's session_id carries the new id.
# New session with a custom ID
aionrs --json-stream --session-id my-conv-123 --provider openai --model gpt-4o

# Resume an existing session
aionrs --json-stream --resume my-conv-123 --provider openai --model gpt-4o

# Fork an existing session into a new id (read it from `ready.session_id`)
aionrs --json-stream --resume my-conv-123 --fork-session --provider openai --model gpt-4o

3.2 Message Turn

Client → stdin:  {"type":"message","msg_id":"m1","content":"Hello"}
Agent  → stdout: {"type":"stream_start","msg_id":"m1"}
Agent  → stdout: {"type":"text_delta","text":"Hi! ","msg_id":"m1"}
Agent  → stdout: {"type":"text_delta","text":"How can I help?","msg_id":"m1"}
Agent  → stdout: {"type":"stream_end","msg_id":"m1","usage":{...}}

3.3 Tool Approval Flow

Client → stdin:  {"type":"message","msg_id":"m2","content":"Create a hello.rs file"}
Agent  → stdout: {"type":"stream_start","msg_id":"m2"}
Agent  → stdout: {"type":"text_delta","text":"I'll create the file.","msg_id":"m2"}
Agent  → stdout: {"type":"tool_request","msg_id":"m2","call_id":"t1","tool":{"name":"Write","category":"edit",...}}
  ← Agent PAUSES here, waiting for approval →
Client → stdin:  {"type":"tool_approve","call_id":"t1","scope":"once"}
Agent  → stdout: {"type":"tool_running","msg_id":"m2","call_id":"t1","tool_name":"Write"}
Agent  → stdout: {"type":"tool_result","msg_id":"m2","call_id":"t1","status":"success",...}
Agent  → stdout: {"type":"text_delta","text":"File created successfully.","msg_id":"m2"}
Agent  → stdout: {"type":"stream_end","msg_id":"m2","usage":{...}}

3.4 Multi-Tool Parallel Execution

When the LLM requests multiple tools in one turn within the current run, the agent emits multiple tool_request events. Client can approve/deny them independently.

Agent  → stdout: {"type":"tool_request","call_id":"t1","tool":{"name":"Read","category":"info",...}}
Agent  → stdout: {"type":"tool_request","call_id":"t2","tool":{"name":"Read","category":"info",...}}
Client → stdin:  {"type":"tool_approve","call_id":"t1","scope":"once"}
Client → stdin:  {"type":"tool_approve","call_id":"t2","scope":"once"}
Agent  → stdout: {"type":"tool_running","call_id":"t1",...}
Agent  → stdout: {"type":"tool_running","call_id":"t2",...}
Agent  → stdout: {"type":"tool_result","call_id":"t1",...}
Agent  → stdout: {"type":"tool_result","call_id":"t2",...}

3.5 Shutdown

Client closes stdin (EOF) or sends SIGTERM. Agent cleans up and exits.

4. Error Handling

4.1 Invalid Command

If client sends malformed JSON or unknown command type:

{
  "type": "error",
  "msg_id": null,
  "error": {
    "code": "protocol_error",
    "message": "Unknown command type: foo",
    "retryable": false
  }
}

4.2 Provider Errors

Agent should emit error and let the conversation continue if possible:

{
  "type": "error",
  "msg_id": "m3",
  "error": {
    "code": "provider_error",
    "message": "Rate limit exceeded. Retry after 30s.",
    "retryable": true
  }
}

4.3 Fatal Errors

For unrecoverable errors, agent emits error and exits with non-zero status:

{
  "type": "error",
  "msg_id": null,
  "error": {
    "code": "config_error",
    "message": "ANTHROPIC_API_KEY not set",
    "retryable": false
  }
}

5. Configuration via CLI Flags

When spawned in --json-stream mode, all configuration is passed via CLI flags and environment variables:

aionrs --json-stream \
  --provider <anthropic|openai|bedrock|vertex> \
  --model <model-id> \
  --max-tokens <N> \
  --max-turns <N> \
  --base-url <URL> \
  --system-prompt <TEXT> \
  --auto-approve          # Start in yolo mode
  --workspace <PATH>      # Working directory for file operations

Environment variables (set by client before spawn):

ProviderVariables
AnthropicANTHROPIC_API_KEY, ANTHROPIC_BASE_URL
OpenAIOPENAI_API_KEY, OPENAI_BASE_URL
BedrockAWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_PROFILE
Vertex AIGOOGLE_APPLICATION_CREDENTIALS, VERTEX_PROJECT_ID, VERTEX_REGION

6. Protocol Versioning

The ready event includes a version field. Clients should check version compatibility.

  • Minor version bump: New optional event types or fields added (backward compatible)
  • Major version bump: Breaking changes to existing events/commands

Current version: 0.2.0