experimental-a2 0.17.0 → 0.18.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 +32 -0
- package/README.md +1 -1
- package/dist/{actor-DBbe5xeE.d.ts → actor-C7b-1arn.d.ts} +5 -5
- package/dist/{actor-DBbe5xeE.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +2 -2
- package/dist/{ai-vpGtcaCB.d.ts → ai-BMLlI2Ff.d.ts} +3 -3
- package/dist/{ai-vpGtcaCB.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
- package/dist/{ai-UrWfKeuv.js → ai-D6mGlO8j.js} +2 -2
- package/dist/{ai-UrWfKeuv.js.map → ai-D6mGlO8j.js.map} +1 -1
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.browser.d.ts +1 -0
- package/dist/ai-fx.browser.js +6 -0
- package/dist/ai-fx.browser.js.map +1 -0
- package/dist/ai-fx.d.ts +10 -0
- package/dist/ai-fx.d.ts.map +1 -0
- package/dist/ai-fx.js +692 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-B2suP2Qh.d.ts +26 -0
- package/dist/ai-harness-B2suP2Qh.d.ts.map +1 -0
- package/dist/ai-harness-BuLh_EYi.js +273 -0
- package/dist/ai-harness-BuLh_EYi.js.map +1 -0
- package/dist/ai-pi.browser.d.ts +1 -0
- package/dist/ai-pi.browser.js +6 -0
- package/dist/ai-pi.browser.js.map +1 -0
- package/dist/ai-pi.d.ts +17 -0
- package/dist/ai-pi.d.ts.map +1 -0
- package/dist/ai-pi.js +950 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +47 -25
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +113 -40
- package/dist/ai-server.js.map +1 -1
- package/dist/ai-tool-input-DmSqMUQi.js +40 -0
- package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-COaiaDub.js → client-BhHLoNKF.js} +3 -3
- package/dist/{client-COaiaDub.js.map → client-BhHLoNKF.js.map} +1 -1
- package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
- package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
- package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
- package/dist/{internal-CbVvEwad.js → internal-CscSY7K_.js} +11 -3
- package/dist/internal-CscSY7K_.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
- package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/reducer-BAXc8JyF.js.map +1 -1
- package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
- package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +7 -7
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +9 -10
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Rj64fNvX.js → server-D-Y4pqwu.js} +126 -67
- package/dist/server-D-Y4pqwu.js.map +1 -0
- package/dist/{server-CGM8WzRG.d.ts → server-DYAATFuT.d.ts} +42 -7
- package/dist/server-DYAATFuT.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +1 -1
- package/dist/{session-clear-Dshpuhff.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
- package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
- package/dist/{store-Gpkq7WBo.d.ts → store-CyLUy86v.d.ts} +53 -5
- package/dist/store-CyLUy86v.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +56 -11
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-4_y8lETm.js → store-polling-7WJV0YWP.js} +2 -2
- package/dist/{store-polling-4_y8lETm.js.map → store-polling-7WJV0YWP.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 +106 -25
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-B1ixeqIS.js} +3 -3
- package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-CM0AfLY0.js} +271 -101
- package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +66 -21
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-DjfHOvdW.js → wire-BKxH1vbP.js} +2 -2
- package/dist/{wire-DjfHOvdW.js.map → wire-BKxH1vbP.js.map} +1 -1
- package/docs/01-quickstart.mdx +49 -83
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -86
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -150
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +257 -0
- package/docs/agents/02-tools.mdx +247 -0
- package/docs/agents/03-conversations.mdx +538 -0
- package/docs/agents/04-harnesses.mdx +136 -0
- package/docs/agents/meta.ts +1 -0
- package/docs/client/01-react.mdx +294 -0
- package/docs/client/02-transports.mdx +86 -0
- package/docs/client/03-presence.mdx +224 -0
- package/docs/client/meta.ts +1 -0
- package/docs/contract/01-events.mdx +57 -0
- package/docs/contract/02-state.mdx +142 -0
- package/docs/contract/03-sessions.mdx +175 -0
- package/docs/index.mdx +109 -145
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +860 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +731 -0
- package/docs/reference/05-adapters.mdx +351 -0
- package/docs/reference/06-errors.mdx +138 -0
- package/docs/reference/meta.ts +1 -1
- package/docs/server/01-handlers.mdx +200 -0
- package/docs/server/02-timers.mdx +130 -0
- package/docs/server/03-cancellation.mdx +142 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +323 -0
- package/docs/server/06-guarantees.mdx +132 -0
- package/examples/playground/DEPLOY.md +3 -5
- package/examples/playground/app/a0/[appId]/page.tsx +12 -13
- package/examples/playground/app/a0/app-list.tsx +9 -2
- package/examples/playground/app/a0/app-view.tsx +9 -1
- package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
- package/examples/playground/app/a0/apps.test.ts +12 -0
- package/examples/playground/app/a0/apps.ts +22 -7
- package/examples/playground/app/a0/chat.tsx +47 -1
- package/examples/playground/app/a0/devtools.tsx +2 -8
- package/examples/playground/app/a0/fx/events/route.ts +9 -0
- package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
- package/examples/playground/app/a0/model.test.ts +18 -1
- package/examples/playground/app/a0/model.ts +47 -10
- package/examples/playground/app/a0/no-photon.cjs +4 -0
- package/examples/playground/app/a0/no-quickjs.cjs +3 -0
- package/examples/playground/app/a0/pi/events/route.ts +9 -0
- package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
- package/examples/playground/app/a0/sandbox-log.ts +10 -4
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/scheduler/route.ts +2 -2
- package/examples/playground/app/a0/server.ts +98 -54
- package/examples/playground/app/a0/session.ts +17 -5
- package/examples/playground/app/a0/shell.tsx +11 -3
- package/examples/playground/app/agent/ingress.test.ts +6 -3
- package/examples/playground/app/agent/server.ts +9 -6
- package/examples/playground/app/agent/telemetry.test.ts +71 -50
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +30 -26
- package/examples/playground/app/chat/server.ts +46 -46
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +4 -1
- package/examples/playground/vercel.json +18 -0
- package/package.json +27 -3
- package/src/ai-agent.ts +271 -90
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +912 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1322 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.ts +3 -1
- package/src/ai-tool-input.ts +45 -0
- package/src/internal.ts +13 -0
- package/src/libfx.d.ts +34 -0
- package/src/reducer.ts +1 -1
- package/src/scheduler-qstash.ts +5 -5
- package/src/scheduler-vercel.ts +14 -15
- package/src/server.ts +233 -29
- package/src/store-memory.ts +92 -16
- package/src/store-postgres.ts +156 -25
- package/src/store-redis-core.ts +322 -110
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +121 -28
- package/src/store.ts +55 -3
- package/src/telemetry.ts +1 -0
- package/dist/internal-CbVvEwad.js.map +0 -1
- package/dist/server-CGM8WzRG.d.ts.map +0 -1
- package/dist/server-Rj64fNvX.js.map +0 -1
- package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
- package/dist/store-Gpkq7WBo.d.ts.map +0 -1
- package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
- package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -237
- package/docs/concepts/02-handlers.mdx +0 -284
- package/docs/concepts/03-durability.mdx +0 -314
- package/docs/concepts/04-state.mdx +0 -209
- package/docs/guides/01-timers.mdx +0 -193
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -324
- package/docs/guides/04-local-first.mdx +0 -47
- package/docs/guides/05-production.mdx +0 -534
- package/docs/guides/06-ai-agents.mdx +0 -1408
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -262
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -2358
- package/docs/reference/02-errors.mdx +0 -150
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /package/docs/{guides → server}/meta.ts +0 -0
package/docs/index.mdx
CHANGED
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Introduction
|
|
3
|
-
description: A2
|
|
3
|
+
description: A2 syncs your immutable event log, from server to browser.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
7
|
pnpm add experimental-a2
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Handlers react to them, and usually return the next one. The browser follows
|
|
14
|
-
the same log and folds the same state as the server. That's the whole model;
|
|
15
|
-
the rest of this page is it happening.
|
|
16
|
-
|
|
17
|
-
Events record facts, reducers compute the current view, and handlers perform
|
|
18
|
-
the reactions.
|
|
10
|
+
```bash
|
|
11
|
+
npx skills add vercel-labs/a2
|
|
12
|
+
```
|
|
19
13
|
|
|
20
|
-
## Define a contract
|
|
14
|
+
## Define a shared contract
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
off it later.
|
|
16
|
+
Name the events once. The server implements the contract, and the browser
|
|
17
|
+
types itself off it.
|
|
25
18
|
|
|
26
19
|
```ts contracts.ts
|
|
27
20
|
import { z } from 'zod'
|
|
@@ -30,144 +23,143 @@ import * as a2 from 'experimental-a2'
|
|
|
30
23
|
export const orders = a2.contract({
|
|
31
24
|
name: 'orders',
|
|
32
25
|
events: {
|
|
33
|
-
created: z.object({
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
'order.created': z.object({
|
|
27
|
+
shopId: z.string(),
|
|
28
|
+
items: z.array(z.string()),
|
|
29
|
+
}),
|
|
30
|
+
'shop.notified': z.object({ shopId: z.string() }),
|
|
31
|
+
'courier.assigned': z.object({ courierId: z.string() }),
|
|
32
|
+
'order.delivered': z.object({ signedBy: z.string() }),
|
|
33
|
+
'order.cancelled': z.object({ reason: z.string() }),
|
|
36
34
|
},
|
|
37
35
|
reducer: {
|
|
38
36
|
name: 'order-status',
|
|
39
37
|
initialState: { status: 'new' },
|
|
40
|
-
fold: (state, event) => ({ status: event.type
|
|
38
|
+
fold: (state, event) => ({ status: event.type }),
|
|
41
39
|
},
|
|
42
40
|
})
|
|
43
41
|
```
|
|
44
42
|
|
|
45
|
-
##
|
|
43
|
+
## Implement the server
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
Bind the contract to a store, a scheduler, and the handlers that react to its
|
|
46
|
+
events.
|
|
49
47
|
|
|
50
48
|
```ts server/orders.ts
|
|
51
49
|
import { createServer } from 'experimental-a2/server'
|
|
50
|
+
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
51
|
+
import { redis } from 'experimental-a2/store-redis'
|
|
52
52
|
import { orders } from '@/contracts'
|
|
53
53
|
|
|
54
54
|
export const ordersServer = createServer({
|
|
55
55
|
contract: orders,
|
|
56
|
+
store: redis({ url: process.env.REDIS_URL }),
|
|
57
|
+
scheduler: vercelQueues(),
|
|
56
58
|
handlers: {
|
|
57
|
-
created: async ({ event }) => {
|
|
59
|
+
'order.created': async ({ event }) => {
|
|
58
60
|
// your side effect; event.id makes a stable idempotency key:
|
|
59
61
|
// await notifyShop(event.payload, { idempotencyKey: event.id })
|
|
60
|
-
|
|
62
|
+
const { shopId } = event.payload
|
|
63
|
+
return { type: 'shop.notified', payload: { shopId } }
|
|
61
64
|
},
|
|
62
65
|
},
|
|
63
66
|
})
|
|
64
67
|
```
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
A session is one instance of the contract, with its own ordered log. It is the
|
|
69
|
-
thing A2 keeps durable and live. The id is yours:
|
|
70
|
-
|
|
71
|
-
- `session('order-8194')`: an order moving through fulfillment
|
|
72
|
-
- `session('chat-a8c1')`: an agent chat streaming its reply
|
|
73
|
-
- `session('cart-julian')`: a shopping cart
|
|
74
|
-
- `session('doc-roadmap')`: a document with three people typing in it
|
|
75
|
-
|
|
76
|
-
Anything whose story spans more than one request. Put events in the same
|
|
77
|
-
session when they form one history or view. Handlers are concurrent unless
|
|
78
|
-
they share a lane. Any server code can open
|
|
79
|
-
one and append: a route handler, a webhook, a cron job.
|
|
69
|
+
One route serves every session to the browser:
|
|
80
70
|
|
|
81
71
|
```ts app/api/orders/route.ts
|
|
82
72
|
import { ordersServer } from '@/server/orders'
|
|
83
73
|
|
|
84
|
-
export
|
|
85
|
-
|
|
86
|
-
await ordersServer.session(body.orderId).append({
|
|
87
|
-
type: 'created',
|
|
88
|
-
payload: { items: body.items },
|
|
89
|
-
})
|
|
90
|
-
return Response.json({ ok: true })
|
|
91
|
-
}
|
|
74
|
+
export const GET = ordersServer.fetch
|
|
75
|
+
export const POST = ordersServer.fetch
|
|
92
76
|
```
|
|
93
77
|
|
|
94
|
-
|
|
95
|
-
`append` returns; the return confirms durability, not handler completion.
|
|
96
|
-
A queue-backed scheduler wakes pending work after a crash or timeout. The same
|
|
97
|
-
scheduler lets a session append a typed event later with
|
|
98
|
-
`session.schedule(name, { delay: '30s' }, event)`.
|
|
99
|
-
|
|
100
|
-
## State is a fold
|
|
78
|
+
## Implement the client
|
|
101
79
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
80
|
+
The browser follows a session over SSE or a
|
|
81
|
+
[WebSocket](/client/transports) and folds the same state. `push` applies
|
|
82
|
+
instantly and rolls back if the server says no.
|
|
105
83
|
|
|
106
|
-
```
|
|
84
|
+
```tsx app/orders/[orderId]/order.tsx
|
|
85
|
+
'use client'
|
|
86
|
+
import { createClient } from 'experimental-a2/client'
|
|
87
|
+
import { useSession } from 'experimental-a2/react'
|
|
107
88
|
import { orders } from '@/contracts'
|
|
108
89
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
import { ordersServer } from '@/server/orders'
|
|
115
|
-
import { orders } from '@/contracts'
|
|
90
|
+
const ordersClient = createClient({
|
|
91
|
+
contract: orders,
|
|
92
|
+
// SSE; for a WebSocket: { type: 'ws', url: '/api/orders' }
|
|
93
|
+
api: '/api/orders',
|
|
94
|
+
})
|
|
116
95
|
|
|
117
|
-
|
|
118
|
-
|
|
96
|
+
export function Order({ orderId }: { orderId: string }) {
|
|
97
|
+
const { state, push } = useSession(ordersClient, orderId)
|
|
98
|
+
return (
|
|
99
|
+
<button
|
|
100
|
+
onClick={() =>
|
|
101
|
+
void push({ type: 'order.cancelled', payload: { reason: 'customer' } })
|
|
102
|
+
}
|
|
103
|
+
>
|
|
104
|
+
{state.status} · cancel
|
|
105
|
+
</button>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
119
108
|
```
|
|
120
109
|
|
|
121
|
-
##
|
|
110
|
+
## It's just an event log
|
|
122
111
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
```ts app/orders/[orderId]/session.ts
|
|
128
|
-
'use client'
|
|
129
|
-
import { createReact } from 'experimental-a2/react'
|
|
130
|
-
import { orders } from '@/contracts'
|
|
112
|
+
```ts
|
|
113
|
+
// anywhere on the server: a route, a webhook, a cron job
|
|
114
|
+
import { ordersServer } from '@/server/orders'
|
|
131
115
|
|
|
132
|
-
|
|
133
|
-
|
|
116
|
+
const order = ordersServer.session('order-8194')
|
|
117
|
+
await order.append({
|
|
118
|
+
type: 'courier.assigned',
|
|
119
|
+
payload: { courierId: 'courier-7' },
|
|
134
120
|
})
|
|
121
|
+
const events = await order.history()
|
|
122
|
+
// order.created, shop.notified, courier.assigned
|
|
135
123
|
```
|
|
136
124
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
125
|
+
A session is the ordered list of what happened. State is a fold over it, on
|
|
126
|
+
both sides of the wire. Everything else A2 keeps is a cache it can rebuild
|
|
127
|
+
from the log.
|
|
140
128
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
</div>
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
```
|
|
129
|
+
## With strong guarantees
|
|
130
|
+
|
|
131
|
+
- Appends are effectively-once.
|
|
132
|
+
- The log stays ordered.
|
|
133
|
+
- Handlers are at-least-once.
|
|
134
|
+
- Returned events commit with the handler's completion.
|
|
135
|
+
- Handlers run concurrently, unless they share a lane.
|
|
136
|
+
- Failures stay contained.
|
|
137
|
+
- A failing handler never makes `append` throw.
|
|
154
138
|
|
|
155
|
-
|
|
156
|
-
leaves the browser, and rolls back if the server says no. Mounting
|
|
157
|
-
`SessionProvider` and the small stream route it talks to takes about
|
|
158
|
-
twenty lines; [Live UI](/guides/react) wires it end to end.
|
|
139
|
+
[Guarantees](/server/guarantees) has the details and the limits.
|
|
159
140
|
|
|
160
|
-
##
|
|
141
|
+
## Go further...
|
|
161
142
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
143
|
+
<CardGroup cols={2}>
|
|
144
|
+
<Card title="Handlers" href="/server/handlers" icon="zap">
|
|
145
|
+
Retries, lanes, and idempotency.
|
|
146
|
+
</Card>
|
|
147
|
+
<Card title="State" href="/contract/state" icon="layers">
|
|
148
|
+
Reducers and the current view.
|
|
149
|
+
</Card>
|
|
150
|
+
<Card title="History" href="/contract/state#history" icon="history">
|
|
151
|
+
The raw log, oldest first.
|
|
152
|
+
</Card>
|
|
153
|
+
<Card title="AI" href="/agents/introduction" icon="bot">
|
|
154
|
+
Agents with tools and approvals.
|
|
155
|
+
</Card>
|
|
156
|
+
<Card title="Timers" href="/server/timers" icon="clock">
|
|
157
|
+
Schedule an event for later.
|
|
158
|
+
</Card>
|
|
159
|
+
<Card title="Production" href="/server/production" icon="rocket">
|
|
160
|
+
A store and a scheduler.
|
|
161
|
+
</Card>
|
|
162
|
+
</CardGroup>
|
|
171
163
|
|
|
172
164
|
## FAQ
|
|
173
165
|
|
|
@@ -179,7 +171,7 @@ a durable event log. Events settle in their append transaction, with no drain,
|
|
|
179
171
|
claim, or queue message. A contract can also mix handled and history-only event
|
|
180
172
|
types. A2 decides per event type.
|
|
181
173
|
|
|
182
|
-
See [Events without handlers](/
|
|
174
|
+
See [Events without handlers](/server/handlers#events-without-handlers).
|
|
183
175
|
|
|
184
176
|
</details>
|
|
185
177
|
|
|
@@ -189,7 +181,7 @@ See [Events without handlers](/concepts/handlers#events-without-handlers).
|
|
|
189
181
|
Configure Vercel Queues or QStash, then call
|
|
190
182
|
`session.schedule(name, { delay: '30s' }, event)` or use `{ at: Date }`. The
|
|
191
183
|
same scheduler route handles recovery and the delayed append. Names and omitted
|
|
192
|
-
event ids stay stable across retries. See [Timers](/
|
|
184
|
+
event ids stay stable across retries. See [Timers](/server/timers).
|
|
193
185
|
|
|
194
186
|
</details>
|
|
195
187
|
|
|
@@ -198,16 +190,17 @@ event ids stay stable across retries. See [Timers](/guides/timers).
|
|
|
198
190
|
|
|
199
191
|
Cancellation is an event too: same route, same auth, same log. Handlers
|
|
200
192
|
opt in with `abortOn` and receive a live `AbortSignal`; reducers cut the
|
|
201
|
-
derived view exactly. See [Cancellation](/
|
|
193
|
+
derived view exactly. See [Cancellation](/server/cancellation).
|
|
202
194
|
|
|
203
195
|
</details>
|
|
204
196
|
|
|
205
197
|
<details>
|
|
206
198
|
<summary>What happens when a handler keeps failing?</summary>
|
|
207
199
|
|
|
208
|
-
It retries with backoff, then dead-letters after ten
|
|
209
|
-
|
|
210
|
-
|
|
200
|
+
It retries with backoff, then dead-letters after ten failed attempts, whether
|
|
201
|
+
the handler threw or its process ended. Later events in the same lane wait
|
|
202
|
+
until you retry or skip it; unlaned events and other lanes continue.
|
|
203
|
+
[Guarantees](/server/guarantees) has the exact guarantees and the
|
|
211
204
|
recovery story behind them.
|
|
212
205
|
|
|
213
206
|
</details>
|
|
@@ -219,7 +212,7 @@ A durable store backend, such as Postgres or Redis. Add a queue-backed scheduler
|
|
|
219
212
|
when the contract has handlers or calls `session.schedule`. A history-only
|
|
220
213
|
contract with no timers needs no reaction infrastructure. Development needs
|
|
221
214
|
neither: SQLite appears under `.a2/` and state survives restarts. See
|
|
222
|
-
[
|
|
215
|
+
[Production](/server/production).
|
|
223
216
|
|
|
224
217
|
</details>
|
|
225
218
|
|
|
@@ -235,10 +228,10 @@ does not replicate database tables or subscribe to queries across sessions.
|
|
|
235
228
|
<details>
|
|
236
229
|
<summary>Where does my application database fit?</summary>
|
|
237
230
|
|
|
238
|
-
A2 does not replace it.
|
|
231
|
+
A2 does not replace it. Keep collections,
|
|
239
232
|
relationships, constraints, search, and reporting in your application
|
|
240
233
|
database. Split ownership instead of writing the same fact through both paths.
|
|
241
|
-
See [
|
|
234
|
+
See [Sessions and your database](/contract/sessions#sessions-and-your-database).
|
|
242
235
|
|
|
243
236
|
</details>
|
|
244
237
|
|
|
@@ -248,35 +241,6 @@ See [A2 and your database](/guides/application-data).
|
|
|
248
241
|
Yes. Every installation includes a standalone Next.js playground at
|
|
249
242
|
`node_modules/experimental-a2/examples/playground`. It contains complete
|
|
250
243
|
contracts, reducers, servers, route handlers, clients, and schedulers using only
|
|
251
|
-
public package entry points. See [Complete examples](/
|
|
244
|
+
public package entry points. See [Complete examples](/examples).
|
|
252
245
|
|
|
253
246
|
</details>
|
|
254
|
-
|
|
255
|
-
## Where to next
|
|
256
|
-
|
|
257
|
-
<CardGroup cols={2}>
|
|
258
|
-
<Card title="Quickstart" href="/quickstart" icon="rocket">
|
|
259
|
-
A contract, a handler, and a durable append in five minutes.
|
|
260
|
-
</Card>
|
|
261
|
-
<Card title="Contracts and sessions" href="/concepts/contracts" icon="folder">
|
|
262
|
-
The two nouns you'll use everywhere.
|
|
263
|
-
</Card>
|
|
264
|
-
<Card title="Durability" href="/concepts/durability" icon="wrench">
|
|
265
|
-
What A2 guarantees, and exactly how.
|
|
266
|
-
</Card>
|
|
267
|
-
<Card title="Live UI with React" href="/guides/react" icon="lightbulb">
|
|
268
|
-
Stream a session to the browser with optimistic writes.
|
|
269
|
-
</Card>
|
|
270
|
-
<Card title="Durable AI agents" href="/guides/ai-agents" icon="bot">
|
|
271
|
-
Run AI SDK agents with durable messages, progress, tools, and approvals.
|
|
272
|
-
</Card>
|
|
273
|
-
<Card title="A2 and your database" href="/guides/application-data" icon="database">
|
|
274
|
-
Decide what belongs in a session and what belongs in ordinary tables.
|
|
275
|
-
</Card>
|
|
276
|
-
<Card title="Actors" href="/actors/introduction" icon="boxes">
|
|
277
|
-
Durable objects on the log: typed state, one message at a time.
|
|
278
|
-
</Card>
|
|
279
|
-
<Card title="Complete examples" href="/guides/examples" icon="code">
|
|
280
|
-
Read or copy the standalone Next.js playground included with the package.
|
|
281
|
-
</Card>
|
|
282
|
-
</CardGroup>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Core
|
|
3
|
+
description: "Every entry point, and the isomorphic `experimental-a2` core: contracts, reducers, IDs, and the event shape."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Entry points
|
|
7
|
+
|
|
8
|
+
| Entry point | Ships | Peer dependency |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| `experimental-a2` | `contract`, `A2Error`: isomorphic core | none |
|
|
11
|
+
| `experimental-a2/server` | `createServer`: implement a contract | none |
|
|
12
|
+
| `experimental-a2/client` | `createClient`: framework-agnostic session client | none |
|
|
13
|
+
| `experimental-a2/react` | `createReact` | `react` |
|
|
14
|
+
| `experimental-a2/ai` | `agentContract`, `handlerContext`, `inputs`, and the AI reducer: isomorphic | `ai` |
|
|
15
|
+
| `experimental-a2/ai/server` | `agent`: an agent's loop as an A2 server | `ai` |
|
|
16
|
+
| `experimental-a2/ai/fx` | `fx`: the FX harness for `agent()` | `ai`, `libfx` |
|
|
17
|
+
| `experimental-a2/ai/pi` | `pi`: the Pi harness for `agent()` | `ai`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-ai` |
|
|
18
|
+
| `experimental-a2/ai/client` | `createAgentClient`: contract-free agent client | `ai` |
|
|
19
|
+
| `experimental-a2/ai/react` | `useAgent` | `ai`, `react` |
|
|
20
|
+
| [`experimental-a2/store-postgres`](/reference/adapters#experimental-a2store-postgres) | `postgres`: Postgres store backend | `pg` (or inject a client) |
|
|
21
|
+
| [`experimental-a2/store-redis`](/reference/adapters#experimental-a2store-redis) | `redis`: Redis Streams store backend, push-native streaming | `ioredis` (or inject a client) |
|
|
22
|
+
| [`experimental-a2/store-redis-http`](/reference/adapters#experimental-a2store-redis-http) | `redisHttp`: the same Redis store over provider REST APIs (Upstash) | none |
|
|
23
|
+
| `experimental-a2/store-sqlite` | SQLite store backend | none |
|
|
24
|
+
| `experimental-a2/store-memory` | in-memory store backend | none |
|
|
25
|
+
| `experimental-a2/scheduler-vercel` | `vercelQueues`: recovery and timers | `@vercel/queue` |
|
|
26
|
+
| `experimental-a2/scheduler-qstash` | `qstash`: signed recovery and timers | `@upstash/qstash` |
|
|
27
|
+
| `experimental-a2/cache-indexeddb` | `indexedDb` browser cache | none |
|
|
28
|
+
| `experimental-a2/otel` | `otel` telemetry adapter | `@opentelemetry/api` |
|
|
29
|
+
|
|
30
|
+
`experimental-a2` imports none of the backends, and a browser-bundle test in CI
|
|
31
|
+
enforces it. Importing a backend's entry point, such as
|
|
32
|
+
`experimental-a2/store-postgres`, is what pulls that backend in; core never puts
|
|
33
|
+
one in your client bundle.
|
|
34
|
+
|
|
35
|
+
## `experimental-a2`
|
|
36
|
+
|
|
37
|
+
### `a2.contract(options)`
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
a2.contract(options: {
|
|
41
|
+
name: string
|
|
42
|
+
events: Record<string, StandardSchemaV1>
|
|
43
|
+
presence?: Record<string, StandardSchemaV1>
|
|
44
|
+
reducer?: { name: string; initialState: S; stateSchema?: StandardSchemaV1; fold: (state: S, event: Event) => S }
|
|
45
|
+
}): Contract
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Defines one kind of lifecycle. The result is a plain, importable,
|
|
49
|
+
**isomorphic** value: the server implements it, reducers derive from it, and
|
|
50
|
+
the browser types its pushes off it. See [Events](/contract/events).
|
|
51
|
+
|
|
52
|
+
| Option | Meaning |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `name` | Unique per app; identifies the contract in storage and queue messages. May not contain the control character U+001F. |
|
|
55
|
+
| `events` | Event type to payload schema. Types may not start with `a2.`, which A2 reserves for its own events, such as `a2.cleared`. |
|
|
56
|
+
| `presence` | Optional. Field to schema for one participant's ephemeral state. Replicated last-writer-wins, never stored in the log, invisible to reducers. See [Presence](/client/presence). |
|
|
57
|
+
| `reducer` | Optional. The one reducer the contract publishes to clients; an inline definition includes `fold`. See [`reducer(options)`](#reduceroptions). |
|
|
58
|
+
|
|
59
|
+
Schemas are [Standard Schema](https://standardschema.dev): Zod, Valibot,
|
|
60
|
+
ArkType, anything that implements it. Validators must be synchronous; async
|
|
61
|
+
ones are rejected at definition time. The validated output is what's stored.
|
|
62
|
+
|
|
63
|
+
### `contract.batch(...events)`
|
|
64
|
+
|
|
65
|
+
Returns the same events as a readonly tuple, preserving each literal type in a
|
|
66
|
+
heterogeneous handler-returned array. A2 validates them when the handler
|
|
67
|
+
completes. See [Handlers](/server/handlers#return-what-happens-after-success).
|
|
68
|
+
|
|
69
|
+
### `reducer(options)`
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
reducer(options: {
|
|
73
|
+
contract: Contract
|
|
74
|
+
name: string // identity; keys cached snapshots
|
|
75
|
+
initialState: S // the seed (schema input, when stateSchema is set)
|
|
76
|
+
stateSchema?: StandardSchemaV1 // declares S, checks initialState, guards cached folds
|
|
77
|
+
}): { fold(fn: (state: S, event: Event) => S): Reducer<S> }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Builds a standalone reducer for server reads or for the contract's `reducer`
|
|
81
|
+
field. See [State](/contract/state#reducers).
|
|
82
|
+
|
|
83
|
+
- The first call fixes the state and event types, so `.fold()` receives
|
|
84
|
+
concrete ones. Literal unions in your state survive with zero annotations.
|
|
85
|
+
- `name` keys cached snapshots, so two reducers never fight over a cache
|
|
86
|
+
entry. Rename it when the fold's logic changes: a rename (or a `stateSchema`
|
|
87
|
+
mismatch, when present) invalidates, and the next read refolds from raw
|
|
88
|
+
events.
|
|
89
|
+
|
|
90
|
+
### `A2Error`
|
|
91
|
+
|
|
92
|
+
The shared base for A2 failures, discriminated by `code`. The session client
|
|
93
|
+
exposes its `A2ClientError` subclass. See [Errors](/reference/errors).
|
|
94
|
+
|
|
95
|
+
### `idempotentId(...parts)`
|
|
96
|
+
|
|
97
|
+
`idempotentId(...parts: string[]): Promise<string>` derives a stable UUIDv8
|
|
98
|
+
from an ordered tuple of strings. It performs no I/O and runs on the server and
|
|
99
|
+
in the browser.
|
|
100
|
+
|
|
101
|
+
```ts shared/request-id.ts
|
|
102
|
+
import { idempotentId } from 'experimental-a2'
|
|
103
|
+
|
|
104
|
+
export const id = await idempotentId('orders', 'shop-7', 'request-42')
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- Pass the result as an event's `id` when retries should converge on the same
|
|
108
|
+
event.
|
|
109
|
+
- The same tuple produces the same ID across runtimes and A2 versions.
|
|
110
|
+
- String boundaries, order, and case matter; strings are not normalized. Empty
|
|
111
|
+
strings and an empty tuple are valid.
|
|
112
|
+
- Every argument has the same role; a prefix such as `'orders'` is a namespace
|
|
113
|
+
by convention. Include enough context to distinguish operations across
|
|
114
|
+
contracts and sessions: event IDs are globally unique within a store.
|
|
115
|
+
- A2 preserves explicit IDs and does not change its automatic ID generation.
|
|
116
|
+
Changing an existing recipe changes retry identity, so retries of already
|
|
117
|
+
committed requests must keep their original IDs.
|
|
118
|
+
|
|
119
|
+
### `SessionCleared`
|
|
120
|
+
|
|
121
|
+
An `Event` whose `type` is `'a2.cleared'` and whose `payload` is `null`: the
|
|
122
|
+
marker a [clear](/reference/server#sessionclearoptions) leaves in
|
|
123
|
+
`session.stream()`. It never reaches handlers or `history()`. Comparing
|
|
124
|
+
`event.type` with that string narrows a stream item to `SessionCleared`. The
|
|
125
|
+
session client handles the marker itself, so it matters only when you read the
|
|
126
|
+
server stream yourself.
|
|
127
|
+
|
|
128
|
+
## Event
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
type Event = {
|
|
132
|
+
id: string
|
|
133
|
+
type: string
|
|
134
|
+
payload: unknown // typed by the contract's schema for `type`
|
|
135
|
+
index: number // position in the session's log, from 1
|
|
136
|
+
sessionId: string
|
|
137
|
+
createdAt: Date
|
|
138
|
+
}
|
|
139
|
+
```
|