experimental-a2 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +1 -1
- package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
- package/dist/actor-CqibLgjQ.d.ts.map +1 -0
- 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-DFET5q16.js.map +1 -0
- 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-DAu77CDr.d.ts} +8 -8
- package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
- package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
- package/dist/ai-EEfKyc-z.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 +4 -2
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +214 -67
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
- package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
- package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +3 -3
- 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 +86 -73
- 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-BQS4i_V9.js} +29 -6
- package/dist/client-BQS4i_V9.js.map +1 -0
- 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-CvsrprnU.js} +40 -16
- package/dist/internal-CvsrprnU.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-CIeL33wj.js +12 -0
- package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +3 -3
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
- package/dist/server-DtFrWxi3.js.map +1 -0
- package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
- package/dist/server-qgez0Xny.d.ts.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-D1PFe7xl.d.ts} +2 -2
- package/dist/session-clear-D1PFe7xl.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-D0MhBvg6.d.ts +532 -0
- package/dist/store-D0MhBvg6.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- 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 +406 -225
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
- package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.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 +1598 -587
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
- package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +4 -4
- package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
- package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +4 -5
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +527 -299
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-DrTyiIuo.js +57 -0
- package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
- 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-CV-Z7wz-.js} +6 -5
- package/dist/wire-CV-Z7wz-.js.map +1 -0
- package/docs/01-quickstart.mdx +21 -15
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +13 -2
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +12 -11
- package/docs/agents/03-conversations.mdx +36 -25
- package/docs/agents/04-harnesses.mdx +14 -7
- package/docs/client/03-presence.mdx +18 -14
- 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 +29 -24
- package/docs/reference/01-core.mdx +7 -6
- package/docs/reference/02-server.mdx +250 -191
- package/docs/reference/04-ai.mdx +33 -23
- package/docs/reference/05-telemetry.mdx +27 -13
- package/docs/reference/06-errors.mdx +25 -19
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +8 -8
- package/docs/schedulers/02-vercel-queues.mdx +2 -2
- package/docs/schedulers/03-qstash.mdx +4 -4
- package/docs/server/01-consumers.mdx +418 -0
- package/docs/server/02-timers.mdx +31 -21
- 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 +61 -51
- package/docs/stores/01-overview.mdx +3 -3
- package/docs/stores/02-redis.mdx +24 -10
- package/docs/stores/03-redis-http.mdx +8 -6
- package/docs/stores/04-postgres.mdx +65 -14
- package/docs/stores/05-sqlite.mdx +5 -3
- package/docs/upgrades/01-overview.mdx +31 -0
- package/docs/upgrades/78-0-22.mdx +115 -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 +39 -7
- package/examples/playground/app/a0/chat.test.tsx +2 -2
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/sandbox.ts +58 -13
- package/examples/playground/app/a0/server.ts +4 -4
- 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 +209 -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/0004_a2_session_event_ids.sql +2 -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/0004_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -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/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- 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/lib/store.ts +1 -1
- package/examples/playground/next.config.ts +6 -1
- package/examples/playground/package.json +12 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +5 -2
- package/sql/postgres/drizzle-schema.ts +209 -0
- package/sql/postgres/schema.sql +227 -67
- package/src/actor-shared.ts +10 -2
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +162 -111
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +324 -82
- package/src/ai-harness.ts +17 -2
- package/src/ai-pi.ts +2 -2
- package/src/ai-server.ts +2 -2
- package/src/ai.ts +7 -7
- package/src/client.ts +19 -2
- package/src/contract.ts +1 -1
- package/src/deterministic-id.ts +27 -4
- package/src/errors.ts +8 -5
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +57 -16
- package/src/otel.ts +1 -1
- package/src/postgres-schema.ts +98 -57
- package/src/presence-recovery.ts +18 -11
- package/src/presence-tombstones.ts +52 -0
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +1 -1
- package/src/server.browser.ts +1 -1
- package/src/server.ts +819 -218
- package/src/session-socket.ts +1 -1
- package/src/store-codec.ts +66 -0
- package/src/store-memory.ts +604 -286
- package/src/store-postgres.ts +2006 -857
- package/src/store-presence-polling.ts +59 -34
- package/src/store-redis-core.ts +1961 -1333
- package/src/store-redis-notify.ts +76 -26
- package/src/store-redis.ts +2 -3
- package/src/store-sqlite.ts +915 -516
- package/src/store.ts +241 -102
- package/src/telemetry.ts +2 -1
- package/src/wire.ts +1 -0
- package/dist/actor-C7b-1arn.d.ts.map +0 -1
- package/dist/actor-shared-t7w8xIKP.js.map +0 -1
- package/dist/ai-harness-Clbg5ei7.js.map +0 -1
- package/dist/ai-mMiWul1T.js.map +0 -1
- package/dist/client-BhHLoNKF.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-DYAATFuT.d.ts.map +0 -1
- package/dist/server-qyb5aJJG.js.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-presence-polling-B1ixeqIS.js +0 -94
- package/dist/store-presence-polling-B1ixeqIS.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/stream-buffer-Cx7AAPGq.js +0 -23
- package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
- package/dist/wire-BKxH1vbP.js.map +0 -1
- package/docs/server/01-handlers.mdx +0 -227
|
@@ -15,14 +15,15 @@ if (!url || !token) throw new Error('set the Upstash REST URL and token')
|
|
|
15
15
|
export const ordersServer = createServer({
|
|
16
16
|
contract: orders,
|
|
17
17
|
store: redisHttp({ url, token }),
|
|
18
|
-
|
|
18
|
+
consumers: [
|
|
19
19
|
/* ... */
|
|
20
|
-
|
|
20
|
+
],
|
|
21
21
|
})
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Stores sessions exactly like [Redis](/stores/redis), with no client dependency
|
|
25
|
-
and nothing to set up. Use it when your provider only offers a REST API.
|
|
25
|
+
and nothing to set up. Use it when your provider only offers a REST API. On
|
|
26
|
+
Upstash, each script locks only its session's keys, never the whole database.
|
|
26
27
|
|
|
27
28
|
:::warning
|
|
28
29
|
Live streams trust the messages on the store's `:notify` channels. Browsers
|
|
@@ -37,7 +38,7 @@ credentials that can also write the store.
|
|
|
37
38
|
| Option | |
|
|
38
39
|
| --- | --- |
|
|
39
40
|
| `url`, `token` | your provider's REST endpoint and token |
|
|
40
|
-
| `keyPrefix` | isolates several apps on one Redis. Default `'a2'
|
|
41
|
+
| `keyPrefix` | isolates several apps on one Redis. Default `'a2'`; no braces |
|
|
41
42
|
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
42
43
|
|
|
43
44
|
## Live updates
|
|
@@ -46,8 +47,9 @@ credentials that can also write the store.
|
|
|
46
47
|
same URL and token. Each active channel holds one outbound HTTP subscription
|
|
47
48
|
per store instance, shared by its viewers and closed when the last one
|
|
48
49
|
leaves.
|
|
49
|
-
-
|
|
50
|
-
rejected notification doesn't
|
|
50
|
+
- Each write publishes its wake-up after its script commits, so a slow
|
|
51
|
+
subscriber never holds the script's lock. A rejected notification doesn't
|
|
52
|
+
fail the write.
|
|
51
53
|
- Idle streams re-read every ten seconds to recover a missed notification.
|
|
52
54
|
Subscriptions reconnect on their own and catch up from the event cursor.
|
|
53
55
|
- After a reconnect, A2 re-reads presence and clears fields deleted during the
|
|
@@ -11,9 +11,9 @@ import { orders } from '@/contracts'
|
|
|
11
11
|
export const ordersServer = createServer({
|
|
12
12
|
contract: orders,
|
|
13
13
|
store: postgres({ connectionString: process.env.DATABASE_URL }),
|
|
14
|
-
|
|
14
|
+
consumers: [
|
|
15
15
|
/* ... */
|
|
16
|
-
|
|
16
|
+
],
|
|
17
17
|
})
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -26,22 +26,70 @@ poolers.
|
|
|
26
26
|
|
|
27
27
|
## Create the tables
|
|
28
28
|
|
|
29
|
-
A2 doesn't create tables in your database. This is the SQL it needs:
|
|
29
|
+
A2 doesn't create or change tables in your database. This is the SQL it needs:
|
|
30
30
|
[`schema.sql`](https://github.com/vercel-labs/a2/blob/main/packages/a2/sql/postgres/schema.sql),
|
|
31
31
|
also in the package at `node_modules/experimental-a2/sql/postgres/schema.sql`.
|
|
32
|
-
Apply it
|
|
33
|
-
|
|
32
|
+
Apply it before your first deploy, however you run SQL against your database
|
|
33
|
+
or as one of your migrations, as the role your app connects with. A breakpoint
|
|
34
|
+
comment ends each statement, for tools that send one statement per query.
|
|
35
|
+
`a2_batch` runs the statements it's handed, so only its owner may call it,
|
|
36
|
+
whatever your default privileges grant new functions. An app that connects as
|
|
37
|
+
another role needs `grant execute on function a2_batch(text[], jsonb)` beside
|
|
38
|
+
its grants on the tables, and keeps it when you apply the file again.
|
|
39
|
+
|
|
40
|
+
A2 checks nothing up front. It relies on the table and column names, the
|
|
41
|
+
column types, the primary keys, the unique index on `a2_events (session_id,
|
|
42
|
+
event_id)`, and the `a2_batch` function; the other indexes keep its queries fast. A query that
|
|
43
|
+
finds a table, column, or the function missing throws `STORE_NOT_CONFIGURED`,
|
|
44
|
+
naming the file. Once it's applied, the next request works, with no redeploy.
|
|
45
|
+
|
|
46
|
+
`a2_batch` runs several statements in one round trip. Each append, claim,
|
|
47
|
+
completion, and `consume` poll is one call of it, so they hold a session's lock
|
|
48
|
+
only while the database runs the call, not across round trips from your app.
|
|
49
|
+
That's what keeps one busy session fast on a database a few milliseconds
|
|
50
|
+
away. It needs `read committed`, Postgres's default isolation level, and
|
|
51
|
+
throws `STORE_NOT_CONFIGURED` under a stricter default.
|
|
52
|
+
|
|
53
|
+
## With Drizzle
|
|
54
|
+
|
|
55
|
+
[`drizzle-schema.ts`](https://github.com/vercel-labs/a2/blob/main/packages/a2/sql/postgres/drizzle-schema.ts),
|
|
56
|
+
next to `schema.sql`, shows the same tables as Drizzle definitions. It's an
|
|
57
|
+
example, not an API: one way to fit A2's tables into a Drizzle project is a
|
|
58
|
+
copy next to your own schema, so your migrations create them with yours.
|
|
59
|
+
|
|
60
|
+
```ts drizzle.config.ts
|
|
61
|
+
import { defineConfig } from 'drizzle-kit'
|
|
62
|
+
|
|
63
|
+
export default defineConfig({
|
|
64
|
+
dialect: 'postgresql',
|
|
65
|
+
// your tables, and a copy of drizzle-schema.ts:
|
|
66
|
+
schema: ['./db/schema.ts', './db/a2.ts'],
|
|
67
|
+
out: './drizzle',
|
|
68
|
+
dbCredentials: { url: process.env.DATABASE_URL ?? '' },
|
|
69
|
+
})
|
|
70
|
+
```
|
|
34
71
|
|
|
35
|
-
|
|
72
|
+
`drizzle-kit generate` then writes A2's tables into a migration you can read,
|
|
73
|
+
and `drizzle-kit migrate` applies it. Adapt the copy to your Drizzle version
|
|
74
|
+
and layout, as long as the names and types stay A2's.
|
|
36
75
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
76
|
+
Drizzle can't declare a function, so add `a2_batch` as a custom migration:
|
|
77
|
+
run `drizzle-kit generate --custom`, and paste the `do` block that creates
|
|
78
|
+
`a2_batch` from `schema.sql` into the file it makes. It creates the function
|
|
79
|
+
only if it's missing, so the migration is safe on any database.
|
|
80
|
+
|
|
81
|
+
A generated migration only creates tables. On a database that already has
|
|
82
|
+
A2's tables, from an earlier release or a `schema.sql` applied by hand, put
|
|
83
|
+
`schema.sql` in place of the SQL drizzle-kit generated for them: it creates
|
|
84
|
+
what's missing and upgrades the rest, keeping their events, and your schema
|
|
85
|
+
still describes them for the next `generate`. To keep a table of your own
|
|
86
|
+
from A2's logs, see [SQL projections](/server/sql-projections).
|
|
87
|
+
|
|
88
|
+
## Upgrade
|
|
40
89
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
first query, which never waits on or blocks writes.
|
|
90
|
+
A release that changes A2's tables says so in its changelog, and its
|
|
91
|
+
[upgrade page](/upgrades/overview) says when to apply its `schema.sql`. With
|
|
92
|
+
Drizzle, bring your copy of `drizzle-schema.ts` in line with the new one.
|
|
45
93
|
|
|
46
94
|
## Live updates (polling)
|
|
47
95
|
|
|
@@ -56,7 +104,10 @@ run in different invocations.
|
|
|
56
104
|
For instant updates, pass `listenConnectionString`, a direct connection to the
|
|
57
105
|
same database (not through a pooler), and apply
|
|
58
106
|
[`notifications.sql`](https://github.com/vercel-labs/a2/blob/main/packages/a2/sql/postgres/notifications.sql)
|
|
59
|
-
once, the same way as `schema.sql
|
|
107
|
+
once, the same way as `schema.sql` (with Drizzle, as a custom migration:
|
|
108
|
+
`drizzle-kit generate --custom`). A missing trigger fails no query, so this is
|
|
109
|
+
the one thing A2 looks for: a listening store's first subscription throws
|
|
110
|
+
`STORE_NOT_CONFIGURED` until the triggers exist.
|
|
60
111
|
|
|
61
112
|
Its triggers notify listeners of every write, including writes from stores
|
|
62
113
|
without the option. Notifications arrive after commit, a reconnect catches up
|
|
@@ -11,9 +11,9 @@ import { orders } from '@/contracts'
|
|
|
11
11
|
export const ordersServer = createServer({
|
|
12
12
|
contract: orders,
|
|
13
13
|
store: sqlite({ path: '.a2/orders.db' }),
|
|
14
|
-
|
|
14
|
+
consumers: [
|
|
15
15
|
/* ... */
|
|
16
|
-
|
|
16
|
+
],
|
|
17
17
|
})
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -22,7 +22,9 @@ your sessions survive `next dev` restarts. Pass it yourself to pick another
|
|
|
22
22
|
file.
|
|
23
23
|
|
|
24
24
|
It runs on `node:sqlite`, with no dependencies, and creates its tables when it
|
|
25
|
-
opens the file.
|
|
25
|
+
opens the file. A file a release before [0.21](/upgrades/0-21) wrote throws
|
|
26
|
+
`STORE_NOT_CONFIGURED` when it opens: delete it, or pass another `path`. It's
|
|
27
|
+
for development: a serverless filesystem doesn't last or
|
|
26
28
|
share one file across instances, so production needs another
|
|
27
29
|
[store](/stores/overview).
|
|
28
30
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Overview
|
|
3
|
+
description: What to do when you move to a new release. One page per release that needs steps from you, newest first.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## The releases
|
|
7
|
+
|
|
8
|
+
| Release | What you do |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| [0.22](/upgrades/0-22) | Event ids are unique per session. Postgres: apply the new `schema.sql`, then deploy. Redis: use a new `keyPrefix`. |
|
|
11
|
+
| [0.21](/upgrades/0-21) | Rewrite handlers as consumers. Let earlier deployments finish; on Postgres, then apply the new `schema.sql`. SQLite starts fresh. |
|
|
12
|
+
| [0.20](/upgrades/0-20) | Postgres: apply `schema.sql` yourself, before you deploy. |
|
|
13
|
+
| [0.19](/upgrades/0-19) | Start new agent sessions: earlier ones don't fold. |
|
|
14
|
+
| [0.18](/upgrades/0-18) | Move the AI SDK loop's options into `harness`. |
|
|
15
|
+
| [0.17](/upgrades/0-17) | Redis: use a fresh `keyPrefix`. Serve agents with `agent()`. |
|
|
16
|
+
|
|
17
|
+
## Across several releases
|
|
18
|
+
|
|
19
|
+
Read every page between the release you run and the one you're moving to,
|
|
20
|
+
oldest first. Code changes add up: make each page's changes. Store steps
|
|
21
|
+
don't always: each release's `schema.sql` covers a database from any earlier
|
|
22
|
+
release, so a Postgres database needs only the newest one, applied the way the
|
|
23
|
+
newest page says, and one fresh Redis `keyPrefix` covers every release that
|
|
24
|
+
asks for one.
|
|
25
|
+
|
|
26
|
+
## Everything else
|
|
27
|
+
|
|
28
|
+
Each release's changelog lists everything it changes, including what needs no
|
|
29
|
+
steps: [`CHANGELOG.md`](https://github.com/vercel-labs/a2/blob/main/packages/a2/CHANGELOG.md),
|
|
30
|
+
also in the package at `node_modules/experimental-a2/CHANGELOG.md`. Releases
|
|
31
|
+
before 0.17 are only there.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: '0.22'
|
|
3
|
+
description: Event ids are unique per session, not per store, and a presence clear keeps its stamp.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Event ids are unique per session
|
|
7
|
+
|
|
8
|
+
The same event id in two sessions is now two events. Before, appending an id
|
|
9
|
+
that another session already used threw `PARTIAL_DUPLICATE_BATCH`. Inside one
|
|
10
|
+
session nothing changes: an identical retry returns the original rows, a batch
|
|
11
|
+
that mixes sent and fresh events throws, and a consumer's retried completion
|
|
12
|
+
returns the events it committed.
|
|
13
|
+
|
|
14
|
+
- **Ids you derive need only the session's scope.** An
|
|
15
|
+
[`idempotentId()`](/reference/core#idempotentidparts) recipe that includes
|
|
16
|
+
the session still works; one that leaves it out no longer collides with
|
|
17
|
+
another session's events.
|
|
18
|
+
- **Idempotency keys built from ids.** `event.id` and an actor's `ctx.id` are
|
|
19
|
+
unique within their session. Ids your callers pick, and the ids A2 derives
|
|
20
|
+
from them, can now repeat in another session, so a key for an external side
|
|
21
|
+
effect built from one alone can too. When yours can repeat, add the session
|
|
22
|
+
to the key; see
|
|
23
|
+
[Consumers can run twice](/server/consumers#consumers-can-run-twice).
|
|
24
|
+
- **Clear markers** follow the same rule.
|
|
25
|
+
- **Custom stores** check ids against the session's own rows only: the
|
|
26
|
+
idempotent-replay and partial-batch checks in `append` and
|
|
27
|
+
`completeAttempt` ignore other sessions. See
|
|
28
|
+
[`A2Store`](/reference/server#a2store).
|
|
29
|
+
|
|
30
|
+
## Postgres
|
|
31
|
+
|
|
32
|
+
0.22's `schema.sql` replaces the unique index on `a2_events (event_id)`,
|
|
33
|
+
`a2_events_event_id`, with one on `a2_events (session_id, event_id)`,
|
|
34
|
+
`a2_events_session_event_id`. Every row is kept, and building the new index
|
|
35
|
+
can't fail: the old one already keeps those pairs unique.
|
|
36
|
+
|
|
37
|
+
1. **Apply 0.22's `schema.sql`**, as the role your app connects with:
|
|
38
|
+
`node_modules/experimental-a2/sql/postgres/schema.sql`.
|
|
39
|
+
2. **Deploy 0.22.**
|
|
40
|
+
|
|
41
|
+
The first time it runs, the swap locks `a2_events` against every read and write
|
|
42
|
+
until the new index is built, then drops the old one. On a large table, make
|
|
43
|
+
both changes by hand instead, with nothing blocked. Run each statement on its
|
|
44
|
+
own, outside a transaction:
|
|
45
|
+
|
|
46
|
+
```sql
|
|
47
|
+
create unique index concurrently if not exists a2_events_session_event_id
|
|
48
|
+
on a2_events (session_id, event_id);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A concurrent build that fails leaves an invalid index of the same name behind.
|
|
52
|
+
Check that this returns `true` before you drop the old index; if it doesn't,
|
|
53
|
+
drop the new one and build it again:
|
|
54
|
+
|
|
55
|
+
```sql
|
|
56
|
+
select indisvalid from pg_index
|
|
57
|
+
where indexrelid = 'a2_events_session_event_id'::regclass;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```sql
|
|
61
|
+
drop index concurrently if exists a2_events_event_id;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then apply `schema.sql`: it finds both done and waits on nothing. It drops the
|
|
65
|
+
old index only once the new one is valid.
|
|
66
|
+
|
|
67
|
+
With Drizzle, update your copy of `drizzle-schema.ts`:
|
|
68
|
+
`a2_events_session_event_id` on `(session_id, event_id)` replaces
|
|
69
|
+
`a2_events_event_id`. On a large table, run the statements above by hand
|
|
70
|
+
instead, and take them out of the migration Drizzle generates.
|
|
71
|
+
|
|
72
|
+
## SQLite
|
|
73
|
+
|
|
74
|
+
Nothing to do. The store creates the new index and drops the old one when it
|
|
75
|
+
opens the file. Once 0.22 has opened a file, don't open it with an earlier
|
|
76
|
+
release: that release builds the old index again, and can't open the file at
|
|
77
|
+
all once two sessions share an id.
|
|
78
|
+
|
|
79
|
+
## Redis
|
|
80
|
+
|
|
81
|
+
Redis names every key differently in this release, so it starts fresh:
|
|
82
|
+
**point the store at a new `keyPrefix`**, then deploy. Under its new prefix,
|
|
83
|
+
0.22 never reads what an earlier release wrote; delete the old prefix's keys
|
|
84
|
+
whenever convenient. On a prefix 0.21 wrote, 0.22 refuses to start with
|
|
85
|
+
`STORE_NOT_CONFIGURED`. A `keyPrefix` can't contain braces.
|
|
86
|
+
|
|
87
|
+
What changes:
|
|
88
|
+
|
|
89
|
+
- **Scripts lock only their session on Upstash.** Every key of a session
|
|
90
|
+
shares one hash tag, so each script sets Upstash's `allow-key-locking` and
|
|
91
|
+
locks its session's keys instead of the whole database. The store asks the
|
|
92
|
+
server once whether it accepts the flag; Redis and Valkey don't, and run one
|
|
93
|
+
command at a time anyway.
|
|
94
|
+
- **No script publishes.** Each write publishes its wake-up after its script,
|
|
95
|
+
so a slow subscriber never holds a lock.
|
|
96
|
+
- **`clear()` works on Upstash.** It used to rename the log aside, which
|
|
97
|
+
Upstash refuses for streams. The marker now joins the same log, and the old
|
|
98
|
+
lifetime's entries stay below it, unread, until `clear()` finishes deleting
|
|
99
|
+
them. A session that stays empty after its clear keeps only its clear marker,
|
|
100
|
+
the log holding it, and its log consumers' positions.
|
|
101
|
+
|
|
102
|
+
## Memory
|
|
103
|
+
|
|
104
|
+
Nothing to do: the memory store starts empty in every process.
|
|
105
|
+
|
|
106
|
+
## Presence clears keep their stamp
|
|
107
|
+
|
|
108
|
+
A `null` from `setPresence()` now leaves a tombstone in the store, carrying the
|
|
109
|
+
clear's `at`, until the field's TTL runs out. An older value that reaches the
|
|
110
|
+
store after the clear, such as one sent through another server instance, still
|
|
111
|
+
loses. Before, it brought the field back. Nothing to do: Postgres and SQLite
|
|
112
|
+
keep tombstones in `a2_presence` as a JSON `null` value.
|
|
113
|
+
|
|
114
|
+
Custom stores with presence keep a tombstone for each `null`, compare it by
|
|
115
|
+
`at` like a value until it expires, and leave tombstones out of `read`.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: '0.21'
|
|
3
|
+
description: Handlers become consumers, and every store keeps their places in a new layout.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Handlers become consumers
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// before: server/orders.ts on 0.20
|
|
10
|
+
export const ordersServer = createServer({
|
|
11
|
+
contract: orders,
|
|
12
|
+
handlers: {
|
|
13
|
+
created: async ({ event }) => {
|
|
14
|
+
// await sendEmailToShop(event.payload.shopId, { idempotencyKey: event.id })
|
|
15
|
+
return { type: 'shop.notified', payload: {} }
|
|
16
|
+
},
|
|
17
|
+
'shop.started': { lane: 'shop', handler: async () => {} },
|
|
18
|
+
'shop.notified': { lane: 'shop', handler: async () => {} },
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```ts server/orders.ts
|
|
24
|
+
import { createServer } from 'experimental-a2/server'
|
|
25
|
+
import { orders } from '@/contracts'
|
|
26
|
+
|
|
27
|
+
export const ordersServer = createServer({
|
|
28
|
+
contract: orders,
|
|
29
|
+
consumers: [
|
|
30
|
+
{
|
|
31
|
+
name: 'notify-shop',
|
|
32
|
+
types: ['created'],
|
|
33
|
+
run: async ({ event }) => {
|
|
34
|
+
// await sendEmailToShop(event.payload.shopId, { idempotencyKey: event.id })
|
|
35
|
+
return { type: 'shop.notified', payload: {} }
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
// the types that shared the 'shop' lane, as one consumer:
|
|
39
|
+
{
|
|
40
|
+
name: 'shop-work',
|
|
41
|
+
types: ['shop.started', 'shop.notified'],
|
|
42
|
+
lane: () => 'shop',
|
|
43
|
+
run: async ({ event }) => {
|
|
44
|
+
// your work for event.type
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`handlers` are gone: [consumers](/server/consumers) are the one way to react
|
|
52
|
+
to events. Each entry of a handler table becomes a consumer that takes its
|
|
53
|
+
type, with the same function as its `run`, and its `abortOn` if it had one. A
|
|
54
|
+
lane belongs to one consumer, so entries that shared a lane become one consumer
|
|
55
|
+
that takes all their types, with a lane function returning that lane. A
|
|
56
|
+
consumer's name is its identity: its cursors, failures, and dead letters key
|
|
57
|
+
on it, so pick names you'll keep.
|
|
58
|
+
|
|
59
|
+
| Before | 0.21 |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `createServer({ handlers })` | `createServer({ consumers })` |
|
|
62
|
+
| `handler(entry)` | `{ name, types?, lane?, run }` in `consumers`, or `consumer({ contract, ... }).run(fn)` |
|
|
63
|
+
| `Handler`, `HandlerEntry` | removed: `consumer()` types `run`'s context |
|
|
64
|
+
| `Lane` | `ConsumerLane` |
|
|
65
|
+
| `HandlerContext` | `ConsumerContext` |
|
|
66
|
+
| `HandlerAppend` | `ConsumerAppend` |
|
|
67
|
+
| `handlerContext()`, from `experimental-a2/ai` | `consumerContext()` |
|
|
68
|
+
| `runWithHandlerContext`, from `experimental-a2/ai/server` | `runWithConsumerContext` |
|
|
69
|
+
| `agent({ handlers })` | `agent({ consumers })` |
|
|
70
|
+
| `session.redrive(index, { action })` | `session.redrive({ consumer, index }, { action })` |
|
|
71
|
+
| `a2.append.source` = `handler` | `a2.append.source` = `consumer` |
|
|
72
|
+
|
|
73
|
+
- **Dead letters name their consumer.** An event dead-letters for one
|
|
74
|
+
consumer, so each entry from `session.deadLetters()` carries `consumer`.
|
|
75
|
+
Pass it to `redrive` with the index. See
|
|
76
|
+
[Resolve a dead letter](/server/consumers#resolve-a-dead-letter).
|
|
77
|
+
- **Agents take `consumers`.** Write them with `consumer()` against the agent's
|
|
78
|
+
contract, as in [Conversations](/agents/conversations). A consumer of an AI
|
|
79
|
+
event type runs beside the agent's own, so `agent()` no longer rejects one.
|
|
80
|
+
- **Actors keep `actor({ handlers })`.** Nothing changes in an actor's code.
|
|
81
|
+
- **A2's own consumers start with `a2.`** Actors and agents run on consumers
|
|
82
|
+
named `a2.actor`, `a2.actor.concurrent`, `a2.ai.metadata`, `a2.ai.control`,
|
|
83
|
+
`a2.ai.model`, and `a2.ai.tools`, which their dead letters name. Their event
|
|
84
|
+
ids are unchanged. Names starting with `a2.` are reserved.
|
|
85
|
+
- **Telemetry.** Dashboards that filter `a2.append.source` on `handler` read
|
|
86
|
+
`consumer` instead. Each `a2.event` span also carries `a2.event.consumer`;
|
|
87
|
+
see [Telemetry](/reference/telemetry).
|
|
88
|
+
- **Custom stores** implement the cursor contract: `cursors()`, `wakeLog`, and
|
|
89
|
+
the cursor-keyed claim, completion, failure, and redrive methods. See
|
|
90
|
+
[`A2Store`](/reference/server#a2store).
|
|
91
|
+
|
|
92
|
+
## Postgres
|
|
93
|
+
|
|
94
|
+
0.21 keeps each consumer's place in new tables, `a2_cursors` and
|
|
95
|
+
`a2_lane_events`. Its `schema.sql` creates them, then drops the columns earlier
|
|
96
|
+
releases dispatched by, and the `a2_schema` table 0.20 reads on a cold start.
|
|
97
|
+
Earlier deployments can't run against the new tables, and 0.21 needs them.
|
|
98
|
+
|
|
99
|
+
1. **Let earlier deployments finish their work.** Applying the schema drops
|
|
100
|
+
any work their handlers haven't finished, since 0.21 runs no handlers to
|
|
101
|
+
finish it. This query counts that work, dead letters included (retry or
|
|
102
|
+
skip those first, or accept that they're dropped):
|
|
103
|
+
|
|
104
|
+
```sql
|
|
105
|
+
select count(*) from a2_events where processed_at is null
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
It doesn't count timers. On [Vercel Queues](/schedulers/vercel-queues#deployments),
|
|
109
|
+
each one goes to the deployment that scheduled it, which can't write once
|
|
110
|
+
the schema is applied, so a timer still waiting then is lost. Let them fire,
|
|
111
|
+
or schedule the ones you need again on 0.21.
|
|
112
|
+
|
|
113
|
+
2. **Apply 0.21's `schema.sql` while none of them is running**, as the role
|
|
114
|
+
your app connects with:
|
|
115
|
+
`node_modules/experimental-a2/sql/postgres/schema.sql`. Every event is
|
|
116
|
+
kept. There's no separate migration: the file is the whole schema, and
|
|
117
|
+
each statement creates what's missing or removes what an earlier release
|
|
118
|
+
used, so it brings a database from any release to 0.21 and changes nothing
|
|
119
|
+
when you apply it again.
|
|
120
|
+
3. **Deploy 0.21.** Deployed before the schema, its queries that reach a
|
|
121
|
+
missing table throw `STORE_NOT_CONFIGURED`, naming the file, and the next
|
|
122
|
+
request after you apply it works, with no redeploy.
|
|
123
|
+
|
|
124
|
+
0.21's `schema.sql` also creates a function, `a2_batch`, that each append,
|
|
125
|
+
claim, completion, and `consume` poll runs through in one round trip. Applying
|
|
126
|
+
the file creates it; with Drizzle, add it as a custom migration (see
|
|
127
|
+
[With Drizzle](/stores/postgres#with-drizzle)). Without it, 0.21's writes throw
|
|
128
|
+
`STORE_NOT_CONFIGURED`, naming `schema.sql`.
|
|
129
|
+
|
|
130
|
+
0.21 also checks nothing up front: no schema version read before the first
|
|
131
|
+
query. A query that finds a table or column missing, or that its role can't
|
|
132
|
+
use, throws `STORE_NOT_CONFIGURED`, naming the file to apply or the grant to
|
|
133
|
+
make. With Drizzle, `drizzle-schema.ts` shows the new tables; on a database
|
|
134
|
+
that already has A2's tables, use `schema.sql` as the migration for them. See
|
|
135
|
+
[With Drizzle](/stores/postgres#with-drizzle).
|
|
136
|
+
|
|
137
|
+
## SQLite
|
|
138
|
+
|
|
139
|
+
A file an earlier release wrote throws `STORE_NOT_CONFIGURED` when the store
|
|
140
|
+
opens it. Delete it, `.a2/dev.db` by default, or pass another `path`. Its
|
|
141
|
+
sessions don't carry over.
|
|
142
|
+
|
|
143
|
+
## Redis
|
|
144
|
+
|
|
145
|
+
0.21 keeps each consumer's place in keys of its own, and reads the log,
|
|
146
|
+
snapshots, and clears that earlier releases wrote as they are. Keep your
|
|
147
|
+
`keyPrefix`.
|
|
148
|
+
|
|
149
|
+
1. **Let earlier deployments finish their work, then stop them.** 0.21 runs
|
|
150
|
+
no handlers, so the work theirs haven't finished, dead letters included, is
|
|
151
|
+
dropped. An earlier deployment still running beside 0.21 keeps its own lanes
|
|
152
|
+
on the same prefix, so one lane's events can run on both at once. On
|
|
153
|
+
[Vercel Queues](/schedulers/vercel-queues#deployments) it also keeps
|
|
154
|
+
receiving the timers it scheduled, until they fire: let them, or remove the
|
|
155
|
+
deployment and schedule the ones you need again on 0.21.
|
|
156
|
+
2. **Deploy 0.21.** Every event is kept. A consumer starts at the next event
|
|
157
|
+
appended, or with `backfill` at the beginning of the log.
|
|
158
|
+
|
|
159
|
+
The keys earlier releases dispatched by (`meta`, `meta:{n}`, `meta-split`,
|
|
160
|
+
`pending`, `ready`, `snapshot-pins:{n}`, `snapshot-pin-counts`, under each
|
|
161
|
+
session) stay until you delete them; 0.21 never reads them.
|
|
162
|
+
|
|
163
|
+
## Memory
|
|
164
|
+
|
|
165
|
+
Nothing to do: the memory store starts empty in every process.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: '0.20'
|
|
3
|
+
description: The Postgres store no longer creates its tables. You apply them, before you deploy.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Postgres
|
|
7
|
+
|
|
8
|
+
Before you deploy 0.20, apply the SQL its store needs,
|
|
9
|
+
`node_modules/experimental-a2/sql/postgres/schema.sql`, once per database,
|
|
10
|
+
however you run SQL against it. It creates only what's missing and leaves
|
|
11
|
+
existing data alone, so it's safe on a database an earlier release set up, and
|
|
12
|
+
the deployment running now keeps working against it, without waiting on its
|
|
13
|
+
writes.
|
|
14
|
+
|
|
15
|
+
- **Apply it as the role your app connects with.** It records a schema version
|
|
16
|
+
in a new `a2_schema` table, which the store reads on a cold start. If
|
|
17
|
+
another role applies it, grant your app's role `select` on it:
|
|
18
|
+
|
|
19
|
+
```sql
|
|
20
|
+
grant select on a2_schema to your_app_role;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **With `listenConnectionString`**, also apply
|
|
24
|
+
`sql/postgres/notifications.sql`. It installs the triggers the store used to
|
|
25
|
+
install for you.
|
|
26
|
+
- **The app's role needs no DDL rights.** Read and write access to A2's tables
|
|
27
|
+
is enough.
|
|
28
|
+
|
|
29
|
+
## If you deploy first
|
|
30
|
+
|
|
31
|
+
The store throws `STORE_NOT_CONFIGURED` on its first query, naming the file to
|
|
32
|
+
apply. Until you apply it, nothing is written: appends fail, and scheduled work
|
|
33
|
+
waits. Vercel Queues keeps redelivering it, and the QStash scheduler hands each
|
|
34
|
+
task to a later message, 30 seconds out and backing off to one day. The next
|
|
35
|
+
call after you apply the file succeeds, with no redeploy.
|
|
36
|
+
|
|
37
|
+
## Other stores
|
|
38
|
+
|
|
39
|
+
Nothing to do. SQLite still creates its tables when it opens its file, and
|
|
40
|
+
Redis creates its keys as it writes.
|
|
41
|
+
|
|
42
|
+
Moving past 0.20 to 0.21 or later? Apply that release's `schema.sql` instead,
|
|
43
|
+
the way [0.21](/upgrades/0-21) says. It drops `a2_schema`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: '0.19'
|
|
3
|
+
description: An agent's log stores each message once, so agent sessions from earlier releases don't fold.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Start new agent sessions
|
|
7
|
+
|
|
8
|
+
0.19 stores each message's content once, in the `ai.control.requested` command
|
|
9
|
+
that brought it in. The AI reducers advance to `a2.ai.state.v27`,
|
|
10
|
+
`a2.ai.context.v7`, and `a2.ai.control.v6`, and a log written before 0.19
|
|
11
|
+
doesn't fold: `reduceAIState()` and `deriveUIMessages()` throw on an
|
|
12
|
+
`ai.message.created` that no command explains. Start new agent sessions after
|
|
13
|
+
you deploy. Earlier sessions keep their events in history, but their state
|
|
14
|
+
can't be read.
|
|
15
|
+
|
|
16
|
+
## Payloads and types
|
|
17
|
+
|
|
18
|
+
Code that reads the AI log directly changes with it:
|
|
19
|
+
|
|
20
|
+
- **`ai.message.created`** records an admission as
|
|
21
|
+
`{ messageId, role, generate? }`, without the message.
|
|
22
|
+
`MessageCreatedPayload` takes no type argument.
|
|
23
|
+
- **`ai.control.committed`** carries no message content. Queued inputs and
|
|
24
|
+
`AIState.active.input` are `InputRef`s, `{ id, role, generate, afterStepOf? }`,
|
|
25
|
+
and `ActiveTurn` takes no type argument. `AIState.inbox.items` keep their
|
|
26
|
+
`message`, now with `role`.
|
|
27
|
+
|
|
28
|
+
## Provider tools under `fx()` and `pi()`
|
|
29
|
+
|
|
30
|
+
`agent()` throws a `TypeError` when a provider-defined tool
|
|
31
|
+
(`type: 'provider'`, such as a provider's web search) is passed with `fx()` or
|
|
32
|
+
`pi()`. Before, both harnesses dropped it without a word, so the model never
|
|
33
|
+
saw it. Take it out of those agents' `tools`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: '0.18'
|
|
3
|
+
description: An agent's loop is its harness, and the AI SDK loop's options move into it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Move loop options into `harness`
|
|
7
|
+
|
|
8
|
+
```ts server/assistant.ts
|
|
9
|
+
import { agent } from 'experimental-a2/ai/server'
|
|
10
|
+
|
|
11
|
+
export const assistant = agent({
|
|
12
|
+
name: 'support-agent',
|
|
13
|
+
model: 'openai/gpt-5.6-terra',
|
|
14
|
+
instructions: 'Help the customer. Be concise.',
|
|
15
|
+
harness: {
|
|
16
|
+
type: 'ai-sdk',
|
|
17
|
+
// generation, generate, compaction, and messageMetadata go here:
|
|
18
|
+
compaction: { thresholdTokens: 80_000 },
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`agent()` takes its loop as `harness`. The AI SDK's loop is the default,
|
|
24
|
+
`{ type: 'ai-sdk' }`, and its options move into it: `generation`, `generate`,
|
|
25
|
+
`compaction`, and `messageMetadata` are no longer top-level `agent()` options,
|
|
26
|
+
and passing one there throws a `TypeError`. See
|
|
27
|
+
[Harnesses](/agents/harnesses).
|
|
28
|
+
|
|
29
|
+
## Models are AI Gateway IDs
|
|
30
|
+
|
|
31
|
+
`model` is an AI Gateway model ID, or `ModelSettings` with one, under every
|
|
32
|
+
harness. `agent()` no longer takes AI SDK provider model objects, and
|
|
33
|
+
`HarnessModelSettings` is gone: use `ModelSettings`.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: '0.17'
|
|
3
|
+
description: Redis stores need a fresh key prefix, and agents are served with `agent()`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Redis: a fresh `keyPrefix`
|
|
7
|
+
|
|
8
|
+
```ts server/store.ts
|
|
9
|
+
import { redis } from 'experimental-a2/store-redis'
|
|
10
|
+
|
|
11
|
+
export const store = redis({
|
|
12
|
+
url: process.env.REDIS_URL,
|
|
13
|
+
// a prefix no earlier release wrote under:
|
|
14
|
+
keyPrefix: 'a2-017',
|
|
15
|
+
})
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Both Redis stores change how they keep event ids and per-event metadata: one
|
|
19
|
+
key per event id instead of one shared hash, and metadata in hashes of 1,000
|
|
20
|
+
events. 0.17 no longer reads the shared id hash, and earlier releases can't
|
|
21
|
+
see what 0.17 writes, so on a shared prefix a retry on either side can append
|
|
22
|
+
a duplicate event, re-run work, or stall a lane. Switch to a fresh `keyPrefix`
|
|
23
|
+
when you upgrade, and don't roll back to an earlier release on a prefix 0.17
|
|
24
|
+
has written. Vercel Queues keeps running recovery and scheduled work on the
|
|
25
|
+
deployment that published it for up to seven days, so earlier deployments keep
|
|
26
|
+
touching their sessions after you upgrade.
|
|
27
|
+
|
|
28
|
+
## Before you clear sessions
|
|
29
|
+
|
|
30
|
+
0.17 adds [`session.clear()`](/contract/sessions#clearing-a-session). Upgrade
|
|
31
|
+
every instance before you clear any session: an earlier release ignores the
|
|
32
|
+
clear and can run cleared events in the new lifetime. Upgrade every scheduler
|
|
33
|
+
route before you schedule clears, and don't roll back while clear tasks are
|
|
34
|
+
pending.
|
|
35
|
+
|
|
36
|
+
## Vercel Queues
|
|
37
|
+
|
|
38
|
+
`vercelQueues({ scheduledAppends })` is replaced by
|
|
39
|
+
`deploymentId?: string | null`: leave it out to deliver to the current
|
|
40
|
+
deployment, pass a deployment ID to target one, or pass `null` for
|
|
41
|
+
deploymentless delivery. It applies to every task, recovery included, so
|
|
42
|
+
`null` makes recovery deploymentless too, unlike `'deploymentless'` did.
|
|
43
|
+
Changing it doesn't move tasks already queued. A custom `QueueTransport.send`
|
|
44
|
+
no longer takes a fourth routing argument. See
|
|
45
|
+
[Deployments](/schedulers/vercel-queues#deployments).
|
|
46
|
+
|
|
47
|
+
## Agents
|
|
48
|
+
|
|
49
|
+
| Before | 0.17 |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `createAgentServer({ agent: definition, ... })` | `agent({ name, ... })`, or `agent({ contract, ... })` with a shared contract |
|
|
52
|
+
| `agent()` from `experimental-a2/ai` | `agentContract()` |
|
|
53
|
+
| `createReducer()`, `reducerName` | `agentContract({ reducer })` |
|
|
54
|
+
| `createHandlers()`, `authorizeAgentIngress` | removed: pass `handlers` and `events` to `agent()` |
|
|
55
|
+
| `handlerContext(agent)` | `handlerContext()`, or `handlerContext(contract)` |
|
|
56
|
+
| `request` in `generate` and `messageMetadata` | `event` |
|
|
57
|
+
| `requestId` | `event.id` |
|
|
58
|
+
| `responseMessageId` | `event.payload.responseMessageId` |
|
|
59
|
+
|
|
60
|
+
- **`agent({ contract })` takes only an `agentContract()`.** Any other contract
|
|
61
|
+
throws. A fully custom contract is core A2, served by `createServer()`.
|
|
62
|
+
- **Upgrade clients with the server.** A client from before 0.17 ignores the
|
|
63
|
+
new shape of streamed progress, so a response's output doesn't appear in it
|
|
64
|
+
until it reloads.
|
|
65
|
+
- **Custom telemetry recorders** must accept the new span names: core claim
|
|
66
|
+
and commit spans, and `a2.ai.*`.
|
|
67
|
+
|
|
68
|
+
0.21 later renames `handlers` to `consumers` and `handlerContext()` to
|
|
69
|
+
`consumerContext()`; see [0.21](/upgrades/0-21).
|