# Events

Read an agent's events while a turn runs, print answer text and tool calls, and read or serve the session's event log.

## Overview

Every agent emits events while it runs: turn lifecycle, answer text, reasoning, tool activity, and model requests. Each event has a dotted type, such as `assistant.delta`, and a JSON payload. Both SDKs emit the same types.

A Rust agent returns an `AgentEvents` stream from `build()`, and `AgentEvents::recv` returns the next event. A TypeScript runtime logs every event to SQL first. Then it calls `published(events, bytes)` with each committed batch, in sequence order.

#### Rust

```rust
use nanocode::agent::events::AgentEventKind;

// Print every event kind and its raw JSON payload
while let Some(event) = events.recv().await {
    if event.kind == AgentEventKind::RunCompleted {
        break;
    }
    println!("{:?} {}", event.kind, event.payload.get());
}
```

#### TypeScript

```ts
import { AgentRuntime } from 'nanocode-wasm'

// Print each logged event after its batch is committed
const runtime = await AgentRuntime.instantiate(module, sql, {
  published: (events) => {
    for (const entry of events) console.log(entry.seq, entry.event)
  },
})
```

## Walkthrough

The steps below stream a turn's answer text and tool calls while the turn runs. The Rust steps follow `crates/nanocode/examples/support/stream.rs`, and the TypeScript steps follow this site's browser agent, `src/demo/Agent.ts`.

### Read the Shape

A Rust `AgentEvent` has a `kind` and a raw JSON `payload`. A TypeScript `LoggedEvent` has a `seq`, a `request_id`, a `created_at` time, and an `event`. An agent event in the log has `source` set to `agent`, with the SDK event under `event`.

#### Rust

```toml title="Cargo.toml"
[dependencies]
# serde_json reads the fields of event payloads
nanocode = { git = "https://github.com/gakonst/nanocode" }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

#### TypeScript

```ts
// The fields of LoggedEvent.event that an agent event carries
type Logged = {
  event?: { payload?: Record<string, unknown>; type?: string }
  source?: string
}
```

The log also records turn lifecycle entries without a `source`, such as `{"type":"queued"}` when a turn is submitted and `{"type":"result"}` with its `status` when it finishes.

### Handle Each Event

`assistant.delta` payloads carry a piece of answer text in `text`. `tool.call` payloads carry the tool name in `tool` and its input in `arguments`.

#### Rust

```rust title="src/main.rs"
use nanocode::agent::events::{AgentEvent, AgentEventKind};
use serde_json::Value;

fn print_event(event: &AgentEvent) -> serde_json::Result<()> {
    // Answer text goes to stdout, tool calls to stderr
    let payload: Value = serde_json::from_str(event.payload.get())?;
    match event.kind {
        AgentEventKind::AssistantDelta => {
            print!("{}", payload["text"].as_str().unwrap_or_default())
        }
        AgentEventKind::ToolCall => eprintln!(
            "· {}",
            payload["tool"].as_str().unwrap_or("tool")
        ),
        _ => {}
    }
    Ok(())
}
```

#### TypeScript

```ts title="agent.ts"
import type { LoggedEvent } from 'nanocode-wasm'

// Answer text goes to the page, tool calls to the console
function render(entry: LoggedEvent) {
  const logged = entry.event as Logged
  if (logged.source !== 'agent' || !logged.event) return
  const { payload = {}, type } = logged.event
  if (type === 'assistant.delta')
    output.append(String(payload.text ?? ''))
  if (type === 'tool.call') console.log(payload.tool, payload.arguments)
}
```

A provider can also send a complete `assistant.message` without deltas. `stream.rs` prints such a message only when no delta for the same `item_id` arrived.

### Run the Turn

A turn finishes when its result resolves, and events can still arrive before that. In Rust, `tokio::select!` with `biased` reads ready events first and stops when the result resolves. In TypeScript, `published` runs while `drive` runs the turn.

#### Rust

```rust title="src/main.rs"
// 1. Pin the result future so the loop can poll it repeatedly
let turn = agent
    .prompt("Explain durable agents in two sentences.")
    .await?;
let mut result = std::pin::pin!(turn.result());

// 2. Handle events until the turn's result resolves
let result = loop {
    tokio::select! {
        biased;
        Some(event) = events.recv() => print_event(&event)?,
        result = &mut result => break result?,
    }
};
eprintln!("\n{} characters", result.final_message().len());
```

#### TypeScript

```ts title="agent.ts"
// 1. Send each committed batch to render
const runtime = await AgentRuntime.instantiate(module, sql, {
  published: (events) => events.forEach(render),
})
await runtime.open(config, { dispatch })

// 2. Queue the prompt and drive it. Deltas reach render during drive.
await runtime.submit(scope, {
  metadata: {},
  prompt: 'Explain durable agents in two sentences.',
  request_id: 'turn-1',
})
for (;;) {
  const next = (await runtime.pending(scope))[0]
  if (!next) break
  const driven = await runtime.drive(next.report.request_id)
  if (driven.retry)
    throw new Error(
      'the runtime closed, so reopen it from the database',
    )
  if (driven.waiting)
    await new Promise((resolve) => setTimeout(resolve, 100))
}
```

See [Agents and Turns](/docs/guides/agents) for the runtime setup and the `drive` results.

## Recipes

### Know the Types

The event types below are the same in both SDKs. In Rust, `AgentEventKind` serializes to these names, and `AgentEventKind::is_terminal` is true for `run.completed` and `run.failed`.

| Types                                                                     | Behavior                                  |
| ------------------------------------------------------------------------- | ----------------------------------------- |
| `input.accepted`                                                          | The agent accepted a prompt               |
| `run.started`, `run.steered`, `run.completed`, `run.failed`, `run.error`  | Turn lifecycle                            |
| `assistant.delta`, `assistant.message`                                    | Answer text, streamed or whole, in `text` |
| `reasoning.summary.delta`                                                 | Reasoning summary text                    |
| `tool.call`, `tool.result`, `tool.notification`                           | Tool activity                             |
| `model.call.*`, `model.attempt.*`, `model.connection.*`, `model.warmup.*` | Model requests and retries                |
| `model.compaction.*`                                                      | Context compaction                        |
| `api.event`                                                               | Raw provider events                       |

A `tool.result` payload has the output in `result` and a `status` of `completed` or `failed`. A `run.error` payload with `"observational": true` is a warning, and the turn continues.

### Coalesce Deltas

Consecutive deltas of the same agent, request, and item can merge into one event within a time window. This reduces the events that a view renders. The Rust window is an argument of `SessionEvents::start`. The TypeScript window is `config.event_coalesce_ms`, which defaults to `0`.

```ts
// Merge deltas logged within 50 ms into one event
await runtime.open({ ...config, event_coalesce_ms: 50 }, { dispatch })
```

### Read the Log

Only the TypeScript SDK keeps an event log. `page(scope, after, limit, request_id?)` returns the events after a sequence number, optionally for one turn. Read again from `next_cursor` while `has_more` is `true`.

```ts
// Read every logged event of turn-1, 100 at a time
let after = 0
for (;;) {
  const page = await runtime.page(scope, after, 100, 'turn-1')
  page.events.forEach(render)
  after = page.next_cursor
  if (!page.has_more) break
}
```

Logged events survive across instances, so a new instance over the same database reads the same pages.

### Serve the Events

`EventHub` keeps a bounded tail of recent events in memory and serves Server-Sent Events. Readers inside the tail read from memory, and readers behind it page from the log. This recipe applies only to TypeScript.

```ts
import { AgentRuntime, EventHub } from 'nanocode-wasm'

// 1. Keep at most 128 events and 4 MiB in the tail
const hub = new EventHub(
  () => 128,
  () => 4 * 1024 * 1024,
)
const runtime = await AgentRuntime.instantiate(module, sql, {
  published: (events, bytes) => hub.publish(events, bytes),
})

// 2. Serve the log from the tail, or from storage for readers behind it
export function events(after: number): Response {
  return hub.stream(
    after,
    15_000,
    async (cursor) =>
      hub.recent(cursor) ?? runtime.page(scope, cursor, 128),
  )
}
```

Each event goes out with `id: <seq>`. A reconnecting client passes the last `seq` it received as `after`.

### Decode Typed Data

Only the Rust SDK decodes payloads into types. `AgentEvent::data` returns `AgentEventData`, a typed enum grouped by category, instead of a `serde_json::Value`.

```rust
use nanocode::agent::events::{AgentEventData, AssistantEvent};

// Read the text of an answer delta through the typed payload
if let Ok(AgentEventData::Assistant(AssistantEvent::Delta(delta))) =
    event.data()
{
    print!("{}", delta.text);
}
```

### Write JSON Lines

`AgentEvents::write_turn_jsonl` writes each event as one JSON line until the turn's terminal event. `write_jsonl` writes until the stream closes. Both are Rust only.

```rust
// Write one turn's events to stdout as JSON lines
let turn = agent.prompt("Say hello.").await?;
events.write_turn_jsonl(std::io::stdout()).await?;
let result = turn.result().await?;
```

### Subscribe Again

`agent.subscribe()` returns a new `AgentEvents` stream for an existing Rust agent, for example in a second task. A TypeScript host forwards `published` to each reader, or uses `EventHub`.

```rust
// A second stream that receives the agent's events from now on
let mut audit = agent.subscribe();
tokio::spawn(async move {
    while let Some(event) = audit.recv().await {
        eprintln!("{:?}", event.kind);
    }
});
```

### Batch Session Events

`SessionEvents::start` merges a root agent's events and its subagent updates into batches for an `EventSink`. It holds a batch that ends in a delta until the window elapses. The TypeScript runtime runs this pump itself and logs each batch.

```rust
use std::time::Duration;

use nanocode::tools::embedded::HostFuture;
use nanocode::{EventSink, SessionEvent, SessionEvents};

struct Stdout;

impl EventSink for Stdout {
    // Each SessionEvent serializes to the hosts' wire format
    fn deliver(
        &self,
        events: Vec<SessionEvent>,
    ) -> HostFuture<'_, Result<(), String>> {
        Box::pin(async move {
            for event in events {
                println!(
                    "{}",
                    serde_json::to_string(&event)
                        .map_err(|error| error.to_string())?
                );
            }
            Ok(())
        })
    }
}

// 1. Start the pump, and spawn the future that it returns
let (session, pump) = SessionEvents::start(
    events,
    runtime.take_subagent_updates(),
    Duration::from_millis(50),
    Stdout,
);
tokio::spawn(pump);

// 2. After the turn, wait until every received event is delivered
session.drained().await?;
```

Agent events serialize as `{"source":"agent","event","sdk_timing"}`, and subagent updates as `{"source":"subagent","root_session_id","detach","update"}`. These are the same shapes as `LoggedEvent.event` in TypeScript.

## More

- [Agents and Turns](/docs/guides/agents): Build a runtime and run turns.

- [Subagents](/docs/guides/subagents): Stream updates from the whole agent tree.

- [In the Browser](/docs/guides/browser): Render a chat from logged events in a page.

- [Agent](/docs/api/rust/Agent): Reference for prompts, subscriptions, and flushing.

- [Models and Types](/docs/api/rust/types): Reference for event kinds and payload types.

- [page](/docs/api/wasm/page): Read logged events by cursor or by turn.

- [EventHub](/docs/api/wasm/EventHub): Serve recent events as Server-Sent Events.

- [Types](/docs/api/wasm/types): LoggedEvent, LogPage, and the other package types.
