@aexhq/sdk 0.40.6 → 0.40.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/events.md CHANGED
@@ -1,243 +1,243 @@
1
- ---
2
- title: Events
3
- ---
4
-
5
- # Events
6
-
7
- aex runs agent sessions on the managed runtime. Sessions are **non-blocking**:
8
- the managed runtime advances the agent while aex observes lifecycle state, maps
9
- runtime output into one event shape, and persists every captured event. The SDK
10
- and CLI observe the durable event timeline from aex — there is no in-process
11
- tool-approval hook.
12
-
13
- ## Two ways to consume events
14
-
15
- A session's reads and streams are grouped under accessor sub-resources:
16
- `session.events()` owns the event timeline, `session.messages()` owns the decoded
17
- assistant text, and `session.outputs()` owns the captured files. Reach a verb by
18
- chaining it off the accessor.
19
-
20
- ```ts
21
- // Pull a snapshot of every event captured so far.
22
- const events = await session.events().list();
23
- ```
24
-
25
- ```ts
26
- // Stream events as guarded `AexEventView`s: yields each event once, stops when
27
- // the session parks. Backed by polling the aex events endpoint. Every surface —
28
- // list(), stream(), streamEnvelopes() — yields the same `AexEventView` with a
29
- // populated `sequence` and the `is*()` guard methods.
30
- for await (const event of session.events().stream({ intervalMs: 1000 })) {
31
- if (event.isTextMessage()) {
32
- // ...
33
- }
34
- }
35
- ```
36
-
37
- For the canonical event envelope, use the coordinator WebSocket stream:
38
-
39
- ```ts
40
- for await (const event of session.events().streamEnvelopes({ from: 0 })) {
41
- console.log(event.sequence, event.type, event.source);
42
- }
43
- ```
44
-
45
- `session.events().streamEnvelopes()` uses a short-lived ticket minted by the hosted API, then subscribes directly to the per-session coordinator. Subscribe means read-from-cursor plus tail: reconnects resume from the last sequence.
46
-
47
- ## Assistant text
48
-
49
- To collect just the agent's assistant messages, use the `messages()` accessor —
50
- `list()` returns every decoded `AssistantTextEntry` oldest-first, and
51
- `last()`/`first()` return one entry (or `undefined` when empty). Read `.text`
52
- for the string:
53
-
54
- ```ts
55
- const lastText = (await session.messages().last())?.text;
56
- ```
57
-
58
- Prefer `session.messages().list()` or the collected `result.messages` /
59
- `result.text` fields for assistant text. Low-level event helpers remain exported
60
- for callers that build custom collectors.
61
-
62
- The CLI mirrors the same surface:
63
-
64
- ```bash
65
- aex events <session-id> --api-key … [--aex-url …] # snapshot (polling)
66
- aex events <session-id> --follow [--timeout 8m] --api-key … [--aex-url …] # stream until the session parks (polling)
67
- aex tail <session-id> [--json] [--filter <type|source>] [--logs] [--settle] [--timeout 8m] --api-key … # live, human-readable, over the WS envelope stream
68
- aex inspect <session-id> [--json] [--filter <type|source>] [--logs] [--timeout 8m] --api-key … # one-shot full timeline + jump-to-failure + cost/usage
69
- aex wait <session-id> [--timeout 8m] [--interval 2s] --api-key … # block, print final session
70
- ```
71
-
72
- `aex tail` and `aex inspect` consume the same coordinator WebSocket envelope
73
- stream as `session.events().streamEnvelopes()` (replay-from-cursor + tail +
74
- exactly-once resume), so they are the low-latency equivalents of
75
- `events --follow`'s polling. `--json` is the raw-NDJSON escape hatch; `--filter`
76
- keeps only the named AG-UI types (`TEXT_MESSAGE_CONTENT`, `TOOL_CALL_START`, …)
77
- or sources (`agent`/`runtime`/…); a `RUN_ERROR` is surfaced as a jump-to-failure
78
- line. `aex inspect` adds a header, a settle-consistent full timeline, and a
79
- cost/usage footer. Both exit `0` parked cleanly / `1` error park / `3` timeout.
80
- They need a global `WebSocket` (Bun or Node ≥ 22).
81
-
82
- `aex wait` is the host mirror of `session.wait()`:
83
- it polls until the session parks and prints the final `Session` record. Exit `0`
84
- when the session parked cleanly (`idle`/`suspended`/`succeeded`), `1` for any
85
- non-clean terminal outcome (`failed`/`timed_out`/`cancelled`), and `3` when
86
- `--timeout` elapses first
87
- (a `--timeout` on `events --follow` / `run --follow` uses the same exit-`3`
88
- convention). Durations accept `ms`/`s`/`m`/`h` suffixes or a bare millisecond integer.
89
-
90
- Both surfaces observe the same events. A subscriber attached after a session
91
- message is accepted replays the events it missed, then continues live.
92
-
93
- ## Session turn events
94
-
95
- The canonical SDK session surface stops a turn on session lifecycle events, not
96
- terminal run events:
97
-
98
- ```ts
99
- const session = await aex.openSession(config);
100
- const turn = session.send("Continue the task.");
101
-
102
- for await (const event of turn) {
103
- console.log(event.sequence, event.type);
104
- }
105
-
106
- const result = await turn.done(); // status is the terminal outcome, e.g. "succeeded"
107
- ```
108
-
109
- The turn stream ends on the turn's park terminal: a resumable
110
- `aex.session.idle` / `aex.session.suspended`, a terminal outcome
111
- `aex.session.succeeded` / `.failed` / `.timed_out` / `.cancelled`, or the held
112
- `aex.session.awaiting_approval`. (The bare `aex.session.error` park is retired —
113
- a failed turn is `failed`, a wall-clock kill `timed_out`, a cancel `cancelled`.)
114
- `aex.run(config)` is a convenience wrapper over the same flow: it opens a
115
- session, sends `message` once, and returns the collected session turn. The
116
- returned `runId` is the session id.
117
-
118
- ## Per-token streaming (`outputMode: 'stream'`)
119
-
120
- Assistant-text granularity is controlled by `outputMode` on `openSession` / `run`:
121
-
122
- - **`buffered`** (the default) — the assistant text arrives coalesced as one
123
- `TEXT_MESSAGE_CONTENT` event per message.
124
- - **`stream`** — on a **streamable** provider you also receive per-token
125
- `TEXT_MESSAGE_CONTENT` **deltas** as the model produces them, and a coalesced
126
- final `TEXT_MESSAGE_CONTENT` (the archived twin) always follows. Streamable
127
- providers today are Anthropic and the OpenAI-chat-shaped providers (DeepSeek,
128
- OpenAI, Mistral, OpenRouter, Doubao / Doubao China).
129
-
130
- Streaming is **capability-gated and fail-closed**: requesting `outputMode: 'stream'`
131
- on a provider that has no streaming producer (currently `gemini`) is a typed
132
- rejection at submission parse — **not** a silent downgrade to buffered. Choose a
133
- streamable provider or drop back to `buffered` explicitly.
134
-
135
- ## Terminal events vs. the run record
136
-
137
- Two families of events can end a turn's stream, and which one you see depends
138
- on how the turn ends:
139
-
140
- - **AG-UI terminals** — `RUN_FINISHED` / `RUN_ERROR`. These are *render-complete*
141
- signals emitted by the agent stream itself. On the managed plane a normal
142
- session turn usually does **not** emit `RUN_FINISHED`: the session *parks*
143
- instead (see below). Expect `RUN_ERROR` on stream-level failures, and treat
144
- `RUN_FINISHED` — when it does appear — as a low-latency "stop the spinner"
145
- hint, not a read-consistency barrier.
146
- - **`aex.session.*` park terminals** — `CUSTOM` events carrying the turn's
147
- OUTCOME: `aex.session.succeeded` / `aex.session.failed` / `aex.session.timed_out`
148
- / `aex.session.cancelled`, or a resumable/held park `aex.session.idle` /
149
- `aex.session.suspended` / `aex.session.awaiting_approval`. On the managed plane
150
- these are what actually end a turn. The park event ends the RENDER; the settle
151
- commit (cost + usage + the authoritative outcome) lands shortly after — which
152
- is why `run()`/`done()` await settle by DEFAULT and return the outcome as
153
- `result.status` with `result.costUsd`/`result.usage` always populated (never a
154
- bare `idle`). Pass `await: 'park'` (on `run(...)` or `session.send(msg, {...})`)
155
- to return early at the park event when you don't need cost/usage. `result.costUsd`
156
- is aex runtime/storage spend and **excludes** your BYOK provider charges — price
157
- those from `result.usage` token counts. Ending a raw stream on the park event is
158
- fine for live UI; read the settled record (or the default await-settle result)
159
- when you need cost/usage/outcome.
160
-
161
- Each event the stream yields carries method guards that cover both families so
162
- you never have to switch on the plane:
163
-
164
- - `event.isRunTerminal()` — true for the AG-UI `RUN_FINISHED` / `RUN_ERROR` pair.
165
- - `event.isRunSettled()` — true for the `aex.run.settled` settle barrier **and**
166
- for any `aex.session.*` park terminal. The managed plane does not broadcast a
167
- separate `aex.run.settled` barrier — the park event plays that role — so
168
- `event.isRunSettled()` is the one check that reliably means "this stream is done
169
- and the record is authoritative".
170
-
171
- To read the authoritative status consistently, use one of:
172
-
173
- ```ts
174
- // Session record path: send a turn, then wait for the session to park.
175
- const session = await aex.openSession(config);
176
- await session.send("Continue the task.").done();
177
- const record = await session.wait(); // the parked session record
178
- ```
179
-
180
- ```ts
181
- // Live events AND a settle-consistent end: the iterator ends on the settle
182
- // barrier OR the aex.session.* park terminal, whichever the plane emits —
183
- // so when it ends, the session record is already parked/terminal.
184
- for await (const event of session.events().streamEnvelopes({ settleConsistent: true })) {
185
- // render events live…
186
- }
187
- const settled = await aex.sessions.get(session.id); // parked/terminal here
188
- ```
189
-
190
- `settleConsistent: true` makes the iterator end exactly when `event.isRunSettled()`
191
- first fires; on a raw stream, call `event.isRunSettled()` yourself. What it
192
- guarantees: when the stream ends, a subsequent `aex.sessions.get(id)` reads a
193
- parked/terminal status and `session.outputs().list()` is complete. Outputs are
194
- uploaded before the terminal is broadcast, so they are readable the moment the
195
- stream ends.
196
-
197
- ## Temporary event archive links
198
-
199
- For terminal runs, `session.events().archiveLink(options?)` returns a temporary direct URL to `events.jsonl`, the same redacted customer-visible event export used by `session.events().download()`.
200
-
201
- ```ts
202
- const link = await session.events().archiveLink({ expiresIn: "1h" });
203
- const response = await fetch(link.url);
204
- const jsonl = await response.text();
205
- ```
206
-
207
- `expiresIn` accepts seconds or `"15m"`, `"1h"`, or `"1d"`; the default is `"1h"`. The URL is a reusable bearer URL until it expires, so treat it like a short-lived secret. Internal runtime, host, and provider diagnostics are not included in this export.
208
-
209
- ## Event shape
210
-
211
- Every read and stream surface yields one canonical shape — the versioned coordinator envelope, wrapped as an `AexEventView` with a populated non-optional `sequence` and the `is*()` guard methods. `sequence` is **sparse by design** (it encodes journal position, not a dense 0..N counter), so resume by cursor VALUE (`from: lastSequence`), never by integer adjacency. Tool-call START and RESULT events share a `data.id` join key, exposed as `event.toolCallId()` on those views. aex records raw runtime/provider payloads **after** secret redaction and structural sanitization, so the bytes you see never contain provider keys, MCP credentials, or runtime secrets supplied when the session was opened.
212
-
213
- ## Typed helpers
214
-
215
- Every event the SDK yields — the turn stream (`session.send()`),
216
- `session.events().list()`, `session.events().streamEnvelopes()`, and
217
- `RunResult.events` — carries a type-guard **method** for each standardized event
218
- type, so you branch on the event without importing a free function or writing a
219
- raw `event.type === "…"` compare:
220
-
221
- ```ts
222
- for await (const event of session.send("Continue the task.")) {
223
- if (event.isTextMessage()) {
224
- process.stdout.write(event.data.text); // `data.text` is typed `string` here
225
- } else if (event.isToolCallStart()) {
226
- console.log("tool:", event.data.name); // `data.name` is typed `string` here
227
- } else if (event.isRunError()) {
228
- console.error("run error");
229
- }
230
- }
231
- ```
232
-
233
- The full method set: `isRunStarted()`, `isRunFinished()`, `isRunError()`,
234
- `isRunTerminal()`, `isTextMessage()`, `isToolCallStart()`, `isToolCallResult()`,
235
- `isCustom()`, `isLog()`, `isEventChannel()`, `isRunSettled()`, and
236
- `isFromSource(source)`. All test the `type` (or `channel`/`source`) discriminant
237
- at runtime. `isTextMessage()`, `isToolCallStart()`, and `isToolCallResult()` are
238
- TS type predicates: inside the guarded branch `event.data` NARROWS to that event
239
- type's payload fields (e.g. `event.data.text` is typed `string`). Annotate a
240
- narrowed event with the exported `TextMessageEventView` / `ToolCallStartEventView`
241
- / `ToolCallResultEventView` types, or a raw event with `AexEventView`. Use
242
- `result.text` or `session.messages.all()` when you need assistant text without
243
- inspecting the event stream directly.
1
+ ---
2
+ title: Events
3
+ ---
4
+
5
+ # Events
6
+
7
+ aex runs agent sessions on the managed runtime. Sessions are **non-blocking**:
8
+ the managed runtime advances the agent while aex observes lifecycle state, maps
9
+ runtime output into one event shape, and persists every captured event. The SDK
10
+ and CLI observe the durable event timeline from aex — there is no in-process
11
+ tool-approval hook.
12
+
13
+ ## Two ways to consume events
14
+
15
+ A session's reads and streams are grouped under accessor sub-resources:
16
+ `session.events()` owns the event timeline, `session.messages()` owns the decoded
17
+ assistant text, and `session.outputs()` owns the captured files. Reach a verb by
18
+ chaining it off the accessor.
19
+
20
+ ```ts
21
+ // Pull a snapshot of every event captured so far.
22
+ const events = await session.events().list();
23
+ ```
24
+
25
+ ```ts
26
+ // Stream events as guarded `AexEventView`s: yields each event once, stops when
27
+ // the session parks. Backed by polling the aex events endpoint. Every surface —
28
+ // list(), stream(), streamEnvelopes() — yields the same `AexEventView` with a
29
+ // populated `sequence` and the `is*()` guard methods.
30
+ for await (const event of session.events().stream({ intervalMs: 1000 })) {
31
+ if (event.isTextMessage()) {
32
+ // ...
33
+ }
34
+ }
35
+ ```
36
+
37
+ For the canonical event envelope, use the coordinator WebSocket stream:
38
+
39
+ ```ts
40
+ for await (const event of session.events().streamEnvelopes({ from: 0 })) {
41
+ console.log(event.sequence, event.type, event.source);
42
+ }
43
+ ```
44
+
45
+ `session.events().streamEnvelopes()` uses a short-lived ticket minted by the hosted API, then subscribes directly to the per-session coordinator. Subscribe means read-from-cursor plus tail: reconnects resume from the last sequence.
46
+
47
+ ## Assistant text
48
+
49
+ To collect just the agent's assistant messages, use the `messages()` accessor —
50
+ `list()` returns every decoded `AssistantTextEntry` oldest-first, and
51
+ `last()`/`first()` return one entry (or `undefined` when empty). Read `.text`
52
+ for the string:
53
+
54
+ ```ts
55
+ const lastText = (await session.messages().last())?.text;
56
+ ```
57
+
58
+ Prefer `session.messages().list()` or the collected `result.messages` /
59
+ `result.text` fields for assistant text. Low-level event helpers remain exported
60
+ for callers that build custom collectors.
61
+
62
+ The CLI mirrors the same surface:
63
+
64
+ ```bash
65
+ aex events <session-id> --api-key … [--aex-url …] # snapshot (polling)
66
+ aex events <session-id> --follow [--timeout 8m] --api-key … [--aex-url …] # stream until the session parks (polling)
67
+ aex tail <session-id> [--json] [--filter <type|source>] [--logs] [--settle] [--timeout 8m] --api-key … # live, human-readable, over the WS envelope stream
68
+ aex inspect <session-id> [--json] [--filter <type|source>] [--logs] [--timeout 8m] --api-key … # one-shot full timeline + jump-to-failure + cost/usage
69
+ aex wait <session-id> [--timeout 8m] [--interval 2s] --api-key … # block, print final session
70
+ ```
71
+
72
+ `aex tail` and `aex inspect` consume the same coordinator WebSocket envelope
73
+ stream as `session.events().streamEnvelopes()` (replay-from-cursor + tail +
74
+ exactly-once resume), so they are the low-latency equivalents of
75
+ `events --follow`'s polling. `--json` is the raw-NDJSON escape hatch; `--filter`
76
+ keeps only the named AG-UI types (`TEXT_MESSAGE_CONTENT`, `TOOL_CALL_START`, …)
77
+ or sources (`agent`/`runtime`/…); a `RUN_ERROR` is surfaced as a jump-to-failure
78
+ line. `aex inspect` adds a header, a settle-consistent full timeline, and a
79
+ cost/usage footer. Both exit `0` parked cleanly / `1` error park / `3` timeout.
80
+ They need a global `WebSocket` (Bun or Node ≥ 22).
81
+
82
+ `aex wait` is the host mirror of `session.wait()`:
83
+ it polls until the session parks and prints the final `Session` record. Exit `0`
84
+ when the session parked cleanly (`idle`/`suspended`/`succeeded`), `1` for any
85
+ non-clean terminal outcome (`failed`/`timed_out`/`cancelled`), and `3` when
86
+ `--timeout` elapses first
87
+ (a `--timeout` on `events --follow` / `run --follow` uses the same exit-`3`
88
+ convention). Durations accept `ms`/`s`/`m`/`h` suffixes or a bare millisecond integer.
89
+
90
+ Both surfaces observe the same events. A subscriber attached after a session
91
+ message is accepted replays the events it missed, then continues live.
92
+
93
+ ## Session turn events
94
+
95
+ The canonical SDK session surface stops a turn on session lifecycle events, not
96
+ terminal run events:
97
+
98
+ ```ts
99
+ const session = await aex.openSession(config);
100
+ const turn = session.send("Continue the task.");
101
+
102
+ for await (const event of turn) {
103
+ console.log(event.sequence, event.type);
104
+ }
105
+
106
+ const result = await turn.done(); // status is the terminal outcome, e.g. "succeeded"
107
+ ```
108
+
109
+ The turn stream ends on the turn's park terminal: a resumable
110
+ `aex.session.idle` / `aex.session.suspended`, a terminal outcome
111
+ `aex.session.succeeded` / `.failed` / `.timed_out` / `.cancelled`, or the held
112
+ `aex.session.awaiting_approval`. (The bare `aex.session.error` park is retired —
113
+ a failed turn is `failed`, a wall-clock kill `timed_out`, a cancel `cancelled`.)
114
+ `aex.run(config)` is a convenience wrapper over the same flow: it opens a
115
+ session, sends `message` once, and returns the collected session turn. The
116
+ returned `runId` is the session id.
117
+
118
+ ## Per-token streaming (`outputMode: 'stream'`)
119
+
120
+ Assistant-text granularity is controlled by `outputMode` on `openSession` / `run`:
121
+
122
+ - **`buffered`** (the default) — the assistant text arrives coalesced as one
123
+ `TEXT_MESSAGE_CONTENT` event per message.
124
+ - **`stream`** — on a **streamable** provider you also receive per-token
125
+ `TEXT_MESSAGE_CONTENT` **deltas** as the model produces them, and a coalesced
126
+ final `TEXT_MESSAGE_CONTENT` (the archived twin) always follows. Streamable
127
+ providers today are Anthropic and the OpenAI-chat-shaped providers (DeepSeek,
128
+ OpenAI, Mistral, OpenRouter, Doubao / Doubao China).
129
+
130
+ Streaming is **capability-gated and fail-closed**: requesting `outputMode: 'stream'`
131
+ on a provider that has no streaming producer (currently `gemini`) is a typed
132
+ rejection at submission parse — **not** a silent downgrade to buffered. Choose a
133
+ streamable provider or drop back to `buffered` explicitly.
134
+
135
+ ## Terminal events vs. the run record
136
+
137
+ Two families of events can end a turn's stream, and which one you see depends
138
+ on how the turn ends:
139
+
140
+ - **AG-UI terminals** — `RUN_FINISHED` / `RUN_ERROR`. These are *render-complete*
141
+ signals emitted by the agent stream itself. On the managed plane a normal
142
+ session turn usually does **not** emit `RUN_FINISHED`: the session *parks*
143
+ instead (see below). Expect `RUN_ERROR` on stream-level failures, and treat
144
+ `RUN_FINISHED` — when it does appear — as a low-latency "stop the spinner"
145
+ hint, not a read-consistency barrier.
146
+ - **`aex.session.*` park terminals** — `CUSTOM` events carrying the turn's
147
+ OUTCOME: `aex.session.succeeded` / `aex.session.failed` / `aex.session.timed_out`
148
+ / `aex.session.cancelled`, or a resumable/held park `aex.session.idle` /
149
+ `aex.session.suspended` / `aex.session.awaiting_approval`. On the managed plane
150
+ these are what actually end a turn. The park event ends the RENDER; the settle
151
+ commit (cost + usage + the authoritative outcome) lands shortly after — which
152
+ is why `run()`/`done()` await settle by DEFAULT and return the outcome as
153
+ `result.status` with `result.costUsd`/`result.usage` always populated (never a
154
+ bare `idle`). Pass `await: 'park'` (on `run(...)` or `session.send(msg, {...})`)
155
+ to return early at the park event when you don't need cost/usage. `result.costUsd`
156
+ is aex runtime/storage spend and **excludes** your BYOK provider charges — price
157
+ those from `result.usage` token counts. Ending a raw stream on the park event is
158
+ fine for live UI; read the settled record (or the default await-settle result)
159
+ when you need cost/usage/outcome.
160
+
161
+ Each event the stream yields carries method guards that cover both families so
162
+ you never have to switch on the plane:
163
+
164
+ - `event.isRunTerminal()` — true for the AG-UI `RUN_FINISHED` / `RUN_ERROR` pair.
165
+ - `event.isRunSettled()` — true for the `aex.run.settled` settle barrier **and**
166
+ for any `aex.session.*` park terminal. The managed plane does not broadcast a
167
+ separate `aex.run.settled` barrier — the park event plays that role — so
168
+ `event.isRunSettled()` is the one check that reliably means "this stream is done
169
+ and the record is authoritative".
170
+
171
+ To read the authoritative status consistently, use one of:
172
+
173
+ ```ts
174
+ // Session record path: send a turn, then wait for the session to park.
175
+ const session = await aex.openSession(config);
176
+ await session.send("Continue the task.").done();
177
+ const record = await session.wait(); // the parked session record
178
+ ```
179
+
180
+ ```ts
181
+ // Live events AND a settle-consistent end: the iterator ends on the settle
182
+ // barrier OR the aex.session.* park terminal, whichever the plane emits —
183
+ // so when it ends, the session record is already parked/terminal.
184
+ for await (const event of session.events().streamEnvelopes({ settleConsistent: true })) {
185
+ // render events live…
186
+ }
187
+ const settled = await aex.sessions.get(session.id); // parked/terminal here
188
+ ```
189
+
190
+ `settleConsistent: true` makes the iterator end exactly when `event.isRunSettled()`
191
+ first fires; on a raw stream, call `event.isRunSettled()` yourself. What it
192
+ guarantees: when the stream ends, a subsequent `aex.sessions.get(id)` reads a
193
+ parked/terminal status and `session.outputs().list()` is complete. Outputs are
194
+ uploaded before the terminal is broadcast, so they are readable the moment the
195
+ stream ends.
196
+
197
+ ## Temporary event archive links
198
+
199
+ For terminal runs, `session.events().archiveLink(options?)` returns a temporary direct URL to `events.jsonl`, the same redacted customer-visible event export used by `session.events().download()`.
200
+
201
+ ```ts
202
+ const link = await session.events().archiveLink({ expiresIn: "1h" });
203
+ const response = await fetch(link.url);
204
+ const jsonl = await response.text();
205
+ ```
206
+
207
+ `expiresIn` accepts seconds or `"15m"`, `"1h"`, or `"1d"`; the default is `"1h"`. The URL is a reusable bearer URL until it expires, so treat it like a short-lived secret. Internal runtime, host, and provider diagnostics are not included in this export.
208
+
209
+ ## Event shape
210
+
211
+ Every read and stream surface yields one canonical shape — the versioned coordinator envelope, wrapped as an `AexEventView` with a populated non-optional `sequence` and the `is*()` guard methods. `sequence` is **sparse by design** (it encodes journal position, not a dense 0..N counter), so resume by cursor VALUE (`from: lastSequence`), never by integer adjacency. Tool-call START and RESULT events share a `data.id` join key, exposed as `event.toolCallId()` on those views. aex records raw runtime/provider payloads **after** secret redaction and structural sanitization, so the bytes you see never contain provider keys, MCP credentials, or runtime secrets supplied when the session was opened.
212
+
213
+ ## Typed helpers
214
+
215
+ Every event the SDK yields — the turn stream (`session.send()`),
216
+ `session.events().list()`, `session.events().streamEnvelopes()`, and
217
+ `RunResult.events` — carries a type-guard **method** for each standardized event
218
+ type, so you branch on the event without importing a free function or writing a
219
+ raw `event.type === "…"` compare:
220
+
221
+ ```ts
222
+ for await (const event of session.send("Continue the task.")) {
223
+ if (event.isTextMessage()) {
224
+ process.stdout.write(event.data.text); // `data.text` is typed `string` here
225
+ } else if (event.isToolCallStart()) {
226
+ console.log("tool:", event.data.name); // `data.name` is typed `string` here
227
+ } else if (event.isRunError()) {
228
+ console.error("run error");
229
+ }
230
+ }
231
+ ```
232
+
233
+ The full method set: `isRunStarted()`, `isRunFinished()`, `isRunError()`,
234
+ `isRunTerminal()`, `isTextMessage()`, `isToolCallStart()`, `isToolCallResult()`,
235
+ `isCustom()`, `isLog()`, `isEventChannel()`, `isRunSettled()`, and
236
+ `isFromSource(source)`. All test the `type` (or `channel`/`source`) discriminant
237
+ at runtime. `isTextMessage()`, `isToolCallStart()`, and `isToolCallResult()` are
238
+ TS type predicates: inside the guarded branch `event.data` NARROWS to that event
239
+ type's payload fields (e.g. `event.data.text` is typed `string`). Annotate a
240
+ narrowed event with the exported `TextMessageEventView` / `ToolCallStartEventView`
241
+ / `ToolCallResultEventView` types, or a raw event with `AexEventView`. Use
242
+ `result.text` or `session.messages.all()` when you need assistant text without
243
+ inspecting the event stream directly.