@aexhq/sdk 0.40.5 → 0.40.6
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/README.md +159 -159
- package/dist/_contracts/api-key.js +0 -1
- package/dist/_contracts/bundle-manifest.js +0 -1
- package/dist/_contracts/connection-ticket.js +0 -1
- package/dist/_contracts/error-codes.js +0 -1
- package/dist/_contracts/error-factory.js +0 -1
- package/dist/_contracts/event-envelope.js +0 -1
- package/dist/_contracts/event-stream-client.js +0 -1
- package/dist/_contracts/event-view.js +0 -1
- package/dist/_contracts/http.js +0 -1
- package/dist/_contracts/index.js +0 -1
- package/dist/_contracts/internal.d.ts +36 -0
- package/dist/_contracts/internal.js +155 -6
- package/dist/_contracts/models.js +0 -1
- package/dist/_contracts/operations.js +1 -2
- package/dist/_contracts/post-hook.js +0 -1
- package/dist/_contracts/provider-support.js +0 -1
- package/dist/_contracts/run-artifacts.js +0 -1
- package/dist/_contracts/run-config.js +0 -1
- package/dist/_contracts/run-cost.js +0 -1
- package/dist/_contracts/run-custody.js +0 -1
- package/dist/_contracts/run-record.js +0 -1
- package/dist/_contracts/run-retention.js +0 -1
- package/dist/_contracts/run-trace.js +0 -1
- package/dist/_contracts/run-unit.js +0 -1
- package/dist/_contracts/runner-event.js +0 -1
- package/dist/_contracts/runtime-manifest.js +0 -1
- package/dist/_contracts/runtime-security-profile.js +0 -1
- package/dist/_contracts/runtime-sizes.js +0 -1
- package/dist/_contracts/runtime-types.js +0 -1
- package/dist/_contracts/sdk-errors.js +0 -1
- package/dist/_contracts/sdk-secrets.js +0 -1
- package/dist/_contracts/side-effect-audit.js +0 -1
- package/dist/_contracts/sse.js +0 -0
- package/dist/_contracts/stable.js +0 -1
- package/dist/_contracts/status.js +0 -1
- package/dist/_contracts/submission.js +0 -1
- package/dist/_contracts/suggest.js +0 -1
- package/dist/_contracts/webhook-verify.js +0 -1
- package/dist/asset-upload.d.ts +3 -43
- package/dist/asset-upload.js +1 -109
- package/dist/asset-upload.js.map +1 -1
- package/dist/cli.mjs +354 -781
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/fetch-archive.js +76 -16
- package/dist/fetch-archive.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/authentication.md +125 -125
- package/docs/billing.md +118 -118
- package/docs/concepts/agent-tools.md +66 -66
- package/docs/concepts/composition.md +37 -37
- package/docs/concepts/providers-and-runtimes.md +54 -54
- package/docs/concepts/runs.md +49 -49
- package/docs/concepts/subagents.md +88 -88
- package/docs/credentials.md +125 -125
- package/docs/defaults.md +64 -64
- package/docs/errors.md +196 -196
- package/docs/events.md +243 -243
- package/docs/limits-and-quotas.md +144 -144
- package/docs/limits.md +50 -50
- package/docs/mcp.md +44 -44
- package/docs/networking.md +163 -163
- package/docs/outputs.md +267 -267
- package/docs/public-surface.json +77 -77
- package/docs/quickstart.md +119 -119
- package/docs/release.md +38 -38
- package/docs/retries.md +129 -129
- package/docs/run-config.md +58 -58
- package/docs/run-record.md +55 -55
- package/docs/secrets.md +125 -130
- package/docs/testing.md +35 -35
- package/docs/vision-skills.md +91 -91
- package/docs/webhooks.md +127 -131
- package/examples/feature-tour.ts +282 -282
- package/examples/spike-settle-latency.ts +125 -125
- package/package.json +1 -1
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.
|