Part 12 of 18 · Arunank Sharan · Kuzushi Labs · 1,119 words · 19 August 2026

← Series contents · Complete reading view

One event spine, several ways to deliver the result

A web operator wants to see progress as a run unfolds. A messaging integration usually wants a completed response payload. An auditor wants a durable sequence. A metrics system wants counters and durations. These are different views of the same execution.

The client harness emits typed runtime events and attaches consumers to that stream. This replaces a growing collection of special callbacks with a shared vocabulary of facts.

One runtime event stream feeds the durable log, web stream, synchronous response, audit outbox and telemetry.

Events describe what happened

The event envelope includes tenant identity and, where available, run, session and attempt IDs. Event classes are frozen at the model level and use explicit type names. Storage adds ordering information when events are persisted.

The vocabulary covers ingress, routing, hydration, assembly, attempts, model requests, tool execution, user-visible text, components and terminal outcomes. This granularity lets a run trace explain where a decision came from.

{
  "type": "tool.call.completed",
  "tenant_id": "tenant-example",
  "run_id": "run-example",
  "attempt_id": "attempt-example",
  "tool_call_id": "call-2",
  "tool_name": "get_task_status",
  "duration_ms": 180,
  "outcome": "ok"
}

The timing is illustrative. The useful contract is the correlation: the event belongs to a particular request and tool call, and its semantic outcome is available without parsing a log sentence.

Frozen event objects reduce accidental reassignment, but nested dictionaries still need disciplined ownership. “Immutable” should not become an excuse to pass a shared mutable payload to several consumers and then modify it later.

The result is a summary; the events are the explanation

A run result can say completed, include final text, usage and selected summary fields. It should not need to duplicate every emitted event. The durable log already contains the sequence.

This keeps the caller’s contract small while preserving debugging depth. A synchronous API can return the answer and a run ID. The operator can follow that ID to routing, model turns and tool calls.

It also avoids a second event history being built independently inside the result object. Two histories assembled by different code paths will eventually disagree.

Project the stream for the channel

The web path uses an SSE projection. It can show status changes, visible text and structured components. The synchronous path uses a buffer projection and returns the completed payload. Both invoke the same orchestrator.

The loop therefore does not need a branch saying “if WhatsApp, run this other algorithm.” Channel differences belong in input adaptation, prompt presentation rules and output projection.

This preserves evaluation parity. A test using the synchronous path exercises the same routing and tool logic as a messaging request. It still does not prove that the external messaging provider delivered the result; delivery is a later boundary.

Presentation tools are not domain actions

The model can request a card, chart or quick reply. The loop recognises these presentation calls and emits component events rather than executing a backend mutation.

A component stream can include start, delta and completion events. The UI should not assume a partially received JSON object is valid component props. It can display a placeholder while accumulating data, then validate the complete payload before rendering the final component.

Component names should come from an allowed registry. A model-provided name should not become arbitrary component import or script execution. The same principle applies to links and rich text: presentation is still an input boundary.

Keep internal narration separate

The raw model stream is useful for investigation. It is not automatically the guest transcript. The runtime distinguishes model text from assistant-visible text so it can suppress inter-step narration before data and action tools.

This prevents an implementation detail such as “calling the booking API now” from becoming a guest-facing message. It also makes the publication boundary explicit for final claims.

A browser cannot unsee a streamed sentence. Where correctness depends on tool evidence, use a committed answer stage or another explicit UI treatment. A final corrected message alone does not erase an earlier incorrect claim.

Sink isolation has limits

One telemetry consumer failing should not break unrelated consumers. The composite sink isolates failures so a metrics or tracing outage does not automatically destroy a guest response.

However, failure isolation is not durability. If the durable event consumer fails and the system continues, the missing evidence needs a visible degraded state or recovery path. Otherwise “sink-first” can be mistaken for “every event is guaranteed persisted.”

A robust design distinguishes best-effort telemetry from required audit facts. For a read-only answer, proceeding with reduced telemetry may be acceptable. For a consequential action, the policy may require durable action evidence before acknowledging success.

Delivery is another state machine

Generated, committed, queued for delivery, accepted by the provider and delivered to the user are different states. The run trace should carry enough correlation to connect them, but the agent service should not invent provider delivery status.

The backend owns the messaging integrations in this architecture. It can apply templates, account modes and provider-specific rules after the agent returns. A task-status update follows the same distinction: composition is not delivery.

This is particularly important for escalation. “An escalation item was recorded” and “the host was notified on a messaging channel” are different observable events. The final language should follow the stronger claim only when the stronger evidence exists.

Backpressure is part of streaming

A slow browser or event sink can turn a fast model stream into a growing memory buffer. Each projection needs a policy for queue limits, cancellation and dropped optional updates.

Status updates can often be coalesced. Tool terminal events and final answers cannot simply disappear. If a client disconnects, decide whether the run should stop or continue as durable background work; make that choice by product contract rather than an accidental socket exception.

Separate channel projections make these behaviours testable. Run the same event sequence through a fast consumer, a slow consumer and a disconnected consumer. The business outcome should remain consistent even when optional progress updates are coalesced or delivery is retried.

Test event meaning and order

Useful tests verify that a tool completion has a corresponding call identity, that terminal outcomes are not emitted twice, that a synchronous response matches the final committed result and that one broken optional sink does not prevent other sinks from receiving events.

For UI components, test partial argument chunks and malformed payloads. For delivery, test retries independently from model execution. Re-sending a message should not rerun a paid model turn or recreate a maintenance task unnecessarily.

The event spine makes the system easier to extend because each consumer interprets the same execution facts. It also makes disagreements visible: a polished answer, a failed tool and a missing notification can no longer hide inside one undifferentiated “success” response.