Skip to content

Stream Events

Kedi exposes completed semantic agent messages as an adapter-neutral event stream. This lets a terminal, editor, web UI, or orchestrator show useful progress while the authoritative adapter call continues normally.

The stream is deliberately not a token stream. Provider deltas, mutable snapshots, and hidden reasoning stay private. Kedi emits a message only after the adapter has identified a complete assistant message and classified it as commentary or final output.

Event Lifecycle

Every observed run emits one start event and one terminal event:

started
commentary  Inspecting the package manifest.
commentary  The dependency graph is consistent.
final       Validation passed with no blocking findings.
completed

The terminal state is exactly one of completed, failed, or cancelled. Failed and cancelled runs do not publish unfinished private text as a final message. The adapter's return value or exception remains authoritative; events are an observation side channel.

Commentary

phase="commentary" is a complete progress message that is not the run's final answer. Kedi can identify commentary when:

  • a provider or harness marks a completed message as commentary;
  • a tool-call boundary seals completed assistant text produced before the tool runs;
  • a later completed assistant message makes an earlier message intermediate.

Kedi does not expose chain-of-thought or infer commentary from partial token chunks. If a backend provides only a final response, the run may contain no commentary events.

Final

phase="final" is the completed natural-language response returned by a successful raw invoke() call. At most one final message is authoritative for one run. Structured produce() results remain typed data and are not mirrored as invented natural-language final messages.

Tool calls are not AgentMessageEvent values. Use Kedi's tool telemetry or the backend's native event surface when a UI also needs tool-call cards. A tool boundary only determines that preceding completed text is commentary.

Observe With a Callback

The public types and observer are exported from kedi:

import asyncio

from kedi import AgentMessageEvent, AgentRunStateEvent, observe_agent_events
from kedi.agent_adapter import PydanticAdapter


def render(event: AgentMessageEvent | AgentRunStateEvent) -> None:
    if isinstance(event, AgentMessageEvent):
        print(f"{event.phase}: {event.content}")
    else:
        print(f"run {event.run_id}: {event.state}")


adapter = PydanticAdapter("openai:gpt-4o-mini")

with observe_agent_events(render):
    result = asyncio.run(adapter.invoke(prompt="Inspect the parser."))

print("result:", result)

The callback runs on Kedi's dispatcher thread. Keep it short and thread-safe. For UI frameworks or asyncio applications, bridge through their event loop instead of mutating UI state directly from the callback.

Event observation does not modify prompts, schemas, tools, message history, usage accounting, return values, or exception behavior.

Consume From Async Code

AsyncAgentEventQueue is a thread-safe sink that forwards dispatcher events to an asyncio.Queue:

import asyncio

from kedi import AgentRunStateEvent, AsyncAgentEventQueue, observe_agent_events
from kedi.agent_adapter import PydanticAdapter


async def main() -> str:
    adapter = PydanticAdapter("openai:gpt-4o-mini")
    events = AsyncAgentEventQueue()
    root_run_id = None

    with observe_agent_events(events):
        task = asyncio.create_task(adapter.invoke(prompt="Inspect the parser."))

        while True:
            event = await events.queue.get()
            if (
                isinstance(event, AgentRunStateEvent)
                and event.state == "started"
                and event.parent_run_id is None
            ):
                root_run_id = event.run_id

            if (
                isinstance(event, AgentRunStateEvent)
                and event.run_id == root_run_id
                and event.state in {"completed", "failed", "cancelled"}
            ):
                break

        return await task


print(asyncio.run(main()))

Construct the queue inside the target event loop. Pass loop= explicitly only when construction and consumption occur in different setup code.

Event Fields

AgentMessageEvent carries:

Field Meaning
run_id Stable identity of the emitting agent run
parent_run_id Parent run for subagents, otherwise None
message_id Backend message identity normalized by the adapter
adapter Adapter shortname such as pydantic, langchain, or codex
sequence Monotonic event order within that run
phase commentary or final
content Complete immutable message text
segment_index, segment_count Ordering metadata for a segmented completed message

AgentRunStateEvent carries run_id, parent_run_id, adapter, sequence, and state. Sequence numbers are scoped per run; use (run_id, sequence) when merging concurrently emitted parent and subagent events.

Subagents and Concurrent Runs

Subagent events use the coordinator-assigned child run_id and identify the calling run through parent_run_id. Do not assume events from concurrent runs are globally ordered. Maintain independent render state for each run_id, then use the parent relationship to nest child activity.

A root run has parent_run_id=None. A terminal event closes only its own run; a child completion must not be mistaken for root completion.

Delivery and Backpressure

Kedi dispatches events away from provider and SDK reader threads so a slow observer does not directly block model execution. Control and final events are kept separate from the bounded commentary queue. Under sustained commentary overflow, Kedi drops the oldest pending commentary and logs a warning rather than allowing unbounded memory growth.

Observer failures are isolated from the model call. Repeated callback failures detach that observer, and a callback that cannot drain during shutdown is detached after a bounded wait. Observation is therefore suitable for progress reporting, not durable audit storage. Persist authoritative traces through Telemetry.

Adapter Support

Semantic message boundaries are capability-driven:

Adapter Stream events
Pydantic AI yes
LangChain yes
Claude Agent SDK yes
Codex App Server yes
ACP yes
DSPy no
WebGPU no

LazyAdapter reports the resolved adapter's capability. Unsupported adapters set supports_stream_events=False; Kedi does not fabricate trustworthy message boundaries from raw token chunks.

See the generated Capability Matrix for the current built-in adapter metadata.