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
package/docs/reference/04-ai.mdx
CHANGED
|
@@ -65,9 +65,10 @@ The `UIMessage` type a contract's input commands (`ai.control.requested`)
|
|
|
65
65
|
carry, such as `AIMessageOf<typeof contract.events>`; `UIMessage` otherwise.
|
|
66
66
|
`agent()` types messages with it.
|
|
67
67
|
|
|
68
|
-
### `
|
|
68
|
+
### `consumerContext(contract?)`
|
|
69
69
|
|
|
70
|
-
Returns the current tool execution's durable
|
|
70
|
+
Returns the current tool execution's durable
|
|
71
|
+
[consumer context](/server/consumers#the-context), an
|
|
71
72
|
`AgentToolContext<D>` with `event`, `attempt`, `session`, and `signal`.
|
|
72
73
|
|
|
73
74
|
- Tools are plain AI SDK `tool()` definitions, passed through uninterpreted;
|
|
@@ -123,7 +124,7 @@ Pure typed builders of `ai.control.requested` commands, for `append()` and
|
|
|
123
124
|
- **Trust.** Browser ingress accepts user messages, approval and input
|
|
124
125
|
responses, interruptions, and explicit retries. `inputs.requestInput()`,
|
|
125
126
|
`inputs.toolResult()`, and non-user `inputs.seed()` messages are trusted
|
|
126
|
-
server appends; only
|
|
127
|
+
server appends; only A2's own consumers append generation requests and AI
|
|
127
128
|
lifecycle events. No builder covers session lifecycle: append
|
|
128
129
|
`ai.session.created` and `ai.session.closed` from trusted server code.
|
|
129
130
|
- **IDs.** Message, seed, and steer commands use the input message ID as their
|
|
@@ -336,7 +337,7 @@ agent({
|
|
|
336
337
|
progress?,
|
|
337
338
|
toolLane?: string, // run local tool executions one at a time on this lane
|
|
338
339
|
toolRecovery?: { [tool]?: ToolRecovery }, // 'rerun' (default) | 'report'
|
|
339
|
-
|
|
340
|
+
consumers?, // your consumers, next to A2's own
|
|
340
341
|
store?, scheduler?, telemetry?,
|
|
341
342
|
}): Agent<Events, State, Name> // an A2Server
|
|
342
343
|
```
|
|
@@ -350,10 +351,10 @@ stub.
|
|
|
350
351
|
| `name` | the built-in protocol, with optional server-only `events` and `messageSchema` |
|
|
351
352
|
| `contract` | a shared `agentContract()`, when clients fold application state |
|
|
352
353
|
| `model` | an AI Gateway model ID, or [`ModelSettings`](#modelsettings) with one, under every harness; a value or per-generation async resolver |
|
|
353
|
-
| `tools` | AI SDK `tool()` definitions; local tools read context with [`
|
|
354
|
+
| `tools` | AI SDK `tool()` definitions; local tools read context with [`consumerContext()`](#consumercontextcontract) |
|
|
354
355
|
| `instructions` | a value or per-generation async resolver |
|
|
355
356
|
| `maxSteps` | a positive integer capping one complete assistant response, under every harness; no limit by default |
|
|
356
|
-
| `harness` | the agent's loop: `{ type: 'ai-sdk', … }` (`AISDKHarness`, the default), [`fx()`](#fx), or [`pi()`](#
|
|
357
|
+
| `harness` | the agent's loop: `{ type: 'ai-sdk', … }` (`AISDKHarness`, the default), [`fx()`](#fx), or [`pi()`](#pi-sandbox-) |
|
|
357
358
|
| `harness.generation` | per-step settings, or a resolver |
|
|
358
359
|
| `harness.messageMetadata` | maps default stream parts to message metadata |
|
|
359
360
|
| `harness.generate` | replaces the default model step |
|
|
@@ -361,8 +362,8 @@ stub.
|
|
|
361
362
|
| `progress` | durable batching: `maxChunks` (128), `maxDelayMs` (0) |
|
|
362
363
|
| `toolLane` | one lane for every local tool execution, one at a time in admission order; model steps keep their own lane |
|
|
363
364
|
| `toolRecovery` | `'rerun'` (default) or `'report'` per tool, the harness's own tools included |
|
|
364
|
-
| `
|
|
365
|
-
| `store`, `scheduler`, `telemetry` | as for any A2 server; `telemetry` covers Core and AI in one trace ([span catalogue](/reference/
|
|
365
|
+
| `consumers` | your [consumers](/server/consumers), next to A2's own: `a2.ai.metadata` (concurrent), `a2.ai.control` (one lane for commands, work reports, and closes), `a2.ai.model` (one lane for generations), and `a2.ai.tools` (on `toolLane`, or concurrent). Yours may take any of the contract's events, the agent's own included, and run beside A2's. Names starting with `a2.` are reserved |
|
|
366
|
+
| `store`, `scheduler`, `telemetry` | as for any A2 server; `telemetry` covers Core and AI in one trace ([span catalogue](/reference/telemetry#the-span-catalogue)) |
|
|
366
367
|
|
|
367
368
|
**Contracts.** A contract not built by `agentContract()` throws a `TypeError`
|
|
368
369
|
and fails to typecheck (serve custom contracts with `createServer()`). Messages
|
|
@@ -384,7 +385,7 @@ overrides model headers. See
|
|
|
384
385
|
**`harness`** is the agent's loop. `{ type: 'ai-sdk' }` (`AISDKHarness`), the
|
|
385
386
|
default, runs one AI SDK `streamText()` step per durable generation, with the
|
|
386
387
|
`generation`, `messageMetadata`, `generate`, and `compaction` options below.
|
|
387
|
-
[`fx()`](#fx) and [`pi()`](#
|
|
388
|
+
[`fx()`](#fx) and [`pi()`](#pi-sandbox-) run a coding agent's own loop instead.
|
|
388
389
|
The four options exist only in `{ type: 'ai-sdk' }`; passing one to `agent()`
|
|
389
390
|
itself throws a `TypeError` naming where it belongs.
|
|
390
391
|
|
|
@@ -445,11 +446,11 @@ compaction. It returns exactly one model step as a
|
|
|
445
446
|
the durable lifecycle. See
|
|
446
447
|
[Replace generation, not durability](/agents/conversations#replace-generation-not-durability).
|
|
447
448
|
|
|
448
|
-
**Tools.** Independent authorized tool
|
|
449
|
-
fixed limit. Results and generation completion
|
|
450
|
-
continuation needs the closed step and every
|
|
451
|
-
Single-owner terminal events commit atomically with
|
|
452
|
-
completion and carry its durable cause; many-owner joins and preliminary
|
|
449
|
+
**Tools.** Independent authorized tool executions run concurrently with no
|
|
450
|
+
fixed limit, unless `toolLane` orders them. Results and generation completion
|
|
451
|
+
arrive in either order; a continuation needs the closed step and every
|
|
452
|
+
required terminal result. Single-owner terminal events commit atomically with
|
|
453
|
+
the parent run's completion and carry its durable cause; many-owner joins and preliminary
|
|
453
454
|
outputs append immediately and idempotently. Provider calls and external side
|
|
454
455
|
effects are at least once: give them their own idempotency. An approved
|
|
455
456
|
provider-executed call can satisfy the join from its approval response, and a
|
|
@@ -489,8 +490,8 @@ See [Delivery semantics](/agents/conversations#delivery-semantics).
|
|
|
489
490
|
- Static options are validated at construction, resolved ones before their
|
|
490
491
|
generation; each session uses its own value.
|
|
491
492
|
- Discovery: without an explicit threshold, the first generation batches a
|
|
492
|
-
metadata request into its start write, and
|
|
493
|
-
model's limits from the public Gateway catalog in `state.modelMetadata`
|
|
493
|
+
metadata request into its start write, and A2's `a2.ai.metadata` consumer
|
|
494
|
+
records the model's limits from the public Gateway catalog in `state.modelMetadata`
|
|
494
495
|
without delaying dispatch (shared one-hour cache, five-second timeout; no new
|
|
495
496
|
request per turn, even after exhausted retries). The default threshold is 75%
|
|
496
497
|
of the context window, lowered for a larger output allowance. While it is
|
|
@@ -525,7 +526,7 @@ See [Compact long conversations](/agents/conversations#compact-long-conversation
|
|
|
525
526
|
**Harnesses.** Under `fx()` and `pi()`, each model call the harness makes is one
|
|
526
527
|
durable step of the response, with the same events, progress, steering,
|
|
527
528
|
interruption, `maxSteps`, and recovery as any step. Tool calls it makes become
|
|
528
|
-
ordinary A2 tool calls, executed by A2's
|
|
529
|
+
ordinary A2 tool calls, executed by A2's `a2.ai.tools` consumer, and `toolRecovery` may
|
|
529
530
|
name the harness's own tools, such as Pi's `bash`. `model` is resolved per step
|
|
530
531
|
as for any agent. `instructions` and `tools` reach the harness; how they
|
|
531
532
|
combine with its own prompt and tools depends on the harness. Tool approval
|
|
@@ -569,11 +570,11 @@ resolver. `reasoning` becomes the AI SDK `reasoning` call option
|
|
|
569
570
|
model has one; `'standard'` removes a configured speed. Both override the same
|
|
570
571
|
settings in `generation`. See [Choose the model](/agents/introduction#choose-the-model).
|
|
571
572
|
|
|
572
|
-
### `
|
|
573
|
+
### `runWithConsumerContext(ctx, fn)`
|
|
573
574
|
|
|
574
575
|
The test seam for context-reading tools: runs `fn` with `ctx` as the ambient
|
|
575
|
-
|
|
576
|
-
`
|
|
576
|
+
consumer context, so tests can call `execute` directly. Inside `fn`,
|
|
577
|
+
`consumerContext()` returns `ctx`, with or without a contract argument.
|
|
577
578
|
|
|
578
579
|
## `experimental-a2/ai/fx`
|
|
579
580
|
|
|
@@ -601,6 +602,14 @@ call. Replay grows with the response, roughly 0.1 to 0.5 seconds for one of 80
|
|
|
601
602
|
to 200 tool calls. When libfx can't take a checkpoint, A2 replays from an
|
|
602
603
|
earlier one.
|
|
603
604
|
|
|
605
|
+
FX's model calls carry the same [AI Gateway](#agentoptions) defaults as the
|
|
606
|
+
AI SDK harness: `caching: 'auto'`, under any Gateway options FX sets itself,
|
|
607
|
+
and the session's stable `x-session-affinity` header in place of FX's own,
|
|
608
|
+
which changes every time A2 rebuilds the kernel. A2 leaves every other byte of
|
|
609
|
+
the request as FX wrote it, so it adds caching only where FX's own options come
|
|
610
|
+
last in the request, as libfx writes them; otherwise the request goes
|
|
611
|
+
unchanged.
|
|
612
|
+
|
|
604
613
|
Credentials come from `AI_GATEWAY_API_KEY`, or Vercel's OIDC token on Vercel.
|
|
605
614
|
This entry point is server-only.
|
|
606
615
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Telemetry
|
|
3
|
+
description: "Trace A2 with OpenTelemetry, and the spans it reports."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { otel } from 'experimental-a2/otel'
|
|
9
|
+
import { orders } from '@/contracts'
|
|
10
|
+
|
|
11
|
+
export const ordersServer = createServer({
|
|
12
|
+
contract: orders,
|
|
13
|
+
telemetry: otel(),
|
|
14
|
+
// ...store, scheduler, and consumers
|
|
15
|
+
})
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
With any OpenTelemetry setup registered (on Vercel, `@vercel/otel`), one trace
|
|
19
|
+
shows the request, `a2.append`, the inline `a2.drain`, an `a2.event` span per
|
|
20
|
+
consumer run, and the appends those runs make. Failures mark their `a2.event`
|
|
21
|
+
span, and `a2.event.outcome = dead_lettered` is the attribute to alert on.
|
|
22
|
+
Agent servers add `a2.ai.*` spans.
|
|
23
|
+
|
|
24
|
+
## `experimental-a2/otel`
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
otel(options?: { tracer?: Tracer; tracerName?: string }): A2Telemetry
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
OpenTelemetry instrumentation for a server (`@opentelemetry/api` is a peer
|
|
31
|
+
dependency). Pass it as `telemetry`. Spans nest through the active context, so
|
|
32
|
+
the happy path lands in one trace. `A2Telemetry` is a one-method interface:
|
|
33
|
+
anything with `span(name, attributes, fn)` works, and `otel()` is the adapter
|
|
34
|
+
A2 ships.
|
|
35
|
+
|
|
36
|
+
## The span catalogue
|
|
37
|
+
|
|
38
|
+
This catalogue is a contract: renames and additions are breaking for
|
|
39
|
+
dashboards and are called out in the package's `CHANGELOG.md`. All spans carry
|
|
40
|
+
`a2.contract` and `a2.session_id`. Core spans describe the durable runtime;
|
|
41
|
+
`a2.ai.*` spans describe the AI runtime's consumers.
|
|
42
|
+
|
|
43
|
+
| Layer | Span | Wraps |
|
|
44
|
+
| ----- | ---- | ----- |
|
|
45
|
+
| Core | `a2.append` | validation + the store write (one batch) |
|
|
46
|
+
| Core | `a2.drain` | one drain pass, including the consumer runs it starts |
|
|
47
|
+
| Core | `a2.event` | one claimed head, including the consumer's run and its completion |
|
|
48
|
+
| Core | `a2.claim` | one `claimAvailable` store operation |
|
|
49
|
+
| Core | `a2.commit` | one `completeAttempt` or `failAttempt` store operation |
|
|
50
|
+
| Core | `a2.state` | one `state()` read: snapshot-plus-tail load + reducer fold |
|
|
51
|
+
| Core | `a2.snapshot` | one background batch of checkpoint writes |
|
|
52
|
+
| Core | `a2.redrive` | one `session.redrive()`: the store operation and starting the session's wakeup |
|
|
53
|
+
| Core | `a2.consume` | one `session.consume()`, including the drains it runs and the waits between them |
|
|
54
|
+
| AI | `a2.ai.control` | applying an input/work report/session-close decision and preparing its returned events |
|
|
55
|
+
| AI | `a2.ai.generate` | one generation run, including Core calls and model/SDK work |
|
|
56
|
+
| AI | `a2.ai.context` | constructing model messages, including tool prompt reconstruction when uncached |
|
|
57
|
+
| AI | `a2.ai.compaction` | automatic summarization or the custom compaction callback |
|
|
58
|
+
| AI | `a2.ai.tool` | one durable tool run, including application execution |
|
|
59
|
+
| AI | `a2.ai.ack` | waiting for a persisted work report's canonical events and control receipt |
|
|
60
|
+
|
|
61
|
+
Durations include nested work: `a2.event` contains its AI run and later
|
|
62
|
+
commit, AI runs call Core, and `a2.state` includes reducer code.
|
|
63
|
+
Acknowledgements overlap control processing, claims can cover several events,
|
|
64
|
+
and snapshots run in the background, so summing spans, or subtracting summed
|
|
65
|
+
children, does not yield exclusive Core or AI overhead. Model and application
|
|
66
|
+
I/O is included; its child spans have the detail.
|
|
67
|
+
|
|
68
|
+
Attributes arrive in two waves: **at start** (passed to `span()`) and
|
|
69
|
+
**mid-span** (via `handle.setAttribute`, for outcomes and counts known only
|
|
70
|
+
after the work ran). A recorder must merge both; the ones you alert on are all
|
|
71
|
+
mid-span.
|
|
72
|
+
|
|
73
|
+
| Attribute | Span | When | Values |
|
|
74
|
+
| --------------------- | ----------- | ----- | ----------------------------------------------------- |
|
|
75
|
+
| `a2.append.source` | `a2.append` | start | `external` \| `consumer` |
|
|
76
|
+
| `a2.append.mode` | `a2.append` | start | `inline` \| `dispatch` |
|
|
77
|
+
| `a2.append.types` | `a2.append` | start | comma-joined event types |
|
|
78
|
+
| `a2.append.count` | `a2.append` | start | batch size |
|
|
79
|
+
| `a2.append.armed` | `a2.append` | mid | `false` when the scheduler arm failed and this append degraded to append-driven healing |
|
|
80
|
+
| `a2.drain.outcome` | `a2.drain` | mid | `settled` \| `busy` \| `stalled` \| `failed` |
|
|
81
|
+
| `a2.drain.processed` | `a2.drain` | mid | events processed this pass |
|
|
82
|
+
| `a2.claim.outcome` | `a2.claim` | mid | `claimed` \| `busy` \| `settled` |
|
|
83
|
+
| `a2.claim.count` | `a2.claim` | mid | number of claimed events, or zero |
|
|
84
|
+
| `a2.commit.operation` | `a2.commit` | start | `complete` \| `fail` |
|
|
85
|
+
| `a2.commit.index` | `a2.commit` | start | event log position |
|
|
86
|
+
| `a2.commit.attempt` | `a2.commit` | start | durable 1-based attempt ordinal |
|
|
87
|
+
| `a2.commit.outcome` | `a2.commit` | mid | `completed` \| `failed` \| `dead_lettered` \| `superseded` |
|
|
88
|
+
| `a2.event.type` | `a2.event` | start | the event's type |
|
|
89
|
+
| `a2.event.index` | `a2.event` | start | log position |
|
|
90
|
+
| `a2.event.id` | `a2.event` | start | event id |
|
|
91
|
+
| `a2.event.consumer` | `a2.event` | start | the consumer running the event |
|
|
92
|
+
| `a2.event.attempt` | `a2.event` | start | same durable 1-based ordinal as `ctx.attempt` |
|
|
93
|
+
| `a2.event.lane` | `a2.event` | start | stored lane value; absent for concurrent unlaned work |
|
|
94
|
+
| `a2.event.handled` | `a2.event` | start | normally `true`; `false` when a claimed head's consumer is not registered |
|
|
95
|
+
| `a2.event.age_ms` | `a2.event` | start | milliseconds since the event was appended, when its run starts: how far behind its consumer is in time |
|
|
96
|
+
| `a2.event.lag` | `a2.event` | start | events appended after this one when it was claimed: how far behind its consumer is in the log. Absent when the store doesn't report its head |
|
|
97
|
+
| `a2.event.outcome` | `a2.event` | mid | `processed` \| `failed` \| `dead_lettered` \| `superseded` \| `surrendered` |
|
|
98
|
+
| `a2.event.aborted` | `a2.event` | mid | `true` when `abortOn` fired during the run |
|
|
99
|
+
| `a2.state.reducer` | `a2.state` | start | the reducer's name |
|
|
100
|
+
| `a2.state.snapshot` | `a2.state` | mid | `hit` \| `miss` \| `rejected` (schema guard discarded it) \| `memory` (served from the server's in-memory state) |
|
|
101
|
+
| `a2.state.folded` | `a2.state` | mid | events folded past the snapshot |
|
|
102
|
+
| `a2.state.index` | `a2.state` | mid | the frontier the returned state reflects |
|
|
103
|
+
| `a2.snapshot.reducer` | `a2.snapshot` | start | the reducer's name |
|
|
104
|
+
| `a2.snapshot.index` | `a2.snapshot` | start | greatest checkpoint index in the write batch |
|
|
105
|
+
| `a2.snapshot.count` | `a2.snapshot` | start | number of checkpoints in the write batch |
|
|
106
|
+
| `a2.consume.consumer` | `a2.consume` | start | the consumer being consumed |
|
|
107
|
+
| `a2.consume.through` | `a2.consume` | mid | the index it runs through |
|
|
108
|
+
| `a2.consume.index` | `a2.consume` | mid | the index it handled every event through |
|
|
109
|
+
| `a2.consume.drains` | `a2.consume` | mid | drains it ran to get there |
|
|
110
|
+
| `a2.redrive.index` | `a2.redrive` | start | log position of the event being redriven |
|
|
111
|
+
| `a2.redrive.consumer` | `a2.redrive` | start | the consumer the event is dead for |
|
|
112
|
+
| `a2.redrive.action` | `a2.redrive` | start | `retry` \| `skip` |
|
|
113
|
+
| `a2.redrive.redriven` | `a2.redrive` | mid | `true` when the event was dead-lettered and is now resolved |
|
|
114
|
+
| `a2.redrive.armed` | `a2.redrive` | mid | `false` when the scheduler arm failed and the wakeup degraded to append-driven healing |
|
|
115
|
+
| `a2.ai.control.type` | `a2.ai.control` | start | `ai.control.requested` \| `ai.work.reported` \| `ai.session.closed` |
|
|
116
|
+
| `a2.ai.control.id` | `a2.ai.control` | start | triggering event id |
|
|
117
|
+
| `a2.ai.control.attempt` | `a2.ai.control` | start | durable 1-based attempt ordinal |
|
|
118
|
+
| `a2.ai.generate.request_id` | `a2.ai.generate` | start | generation request event id |
|
|
119
|
+
| `a2.ai.generate.attempt` | `a2.ai.generate` | start | durable 1-based attempt ordinal |
|
|
120
|
+
| `a2.ai.context.request_id` | `a2.ai.context` | start | generation request event id |
|
|
121
|
+
| `a2.ai.context.phase` | `a2.ai.context` | start | `generation` \| `refresh` \| `tool` |
|
|
122
|
+
| `a2.ai.compaction.request_id` | `a2.ai.compaction` | start | generation request event id |
|
|
123
|
+
| `a2.ai.compaction.generation_id` | `a2.ai.compaction` | start | generation attempt id |
|
|
124
|
+
| `a2.ai.compaction.policy` | `a2.ai.compaction` | start | `automatic` \| `custom` |
|
|
125
|
+
| `a2.ai.tool.request_id` | `a2.ai.tool` | start | originating generation request event id |
|
|
126
|
+
| `a2.ai.tool.call_id` | `a2.ai.tool` | start | tool call id |
|
|
127
|
+
| `a2.ai.tool.name` | `a2.ai.tool` | start | tool name |
|
|
128
|
+
| `a2.ai.tool.attempt` | `a2.ai.tool` | start | durable 1-based attempt ordinal |
|
|
129
|
+
| `a2.ai.ack.request_id` | `a2.ai.ack` | start | reporting worker event id |
|
|
130
|
+
| `a2.ai.ack.report_id` | `a2.ai.ack` | start | work report event id, matching the control span's id |
|
|
131
|
+
|
|
132
|
+
- Consumer lag: group `a2.event` spans by `a2.event.consumer`. `a2.event.lag`
|
|
133
|
+
is how many events the session had appended after the one it claimed, and
|
|
134
|
+
`a2.event.age_ms` how old that event was when its run started. A consumer
|
|
135
|
+
that keeps up keeps both near zero; a rising `a2.event.lag` means it is
|
|
136
|
+
falling behind its log.
|
|
137
|
+
- Drain outcomes: `settled` means nothing actionable is left; `busy` means live
|
|
138
|
+
per-event claims or a failed event's retry hold remain; `stalled` means a
|
|
139
|
+
caught failure in this drain needs a later retry. `failed` appears only on
|
|
140
|
+
scheduler deliveries: the drain threw after arming its successor watchdog,
|
|
141
|
+
the span records the error, and that successor retries the session.
|
|
142
|
+
- A failing consumer run marks its `a2.event` span with the exception and
|
|
143
|
+
error status. Alert on `a2.event.outcome = dead_lettered`. A dead-lettered event
|
|
144
|
+
blocks only later events in its lane, until `session.redrive()` resolves it;
|
|
145
|
+
other lanes stay eligible.
|
|
146
|
+
- `superseded` means a stale completion or failure did not change the log.
|
|
@@ -10,7 +10,7 @@ exposes `A2ClientError`, which extends `A2Error`, for server failures and local
|
|
|
10
10
|
ones alike, so `instanceof A2Error` matches both.
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
|
-
// e.g. in a route
|
|
13
|
+
// e.g. in a route:
|
|
14
14
|
import { A2Error } from 'experimental-a2'
|
|
15
15
|
|
|
16
16
|
export async function POST(req: Request) {
|
|
@@ -40,16 +40,18 @@ export async function POST(req: Request) {
|
|
|
40
40
|
| `UNKNOWN_PRESENCE_FIELD` | a presence field isn't in the contract's `presence` map | no |
|
|
41
41
|
| `PRESENCE_NOT_SUPPORTED` | the contract declares `presence` but the store has no presence capability. Thrown at construction; for environment-resolved stores, at first store use | no |
|
|
42
42
|
| `PARTIAL_DUPLICATE_BATCH` | a batch mixes already-appended and fresh events, contains the same id twice, or uses an id already used in a different session (event ids are globally unique). Always a caller bug; nothing was written | no |
|
|
43
|
-
| `SUPERSEDED_ATTEMPT` | a
|
|
44
|
-
| `SESSION_CLEARED` | a state read reaches below the latest clear, a
|
|
43
|
+
| `SUPERSEDED_ATTEMPT` | a consumer's append or clear comes from an attempt that a recovery claim or a session clear replaced; nothing was written. Also the abort reason on that attempt's `ctx.signal` | no |
|
|
44
|
+
| `SESSION_CLEARED` | a state read reaches below the latest clear, a consumer reads state past a clear of its event, or an actor call is cleared before it receives its answer | no |
|
|
45
45
|
| `CLAIM_EXPIRED` | never thrown; only the `ctx.signal` abort reason when the attempt's lease lapsed without renewal (assumed loss, decided locally, not proof) | no |
|
|
46
|
+
| `CONSUMER_DEAD_LETTERED` | `session.consume()` reached an event that dead-lettered for that consumer. Resolve it with `session.redrive()`, then consume again | no |
|
|
46
47
|
| `STORE_UNAVAILABLE` | the store backend failed; nothing was written. The original error is the `cause` | **yes** |
|
|
47
|
-
| `STORE_NOT_CONFIGURED` | production boots with no `store` configured (during `next build` page collection, deferred to first use) | no |
|
|
48
|
+
| `STORE_NOT_CONFIGURED` | production boots with no `store` configured (during `next build` page collection, deferred to first use), a store's optional peer dependency is missing, or a Postgres query finds one of A2's tables or columns missing, or that its role can't use (and, with `listenConnectionString`, a subscription finds the notification triggers missing). The message names the fix | no |
|
|
48
49
|
|
|
49
50
|
Retrying an *identical* batch is not `PARTIAL_DUPLICATE_BATCH`: it's an
|
|
50
51
|
idempotent success that returns the previously appended events. A failing
|
|
51
|
-
|
|
52
|
-
[dead-letter the event](/server/guarantees#when-a-
|
|
52
|
+
consumer never makes `append` throw; its failures retry and, if persistent,
|
|
53
|
+
[dead-letter the event](/server/guarantees#when-a-consumer-keeps-failing) for
|
|
54
|
+
that consumer. See
|
|
53
55
|
[What append never throws for](/server/guarantees#what-append-never-throws-for).
|
|
54
56
|
|
|
55
57
|
## `A2ClientError`
|
|
@@ -82,33 +84,37 @@ or `RangeError`.
|
|
|
82
84
|
|
|
83
85
|
## NonRetriableError
|
|
84
86
|
|
|
85
|
-
Throw it (or a subclass, matched by `instanceof`) from a
|
|
87
|
+
Throw it (or a subclass, matched by `instanceof`) from a consumer when the
|
|
86
88
|
failure is deterministic: a guard that refuses this attempt refuses every
|
|
87
|
-
retry identically. The event dead-letters
|
|
88
|
-
ten-failure retry budget. Any other thrown error keeps the
|
|
89
|
+
retry identically. The event dead-letters for that consumer immediately,
|
|
90
|
+
instead of spending the ten-failure retry budget. Any other thrown error keeps the
|
|
89
91
|
ordinary retry contract.
|
|
90
92
|
|
|
91
93
|
```ts
|
|
92
94
|
import { z } from 'zod'
|
|
93
95
|
import * as a2 from 'experimental-a2'
|
|
94
96
|
import { NonRetriableError } from 'experimental-a2'
|
|
95
|
-
import { createServer } from 'experimental-a2/server'
|
|
97
|
+
import { consumer, createServer } from 'experimental-a2/server'
|
|
96
98
|
|
|
97
99
|
const orders = a2.contract({
|
|
98
100
|
name: 'orders',
|
|
99
101
|
events: { created: z.object({ chargeable: z.boolean() }) },
|
|
100
102
|
})
|
|
101
103
|
|
|
104
|
+
const charge = consumer({
|
|
105
|
+
contract: orders,
|
|
106
|
+
name: 'charge',
|
|
107
|
+
types: ['created'],
|
|
108
|
+
}).run(async ({ event }) => {
|
|
109
|
+
if (!event.payload.chargeable) {
|
|
110
|
+
throw new NonRetriableError('this order cannot be charged')
|
|
111
|
+
}
|
|
112
|
+
// transient failures below retry as usual
|
|
113
|
+
})
|
|
114
|
+
|
|
102
115
|
export const ordersServer = createServer({
|
|
103
116
|
contract: orders,
|
|
104
|
-
|
|
105
|
-
created: async ({ event }) => {
|
|
106
|
-
if (!event.payload.chargeable) {
|
|
107
|
-
throw new NonRetriableError('this order cannot be charged')
|
|
108
|
-
}
|
|
109
|
-
// transient failures below retry as usual
|
|
110
|
-
},
|
|
111
|
-
},
|
|
117
|
+
consumers: [charge],
|
|
112
118
|
})
|
|
113
119
|
```
|
|
114
120
|
|
package/docs/reference/meta.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default { order:
|
|
1
|
+
export default { order: 10 }
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Overview
|
|
3
|
+
description: A clock for consumer retries and timers.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { redis } from 'experimental-a2/store-redis'
|
|
9
|
+
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
10
|
+
import { orders } from '@/contracts'
|
|
11
|
+
|
|
12
|
+
export const scheduler = vercelQueues()
|
|
13
|
+
|
|
14
|
+
export const ordersServer = createServer({
|
|
15
|
+
contract: orders,
|
|
16
|
+
store: redis({ url: process.env.REDIS_URL }),
|
|
17
|
+
scheduler,
|
|
18
|
+
consumers: [
|
|
19
|
+
/* ... */
|
|
20
|
+
],
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```ts app/api/a2/scheduler/route.ts
|
|
25
|
+
import { billingServer, ordersServer, scheduler } from '@/server'
|
|
26
|
+
|
|
27
|
+
export const POST = scheduler.handler(ordersServer, billingServer)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A scheduler wakes sessions on a clock. It retries a consumer that failed or
|
|
31
|
+
whose process died, and it delivers [timers](/server/timers), whose delays must
|
|
32
|
+
fit the [provider's limit](/server/timers#provider-limits). Share one
|
|
33
|
+
scheduler across your servers and mount its route once, with every server it
|
|
34
|
+
delivers to. `scheduler.handler(...)` checks those servers when the module
|
|
35
|
+
loads, so a bad route fails at startup.
|
|
36
|
+
|
|
37
|
+
| Scheduler | Use it for |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| [Vercel Queues](/schedulers/vercel-queues) | Apps on Vercel (recommended) |
|
|
40
|
+
| [QStash](/schedulers/qstash) | A stable public URL, called from separate infrastructure |
|
|
41
|
+
|
|
42
|
+
## Without a scheduler
|
|
43
|
+
|
|
44
|
+
If the server has no consumers and nothing calls `session.schedule`, skip it.
|
|
45
|
+
Events settle as they're appended, with no queue messages. That's a complete
|
|
46
|
+
production setup for history, reducers, and live sync.
|
|
47
|
+
|
|
48
|
+
With consumers but no scheduler, a session only wakes on a top-level append or
|
|
49
|
+
an explicit `server.drain()`. Reads never wake it, so a session nobody writes
|
|
50
|
+
to stays stuck. That's fine for internal tools that can heal on the next
|
|
51
|
+
write. For production, add a scheduler.
|
|
52
|
+
|
|
53
|
+
## More than one scheduler
|
|
54
|
+
|
|
55
|
+
Servers with different scheduler instances need separate routes, even from the
|
|
56
|
+
same adapter. Give each QStash instance its own `url`, and each Vercel Queues
|
|
57
|
+
instance its own `topic` and trigger.
|
|
58
|
+
|
|
59
|
+
## Dispatch in a fresh invocation
|
|
60
|
+
|
|
61
|
+
`append` starts consumers in the same request and uses the scheduler as a
|
|
62
|
+
watchdog. `append.dispatch` stores the event and hands its consumers to the
|
|
63
|
+
queue instead:
|
|
64
|
+
|
|
65
|
+
```ts app/api/imports/route.ts
|
|
66
|
+
import { ordersServer } from '@/server'
|
|
67
|
+
|
|
68
|
+
export async function POST(request: Request) {
|
|
69
|
+
const { orderId, shopId, items } = await request.json()
|
|
70
|
+
await ordersServer.session(orderId).append.dispatch({
|
|
71
|
+
id: `created:${orderId}`,
|
|
72
|
+
type: 'created',
|
|
73
|
+
payload: { shopId, items },
|
|
74
|
+
})
|
|
75
|
+
return Response.json({ accepted: true }, { status: 202 })
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
It needs a scheduler and waits for the queue to accept the message. If that
|
|
80
|
+
send fails, the event may already be stored; its explicit `id` makes a retry
|
|
81
|
+
safe.
|
|
82
|
+
|
|
83
|
+
## Deploys and rollbacks
|
|
84
|
+
|
|
85
|
+
Queued work can reach a different deployment than the one that sent it. Keep
|
|
86
|
+
event schemas compatible across deployments. Each adapter's page explains
|
|
87
|
+
where its tasks go: [Vercel Queues](/schedulers/vercel-queues#deployments) and
|
|
88
|
+
[QStash](/schedulers/qstash#deployments).
|
|
89
|
+
|
|
90
|
+
:::warning
|
|
91
|
+
Don't roll back to an A2 version without timer delivery while scheduled tasks
|
|
92
|
+
are outstanding. An old route can acknowledge a scheduled append without
|
|
93
|
+
writing its events. Rolling back past scheduled clears fails too: an old
|
|
94
|
+
Vercel Queues route keeps failing the clear until retention ends, and an old
|
|
95
|
+
QStash route moves it to QStash's dead-letter queue. Roll forward, or wait for
|
|
96
|
+
those tasks to deliver first.
|
|
97
|
+
:::
|
|
98
|
+
|
|
99
|
+
## Send errors
|
|
100
|
+
|
|
101
|
+
When a provider rejects a send, both adapters keep the provider's original
|
|
102
|
+
error.
|
|
103
|
+
|
|
104
|
+
- **Terminal:** a definite 4xx status or error, except 408, 409, 425, and 429.
|
|
105
|
+
The Vercel adapter also treats the SDK's `BadRequestError`,
|
|
106
|
+
`UnauthorizedError`, and `ForbiddenError` as terminal.
|
|
107
|
+
- **Retryable:** 408, 409, 425, 429, 5xx, network failures, and errors with no
|
|
108
|
+
recognized status.
|
|
109
|
+
- Vercel Queues' duplicate-message error counts as success.
|
|
110
|
+
|
|
111
|
+
A2's durable AI tools use this classification. Direct `session.schedule()`
|
|
112
|
+
calls still reject with the original error.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Vercel Queues
|
|
3
|
+
description: The recommended scheduler on Vercel.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { redis } from 'experimental-a2/store-redis'
|
|
9
|
+
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
10
|
+
import { orders } from '@/contracts'
|
|
11
|
+
|
|
12
|
+
export const scheduler = vercelQueues()
|
|
13
|
+
|
|
14
|
+
export const ordersServer = createServer({
|
|
15
|
+
contract: orders,
|
|
16
|
+
store: redis({ url: process.env.REDIS_URL }),
|
|
17
|
+
scheduler,
|
|
18
|
+
consumers: [
|
|
19
|
+
/* ... */
|
|
20
|
+
],
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```ts app/api/a2/scheduler/route.ts
|
|
25
|
+
import { ordersServer, scheduler } from '@/server'
|
|
26
|
+
|
|
27
|
+
export const POST = scheduler.handler(ordersServer)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```json vercel.json
|
|
31
|
+
{
|
|
32
|
+
"functions": {
|
|
33
|
+
"app/api/a2/scheduler/route.ts": {
|
|
34
|
+
"experimentalTriggers": [
|
|
35
|
+
{
|
|
36
|
+
"type": "queue/v2beta",
|
|
37
|
+
"topic": "a2"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```package-install
|
|
46
|
+
npm i @vercel/queue
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The trigger is required. It connects the route to Vercel Queues and makes the
|
|
50
|
+
function private, so only the queue can call it. Don't also expose the route
|
|
51
|
+
publicly or wrap it in your own authentication.
|
|
52
|
+
|
|
53
|
+
Messages are kept for seven days (Vercel Queues' default is 24 hours, and
|
|
54
|
+
seven days is its maximum), so timers can be scheduled up to seven days out. A
|
|
55
|
+
longer delay goes out as asked, and Vercel Queues refuses it.
|
|
56
|
+
|
|
57
|
+
A drain delivery is acknowledged once its session settles or a later watchdog
|
|
58
|
+
for it is stored, including when the drain fails after storing that watchdog.
|
|
59
|
+
A drain that fails before then stays unacknowledged, so Vercel Queues
|
|
60
|
+
redelivers it.
|
|
61
|
+
|
|
62
|
+
## Options
|
|
63
|
+
|
|
64
|
+
| Option | Default | |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `topic` | `'a2'` | must match the trigger's `topic`, and `[A-Za-z0-9_-]+` |
|
|
67
|
+
| `deploymentId` | `VERCEL_DEPLOYMENT_ID` | which deployment receives tasks; see below |
|
|
68
|
+
| `transport`, `clock` | the `@vercel/queue` client, the system clock | test seams for custom delivery |
|
|
69
|
+
|
|
70
|
+
An injected `transport` receives the topic, the task, and send options, and
|
|
71
|
+
owns its own provider configuration; `deploymentId` only configures the
|
|
72
|
+
built-in client.
|
|
73
|
+
|
|
74
|
+
## Deployments
|
|
75
|
+
|
|
76
|
+
`deploymentId` applies to every task: retries, timers, and scheduled clears.
|
|
77
|
+
Leave it out to deliver to the deployment that sent the task, pass an ID to
|
|
78
|
+
target one deployment, or pass `null` to let Vercel pick. Every deployment that
|
|
79
|
+
sends or receives must use the same setting.
|
|
80
|
+
|
|
81
|
+
- **Tasks stay with their deployment.** Promoting a new deployment doesn't move
|
|
82
|
+
them, so older deployments keep receiving their own work until it's done or
|
|
83
|
+
expires. Upgrading doesn't fix an older deployment's route.
|
|
84
|
+
- **`null` doesn't mean latest.** Vercel picks any eligible deployment, so every
|
|
85
|
+
one of them must use `deploymentId: null` and handle pending tasks.
|
|
86
|
+
- **Changing the setting doesn't move queued tasks.** Keep the old deployments
|
|
87
|
+
running until those tasks finish.
|