experimental-a2 0.19.0 → 0.21.1
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 +50 -0
- package/README.md +1 -1
- package/dist/{actor-C7b-1arn.d.ts → actor-HnGmrOZZ.d.ts} +5 -5
- package/dist/{actor-C7b-1arn.d.ts.map → actor-HnGmrOZZ.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +2 -2
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +5 -5
- package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
- package/dist/{actor-shared-t7w8xIKP.js.map → actor-shared-DFET5q16.js.map} +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +54 -43
- package/dist/actor.js.map +1 -1
- package/dist/{ai-Cz3hqVhk.d.ts → ai-CvEnqVfo.d.ts} +8 -8
- package/dist/{ai-Cz3hqVhk.d.ts.map → ai-CvEnqVfo.d.ts.map} +1 -1
- package/dist/{ai-mMiWul1T.js → ai-CzE672Q3.js} +9 -9
- package/dist/ai-CzE672Q3.js.map +1 -0
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +36 -9
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-Bfk-Lycn.d.ts} +2 -2
- package/dist/ai-harness-Bfk-Lycn.d.ts.map +1 -0
- package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-dQeJaR1t.js} +17 -4
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +4 -4
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +14 -17
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +84 -75
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BhHLoNKF.js → client-9IeTsiAs.js} +5 -5
- package/dist/{client-BhHLoNKF.js.map → client-9IeTsiAs.js.map} +1 -1
- package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
- package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
- package/dist/contract-CWukt01d.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
- package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
- package/dist/{internal-CscSY7K_.js → internal-Bc4PvfHk.js} +10 -3
- package/dist/internal-Bc4PvfHk.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js +1 -1
- package/dist/otel.js.map +1 -1
- package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
- package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
- package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
- package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
- package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
- package/dist/runtime-consumer-BWmht60z.js +12 -0
- package/dist/runtime-consumer-BWmht60z.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +7 -7
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +5 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-DYAATFuT.d.ts → server-BshFZ0eL.d.ts} +108 -41
- package/dist/server-BshFZ0eL.d.ts.map +1 -0
- package/dist/{server-D-Y4pqwu.js → server-DvIB8kj5.js} +423 -135
- package/dist/server-DvIB8kj5.js.map +1 -0
- package/dist/server.browser.js +1 -1
- package/dist/server.browser.js.map +1 -1
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-Bk_29eEw.d.ts} +2 -2
- package/dist/session-clear-Bk_29eEw.d.ts.map +1 -0
- package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
- package/dist/session-clear-wHd5GCpZ.js.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-Z2qjw50n.d.ts +523 -0
- package/dist/store-Z2qjw50n.d.ts.map +1 -0
- package/dist/store-codec-DwrWlgQx.js +65 -0
- package/dist/store-codec-DwrWlgQx.js.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 +393 -210
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-7WJV0YWP.js → store-polling-DYNyxCev.js} +2 -2
- package/dist/{store-polling-7WJV0YWP.js.map → store-polling-DYNyxCev.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +1595 -673
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-B1ixeqIS.js → store-presence-polling-CzQ0wdtM.js} +3 -3
- package/dist/{store-presence-polling-B1ixeqIS.js.map → store-presence-polling-CzQ0wdtM.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +4 -4
- package/dist/store-redis-notify-IiVdlYHc.js +2919 -0
- package/dist/store-redis-notify-IiVdlYHc.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +2 -2
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +517 -287
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
- package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
- package/dist/{wire-BKxH1vbP.js → wire-CZGqNZRx.js} +5 -4
- package/dist/wire-CZGqNZRx.js.map +1 -0
- package/docs/01-quickstart.mdx +28 -19
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +9 -1
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +13 -12
- package/docs/agents/03-conversations.mdx +31 -21
- package/docs/agents/04-harnesses.mdx +7 -3
- package/docs/agents/meta.ts +1 -1
- package/docs/client/03-presence.mdx +18 -14
- package/docs/client/meta.ts +1 -1
- package/docs/contract/01-events.mdx +2 -2
- package/docs/contract/02-state.mdx +18 -14
- package/docs/contract/03-sessions.mdx +32 -25
- package/docs/index.mdx +31 -26
- package/docs/reference/01-core.mdx +12 -12
- package/docs/reference/02-server.mdx +247 -192
- package/docs/reference/04-ai.mdx +29 -20
- package/docs/reference/05-telemetry.mdx +146 -0
- package/docs/reference/06-errors.mdx +24 -18
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +112 -0
- package/docs/schedulers/02-vercel-queues.mdx +87 -0
- package/docs/schedulers/03-qstash.mdx +123 -0
- package/docs/schedulers/meta.ts +1 -0
- package/docs/server/01-consumers.mdx +415 -0
- package/docs/server/02-timers.mdx +33 -23
- package/docs/server/03-cancellation.mdx +52 -44
- package/docs/server/04-routes.mdx +4 -1
- package/docs/server/05-sql-projections.mdx +391 -0
- package/docs/server/06-guarantees.mdx +60 -50
- package/docs/stores/01-overview.mdx +37 -0
- package/docs/stores/02-redis.mdx +57 -0
- package/docs/stores/03-redis-http.mdx +56 -0
- package/docs/stores/04-postgres.mdx +172 -0
- package/docs/stores/05-sqlite.mdx +36 -0
- package/docs/stores/06-memory.mdx +22 -0
- package/docs/stores/meta.ts +1 -0
- package/docs/upgrades/01-overview.mdx +30 -0
- package/docs/upgrades/79-0-21.mdx +165 -0
- package/docs/upgrades/80-0-20.mdx +43 -0
- package/docs/upgrades/81-0-19.mdx +33 -0
- package/docs/upgrades/82-0-18.mdx +33 -0
- package/docs/upgrades/83-0-17.mdx +69 -0
- package/docs/upgrades/meta.ts +3 -0
- package/examples/playground/DEPLOY.md +48 -11
- package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
- package/examples/playground/app/a0/chat.test.tsx +421 -330
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/server.ts +4 -4
- package/examples/playground/app/a0/shell.tsx +1 -1
- package/examples/playground/app/a0/uploads/route.test.ts +59 -0
- package/examples/playground/app/a2slack/a2slack.css +1067 -0
- package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
- package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
- package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
- package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
- package/examples/playground/app/a2slack/consumers/search.ts +37 -0
- package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
- package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
- package/examples/playground/app/a2slack/db/a2.ts +206 -0
- package/examples/playground/app/a2slack/db/index.ts +73 -0
- package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
- package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
- package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
- package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +34 -0
- package/examples/playground/app/a2slack/db/queries.ts +263 -0
- package/examples/playground/app/a2slack/db/schema.ts +145 -0
- package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
- package/examples/playground/app/a2slack/drills/harness.ts +244 -0
- package/examples/playground/app/a2slack/events/route.ts +36 -0
- package/examples/playground/app/a2slack/identity.ts +44 -0
- package/examples/playground/app/a2slack/inbox/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/page.tsx +101 -0
- package/examples/playground/app/a2slack/lag.ts +73 -0
- package/examples/playground/app/a2slack/layout.tsx +43 -0
- package/examples/playground/app/a2slack/messages/route.ts +17 -0
- package/examples/playground/app/a2slack/model.test.ts +208 -0
- package/examples/playground/app/a2slack/model.ts +369 -0
- package/examples/playground/app/a2slack/models.test.ts +72 -0
- package/examples/playground/app/a2slack/page.tsx +10 -0
- package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
- package/examples/playground/app/a2slack/search/route.ts +40 -0
- package/examples/playground/app/a2slack/search-index.ts +108 -0
- package/examples/playground/app/a2slack/server.test.ts +315 -0
- package/examples/playground/app/a2slack/server.ts +55 -0
- package/examples/playground/app/a2slack/sessions.ts +50 -0
- package/examples/playground/app/a2slack/thread/route.ts +8 -0
- package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
- package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
- package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
- package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
- package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
- package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
- package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
- package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
- package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
- package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
- package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
- package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
- package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
- package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
- package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -90
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
- package/examples/playground/app/agent/ingress.test.ts +41 -17
- package/examples/playground/app/agent/model-input.test.tsx +92 -43
- package/examples/playground/app/agent/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +38 -65
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/activity-history.test.tsx +60 -18
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- package/examples/playground/app/components/fast-link.test.tsx +3 -5
- package/examples/playground/app/initial-state.test.tsx +86 -45
- package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
- package/examples/playground/app/orders/page.tsx +1 -1
- package/examples/playground/app/orders/server.ts +49 -30
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
- package/examples/playground/app/recovery/page.tsx +4 -4
- package/examples/playground/app/recovery/server.ts +23 -32
- package/examples/playground/drizzle.config.ts +14 -0
- package/examples/playground/next.config.ts +6 -1
- package/examples/playground/package.json +13 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/test/agent.ts +35 -0
- package/examples/playground/test/model.ts +149 -0
- package/examples/playground/test/serve.ts +184 -0
- package/examples/playground/test/server-only.ts +1 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.config.ts +27 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +7 -2
- package/sql/postgres/drizzle-schema.ts +206 -0
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +276 -0
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +154 -115
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +41 -9
- package/src/ai-harness.ts +28 -2
- package/src/ai-pi.ts +2 -2
- package/src/ai-server.ts +2 -2
- package/src/ai.ts +7 -7
- package/src/contract.ts +1 -1
- package/src/deterministic-id.ts +26 -3
- package/src/errors.ts +9 -6
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +17 -6
- package/src/otel.ts +1 -1
- package/src/postgres-notifications.ts +0 -22
- package/src/postgres-resources.ts +6 -9
- package/src/postgres-schema.ts +121 -0
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-qstash.ts +12 -5
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +10 -4
- package/src/server.browser.ts +1 -1
- package/src/server.ts +839 -238
- package/src/session-socket.ts +1 -1
- package/src/store-codec.ts +66 -0
- package/src/store-memory.ts +581 -255
- package/src/store-postgres.ts +1984 -943
- package/src/store-redis-core.ts +1624 -1152
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +891 -486
- package/src/store.ts +227 -97
- package/src/telemetry.ts +2 -1
- package/src/wire.ts +1 -0
- package/dist/ai-harness-BuuC-gJ3.js.map +0 -1
- package/dist/ai-harness-CV6rXbB3.d.ts.map +0 -1
- package/dist/ai-mMiWul1T.js.map +0 -1
- package/dist/contract-BYKASlCV.js.map +0 -1
- package/dist/idempotent-replay-JdgMQRRm.js +0 -21
- package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
- package/dist/internal-CscSY7K_.js.map +0 -1
- package/dist/server-D-Y4pqwu.js.map +0 -1
- package/dist/server-DYAATFuT.d.ts.map +0 -1
- package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
- package/dist/session-clear-DGI4jTQ5.js.map +0 -1
- package/dist/store-CyLUy86v.d.ts +0 -404
- package/dist/store-CyLUy86v.d.ts.map +0 -1
- package/dist/store-codec-DTG0Ftek.js +0 -10
- package/dist/store-codec-DTG0Ftek.js.map +0 -1
- package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
- package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
- package/dist/wire-BKxH1vbP.js.map +0 -1
- package/docs/reference/05-adapters.mdx +0 -351
- package/docs/server/01-handlers.mdx +0 -200
- package/docs/server/05-production.mdx +0 -323
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Adapters
|
|
3
|
-
description: "Stores, schedulers, and OpenTelemetry."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## `experimental-a2/otel`
|
|
7
|
-
|
|
8
|
-
```ts
|
|
9
|
-
otel(options?: { tracer?: Tracer; tracerName?: string }): A2Telemetry
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
OpenTelemetry instrumentation for a server (`@opentelemetry/api` is a peer
|
|
13
|
-
dependency). Pass it as `telemetry`. Spans nest through the active context, so
|
|
14
|
-
the happy path lands in one trace: request → append → drain → each handler →
|
|
15
|
-
the events those handlers append. `A2Telemetry` is a one-method interface:
|
|
16
|
-
anything with `span(name, attributes, fn)` works, and `otel()` is the adapter
|
|
17
|
-
A2 ships. See [Watch it in traces](/server/production#watch-it-in-traces).
|
|
18
|
-
|
|
19
|
-
### The span catalogue
|
|
20
|
-
|
|
21
|
-
This catalogue is a contract: renames and additions are breaking for
|
|
22
|
-
dashboards and are called out in the package's `CHANGELOG.md`. All spans carry
|
|
23
|
-
`a2.contract` and `a2.session_id`. Core spans describe the durable runtime;
|
|
24
|
-
`a2.ai.*` spans describe the built-in AI handlers.
|
|
25
|
-
|
|
26
|
-
| Layer | Span | Wraps |
|
|
27
|
-
| ----- | ---- | ----- |
|
|
28
|
-
| Core | `a2.append` | validation + the store write (one batch) |
|
|
29
|
-
| Core | `a2.drain` | one drain pass, including concurrent handlers |
|
|
30
|
-
| Core | `a2.event` | one claimed dispatch, including its handler and completion |
|
|
31
|
-
| Core | `a2.claim` | one `claimAvailable` store operation |
|
|
32
|
-
| Core | `a2.commit` | one `completeAttempt` or `failAttempt` store operation |
|
|
33
|
-
| Core | `a2.state` | one `state()` read: snapshot-plus-tail load + reducer fold |
|
|
34
|
-
| Core | `a2.snapshot` | one background batch of checkpoint writes |
|
|
35
|
-
| Core | `a2.redrive` | one `session.redrive()`: the store operation and starting the session's wakeup |
|
|
36
|
-
| AI | `a2.ai.control` | applying an input/work report/session-close decision and preparing its returned events |
|
|
37
|
-
| AI | `a2.ai.generate` | one generation handler, including Core calls and model/SDK work |
|
|
38
|
-
| AI | `a2.ai.context` | constructing model messages, including tool prompt reconstruction when uncached |
|
|
39
|
-
| AI | `a2.ai.compaction` | automatic summarization or the custom compaction callback |
|
|
40
|
-
| AI | `a2.ai.tool` | one durable tool handler, including application execution |
|
|
41
|
-
| AI | `a2.ai.ack` | waiting for a persisted work report's canonical events and control receipt |
|
|
42
|
-
|
|
43
|
-
Durations include nested work: `a2.event` contains its AI handler and later
|
|
44
|
-
commit, AI handlers call Core, and `a2.state` includes reducer code.
|
|
45
|
-
Acknowledgements overlap control processing, claims can cover several events,
|
|
46
|
-
and snapshots run in the background, so summing spans, or subtracting summed
|
|
47
|
-
children, does not yield exclusive Core or AI overhead. Model and application
|
|
48
|
-
I/O is included; its child spans have the detail.
|
|
49
|
-
|
|
50
|
-
Attributes arrive in two waves: **at start** (passed to `span()`) and
|
|
51
|
-
**mid-span** (via `handle.setAttribute`, for outcomes and counts known only
|
|
52
|
-
after the work ran). A recorder must merge both; the ones you alert on are all
|
|
53
|
-
mid-span.
|
|
54
|
-
|
|
55
|
-
| Attribute | Span | When | Values |
|
|
56
|
-
| --------------------- | ----------- | ----- | ----------------------------------------------------- |
|
|
57
|
-
| `a2.append.source` | `a2.append` | start | `external` \| `handler` |
|
|
58
|
-
| `a2.append.mode` | `a2.append` | start | `inline` \| `dispatch` |
|
|
59
|
-
| `a2.append.types` | `a2.append` | start | comma-joined event types |
|
|
60
|
-
| `a2.append.count` | `a2.append` | start | batch size |
|
|
61
|
-
| `a2.append.armed` | `a2.append` | mid | `false` when the scheduler arm failed and this append degraded to append-driven healing |
|
|
62
|
-
| `a2.drain.outcome` | `a2.drain` | mid | `settled` \| `busy` \| `stalled` |
|
|
63
|
-
| `a2.drain.processed` | `a2.drain` | mid | events processed this pass |
|
|
64
|
-
| `a2.claim.outcome` | `a2.claim` | mid | `claimed` \| `busy` \| `settled` |
|
|
65
|
-
| `a2.claim.count` | `a2.claim` | mid | number of claimed events, or zero |
|
|
66
|
-
| `a2.commit.operation` | `a2.commit` | start | `complete` \| `fail` |
|
|
67
|
-
| `a2.commit.index` | `a2.commit` | start | event log position |
|
|
68
|
-
| `a2.commit.attempt` | `a2.commit` | start | durable 1-based attempt ordinal |
|
|
69
|
-
| `a2.commit.outcome` | `a2.commit` | mid | `completed` \| `failed` \| `dead_lettered` \| `superseded` |
|
|
70
|
-
| `a2.event.type` | `a2.event` | start | the event's type |
|
|
71
|
-
| `a2.event.index` | `a2.event` | start | log position |
|
|
72
|
-
| `a2.event.id` | `a2.event` | start | event id |
|
|
73
|
-
| `a2.event.attempt` | `a2.event` | start | same durable 1-based ordinal as `ctx.attempt` |
|
|
74
|
-
| `a2.event.lane` | `a2.event` | start | stored lane value; absent for concurrent unlaned work |
|
|
75
|
-
| `a2.event.handled` | `a2.event` | start | normally `true`; `false` when a custom-adapter row has no handler |
|
|
76
|
-
| `a2.event.outcome` | `a2.event` | mid | `processed` \| `failed` \| `dead_lettered` \| `superseded` \| `surrendered` |
|
|
77
|
-
| `a2.event.aborted` | `a2.event` | mid | `true` when `abortOn` fired during the run |
|
|
78
|
-
| `a2.state.reducer` | `a2.state` | start | the reducer's name |
|
|
79
|
-
| `a2.state.snapshot` | `a2.state` | mid | `hit` \| `miss` \| `rejected` (schema guard discarded it) \| `memory` (served from the server's in-memory state) |
|
|
80
|
-
| `a2.state.folded` | `a2.state` | mid | events folded past the snapshot |
|
|
81
|
-
| `a2.state.index` | `a2.state` | mid | the frontier the returned state reflects |
|
|
82
|
-
| `a2.snapshot.reducer` | `a2.snapshot` | start | the reducer's name |
|
|
83
|
-
| `a2.snapshot.index` | `a2.snapshot` | start | greatest checkpoint index in the write batch |
|
|
84
|
-
| `a2.snapshot.count` | `a2.snapshot` | start | number of checkpoints in the write batch |
|
|
85
|
-
| `a2.redrive.index` | `a2.redrive` | start | log position of the event being redriven |
|
|
86
|
-
| `a2.redrive.action` | `a2.redrive` | start | `retry` \| `skip` |
|
|
87
|
-
| `a2.redrive.redriven` | `a2.redrive` | mid | `true` when the event was dead-lettered and is now resolved |
|
|
88
|
-
| `a2.redrive.armed` | `a2.redrive` | mid | `false` when the scheduler arm failed and the wakeup degraded to append-driven healing |
|
|
89
|
-
| `a2.ai.control.type` | `a2.ai.control` | start | `ai.control.requested` \| `ai.work.reported` \| `ai.session.closed` |
|
|
90
|
-
| `a2.ai.control.id` | `a2.ai.control` | start | triggering event id |
|
|
91
|
-
| `a2.ai.control.attempt` | `a2.ai.control` | start | durable 1-based attempt ordinal |
|
|
92
|
-
| `a2.ai.generate.request_id` | `a2.ai.generate` | start | generation request event id |
|
|
93
|
-
| `a2.ai.generate.attempt` | `a2.ai.generate` | start | durable 1-based attempt ordinal |
|
|
94
|
-
| `a2.ai.context.request_id` | `a2.ai.context` | start | generation request event id |
|
|
95
|
-
| `a2.ai.context.phase` | `a2.ai.context` | start | `generation` \| `refresh` \| `tool` |
|
|
96
|
-
| `a2.ai.compaction.request_id` | `a2.ai.compaction` | start | generation request event id |
|
|
97
|
-
| `a2.ai.compaction.generation_id` | `a2.ai.compaction` | start | generation attempt id |
|
|
98
|
-
| `a2.ai.compaction.policy` | `a2.ai.compaction` | start | `automatic` \| `custom` |
|
|
99
|
-
| `a2.ai.tool.request_id` | `a2.ai.tool` | start | originating generation request event id |
|
|
100
|
-
| `a2.ai.tool.call_id` | `a2.ai.tool` | start | tool call id |
|
|
101
|
-
| `a2.ai.tool.name` | `a2.ai.tool` | start | tool name |
|
|
102
|
-
| `a2.ai.tool.attempt` | `a2.ai.tool` | start | durable 1-based attempt ordinal |
|
|
103
|
-
| `a2.ai.ack.request_id` | `a2.ai.ack` | start | reporting worker event id |
|
|
104
|
-
| `a2.ai.ack.report_id` | `a2.ai.ack` | start | work report event id, matching the control span's id |
|
|
105
|
-
|
|
106
|
-
- Drain outcomes: `settled` means nothing actionable is left; `busy` means live
|
|
107
|
-
per-event claims or a failed event's retry hold remain; `stalled` means a
|
|
108
|
-
caught failure in this drain needs a later retry.
|
|
109
|
-
- A failing handler marks its `a2.event` span with the exception and error
|
|
110
|
-
status. Alert on `a2.event.outcome = dead_lettered`. A dead-lettered event
|
|
111
|
-
blocks only later events in its lane, until `session.redrive()` resolves it;
|
|
112
|
-
other lanes stay eligible.
|
|
113
|
-
- `superseded` means a stale completion or failure did not change the log.
|
|
114
|
-
|
|
115
|
-
## `experimental-a2/store-postgres`
|
|
116
|
-
|
|
117
|
-
`postgres({ connectionString })` returns an `A2Store` on Postgres; pass
|
|
118
|
-
`client` to inject your own pool. See [Postgres](/server/production#postgres)
|
|
119
|
-
for setup.
|
|
120
|
-
|
|
121
|
-
- Without `listenConnectionString`, a live stream polls with an adaptive
|
|
122
|
-
cadence: every 25ms while the session is producing events, backing off to
|
|
123
|
-
two seconds when it goes quiet. After a quiet period, an update from another
|
|
124
|
-
client can take up to the idle interval plus query latency to arrive.
|
|
125
|
-
- A client's own `push()` refreshes its idle stream immediately from the
|
|
126
|
-
current cursor, even when the write and stream routes run in different
|
|
127
|
-
invocations. A WebSocket push wakes the attached stream without reopening
|
|
128
|
-
the socket.
|
|
129
|
-
- With `listenConnectionString`, notifications arrive after commit, and a
|
|
130
|
-
reconnect catches up from the cursor. Native event and presence
|
|
131
|
-
subscriptions also reconcile every ten seconds.
|
|
132
|
-
|
|
133
|
-
## `experimental-a2/store-redis`
|
|
134
|
-
|
|
135
|
-
`redis({ url })` returns an `A2Store` that keeps each session as a Redis
|
|
136
|
-
Stream; pass `client` to inject your own. See
|
|
137
|
-
[Redis](/server/production#redis-recommended) for setup and per-command costs.
|
|
138
|
-
|
|
139
|
-
- Every write publishes a wake-up, so live streams are push-driven, not
|
|
140
|
-
polled.
|
|
141
|
-
- A wake-up is disposable: a safety re-read recovers a lost one within ten
|
|
142
|
-
seconds.
|
|
143
|
-
|
|
144
|
-
## `experimental-a2/store-redis-http`
|
|
145
|
-
|
|
146
|
-
`redisHttp({ url, token })` returns the same Redis store over `fetch`, for
|
|
147
|
-
providers that expose only a REST API, such as Upstash. See
|
|
148
|
-
[Redis](/server/production#redis-recommended) for setup and the trust warning.
|
|
149
|
-
|
|
150
|
-
- Live events and presence use Upstash's HTTP `SUBSCRIBE` endpoint (SSE), with
|
|
151
|
-
the same URL and token. Each active channel holds one outbound HTTP
|
|
152
|
-
subscription per store instance, shared by its local viewers and closed when
|
|
153
|
-
the last viewer leaves.
|
|
154
|
-
- Writes publish their wake-ups in the same Redis script as the append. A
|
|
155
|
-
rejected notification does not fail the applied write.
|
|
156
|
-
- Idle event streams re-read every ten seconds to recover missed
|
|
157
|
-
notifications. Subscriptions reconnect on their own and catch up from the
|
|
158
|
-
event cursor.
|
|
159
|
-
- After a reconnect, A2 re-reads the current presence fields and clears fields
|
|
160
|
-
deleted during the gap, so presence may repeat its latest values. Updates
|
|
161
|
-
received during that read take precedence.
|
|
162
|
-
- If the subscribe endpoint answers 404, 405, or 501, events and presence use
|
|
163
|
-
adaptive polling instead, from 25ms to two seconds while idle.
|
|
164
|
-
|
|
165
|
-
## `experimental-a2/scheduler-vercel`
|
|
166
|
-
|
|
167
|
-
`vercelQueues(options?)` returns an `A2Scheduler` backed by Vercel Queues.
|
|
168
|
-
|
|
169
|
-
```ts server/scheduler-vercel.ts
|
|
170
|
-
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
171
|
-
|
|
172
|
-
export const scheduler = vercelQueues()
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
| Option | Type | Default |
|
|
176
|
-
| --- | --- | --- |
|
|
177
|
-
| `topic` | `string` matching `[A-Za-z0-9_-]+` | `'a2'` |
|
|
178
|
-
| `deploymentId` | `string \| null` | `VERCEL_DEPLOYMENT_ID` |
|
|
179
|
-
| `transport` | `QueueTransport` | lazy `@vercel/queue` client |
|
|
180
|
-
| `clock` | `{ now(): Date }` | system clock |
|
|
181
|
-
|
|
182
|
-
- `deploymentId` passes to the Queue SDK unchanged and applies to every task,
|
|
183
|
-
recovery watchdogs included. Omit it for `VERCEL_DEPLOYMENT_ID`, pass an ID
|
|
184
|
-
to target that deployment, or pass `null` for deploymentless delivery.
|
|
185
|
-
Publishing and consuming clients must use the same deployment scope.
|
|
186
|
-
- One configured client handles sends, payload fetches, acknowledgements, and
|
|
187
|
-
visibility changes; callback contents never choose the client.
|
|
188
|
-
- Pinned tasks stay with their target deployment after another is promoted.
|
|
189
|
-
Deploymentless delivery lets Vercel choose an eligible consumer, not the
|
|
190
|
-
latest production deployment; every eligible consumer must use
|
|
191
|
-
`deploymentId: null` and stay compatible with pending tasks and session work.
|
|
192
|
-
- Tasks are retained for seven days, Vercel Queues' maximum. A delay goes out
|
|
193
|
-
as asked; Vercel Queues refuses one longer than the retention.
|
|
194
|
-
- `transport` and `clock` are advanced injection seams for tests and custom
|
|
195
|
-
delivery. An injected transport receives the topic, an exact
|
|
196
|
-
`SchedulerTask`, and send options, and owns its provider configuration;
|
|
197
|
-
`deploymentId` configures only the built-in client.
|
|
198
|
-
|
|
199
|
-
See [Vercel Queues](/server/production#vercel-queues-recommended).
|
|
200
|
-
|
|
201
|
-
## `experimental-a2/scheduler-qstash`
|
|
202
|
-
|
|
203
|
-
`qstash(options?)` returns an `A2Scheduler` backed by QStash HTTP delivery.
|
|
204
|
-
Vercel Queues remains the recommended default on Vercel; use QStash for a
|
|
205
|
-
scheduler that calls a stable public endpoint.
|
|
206
|
-
|
|
207
|
-
```ts server/scheduler-qstash.ts
|
|
208
|
-
import { qstash } from 'experimental-a2/scheduler-qstash'
|
|
209
|
-
|
|
210
|
-
export const scheduler = qstash()
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
| Option | Type | Default |
|
|
214
|
-
| --- | --- | --- |
|
|
215
|
-
| `url` | `string` | conventional local or Vercel callback; required for process-less runtimes, other production, and custom routes |
|
|
216
|
-
| `token` | `string` | `QSTASH_TOKEN` |
|
|
217
|
-
| `currentSigningKey` | `string` | `QSTASH_CURRENT_SIGNING_KEY` |
|
|
218
|
-
| `nextSigningKey` | `string` | `QSTASH_NEXT_SIGNING_KEY` |
|
|
219
|
-
| `headers` | `Readonly<Record<string, string>>` | no additional callback headers |
|
|
220
|
-
| `client` | `QStashPublishingClient` | lazy `@upstash/qstash` client |
|
|
221
|
-
| `retries` | non-negative integer | the QStash provider default |
|
|
222
|
-
| `maxBodyBytes` | positive integer | `1_048_576` (1 MiB) for callbacks and reserved outbound bodies |
|
|
223
|
-
| `transport` | `QStashTransport` | lazy `@upstash/qstash` client and receiver |
|
|
224
|
-
| `clock` | `{ now(): Date }` | system clock |
|
|
225
|
-
|
|
226
|
-
Mount the application handler once and pass every server that shares the
|
|
227
|
-
scheduler:
|
|
228
|
-
|
|
229
|
-
```ts app/api/a2/scheduler/route.ts
|
|
230
|
-
import { billingServer, ordersServer, scheduler } from '@/server'
|
|
231
|
-
|
|
232
|
-
export const POST = scheduler.handler(ordersServer, billingServer)
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
**Callback URL.** An explicit `url` always wins. Otherwise:
|
|
236
|
-
|
|
237
|
-
- Vercel production: `https://${VERCEL_PROJECT_PRODUCTION_URL}/api/a2/scheduler`.
|
|
238
|
-
Preview: `https://${VERCEL_BRANCH_URL}/api/a2/scheduler`. Both are moving
|
|
239
|
-
aliases, so pending tasks can reach later deployments on the same URL.
|
|
240
|
-
- A runtime that exposes `process.env`, outside `NODE_ENV=production`:
|
|
241
|
-
`http://localhost:${PORT ?? 3000}/api/a2/scheduler`, with `devMode: true` on
|
|
242
|
-
the SDK client and receiver. `QSTASH_DEV` is not required, and
|
|
243
|
-
`QSTASH_DEV=false` does not disable it. `PORT` must be an integer from 1
|
|
244
|
-
through 65535; `QSTASH_DEV_PORT` configures the local QStash server, not the
|
|
245
|
-
callback.
|
|
246
|
-
- Automatic local startup needs `@upstash/qstash` 2.11.0 or newer in Node.js;
|
|
247
|
-
the lazy transport starts it on the first publish or verification. An Edge
|
|
248
|
-
runtime that exposes `process.env` infers the local URL but must start the
|
|
249
|
-
development server separately.
|
|
250
|
-
- Non-Vercel production, process-less runtimes, custom hosts or paths, ports
|
|
251
|
-
not available through `PORT`, and Docker networking need an explicit
|
|
252
|
-
absolute URL.
|
|
253
|
-
- Outside production, an explicit URL leaves SDK development mode to
|
|
254
|
-
`QSTASH_DEV` or a manual `QSTASH_URL` plus credentials. In production, A2
|
|
255
|
-
passes `devMode: false`.
|
|
256
|
-
|
|
257
|
-
**Options.**
|
|
258
|
-
|
|
259
|
-
- `headers`: static headers on every callback delivery, recovery successors
|
|
260
|
-
included. Validated and snapshotted when `qstash()` runs; A2 asks QStash to
|
|
261
|
-
redact every value from its dashboard and API, but QStash still receives,
|
|
262
|
-
stores, and delivers them. Names are case-insensitive. Reserved:
|
|
263
|
-
provider-owned `Upstash-*`, `Content-Type*`, HTTP framing headers, and A2's
|
|
264
|
-
managed Vercel identity headers.
|
|
265
|
-
- `client`: the structural `publishJSON(...)` part of a configured
|
|
266
|
-
`@upstash/qstash` `Client`, for its API endpoint, publish-request retry
|
|
267
|
-
policy, or telemetry. Not combinable with `token` or `transport`; A2 still
|
|
268
|
-
creates the `Receiver` lazily from the signing keys. Put callback credentials
|
|
269
|
-
in `headers`, since client-global callback headers and redaction are outside
|
|
270
|
-
A2's merge contract. The client receives raw callback credentials, including
|
|
271
|
-
an automatic preview identity, and must honor per-publish redaction.
|
|
272
|
-
- `transport`, `clock`: advanced injection seams for tests and custom delivery.
|
|
273
|
-
A `QStashTransport` implements `publish(request)` and `verify(request)`.
|
|
274
|
-
Publish requests carry the callback `url`, a complete `SchedulerTask` body
|
|
275
|
-
(plus provider-private generation metadata for store-outage successors),
|
|
276
|
-
optional `headers` and `redact.header` names, an optional Unix-seconds
|
|
277
|
-
`notBefore`, and a deterministic `deduplicationId`; preserve the body,
|
|
278
|
-
deliver every header, and apply every redaction. Verify requests carry the
|
|
279
|
-
signature, raw body, exact callback URL, and optional Upstash region.
|
|
280
|
-
- `retries`: omitted, QStash uses its provider default; an explicit value is
|
|
281
|
-
forwarded. Every delivery attempt is billable.
|
|
282
|
-
- `maxBodyBytes`: the adapter reserves room for a task's largest possible
|
|
283
|
-
store-outage lineage before its initial publish, so an accepted task never
|
|
284
|
-
exceeds the limit later. Raise it for a larger paid-plan payload limit. Keep
|
|
285
|
-
one value across every deployment that receives this URL: a lower value can
|
|
286
|
-
return a non-retryable 489 for a task an older value accepted.
|
|
287
|
-
|
|
288
|
-
**The route.**
|
|
289
|
-
|
|
290
|
-
- Must be reachable by the selected QStash server after any platform edge
|
|
291
|
-
authentication; the development server calls localhost directly.
|
|
292
|
-
- Reads the raw body once and verifies `Upstash-Signature`, the exact URL, and
|
|
293
|
-
`Upstash-Region` before parsing or touching a server. Missing or invalid
|
|
294
|
-
signatures return 401.
|
|
295
|
-
- A signed malformed envelope, or a declared or streamed body over
|
|
296
|
-
`maxBodyBytes`, returns 489 with `Upstash-NonRetryable-Error: true`.
|
|
297
|
-
- Returns 200 when settled; when busy or stalled, after a durable successor
|
|
298
|
-
watchdog exists, so each caught failure gets a fresh message and a fresh
|
|
299
|
-
per-message retry budget. Append tasks return 200 after their fixed-id append
|
|
300
|
-
succeeds and any recovery watchdog for pending handler work is durable; clear
|
|
301
|
-
tasks after their conditional clear and its deletion finish.
|
|
302
|
-
- A retryable store outage hands drain, append, and clear tasks to a fresh
|
|
303
|
-
successor before acknowledging: 30 seconds, times four per signed
|
|
304
|
-
generation, capped at one day. The generation keeps increasing past the cap,
|
|
305
|
-
so each handoff gets a new deduplication id, and duplicate deliveries of one
|
|
306
|
-
generation publish the same successor.
|
|
307
|
-
- Unknown contracts, missing scheduler configuration, schema failures, and
|
|
308
|
-
successor publish failures keep the delivery retryable.
|
|
309
|
-
|
|
310
|
-
**Delivery.**
|
|
311
|
-
|
|
312
|
-
- At least once, so handler side effects need idempotency keys. QStash's
|
|
313
|
-
documented 10-minute deduplication window can reduce duplicate watchdogs but
|
|
314
|
-
is not part of A2's correctness model; stable scheduled-event ids keep timer
|
|
315
|
-
redelivery safe after it.
|
|
316
|
-
- During a prolonged outage where QStash cannot reach the callback, nothing
|
|
317
|
-
publishes a successor: monitor the DLQ and redrive.
|
|
318
|
-
- One-shot delays are capped at 7 days on free plans and 1 year on usage-based
|
|
319
|
-
plans, with no fixed maximum on fixed plans. Leave one second of margin at a
|
|
320
|
-
plan maximum: `notBefore` uses whole Unix seconds.
|
|
321
|
-
- Carries both recovery watchdogs and `session.schedule` tasks.
|
|
322
|
-
|
|
323
|
-
**Protected previews.** For an inferred preview callback, each publish
|
|
324
|
-
forwards the invocation's `x-vercel-oidc-token` as
|
|
325
|
-
`x-vercel-trusted-oidc-idp-token`, with redaction requested, when it is a
|
|
326
|
-
parseable JWT that expires at least 60 seconds after the task's first due time,
|
|
327
|
-
so a matching Vercel Trusted Sources rule can admit the first delivery. QStash
|
|
328
|
-
still receives it. This is opportunistic: missing, malformed, and near-expiry
|
|
329
|
-
tokens fall back to an ordinary publish; an explicit `url` opts out; inferred
|
|
330
|
-
production and local callbacks never forward it; and QStash reuses the stored
|
|
331
|
-
header on retries, so delayed tasks and retries can outlive it. When every
|
|
332
|
-
protected delivery must stay authorized, use an explicit automation bypass
|
|
333
|
-
through `headers` (pending messages keep the captured value after rotation) or
|
|
334
|
-
public ingress. Signature verification still applies after the edge-access
|
|
335
|
-
check.
|
|
336
|
-
|
|
337
|
-
See [QStash](/server/production#qstash).
|
|
338
|
-
|
|
339
|
-
## Scheduler send errors
|
|
340
|
-
|
|
341
|
-
Both built-in adapters preserve the original provider error from a rejected
|
|
342
|
-
send.
|
|
343
|
-
|
|
344
|
-
- Terminal: a recognized definite 4xx status or error, except 408, 409, 425,
|
|
345
|
-
and 429. The Vercel adapter also treats the SDK's name-only
|
|
346
|
-
`BadRequestError`, `UnauthorizedError`, and `ForbiddenError` as terminal.
|
|
347
|
-
- Retryable: 408, 409, 425, 429, 5xx responses, network failures, and errors
|
|
348
|
-
with no recognized status.
|
|
349
|
-
- The Vercel adapter's native duplicate-message error is success.
|
|
350
|
-
- A2's durable AI tool execution uses this private classification. Direct
|
|
351
|
-
`session.schedule()` calls still reject with the original error object.
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Handlers
|
|
3
|
-
description: Plain async functions that react to events. Concurrent by default, serialized by lanes, retried after failures.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
```ts server/orders.ts
|
|
7
|
-
import { createServer } from 'experimental-a2/server'
|
|
8
|
-
import { orders } from '@/contracts'
|
|
9
|
-
|
|
10
|
-
export const ordersServer = createServer({
|
|
11
|
-
contract: orders,
|
|
12
|
-
handlers: {
|
|
13
|
-
created: async ({ event }) => {
|
|
14
|
-
// your side effect, e.g. email the shop, idempotent via event.id:
|
|
15
|
-
// await sendEmailToShop(event.payload, { idempotencyKey: event.id })
|
|
16
|
-
return { type: 'shop.notified', payload: {} }
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
})
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
A handler runs when its event lands: a plain async function that can call a
|
|
23
|
-
database, hit an API, or stream a model response. A2 claims the event while it
|
|
24
|
-
runs and retries it after a crash.
|
|
25
|
-
|
|
26
|
-
`createServer` from `experimental-a2/server` binds the contract to a
|
|
27
|
-
[store](/server/production#1-choose-a-store) and one complete handler table,
|
|
28
|
-
so a handler can't go missing because its module was never imported. Compose
|
|
29
|
-
the table across files by spreading objects into `handlers`. It is the only
|
|
30
|
-
entry point that touches a store, and it fails loudly in a browser bundle.
|
|
31
|
-
|
|
32
|
-
## Events without handlers
|
|
33
|
-
|
|
34
|
-
```ts server/order-history.ts
|
|
35
|
-
import { createServer } from 'experimental-a2/server'
|
|
36
|
-
import { postgres } from 'experimental-a2/store-postgres'
|
|
37
|
-
import { orders } from '@/contracts'
|
|
38
|
-
|
|
39
|
-
export const orderHistory = createServer({
|
|
40
|
-
contract: orders,
|
|
41
|
-
store: postgres({ connectionString: process.env.DATABASE_URL }),
|
|
42
|
-
})
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Handlers are optional per event type. An event without one is still logged,
|
|
46
|
-
folded into state, and streamed; it settles in its append transaction, with no
|
|
47
|
-
claim or scheduler message. Adding a handler later doesn't replay settled
|
|
48
|
-
events. Append a new event or run a migration when you want that.
|
|
49
|
-
|
|
50
|
-
## The context
|
|
51
|
-
|
|
52
|
-
| Property | What it is |
|
|
53
|
-
| ------------- | ---------- |
|
|
54
|
-
| `ctx.event` | The triggering event: `{ id, type, payload, index, sessionId, createdAt }`. |
|
|
55
|
-
| `ctx.attempt` | The durable 1-based dispatch claim for this event. |
|
|
56
|
-
| `ctx.session` | This session's handle: `id`, `append`, `schedule`, `clear`, `history`, `state`, `stream`, and `setPresence` (presence contracts only). |
|
|
57
|
-
| `ctx.signal` | An `AbortSignal`. Fires on matching `abortOn` events and when the attempt loses its claim (`CLAIM_EXPIRED`, `SUPERSEDED_ATTEMPT`). See [Cancellation](/server/cancellation). |
|
|
58
|
-
|
|
59
|
-
`ctx.attempt` increments on every durable claim. It can skip a number when a
|
|
60
|
-
process dies between the claim and handler entry.
|
|
61
|
-
|
|
62
|
-
[`ctx.session.state(reducer)`](/contract/state) folds through
|
|
63
|
-
`ctx.event.index`, so later concurrent appends don't change what this event
|
|
64
|
-
sees. Pass `{ through: 'latest' }` to read the current committed frontier
|
|
65
|
-
instead.
|
|
66
|
-
|
|
67
|
-
## Return what happens after success
|
|
68
|
-
|
|
69
|
-
```ts server/return-many.ts
|
|
70
|
-
import { createServer } from 'experimental-a2/server'
|
|
71
|
-
import { orders } from '@/contracts'
|
|
72
|
-
|
|
73
|
-
export const fanout = createServer({
|
|
74
|
-
contract: orders,
|
|
75
|
-
handlers: {
|
|
76
|
-
'shop.started': async () => {
|
|
77
|
-
return orders.batch(
|
|
78
|
-
{ type: 'shop.notified', payload: {} },
|
|
79
|
-
{ type: 'driver.notified', payload: { driverId: 'driver-7' } },
|
|
80
|
-
)
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Return one event or an array when it should exist only if the handler
|
|
87
|
-
succeeds. A2 marks the trigger processed and appends the batch in one atomic
|
|
88
|
-
operation, at consecutive positions. If the handler throws or the process dies
|
|
89
|
-
first, none of it exists. Returned events get deterministic ids, so a retry
|
|
90
|
-
after a lost acknowledgement doesn't duplicate them.
|
|
91
|
-
|
|
92
|
-
`contract.batch(...)` is a type helper that keeps each event's literal type;
|
|
93
|
-
A2 still validates at commit.
|
|
94
|
-
|
|
95
|
-
## Append what should happen now
|
|
96
|
-
|
|
97
|
-
```ts server/append-now.ts
|
|
98
|
-
import { createServer } from 'experimental-a2/server'
|
|
99
|
-
import { orders } from '@/contracts'
|
|
100
|
-
|
|
101
|
-
export const immediate = createServer({
|
|
102
|
-
contract: orders,
|
|
103
|
-
handlers: {
|
|
104
|
-
created: async ({ session }) => {
|
|
105
|
-
await session.append('notify-shop', {
|
|
106
|
-
type: 'shop.notified',
|
|
107
|
-
payload: {},
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
// The shop.notified handler may already be running here.
|
|
111
|
-
// await continueCurrentWork()
|
|
112
|
-
},
|
|
113
|
-
'shop.notified': async () => {},
|
|
114
|
-
},
|
|
115
|
-
})
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
`ctx.session.append()` commits immediately, so the appended event can start
|
|
119
|
-
while its parent is still running. Use it for streaming progress, independent
|
|
120
|
-
fan-out, or any fact that stays true even if the handler later fails. Each
|
|
121
|
-
call is atomic.
|
|
122
|
-
|
|
123
|
-
The first argument names the append within the triggering event. A2 derives
|
|
124
|
-
the ids of events without an `id` from the trigger, the name, and each
|
|
125
|
-
event's position, so a retry gets the existing events back, whatever order
|
|
126
|
-
parallel branches finish in. Keep the name stable; reusing it with a different
|
|
127
|
-
batch conflicts. An explicit `id` wins, so different triggers can converge on
|
|
128
|
-
one fact. Top-level `server.session(id).append(...events)` takes no name.
|
|
129
|
-
|
|
130
|
-
## Concurrent by default
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
// anywhere on the server:
|
|
134
|
-
await ordersServer.session('order-42').append(
|
|
135
|
-
{ type: 'created', payload: { shopId: 'shop-7', items: [] } },
|
|
136
|
-
{ type: 'shop.started', payload: {} },
|
|
137
|
-
)
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Every eligible event has its own claim, so both handlers start without
|
|
141
|
-
waiting for each other. Log order stays stable; handler order doesn't. A
|
|
142
|
-
failure retries that event without holding up unlaned events or other lanes.
|
|
143
|
-
|
|
144
|
-
## Lanes
|
|
145
|
-
|
|
146
|
-
```ts server/serial-orders.ts
|
|
147
|
-
import { createServer } from 'experimental-a2/server'
|
|
148
|
-
import { orders } from '@/contracts'
|
|
149
|
-
|
|
150
|
-
export const serialOrders = createServer({
|
|
151
|
-
contract: orders,
|
|
152
|
-
handlers: {
|
|
153
|
-
created: {
|
|
154
|
-
lane: 'order-work',
|
|
155
|
-
handler: async () => {},
|
|
156
|
-
},
|
|
157
|
-
'shop.started': {
|
|
158
|
-
lane: 'order-work',
|
|
159
|
-
handler: async () => {},
|
|
160
|
-
},
|
|
161
|
-
'driver.notified': {
|
|
162
|
-
lane: ({ event }) => `driver:${event.payload.driverId}`,
|
|
163
|
-
handler: async ({ event }) => {
|
|
164
|
-
// your work for this driver, using event.payload.driverId
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
})
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Use a lane when events share a resource and must not overlap. Within one
|
|
172
|
-
session, same-lane events run one at a time in log order; different lanes and
|
|
173
|
-
unlaned events stay concurrent. A lane is a constant (a serial queue) or a
|
|
174
|
-
function of the validated event.
|
|
175
|
-
|
|
176
|
-
A2 stores the resolved lane at append, so a deployment can't reinterpret
|
|
177
|
-
pending work. Lanes are per session, not distributed locks across sessions. If
|
|
178
|
-
a lane's first unfinished event fails, later events in that lane wait; other
|
|
179
|
-
lanes continue.
|
|
180
|
-
|
|
181
|
-
## Handlers can run twice
|
|
182
|
-
|
|
183
|
-
Handlers are at-least-once. A crash after your side effect but before A2
|
|
184
|
-
records completion runs the handler again.
|
|
185
|
-
|
|
186
|
-
- **External side effects take an idempotency key.** `event.id` is stable
|
|
187
|
-
across attempts.
|
|
188
|
-
- **Named appends are deduplicated.** A retry gets the existing events back.
|
|
189
|
-
- **Returned events commit with completion.** They all exist with the
|
|
190
|
-
processed marker, or none do.
|
|
191
|
-
|
|
192
|
-
## When handlers throw
|
|
193
|
-
|
|
194
|
-
Throwing means "retry me." A2 records the failure and retries with backoff.
|
|
195
|
-
The failed event blocks only its lane. After ten failed attempts it
|
|
196
|
-
dead-letters and keeps blocking its lane until
|
|
197
|
-
[you retry or skip it](/server/production#3-when-an-event-dead-letters).
|
|
198
|
-
|
|
199
|
-
When someone presses stop, catch the abort and return normally instead. See
|
|
200
|
-
[Cancellation](/server/cancellation).
|