Skip to content

Wire protocol (JSON-RPC 2.0)

Language: 简体中文 | 日本語 | 四川话 | English

For web / mobile frontends and any transport that speaks JSON lines (HTTP chunked, SSE data: payloads, WebSocket text frames).

This is not a second event system. arun_wire() serializes the same stream as arun_events(); semantics and ordering match events.md.

How Wire fits (diagrams)

Stack: one timeline, two layers

Inbound control (cancel, tool approval, steer) is not on this arrow — use your own HTTP/API beside the stream.

One Python event → one line

Typical stream (single turn, text only)

Client: parse each NDJSON line; append TextDelta to the answer pane.

With tools (two turns)

Full event list and ordering: events.md.

When to use Wire vs native Event

Use Wire (arun_wire, NDJSON)Use native Event (arun_events)
TypeScript / Swift / Kotlin clientPython CLI, FastAPI handler, tests
SSE or WebSocket to the browsermatch event: / isinstance in-process
Persist or replay wire.jsonlRich objects (e.g. OpenAI usage before encode)

| Use arun() only when you need printed answer text and nothing else (e.g. simple scripts).

Python backends that talk to a browser often: arun_events() in the server loop, encode_event_line(event) per chunk to the socket — or arun_wire() directly if you only forward lines.

Python events from Agent.arun_events() map 1:1 to JSON-RPC 2.0 notifications (no id — they are pushed, not request/response pairs).

Message shape

json
{
  "jsonrpc": "2.0",
  "method": "TextDelta",
  "params": { "text": "Hello" }
}
FieldValue
jsonrpcAlways "2.0"
methodEvent class name: RunBegin, TextDelta, ToolCallBegin, RunEnd, …
paramsDataclass fields as a JSON object (see events.md)

There is no id field. Inbound control (approve tool, cancel) is out of scope; use your own API for that.

NDJSON stream

One notification per line (newline-delimited JSON):

text
{"jsonrpc":"2.0","method":"RunBegin","params":{"user_input":"Hi"}}
{"jsonrpc":"2.0","method":"TextDelta","params":{"text":"4"}}
{"jsonrpc":"2.0","method":"RunEnd","params":{"content":"4","tool_calls":[],"reasoning_content":"","usage":null}}

Python

python
from pagent import Agent, LLM, Session, encode_event_line, decode_event_line

async for line in agent.arun_wire("2+2?"):
  # line is already NDJSON (ends with \n)
  send_to_websocket(line)

# Or encode/decode manually:
from pagent import event_to_rpc, rpc_to_event

msg = event_to_rpc(TextDelta("x"))
event = rpc_to_event(msg)

Exports: event_to_rpc, rpc_to_event, encode_event, decode_event, encode_event_line, decode_event_line, JSONRPC_VERSION.

TypeScript consumer (sketch)

typescript
type WireMsg = { jsonrpc: "2.0"; method: string; params: Record<string, unknown> };

function onLine(line: string) {
  const msg: WireMsg = JSON.parse(line);
  switch (msg.method) {
    case "TextDelta":
      appendAnswer(String(msg.params.text ?? ""));
      break;
    case "ReasoningDelta":
      appendThinking(String(msg.params.text ?? ""));
      break;
    case "ToolCallBegin":
      showTool(msg.params.name as string, msg.params.arguments as string);
      break;
    case "RunEnd":
      finish(msg.params.content as string);
      break;
  }
}

usage in StepEnd / RunEnd

When present, params.usage is a plain object:

json
{ "prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15 }

Methods reference

Same semantics as events.md; method equals the Python event class name.

methodparams keys
RunBeginuser_input
TurnBeginturn
TurnEndturn, stopped
TextDeltatext
ReasoningDeltatext
StepEndcontent, tool_calls, reasoning_content, usage
ToolCallBegintool_call_id, name, arguments
ToolResulttool_call_id, name, content
RunEndcontent, tool_calls, reasoning_content, usage

Runnable demo

examples/wire_browser/ — FastAPI server + single-page UI. See Wire demo on this site.

Source

src/pagent/wire.py

Released under the MIT License.