Skip to content

Agent events

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

For developers building custom UIs. New users: Quick start; internals: development.md.

Structured events for observing the Agent loop without coupling to a specific UI (terminal, web, IDE). Inspired by the Soul / Wire split in products like Kimi Code: the loop emits facts; consumers subscribe and render.

Status: Agent.arun_events() emits these events. Agent.arun() is a thin wrapper that yields only TextDelta.text for backward compatibility.

Native Event vs Wire (JSON-RPC)

Same timeline, two shapes — not two different event systems.

LayerWhat you getTypical use
Native EventFrozen Python dataclasses (TextDelta, RunEnd, …)Python services, CLI, tests, notebooks — match / isinstance, IDE types
Wire (JSON-RPC)One NDJSON line per event: {"jsonrpc":"2.0","method":"TextDelta","params":{...}}Browser/mobile, other languages, SSE/WebSocket, log files (wire.jsonl)
APIReturns
agent.run()Final RunEnd only (blocking, no stream)
agent.arun()str text chunks only
agent.arun_events()Event objects (full stream)
agent.arun_wire()str NDJSON lines (same stream, serialized)

Wire is a thin serializer over arun_events(); see wire.md. In Python you can also encode_event_line(event) when bridging to a socket yourself.

Choose native when the consumer is Python and you want type checking and pattern matching.

Choose wire when the consumer is not Python, or you need a stable on-the-wire format for HTTP/SSE/WebSocket.

Import

python
from pagent import (
    Event,
    RunBegin,
    TurnBegin,
    TextDelta,
    ReasoningDelta,
    StepEnd,
    ToolCallBegin,
    ToolResult,
    TurnEnd,
    RunEnd,
)

Event is a union of all concrete event classes (for match / isinstance).

Event reference

Lifecycle

EventFieldsWhen (intended)
RunBeginuser_input: strUser message appended to session; loop starts
TurnBeginturn: intStart of one LLM call inside max_turns (0-based)
TurnEndturn: int, stopped: boolAssistant message written; stopped=True if loop will not call the model again this run
RunEndcontent, tool_calls, reasoning_content, usageEntire run / arun finished (same type as LLM.invoke return)

Streaming

EventFieldsWhen (intended)
TextDeltatext: strChunk of assistant content from invoke_stream
ReasoningDeltatext: strChunk of reasoning_content (provider-specific)

Step boundary

EventFieldsWhen (intended)
StepEndcontent, tool_calls, reasoning_content, usageOne LLM step finished (stream assembled or single invoke)

Same fields as RunEnd for that step. tool_calls uses the OpenAI shape: [{ "id", "type", "function": { "name", "arguments" } }, ...].

Tools

EventFieldsWhen (intended)
ToolCallBegintool_call_id, name, argumentsAbout to execute one tool
ToolResulttool_call_id, name, contentTool output appended to session as role: tool

Typical sequences

Single turn, text only

text
RunBegin
  TurnBegin(0)
    TextDelta …
    StepEnd(content=…, tool_calls=[])
  TurnEnd(0, stopped=True)
RunEnd

One turn with tools, then final answer

text
RunBegin
  TurnBegin(0)
    TextDelta …
    StepEnd(…, tool_calls=[…])
    ToolCallBegin(…)
    ToolResult(…)
  TurnEnd(0, stopped=False)
  TurnBegin(1)
    TextDelta …
    StepEnd(…, tool_calls=[])
  TurnEnd(1, stopped=True)
RunEnd

Hit max_turns with tools still pending

Last TurnEnd may have stopped=False; the final RunEnd event reflects the last assistant message (may still include tool_calls).

Consumer example

python
async for event in agent.arun_events("Hello"):
    match event:
        case TextDelta(text=t):
            print(t, end="", flush=True)
        case ToolCallBegin(name=n):
            print(f"\n[tool {n}]")
        case RunEnd(content=c):
            print(f"\n[done: {c!r}]")

Keep arun() for callers that only need text:

python
async for chunk in agent.arun("Hello"):
    print(chunk, end="")  # str, not Event

Design notes

  • Frozen dataclasses — events are immutable snapshots; safe to queue or log.
  • Wire protocol — JSON-RPC 2.0 notifications + NDJSON: wire.md. Use Agent.arun_wire() or encode_event_line().
  • Inbound control (approval, external tools, steer) — not modeled; would be separate request types, not Event.
  • Session vs eventssession.messages remains the LLM API history; events are a parallel UI timeline (compare Kimi context.jsonl vs wire.jsonl).

Source

Definitions: src/pagent/events.py

reasoning_content examples (run vs stream, --zh 鸡兔同笼): reasoning.md

Released under the MIT License.