experimental-a2 0.6.0 → 0.7.0
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/CHANGELOG.md +22 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +8 -7
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +334 -2
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1 -1
- package/dist/client.d.ts +202 -2
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1025 -1
- package/dist/client.js.map +1 -0
- package/dist/errors-BQuJpe82.js.map +1 -1
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
- package/dist/internal-DRXJ56EI.js.map +1 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.js +4 -5
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +4 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Duw6MVlB.js → server-286j79Mt.js} +708 -79
- package/dist/server-286j79Mt.js.map +1 -0
- package/dist/{server-DpvjhdoE.d.ts → server-DgXmORIq.d.ts} +67 -49
- package/dist/server-DgXmORIq.d.ts.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/{store-DysUkTH3.d.ts → store-flRz1OWh.d.ts} +2 -57
- package/dist/store-flRz1OWh.d.ts.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +1 -59
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
- package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +1 -81
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DEYO8Ryv.js} +48 -134
- package/dist/store-redis-core-DEYO8Ryv.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +2 -3
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +3 -4
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +1 -72
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
- package/dist/wire-B6te_wns.js.map +1 -0
- package/docs/guides/03-react.mdx +20 -28
- package/docs/guides/05-production.mdx +9 -11
- package/docs/guides/06-ai-agents.mdx +8 -14
- package/docs/guides/09-presence.mdx +14 -19
- package/docs/guides/10-transports.mdx +104 -86
- package/docs/reference/01-api.mdx +143 -277
- package/docs/reference/02-errors.mdx +5 -7
- package/package.json +1 -14
- package/src/ai-server.ts +9 -5
- package/src/client.ts +2 -2
- package/src/errors.ts +1 -0
- package/src/internal.ts +3 -62
- package/src/push-envelope.ts +24 -21
- package/src/scheduler-qstash.ts +3 -3
- package/src/scheduler-vercel.ts +2 -2
- package/src/server-fetch.ts +344 -0
- package/src/server.ts +73 -225
- package/src/session-socket.ts +36 -20
- package/src/sse.ts +2 -2
- package/src/store-memory.ts +0 -81
- package/src/store-postgres.ts +0 -100
- package/src/store-redis-core.ts +47 -211
- package/src/store-redis-http.ts +0 -1
- package/src/store-redis.ts +0 -1
- package/src/store-sqlite.ts +0 -119
- package/src/store.ts +0 -60
- package/src/wire.ts +2 -1
- package/dist/ai-D_PGS-JR.d.ts +0 -334
- package/dist/ai-D_PGS-JR.d.ts.map +0 -1
- package/dist/cli-B3VuxoDe.js +0 -599
- package/dist/cli-B3VuxoDe.js.map +0 -1
- package/dist/cli-bin.d.ts +0 -1
- package/dist/cli-bin.js +0 -7
- package/dist/cli-bin.js.map +0 -1
- package/dist/cli.d.ts +0 -20
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client-CdMqi7mC.d.ts +0 -202
- package/dist/client-CdMqi7mC.d.ts.map +0 -1
- package/dist/client-Dj5d3SP_.js +0 -1026
- package/dist/client-Dj5d3SP_.js.map +0 -1
- package/dist/devtools-J_jZ2vQf.d.ts +0 -152
- package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
- package/dist/devtools-kJJaORn-.js +0 -340
- package/dist/devtools-kJJaORn-.js.map +0 -1
- package/dist/devtools-server.browser.d.ts +0 -1
- package/dist/devtools-server.browser.js +0 -6
- package/dist/devtools-server.browser.js.map +0 -1
- package/dist/devtools-server.d.ts +0 -23
- package/dist/devtools-server.d.ts.map +0 -1
- package/dist/devtools-server.js +0 -1270
- package/dist/devtools-server.js.map +0 -1
- package/dist/devtools.d.ts +0 -2
- package/dist/devtools.js +0 -2
- package/dist/errors-W6nwJ-fm.d.ts +0 -21
- package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
- package/dist/http.d.ts +0 -151
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -706
- package/dist/http.js.map +0 -1
- package/dist/inspection-DaxB5jM2.js +0 -13
- package/dist/inspection-DaxB5jM2.js.map +0 -1
- package/dist/internal-DstsI6Re.js.map +0 -1
- package/dist/platform-B4TnJtWu.js +0 -34
- package/dist/platform-B4TnJtWu.js.map +0 -1
- package/dist/server-DpvjhdoE.d.ts.map +0 -1
- package/dist/server-Duw6MVlB.js.map +0 -1
- package/dist/store-DysUkTH3.d.ts.map +0 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
- package/dist/testing.browser.d.ts +0 -1
- package/dist/testing.browser.js +0 -6
- package/dist/testing.browser.js.map +0 -1
- package/dist/testing.d.ts +0 -32
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -103
- package/dist/testing.js.map +0 -1
- package/dist/wire-BFQmSJ-9.js.map +0 -1
- package/docs/guides/07-devtools.mdx +0 -229
- package/src/cli-bin.ts +0 -5
- package/src/cli.ts +0 -1046
- package/src/devtools-app.ts +0 -989
- package/src/devtools-server.browser.ts +0 -5
- package/src/devtools-server.ts +0 -604
- package/src/devtools.ts +0 -716
- package/src/http.ts +0 -394
- package/src/inspection.ts +0 -39
- package/src/testing.browser.ts +0 -5
- package/src/testing.ts +0 -185
|
@@ -72,7 +72,6 @@ createServer(options: {
|
|
|
72
72
|
store?: A2Store // default: sqlite in dev, memory in tests, required in prod
|
|
73
73
|
scheduler?: A2Scheduler
|
|
74
74
|
telemetry?: A2Telemetry // optional instrumentation; see experimental-a2/otel
|
|
75
|
-
validatePush?: (context: PushValidationContext) => void | PromiseLike<void>
|
|
76
75
|
presence?: { ttlMs?: number } // presence expiry policy; default 60s
|
|
77
76
|
handlers?: {
|
|
78
77
|
[type]:
|
|
@@ -123,19 +122,6 @@ Server-only by construction: `experimental-a2/server` is the only entry point th
|
|
|
123
122
|
can reach a store backend, and its exports map resolves to a loud error
|
|
124
123
|
under the browser condition.
|
|
125
124
|
|
|
126
|
-
`validatePush(context)` runs only for input that arrived over the wire
|
|
127
|
-
through `handle`'s push lane, once per plane. The events plane invokes it with `{ sessionId, events }` before
|
|
128
|
-
contract schema validation and before the store append, so throwing rejects the
|
|
129
|
-
complete push without writing anything. The presence plane invokes it with
|
|
130
|
-
`{ sessionId, events: [], presence }`, the whole pushed patch with its
|
|
131
|
-
participant, before field validation and the broadcast, so authorizing the
|
|
132
|
-
participant id (and applying any size or cardinality policy) happens at the
|
|
133
|
-
same seam. The patch arrives frozen: authorize, don't rewrite (a mutation
|
|
134
|
-
attempt throws and fails the push). Direct trusted server appends, handler appends, and server-side
|
|
135
|
-
`setPresence` bypass it. The envelope parser inside `handle` creates
|
|
136
|
-
the runtime provenance brand after reading the envelope; a caller-supplied
|
|
137
|
-
field with the same name is ignored, and the brand is not stored in the log.
|
|
138
|
-
|
|
139
125
|
`presence.ttlMs` sets how long a presence value survives without a
|
|
140
126
|
refreshing set (default 60 seconds, as a positive integer of
|
|
141
127
|
milliseconds). Expiry counts on the storage's own clock, never on the
|
|
@@ -203,6 +189,136 @@ retries may move the frontier between them. A generic join should use a
|
|
|
203
189
|
monotone readiness predicate and a stable explicit output event `id`, so every
|
|
204
190
|
eligible attempt converges on the same append.
|
|
205
191
|
|
|
192
|
+
### `server.fetch(request, options?)`
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
server.fetch(
|
|
196
|
+
request: Request,
|
|
197
|
+
options?: {
|
|
198
|
+
authorize?: (operation: A2Operation) => boolean | Promise<boolean>
|
|
199
|
+
upgradeWebSocket?: (
|
|
200
|
+
attach: (socket: A2Socket) => void,
|
|
201
|
+
) => Response | Promise<Response>
|
|
202
|
+
},
|
|
203
|
+
): Promise<Response>
|
|
204
|
+
|
|
205
|
+
type A2Operation =
|
|
206
|
+
| {
|
|
207
|
+
type: 'stream'
|
|
208
|
+
sessionId: string
|
|
209
|
+
startAfter: number
|
|
210
|
+
transport: 'http' | 'websocket'
|
|
211
|
+
}
|
|
212
|
+
| {
|
|
213
|
+
type: 'history'
|
|
214
|
+
sessionId: string
|
|
215
|
+
gte: number
|
|
216
|
+
lte: number
|
|
217
|
+
transport: 'http'
|
|
218
|
+
}
|
|
219
|
+
| {
|
|
220
|
+
type: 'push'
|
|
221
|
+
sessionId: string
|
|
222
|
+
events: readonly {
|
|
223
|
+
type: ContractEventType | string
|
|
224
|
+
payload: unknown
|
|
225
|
+
id?: string
|
|
226
|
+
}[]
|
|
227
|
+
presence?: {
|
|
228
|
+
participant: string
|
|
229
|
+
values: Readonly<Record<string, unknown>>
|
|
230
|
+
seen?: number
|
|
231
|
+
at?: number
|
|
232
|
+
}
|
|
233
|
+
transport: 'http' | 'websocket'
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The bound Fetch API route for this server. An unprotected route can export it
|
|
238
|
+
directly:
|
|
239
|
+
|
|
240
|
+
```ts app/api/public-order-events/route.ts
|
|
241
|
+
import { ordersServer } from '@/server/orders'
|
|
242
|
+
|
|
243
|
+
export const GET = ordersServer.fetch
|
|
244
|
+
export const POST = ordersServer.fetch
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
A plain GET streams one session over SSE, resumed after the `index` query
|
|
248
|
+
parameter. A GET with `gte` and `lte` returns the closed history range as JSON
|
|
249
|
+
and sets `a2-history-covered` to `true` when every requested index was returned.
|
|
250
|
+
POST accepts `{ sessionId, events, presence? }` and returns the appended events.
|
|
251
|
+
Presence is inferred from the contract. A contract that declares presence
|
|
252
|
+
streams it and accepts presence pushes automatically.
|
|
253
|
+
|
|
254
|
+
`authorize` runs after the request or socket frame has been parsed, but before
|
|
255
|
+
contract schema validation, presence validation, or I/O. Event names and
|
|
256
|
+
presence field names are typed from the contract. Their unvalidated values stay
|
|
257
|
+
`unknown`. Return `false` to deny the operation with `FORBIDDEN`. A thrown error
|
|
258
|
+
becomes `STORE_UNAVAILABLE`, because authorization infrastructure failed rather
|
|
259
|
+
than denied access. Operation objects, push events, event arrays, and presence
|
|
260
|
+
patches are frozen. Authorize them, do not rewrite them.
|
|
261
|
+
|
|
262
|
+
Authentication stays outside A2, where the route can read headers, cookies, and
|
|
263
|
+
framework context. Capture the authenticated principal in `authorize`:
|
|
264
|
+
|
|
265
|
+
```ts app/api/protected-order-events/route.ts
|
|
266
|
+
import { ordersServer } from '@/server/orders'
|
|
267
|
+
|
|
268
|
+
async function fetchOrders(request: Request): Promise<Response> {
|
|
269
|
+
// your authentication:
|
|
270
|
+
const user = { id: 'user-1' }
|
|
271
|
+
if (!user) return new Response(null, { status: 401 })
|
|
272
|
+
|
|
273
|
+
return ordersServer.fetch(request, {
|
|
274
|
+
authorize(operation) {
|
|
275
|
+
// your per-session and per-operation authorization:
|
|
276
|
+
// return canAccess(user, operation)
|
|
277
|
+
return true
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export const GET = fetchOrders
|
|
283
|
+
export const POST = fetchOrders
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Direct `session.append`, handler appends, scheduled appends, and server-side
|
|
287
|
+
`setPresence` bypass `authorize`. It is an ingress policy for this fetch call,
|
|
288
|
+
not a global server policy.
|
|
289
|
+
|
|
290
|
+
A GET with `Upgrade: websocket` uses `upgradeWebSocket`. Without that option it
|
|
291
|
+
returns 426. The upgrade function receives an `attach` callback for the
|
|
292
|
+
platform socket:
|
|
293
|
+
|
|
294
|
+
```ts app/api/socket-order-events/route.ts
|
|
295
|
+
import { experimental_upgradeWebSocket } from '@vercel/functions'
|
|
296
|
+
import { ordersServer } from '@/server/orders'
|
|
297
|
+
|
|
298
|
+
export const POST = ordersServer.fetch
|
|
299
|
+
|
|
300
|
+
export function GET(request: Request): Promise<Response> {
|
|
301
|
+
return ordersServer.fetch(request, {
|
|
302
|
+
upgradeWebSocket: (attach) =>
|
|
303
|
+
experimental_upgradeWebSocket(attach, {
|
|
304
|
+
maxPayload: 4 * 1024 * 1024,
|
|
305
|
+
}),
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Authenticate the physical WebSocket GET before calling `server.fetch`. Then
|
|
311
|
+
use `authorize` for each session subscribe and each event or presence push.
|
|
312
|
+
There is no WebSocket-upgrade operation in `A2Operation`. A denied subscribe
|
|
313
|
+
receives `unsubscribed` and does not affect other sessions. A denied event push
|
|
314
|
+
receives a `FORBIDDEN` ack. Denied presence is silently dropped because it is
|
|
315
|
+
fire-and-forget. Failed presence authorization is dropped the same way and does
|
|
316
|
+
not disconnect unrelated sessions on the multiplexed socket.
|
|
317
|
+
|
|
318
|
+
SSE and WebSocket lifetimes use the platform's ambient invocation deadline.
|
|
319
|
+
A2 also uses the platform's ambient `waitUntil` capability for background work.
|
|
320
|
+
Neither capability is reordered or passed through `server.fetch` options.
|
|
321
|
+
|
|
206
322
|
### `server.session(id)`
|
|
207
323
|
|
|
208
324
|
```ts
|
|
@@ -372,9 +488,9 @@ Custom scheduler adapters implement two methods:
|
|
|
372
488
|
| `schedule(task: SchedulerTask): Promise<void>` | put one versioned task on durable delivery infrastructure |
|
|
373
489
|
| `handler(...servers)` | return the route that receives drain and append tasks |
|
|
374
490
|
|
|
375
|
-
`handler`
|
|
376
|
-
|
|
377
|
-
|
|
491
|
+
Application routes call `scheduler.handler(...servers)` on the configured
|
|
492
|
+
adapter. The handler requires at least one drainable server and rejects
|
|
493
|
+
duplicate contract names synchronously.
|
|
378
494
|
|
|
379
495
|
`SchedulerTask` is `SchedulerDrainTask | SchedulerAppendTask`. These task types,
|
|
380
496
|
plus `ScheduledEvent`, are exported from `experimental-a2/server`.
|
|
@@ -429,7 +545,7 @@ the contract's schemas before anything is written. A multi-event append
|
|
|
429
545
|
is atomic: all-or-nothing, consecutive positions, one transaction. Pass
|
|
430
546
|
`id` to make an append idempotent across retries; re-sending an
|
|
431
547
|
identical batch returns the original rows. (Events that arrived through
|
|
432
|
-
`
|
|
548
|
+
`server.fetch`'s push lane are accepted directly.) See
|
|
433
549
|
[Durability](/concepts/durability).
|
|
434
550
|
|
|
435
551
|
After the write, A2 dispatches only event types with registered handlers.
|
|
@@ -537,7 +653,7 @@ session.stream(options: {
|
|
|
537
653
|
|
|
538
654
|
A live feed of the session's events. `startAfter` is a non-negative safe integer;
|
|
539
655
|
`startAfter: 20` begins with event 21.
|
|
540
|
-
Server-side only; `
|
|
656
|
+
Server-side only; `server.fetch` exposes it over SSE, and over the multiplexed
|
|
541
657
|
socket when `upgrade` is set. Subscribing never dispatches handlers.
|
|
542
658
|
|
|
543
659
|
With `presence: true`, the feed yields one `PresenceSnapshot` first:
|
|
@@ -581,7 +697,7 @@ the storage clock, not the sender stamp, so a hostile stamp can only
|
|
|
581
697
|
vandalize its own field and still expires on schedule.
|
|
582
698
|
|
|
583
699
|
The patch is structurally the `presence` sibling of the push envelope;
|
|
584
|
-
`
|
|
700
|
+
`server.fetch`'s push lane forwards it whole to `session.setPresence(presence)`.
|
|
585
701
|
The API mirrors the wire.
|
|
586
702
|
|
|
587
703
|
The handler-scoped form `ctx.session.setPresence(...)` is the same
|
|
@@ -707,9 +823,7 @@ Both members exist only when the contract declares `presence`; their
|
|
|
707
823
|
value and field types come from its schemas, through the reducer, with
|
|
708
824
|
no type arguments (the reducer is the client's typed handle on the
|
|
709
825
|
contract; it still never folds presence). `setPresence` requires a
|
|
710
|
-
`participant`: the client's default, or the provider's override.
|
|
711
|
-
the map holds only your own echo with
|
|
712
|
-
participants active, the GET route forgot `presence: true`. See
|
|
826
|
+
`participant`: the client's default, or the provider's override. See
|
|
713
827
|
[Presence](/guides/presence).
|
|
714
828
|
|
|
715
829
|
`push` appends optimistically: validated locally, rolled back on
|
|
@@ -1045,17 +1159,10 @@ it into `createServer({ handlers })` beside application handlers when you need
|
|
|
1045
1159
|
a custom assembly. The table handles input facts, generation requests, model
|
|
1046
1160
|
step completion, tool calls, approval responses, and terminal tool results.
|
|
1047
1161
|
Application handlers spread later can deliberately replace a built-in
|
|
1048
|
-
handler.
|
|
1049
|
-
`
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
`validateAgentPush({ sessionId, events }): void`
|
|
1054
|
-
|
|
1055
|
-
Accepts the browser interaction allowlist: user messages, approval and input
|
|
1056
|
-
responses, interruptions, and explicit retries. It rejects server-authored
|
|
1057
|
-
scheduling and lifecycle events, seeded non-user messages, and input requests.
|
|
1058
|
-
`createAgentServer()` installs it automatically.
|
|
1162
|
+
handler. A custom assembly owns its browser ingress policy. Use
|
|
1163
|
+
`server.fetch(request, { authorize })` to reject server-authored AI event names
|
|
1164
|
+
before they reach the server. `createAgentServer()` installs the built-in
|
|
1165
|
+
browser allowlist automatically.
|
|
1059
1166
|
|
|
1060
1167
|
See [Durable AI agents](/guides/ai-agents) for the protocol and complete
|
|
1061
1168
|
examples.
|
|
@@ -1106,96 +1213,6 @@ frontier. Idle handles are evicted after `gcTime`. This memory layer is
|
|
|
1106
1213
|
separate from `experimental-a2/cache-indexeddb`: memory preserves identity
|
|
1107
1214
|
across route transitions, while IndexedDB preserves the replica across reloads.
|
|
1108
1215
|
|
|
1109
|
-
## `experimental-a2/http`
|
|
1110
|
-
|
|
1111
|
-
### `handle(server, options?)`
|
|
1112
|
-
|
|
1113
|
-
```ts
|
|
1114
|
-
handle(server: A2Server, options?: {
|
|
1115
|
-
before?(args: { request: Request; intent: A2Intent }):
|
|
1116
|
-
Response | undefined | void | Promise<Response | undefined | void>
|
|
1117
|
-
after?(args: { request: Request; intent: A2Intent; outcome: A2Outcome; response: Response }):
|
|
1118
|
-
Response | undefined | void | Promise<Response | undefined | void>
|
|
1119
|
-
upgrade?: UpgradeFn // e.g. (attach) => experimental_upgradeWebSocket(attach)
|
|
1120
|
-
presence?: boolean // interleave presence on every stream lane
|
|
1121
|
-
deadline?: number // epoch ms: close sockets cleanly before it
|
|
1122
|
-
}): { GET(req: Request): Promise<Response>; POST(req: Request): Promise<Response> }
|
|
1123
|
-
|
|
1124
|
-
type A2Intent =
|
|
1125
|
-
| { type: 'ws-upgrade' }
|
|
1126
|
-
| { type: 'stream'; sessionId: string; startAfter: number; transport: 'sse' | 'ws' }
|
|
1127
|
-
| { type: 'history'; sessionId: string; gte: number; lte: number }
|
|
1128
|
-
| { type: 'push'; sessionId: string; events: PushedEvent[]; presence?: PushedPresence; transport: 'http' | 'ws' }
|
|
1129
|
-
|
|
1130
|
-
type A2Outcome =
|
|
1131
|
-
| { type: 'stream' }
|
|
1132
|
-
| { type: 'history'; covered: boolean; events: Event[] }
|
|
1133
|
-
| { type: 'push'; appended: Event[] }
|
|
1134
|
-
```
|
|
1135
|
-
|
|
1136
|
-
The session route pair as one call: `export const { GET, POST } =
|
|
1137
|
-
handle(server)` in a route module (any framework speaking
|
|
1138
|
-
`(req: Request) => Promise<Response>`). A plain `GET` is the live SSE
|
|
1139
|
-
stream, resumed after the `index` query parameter, with a `: connected`
|
|
1140
|
-
prelude, a `: ping` heartbeat every 15s, and a clean close one second
|
|
1141
|
-
before an ambient Vercel invocation deadline when available; presence
|
|
1142
|
-
patches ride as named frames when `presence: true`. A `GET` with
|
|
1143
|
-
`gte`/`lte` query parameters is a history slice: the closed log range
|
|
1144
|
-
as JSON wire events, the read `loadHistory` rides. `POST` is the push
|
|
1145
|
-
envelope `{ sessionId, events, presence? }`, answered with the appended
|
|
1146
|
-
events. A `GET` carrying an upgrade header becomes the multiplexed
|
|
1147
|
-
WebSocket when `options.upgrade` is present, and answers `426` when it
|
|
1148
|
-
is not.
|
|
1149
|
-
|
|
1150
|
-
Parsing is protocol, hooks are policy. A request that fails to parse
|
|
1151
|
-
(missing `sessionId`, malformed bounds, a bad push envelope) answers
|
|
1152
|
-
`INVALID_PAYLOAD` on the wire before any hook runs. `before` sees every
|
|
1153
|
-
parsed intent, HTTP requests and socket frames alike; over the socket,
|
|
1154
|
-
each subscribe arrives as a `stream` intent and each push as a `push`
|
|
1155
|
-
intent, with `request` always the original upgrade Request. Returning a
|
|
1156
|
-
Response short-circuits: over HTTP it is the response, verbatim; over
|
|
1157
|
-
the socket it is translated into the wire's own vocabulary (a denied
|
|
1158
|
-
subscribe answers `unsubscribed`, a denied push a non-retryable error
|
|
1159
|
-
ack), because a Response cannot cross a socket.
|
|
1160
|
-
|
|
1161
|
-
`after` runs only where the library produced an HTTP response: never
|
|
1162
|
-
after a short-circuit, never for `ws-upgrade` or socket frames. It may
|
|
1163
|
-
mutate `response.headers` in place or return a replacement Response.
|
|
1164
|
-
`outcome.covered` on a history read means the closed range came back
|
|
1165
|
-
fully covered (`events.length === lte - gte + 1`): an immutable slice
|
|
1166
|
-
of the append-only log, safe to cache under whatever policy your
|
|
1167
|
-
`after` applies. The history response carries no cache headers of its
|
|
1168
|
-
own.
|
|
1169
|
-
|
|
1170
|
-
The socket is one connection for all of a client's sessions:
|
|
1171
|
-
`subscribe`/`unsubscribe` frames open and close per-session lanes at
|
|
1172
|
-
their own resume frontiers, `sessionId` tags route pushes, presence,
|
|
1173
|
-
and acks, and a lane ending or failing answers `unsubscribed` without
|
|
1174
|
-
taking the socket down. See [Transports](/guides/transports).
|
|
1175
|
-
|
|
1176
|
-
### The rest of the entry
|
|
1177
|
-
|
|
1178
|
-
| Helper | What it does |
|
|
1179
|
-
| ----------------------- | ------------------------------------------------------------------------------ |
|
|
1180
|
-
| `schedulerHandler(...servers)` | returns the delivery route after synchronously verifying one shared scheduler |
|
|
1181
|
-
| `errorResponse(err)` | serializes an `A2Error` to `{ error: { code, message, details } }` + status; the natural return value of a refusing `before` hook |
|
|
1182
|
-
| `deserializeError(body)` | rebuilds an `A2Error` from a wire body, or `null` if the body isn't one |
|
|
1183
|
-
|
|
1184
|
-
`schedulerHandler(...servers)` is the application-facing scheduler route. It
|
|
1185
|
-
requires at least one A2 server. Every server must have a scheduler, use the
|
|
1186
|
-
exact same scheduler instance, and have a unique contract name. Non-A2 values
|
|
1187
|
-
also fail. These checks throw before the request handler is returned, so bad
|
|
1188
|
-
wiring fails when the route module loads. The helper then delegates delivery
|
|
1189
|
-
to the shared adapter's `A2Scheduler.handler(...)` method.
|
|
1190
|
-
|
|
1191
|
-
Different scheduler instances use different routes. Match each QStash route to
|
|
1192
|
-
that instance's resolved `url`; additional QStash routes pass an explicit
|
|
1193
|
-
`url`. Match each Vercel Queues route and trigger to that instance's `topic`.
|
|
1194
|
-
|
|
1195
|
-
`errorResponse` and `deserializeError` are the `A2Error` wire format
|
|
1196
|
-
that `push` and the push lane share. See
|
|
1197
|
-
[Errors](/reference/errors#over-the-wire).
|
|
1198
|
-
|
|
1199
1216
|
## `experimental-a2/cache-indexeddb`
|
|
1200
1217
|
|
|
1201
1218
|
```ts
|
|
@@ -1273,112 +1290,6 @@ log.
|
|
|
1273
1290
|
renames and additions are breaking for dashboards, and are called out in the
|
|
1274
1291
|
package's `CHANGELOG.md`.
|
|
1275
1292
|
|
|
1276
|
-
## `experimental-a2/devtools`
|
|
1277
|
-
|
|
1278
|
-
The shared, isomorphic protocol for browser Devtools, the CLI, and `.a2log`
|
|
1279
|
-
captures.
|
|
1280
|
-
|
|
1281
|
-
| Export | Contract |
|
|
1282
|
-
| --- | --- |
|
|
1283
|
-
| `DEVTOOLS_PROTOCOL_VERSION` | Current HTTP wire protocol version |
|
|
1284
|
-
| `DEVTOOLS_CAPTURE_VERSION` | Current `.a2log` record format version |
|
|
1285
|
-
| `DEVTOOLS_CAPTURE_MEDIA_TYPE` | `application/x-ndjson` |
|
|
1286
|
-
| `DEVTOOLS_SESSION_PAGE_LIMIT` | Default event count requested per session page |
|
|
1287
|
-
| `DEVTOOLS_SESSION_PAGE_MAX_LIMIT` | Maximum event count accepted per session page |
|
|
1288
|
-
| `DEVTOOLS_CAPABILITIES` | Protocol resources and capture format advertised by the server |
|
|
1289
|
-
| `devtoolsSessionRevision({ events, snapshots })` | Compute the lifecycle invalidation token after assembling pages |
|
|
1290
|
-
| `encodeDevtoolsCapture(detail, { capturedAt? })` | Encode one session detail as exact NDJSON and add its SHA-256 footer |
|
|
1291
|
-
| `parseDevtoolsCapture(value)` | Parse NDJSON text or bytes into a `DevtoolsCapture` while retaining unknown fields on known records |
|
|
1292
|
-
| `verifyDevtoolsCapture(capture)` | Verify versions, record shapes, session identity, indexes, counts, dates, and the SHA-256 digest |
|
|
1293
|
-
|
|
1294
|
-
The wire types are `DevtoolsWireSessionSummary`, `DevtoolsWireEvent`,
|
|
1295
|
-
`DevtoolsWireSnapshot`, `DevtoolsContractsResponse`,
|
|
1296
|
-
`DevtoolsSessionsResponse`, `DevtoolsSessionDetail`, `DevtoolsSessionPage`, and
|
|
1297
|
-
`DevtoolsCapabilities`. Capture records use `DevtoolsCaptureManifest`,
|
|
1298
|
-
`DevtoolsCaptureEvent`, `DevtoolsCaptureSnapshot`,
|
|
1299
|
-
`DevtoolsCaptureFooter`, and `DevtoolsCaptureRecord`. A parsed
|
|
1300
|
-
`DevtoolsCapture` exposes the same canonical sequence through `records`.
|
|
1301
|
-
|
|
1302
|
-
```ts test/verify-a2log.ts
|
|
1303
|
-
import { readFile } from 'node:fs/promises'
|
|
1304
|
-
import {
|
|
1305
|
-
parseDevtoolsCapture,
|
|
1306
|
-
verifyDevtoolsCapture,
|
|
1307
|
-
} from 'experimental-a2/devtools'
|
|
1308
|
-
|
|
1309
|
-
export async function verifyA2Log(path: string) {
|
|
1310
|
-
const capture = parseDevtoolsCapture(await readFile(path))
|
|
1311
|
-
await verifyDevtoolsCapture(capture)
|
|
1312
|
-
return capture
|
|
1313
|
-
}
|
|
1314
|
-
```
|
|
1315
|
-
|
|
1316
|
-
The manifest records the contract, session, revision, capture time, protocol
|
|
1317
|
-
version, and capture version. Event records contain the exact durable payload
|
|
1318
|
-
and operational bookkeeping. Snapshot records contain metadata but never
|
|
1319
|
-
cached reducer state. The final `end` record contains counts, the highest event
|
|
1320
|
-
index, and a digest of every preceding encoded record. The session revision is
|
|
1321
|
-
an invalidation token, not the integrity digest. Encoding rejects durable
|
|
1322
|
-
values that canonical JSON would coerce or omit.
|
|
1323
|
-
|
|
1324
|
-
## `experimental-a2/devtools/server`
|
|
1325
|
-
|
|
1326
|
-
```ts
|
|
1327
|
-
// anywhere on the server:
|
|
1328
|
-
import type { DevtoolsServer } from 'experimental-a2/devtools/server'
|
|
1329
|
-
|
|
1330
|
-
declare function createDevtools(options: {
|
|
1331
|
-
servers: readonly DevtoolsServer[]
|
|
1332
|
-
authorize?: (
|
|
1333
|
-
request: Request,
|
|
1334
|
-
) => boolean | Response | Promise<boolean | Response>
|
|
1335
|
-
}): {
|
|
1336
|
-
handler(): (request: Request) => Promise<Response>
|
|
1337
|
-
}
|
|
1338
|
-
```
|
|
1339
|
-
|
|
1340
|
-
A read-only dashboard over the servers' durable logs. The handler serves the
|
|
1341
|
-
complete browser application, its JSON endpoints, and live SSE invalidations.
|
|
1342
|
-
It discovers sessions and reads stored causal, dispatch, completion, failure,
|
|
1343
|
-
and snapshot metadata. The causal forest and lifecycle timeline come directly
|
|
1344
|
-
from stored events. The initial page response includes the selected dashboard
|
|
1345
|
-
data so the browser does not need a contracts, sessions, and detail request
|
|
1346
|
-
waterfall. It never drains or heals a session.
|
|
1347
|
-
|
|
1348
|
-
Every route is a `GET`. The versioned resources are `capabilities`,
|
|
1349
|
-
`contracts`, `sessions`, `session`, `watch`, and `export` under the handler's
|
|
1350
|
-
`_a2/` path. `export` returns an integrity-checkable `.a2log` with the media
|
|
1351
|
-
type from `DEVTOOLS_CAPTURE_MEDIA_TYPE`.
|
|
1352
|
-
|
|
1353
|
-
`session` without pagination parameters returns one complete detail. Add a
|
|
1354
|
-
positive `limit` to receive a `DevtoolsSessionPage`. Its `throughIndex` is the
|
|
1355
|
-
inclusive event frontier frozen by the first request. Pass its opaque `cursor`
|
|
1356
|
-
back until the cursor is null. Snapshot metadata appears on the first page
|
|
1357
|
-
only. The cursor is bound to its contract and session, and the server rejects
|
|
1358
|
-
reuse against another log. The advertised maximum page size is clamped by the
|
|
1359
|
-
server.
|
|
1360
|
-
|
|
1361
|
-
The browser, CLI, watch loop, and export route walk these bounded pages for
|
|
1362
|
-
you. They reject a gap or early end. An exact export includes every event
|
|
1363
|
-
through one finite frontier, even when a backend provider limits one range
|
|
1364
|
-
response. Events appended during the walk belong to a later read.
|
|
1365
|
-
|
|
1366
|
-
Without `authorize`, the handler is available only when `NODE_ENV` is exactly
|
|
1367
|
-
`development`. Any other value, including unset, returns 404. When `authorize`
|
|
1368
|
-
is present, only a literal `true` grants access. Returning `false` also returns
|
|
1369
|
-
404. Returning a `Response` passes that response through, which supports
|
|
1370
|
-
redirects and authentication challenges. The mounted handler is the
|
|
1371
|
-
authorization boundary for the browser, CLI, SSE stream, and exact capture
|
|
1372
|
-
download. A2 does not add a separate Devtools credential store.
|
|
1373
|
-
|
|
1374
|
-
The built-in memory, SQLite, Postgres, and Redis stores support inspection.
|
|
1375
|
-
Custom `A2Store` implementations can omit the optional `inspect` interface; the
|
|
1376
|
-
dashboard returns 501 for those logs. A custom inspection implementation may
|
|
1377
|
-
add `readEvents(sessionId, { afterIndex, throughIndex?, limit })` for efficient
|
|
1378
|
-
bounded reads. It returns `{ events, throughIndex }`, where the first call
|
|
1379
|
-
freezes the inclusive frontier and continuations preserve it. Without this
|
|
1380
|
-
method, A2 reads the complete log for each page and slices it in memory.
|
|
1381
|
-
|
|
1382
1293
|
## `experimental-a2/scheduler-vercel`
|
|
1383
1294
|
|
|
1384
1295
|
`vercelQueues(options?)` returns an `A2Scheduler` backed by Vercel Queues.
|
|
@@ -1490,10 +1401,9 @@ Mount the application handler once and pass every server that shares the
|
|
|
1490
1401
|
scheduler:
|
|
1491
1402
|
|
|
1492
1403
|
```ts app/api/a2/scheduler/route.ts
|
|
1493
|
-
import {
|
|
1494
|
-
import { ordersServer, billingServer } from '@/server'
|
|
1404
|
+
import { billingServer, ordersServer, scheduler } from '@/server'
|
|
1495
1405
|
|
|
1496
|
-
export const POST =
|
|
1406
|
+
export const POST = scheduler.handler(ordersServer, billingServer)
|
|
1497
1407
|
```
|
|
1498
1408
|
|
|
1499
1409
|
The route must be reachable by the selected QStash server after any platform
|
|
@@ -1574,46 +1484,6 @@ duplicate-message error is success. A2's durable AI tool execution uses this
|
|
|
1574
1484
|
private classification. Direct `session.schedule()` calls still reject with
|
|
1575
1485
|
the original error object.
|
|
1576
1486
|
|
|
1577
|
-
## `a2 devtools`
|
|
1578
|
-
|
|
1579
|
-
The package installs an `a2` binary with a read-only `devtools` namespace.
|
|
1580
|
-
|
|
1581
|
-
| Command | Purpose |
|
|
1582
|
-
| --- | --- |
|
|
1583
|
-
| `contracts` | List contracts mounted at the Devtools URL |
|
|
1584
|
-
| `sessions --contract NAME` | List session summaries, with optional `--limit` and `--cursor` |
|
|
1585
|
-
| `show --contract NAME --session ID` | Read one session's exact durable detail |
|
|
1586
|
-
| `export --contract NAME --session ID` | Verify and save an `.a2log`; use `--output` and opt into replacement with `--force` |
|
|
1587
|
-
| `check --contract NAME --session ID` | Verify a live capture and optionally require `--settled`, `--no-dead-letters`, `--no-caught-failures`, or `--max-redispatches N` |
|
|
1588
|
-
| `verify FILE` | Verify a saved capture offline |
|
|
1589
|
-
|
|
1590
|
-
Live commands read the mounted URL from `--url` or `A2_DEVTOOLS_URL`.
|
|
1591
|
-
`A2_DEVTOOLS_TOKEN` supplies the default bearer token. `--bearer-env ENV` and
|
|
1592
|
-
repeatable `--header-env HEADER=ENV` reference other environment variables.
|
|
1593
|
-
Credential values are never accepted as CLI arguments or URL components.
|
|
1594
|
-
Output uses `--format human`, `json`, or `ndjson`.
|
|
1595
|
-
|
|
1596
|
-
## `experimental-a2/testing`
|
|
1597
|
-
|
|
1598
|
-
Node-only helpers for verified capture loading and pure reducer replay.
|
|
1599
|
-
|
|
1600
|
-
| Export | Contract |
|
|
1601
|
-
| --- | --- |
|
|
1602
|
-
| `loadCapture(source, contract)` | Read a filesystem path or URL, verify its integrity, validate its contract and payloads, and return typed events |
|
|
1603
|
-
| `prepareCapture(capture, contract)` | Apply the same verification and contract typing to an already parsed capture |
|
|
1604
|
-
| `replayCapture(loaded, reducer, { throughIndex? })` | Fold captured events through the reducer, stopping at an optional inclusive index |
|
|
1605
|
-
|
|
1606
|
-
`LoadedCapture` contains the verified `capture`, the `contract`, typed public
|
|
1607
|
-
`events` with `createdAt` revived as `Date`, and unmodified operational
|
|
1608
|
-
`rawEvents`.
|
|
1609
|
-
`CaptureReplay` returns `{ state, index }`. A cutpoint of 0 returns the initial
|
|
1610
|
-
state and index 0. Supporting types are `CaptureSource`, `LoadedCapture`,
|
|
1611
|
-
`ReplayCaptureOptions`, and `CaptureReplay`.
|
|
1612
|
-
|
|
1613
|
-
Replay does not use cached snapshot state, construct a server, dispatch a
|
|
1614
|
-
handler, invoke an AI model or tool, or repeat external effects. It reproduces
|
|
1615
|
-
only the current reducer's pure projection over already recorded events.
|
|
1616
|
-
|
|
1617
1487
|
## Entry points
|
|
1618
1488
|
|
|
1619
1489
|
| Entry point | Ships | Peer dependency |
|
|
@@ -1624,7 +1494,6 @@ only the current reducer's pure projection over already recorded events.
|
|
|
1624
1494
|
| `experimental-a2/react` | `createReact` | `react` |
|
|
1625
1495
|
| `experimental-a2/ai` | agent contract, `handlerContext`, reducer, and append builders: isomorphic | `ai` |
|
|
1626
1496
|
| `experimental-a2/ai/server` | AI SDK runner and built-in handlers | `ai` |
|
|
1627
|
-
| `experimental-a2/http` | route-side transport and scheduler helpers | none |
|
|
1628
1497
|
| `experimental-a2/store-postgres` | `postgres`: Postgres store backend | `pg` (or inject a client) |
|
|
1629
1498
|
| `experimental-a2/store-redis` | `redis`: Redis Streams store backend, push-native streaming | `ioredis` (or inject a client) |
|
|
1630
1499
|
| `experimental-a2/store-redis-http` | `redisHttp`: the same Redis store over provider REST APIs (Upstash) | none |
|
|
@@ -1634,9 +1503,6 @@ only the current reducer's pure projection over already recorded events.
|
|
|
1634
1503
|
| `experimental-a2/scheduler-qstash` | `qstash`: signed recovery and timers | `@upstash/qstash` |
|
|
1635
1504
|
| `experimental-a2/cache-indexeddb` | `indexedDb` browser cache | none |
|
|
1636
1505
|
| `experimental-a2/otel` | `otel` telemetry adapter | `@opentelemetry/api` |
|
|
1637
|
-
| `experimental-a2/devtools` | versioned wire protocol and exact capture codec | none |
|
|
1638
|
-
| `experimental-a2/devtools/server` | durable read-only dashboard handler | none |
|
|
1639
|
-
| `experimental-a2/testing` | Node-only capture loading and pure reducer replay | none |
|
|
1640
1506
|
|
|
1641
1507
|
Core `experimental-a2` imports none of the backends, enforced by a browser-bundle
|
|
1642
1508
|
test in CI, not just convention. Importing `experimental-a2/store-postgres` is what
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Errors
|
|
3
|
-
description: One error class,
|
|
3
|
+
description: One error class, stable codes, and clear rules about what append will never throw for.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## `A2Error`
|
|
@@ -35,6 +35,7 @@ export async function POST(req: Request) {
|
|
|
35
35
|
| Code | Thrown when | Retryable |
|
|
36
36
|
| ------------------------- | ------------------------------------------------------------------ | --------- |
|
|
37
37
|
| `INVALID_PAYLOAD` | a payload fails its schema; nothing was written, and `details` carries the issues ([Standard Schema](https://standardschema.dev) format) | no |
|
|
38
|
+
| `FORBIDDEN` | `server.fetch` authorization denies a parsed stream, history read, push, or presence update | no |
|
|
38
39
|
| `UNKNOWN_EVENT_TYPE` | an event type isn't in the contract's `events` map | no |
|
|
39
40
|
| `UNKNOWN_PRESENCE_FIELD` | a presence field isn't in the contract's `presence` map | no |
|
|
40
41
|
| `PRESENCE_NOT_SUPPORTED` | the contract declares `presence` but the configured store has no presence capability (thrown at construction; for environment-resolved stores, deferred to the first store use) | no |
|
|
@@ -73,12 +74,9 @@ The push route serializes an `A2Error` as:
|
|
|
73
74
|
```
|
|
74
75
|
|
|
75
76
|
with a mapped status: `INVALID_PAYLOAD`, `UNKNOWN_EVENT_TYPE`, and
|
|
76
|
-
`PARTIAL_DUPLICATE_BATCH` are 400; `
|
|
77
|
-
`STORE_UNAVAILABLE` is 503.
|
|
77
|
+
`PARTIAL_DUPLICATE_BATCH` are 400; `FORBIDDEN` is 403;
|
|
78
|
+
`SUPERSEDED_ATTEMPT` is 409; `STORE_UNAVAILABLE` is 503.
|
|
78
79
|
|
|
79
80
|
The client's `push` deserializes the body back into an `A2Error`, so client
|
|
80
81
|
and server code branch on identical codes. `push` auto-retries only
|
|
81
|
-
`STORE_UNAVAILABLE`.
|
|
82
|
-
`deserializeError`, ships in `experimental-a2/http` alongside `handle`;
|
|
83
|
-
a `before` hook that wants the wire's own error shapes returns
|
|
84
|
-
`errorResponse(new A2Error(...))`.
|
|
82
|
+
`STORE_UNAVAILABLE`. `server.fetch` owns the HTTP serialization.
|
package/package.json
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "experimental-a2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Durable sync and reactions for things with a lifecycle: one event log, derived state, and live client per session.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"a2": "./dist/cli-bin.js"
|
|
9
|
-
},
|
|
10
7
|
"sideEffects": false,
|
|
11
8
|
"engines": {
|
|
12
9
|
"node": ">=22.13"
|
|
@@ -39,7 +36,6 @@
|
|
|
39
36
|
"browser": "./dist/ai-server.browser.js",
|
|
40
37
|
"default": "./dist/ai-server.js"
|
|
41
38
|
},
|
|
42
|
-
"./http": "./dist/http.js",
|
|
43
39
|
"./store-memory": "./dist/store-memory.js",
|
|
44
40
|
"./store-sqlite": "./dist/store-sqlite.js",
|
|
45
41
|
"./store-postgres": "./dist/store-postgres.js",
|
|
@@ -49,15 +45,6 @@
|
|
|
49
45
|
"./scheduler-vercel": "./dist/scheduler-vercel.js",
|
|
50
46
|
"./cache-indexeddb": "./dist/cache-indexeddb.js",
|
|
51
47
|
"./otel": "./dist/otel.js",
|
|
52
|
-
"./devtools": "./dist/devtools.js",
|
|
53
|
-
"./devtools/server": {
|
|
54
|
-
"browser": "./dist/devtools-server.browser.js",
|
|
55
|
-
"default": "./dist/devtools-server.js"
|
|
56
|
-
},
|
|
57
|
-
"./testing": {
|
|
58
|
-
"browser": "./dist/testing.browser.js",
|
|
59
|
-
"default": "./dist/testing.js"
|
|
60
|
-
},
|
|
61
48
|
"./package.json": "./package.json"
|
|
62
49
|
},
|
|
63
50
|
"peerDependencies": {
|
package/src/ai-server.ts
CHANGED
|
@@ -57,11 +57,14 @@ import {
|
|
|
57
57
|
createServer,
|
|
58
58
|
type A2Server,
|
|
59
59
|
type HandlerContext,
|
|
60
|
-
type PushValidationContext,
|
|
61
60
|
type ServerOptions,
|
|
62
61
|
} from './server.ts'
|
|
62
|
+
import {
|
|
63
|
+
setServerFetchHooks,
|
|
64
|
+
type ServerIngressContext,
|
|
65
|
+
} from './server-fetch.ts'
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
function validateAgentIngress(context: ServerIngressContext): void {
|
|
65
68
|
const rejected = context.events.find((event) => {
|
|
66
69
|
if (event.type !== 'ai.message.created') {
|
|
67
70
|
return !(
|
|
@@ -203,7 +206,7 @@ export type CreateAgentServerOptions<
|
|
|
203
206
|
D extends AIEventDefs<M> & EventDefs,
|
|
204
207
|
T extends ToolSet = ToolSet,
|
|
205
208
|
> = CreateHandlersOptions<M, D, T> &
|
|
206
|
-
Omit<ServerOptions<D>, 'contract' | 'handlers'
|
|
209
|
+
Omit<ServerOptions<D>, 'contract' | 'handlers'> & {
|
|
207
210
|
handlers?: ServerOptions<D>['handlers']
|
|
208
211
|
}
|
|
209
212
|
|
|
@@ -1708,12 +1711,13 @@ export function createAgentServer<
|
|
|
1708
1711
|
...(compaction === undefined ? {} : { compaction }),
|
|
1709
1712
|
...(progress === undefined ? {} : { progress }),
|
|
1710
1713
|
})
|
|
1711
|
-
|
|
1714
|
+
const server = createServer({
|
|
1712
1715
|
...serverOptions,
|
|
1713
1716
|
contract: definition.contract,
|
|
1714
1717
|
handlers: { ...builtIns, ...handlers },
|
|
1715
|
-
validatePush: validateAgentPush,
|
|
1716
1718
|
})
|
|
1719
|
+
setServerFetchHooks(server, { validateIngress: validateAgentIngress })
|
|
1720
|
+
return server
|
|
1717
1721
|
}
|
|
1718
1722
|
|
|
1719
1723
|
export type { Instructions, LanguageModel, ToolSet }
|
package/src/client.ts
CHANGED
|
@@ -808,7 +808,7 @@ const wsTransport = (
|
|
|
808
808
|
settleChannel(
|
|
809
809
|
channel,
|
|
810
810
|
new Error(
|
|
811
|
-
'subscribe not confirmed: is the ws route
|
|
811
|
+
'subscribe not confirmed: is the ws route calling server.fetch with upgradeWebSocket?',
|
|
812
812
|
),
|
|
813
813
|
)
|
|
814
814
|
}, STREAM_TIMINGS.stallTimeoutMs)
|
|
@@ -1562,7 +1562,7 @@ export function createClient<
|
|
|
1562
1562
|
const readHistory = transport.history
|
|
1563
1563
|
if (readHistory === undefined) {
|
|
1564
1564
|
throw new TypeError(
|
|
1565
|
-
'loadHistory requires an http api — the ws transport has no history lane; serve the session
|
|
1565
|
+
'loadHistory requires an http api — the ws transport has no history lane; serve the session with server.fetch and pass its route url as the string or { type: "http" } api',
|
|
1566
1566
|
)
|
|
1567
1567
|
}
|
|
1568
1568
|
const before = loadOptions?.before
|
package/src/errors.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
export type A2ErrorCode =
|
|
10
10
|
| 'INVALID_PAYLOAD' // schema validation failed — thrown before anything is written
|
|
11
|
+
| 'FORBIDDEN' // external ingress was denied by server.fetch authorization
|
|
11
12
|
| 'UNKNOWN_EVENT_TYPE' // event type not in the machine's `events` map
|
|
12
13
|
| 'PARTIAL_DUPLICATE_BATCH' // batch mixed already-appended and fresh events
|
|
13
14
|
| 'SUPERSEDED_ATTEMPT' // handler append from an attempt a recovery claim replaced
|