TriOnyx Runtime Protocol¶
Overview¶
The runtime protocol defines the structured JSON messages exchanged between the Elixir gateway and the Python agent runtime. This is a line-based protocol: each message is a single JSON object terminated by a newline (JSON Lines).
The gateway spawns the runtime as a subprocess via uv run runtime/agent_runner.py
and communicates using three channels:
| Channel | Direction | Purpose |
|---|---|---|
| stdin | Gateway -> Runtime | Configuration, prompts, interrupts, shutdown |
| stdout | Runtime -> Gateway | Events, results, errors (structured protocol) |
| stderr | Runtime -> (logs) | Diagnostic logging (not part of protocol) |
Important: The agent executes its own tools via the Claude Agent SDK. The gateway does NOT proxy tool calls. Events on stdout are observational -- the gateway uses them for taint tracking and audit logging, not for mediating tool execution.
Lifecycle¶
Gateway Runtime
| |
|--- spawn via `uv run` ----------->|
| | (process starts)
| |
|--- {"type":"start",...} --------->|
| | (configure SDK)
|<-- {"type":"ready"} -------------|
| |
|--- {"type":"prompt",...} -------->|
| | (drive SDK session)
|<-- {"type":"text",...} ----------| \
|<-- {"type":"tool_use",...} -------| | streaming events
|<-- {"type":"tool_result",...} ----| /
|<-- {"type":"result",...} ---------|
| |
|--- {"type":"prompt",...} -------->| (another trigger)
| ... |
| |
|--- {"type":"shutdown",...} ------>|
| | (exit cleanly)
- Gateway spawns runtime via
uv run runtime/agent_runner.py - Gateway sends
startwith agent configuration - Runtime replies
readyonce SDK is configured - Gateway sends
promptmessages to trigger agent sessions - Runtime streams events (
text,tool_use,tool_result) during each session - Runtime sends
resultwhen each session completes - Gateway sends
shutdown(or closes stdin) to terminate the runtime
Inbound Messages (Gateway -> Runtime)¶
start¶
Configures the agent. Must be the first message sent after spawning.
{
"type": "start",
"agent": {
"name": "code-reviewer",
"tools": ["Read", "Grep", "Glob"],
"model": "claude-sonnet-4-20250514",
"system_prompt": "You are a code reviewer...",
"max_turns": 10,
"cwd": "/workspace"
}
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
agent.name |
string | yes | "unnamed" |
Agent identifier (from agent definition) |
agent.tools |
string[] | yes | [] |
Allowed tools (SDK allowed_tools) |
agent.model |
string | no | "claude-sonnet-4-20250514" |
LLM model identifier |
agent.system_prompt |
string | no | "" |
System prompt (appended to claude_code preset) |
agent.max_turns |
integer | no | 10 |
Maximum SDK turns per session |
agent.cwd |
string | no | "/workspace" |
Working directory for the agent |
agent.skills |
string[] | no | [] |
List of skill names to load into the agent's context |
prompt¶
Delivers a trigger payload to drive an agent session.
{
"type": "prompt",
"content": "Review the changes in the last commit",
"metadata": {
"trigger": "cron",
"session_id": "a3f2c"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
content |
string | yes | The prompt text to send to the LLM |
metadata |
object | no | Opaque metadata from the gateway (not sent to LLM) |
interrupt¶
Requests cancellation of the active prompt. The runtime should cancel the
in-flight SDK call, drain any stale response queues, and emit an interrupted
message once ready for the next prompt.
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | no | Why the interrupt was requested |
shutdown¶
Requests a graceful shutdown. The runtime should finish any active session and exit cleanly.
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | no | Human-readable reason |
Outbound Messages (Runtime -> Gateway)¶
ready¶
Signals that the runtime has processed the start message and is ready to
receive prompts.
interrupted¶
Signals that the runtime has cancelled the active prompt in response to an
interrupt message and is ready for the next prompt.
| Field | Type | Description |
|---|---|---|
reason |
string | Echo of the interrupt reason |
text¶
Streams LLM text output during a session. The gateway logs these for auditability.
| Field | Type | Description |
|---|---|---|
content |
string | LLM-generated text |
tool_use¶
Reports that the agent invoked a tool. Observational only -- the SDK already executed the tool. The gateway uses this for audit logging.
{
"type": "tool_use",
"id": "toolu_01abc",
"name": "Read",
"input": {
"file_path": "/workspace/src/main.py"
}
}
| Field | Type | Description |
|---|---|---|
id |
string | Tool use ID (from SDK) |
name |
string | Tool name (e.g., "Read", "Bash") |
input |
object | Tool input parameters |
tool_result¶
Reports a tool's return value. Observational only -- the gateway uses this for taint tracking: if a tool accessed untrusted external data, the gateway marks the session as tainted.
Tool result content is truncated to 4096 characters to avoid flooding the protocol channel.
{
"type": "tool_result",
"tool_use_id": "toolu_01abc",
"content": "def main():\n print('hello')\n...",
"is_error": false
}
| Field | Type | Description |
|---|---|---|
tool_use_id |
string | Correlates with the preceding tool_use.id |
content |
string | Tool result content (may be truncated) |
is_error |
boolean | Whether the tool returned an error |
result¶
Reports session completion with execution metadata.
| Field | Type | Description |
|---|---|---|
duration_ms |
integer | Wall-clock session duration in milliseconds |
num_turns |
integer | Number of LLM turns (assistant messages) |
cost_usd |
float | Estimated API cost in USD |
is_error |
boolean | Whether the session ended due to an error |
error¶
Reports an error. May be followed by a result with is_error: true, or
may be a standalone protocol error (e.g., malformed input).
| Field | Type | Description |
|---|---|---|
message |
string | Human-readable error text |
Error Handling¶
| Condition | Behavior |
|---|---|
| Malformed JSON on stdin | Log to stderr, send error on stdout, continue |
| Unknown message type | Log to stderr, send error on stdout, continue |
prompt before start |
Send error on stdout, continue waiting for start |
| Empty prompt content | Send error on stdout, continue |
| SDK session exception | Send error + result (is_error=true) on stdout |
| Stdin EOF | Treat as shutdown, exit cleanly |
| SIGTERM | Finish active work, exit cleanly |
SDK Configuration Mapping¶
The start message fields map to Claude Agent SDK options:
| Protocol Field | SDK Option | Notes |
|---|---|---|
agent.tools |
allowed_tools |
Hard boundary -- SDK rejects unlisted tools |
agent.system_prompt |
system_prompt |
Appended to claude_code preset |
agent.model |
model |
Full model ID or short name |
agent.max_turns |
max_turns |
Prevents runaway loops |
agent.cwd |
cwd |
Working directory for tool execution |
| (implicit) | permission_mode |
Always "acceptEdits" for autonomous ops |
Implementation Files¶
| File | Language | Purpose |
|---|---|---|
runtime/agent_runner.py |
Python | Main runner script (PEP 723) |
runtime/protocol.py |
Python | Message types and emitter functions |
lib/tri_onyx/agent_port.ex |
Elixir | GenServer wrapping the Elixir Port |