experimental-a2 0.0.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/ai-server.browser.d.ts +1 -0
  3. package/dist/ai-server.browser.js +4 -0
  4. package/dist/ai-server.d.ts +65 -0
  5. package/dist/ai-server.js +494 -0
  6. package/dist/ai.d.ts +282 -0
  7. package/dist/ai.js +922 -0
  8. package/dist/cache-indexeddb.d.ts +1 -0
  9. package/dist/cache-indexeddb.js +0 -0
  10. package/dist/client.d.ts +90 -0
  11. package/dist/client.js +410 -0
  12. package/dist/contract-B0kAXoaL.js +60 -0
  13. package/dist/contract-DL8btVd9.d.ts +161 -0
  14. package/dist/devtools-server.browser.d.ts +1 -0
  15. package/dist/devtools-server.browser.js +4 -0
  16. package/dist/devtools-server.d.ts +22 -0
  17. package/dist/devtools-server.js +1087 -0
  18. package/dist/errors-BJRMd-h6.js +23 -0
  19. package/dist/errors-xL_JTXsY.d.ts +20 -0
  20. package/dist/http.d.ts +44 -0
  21. package/dist/http.js +119 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.js +3 -0
  24. package/dist/inspection-E7qbD0Xj.js +10 -0
  25. package/dist/internal-Dm8Ejnud.js +36 -0
  26. package/dist/log-Dg1I8NRr.d.ts +245 -0
  27. package/dist/log-memory.d.ts +11 -0
  28. package/dist/log-memory.js +345 -0
  29. package/dist/log-polling-RO7kclzR.js +83 -0
  30. package/dist/log-postgres.d.ts +40 -0
  31. package/dist/log-postgres.js +628 -0
  32. package/dist/log-redis.d.ts +31 -0
  33. package/dist/log-redis.js +711 -0
  34. package/dist/log-sqlite.d.ts +17 -0
  35. package/dist/log-sqlite.js +450 -0
  36. package/dist/log-yJbXUf72.js +5 -0
  37. package/dist/otel.d.ts +12 -0
  38. package/dist/otel.js +41 -0
  39. package/dist/react.d.ts +54 -0
  40. package/dist/react.js +85 -0
  41. package/dist/recovery-vercel.d.ts +60 -0
  42. package/dist/recovery-vercel.js +120 -0
  43. package/dist/retryable-lazy-DZWmHpii.js +19 -0
  44. package/dist/server-DYsnKTTy.js +780 -0
  45. package/dist/server.browser.d.ts +1 -0
  46. package/dist/server.browser.js +11 -0
  47. package/dist/server.d.ts +136 -0
  48. package/dist/server.js +2 -0
  49. package/dist/telemetry-C78al20p.d.ts +32 -0
  50. package/dist/validate-XKT4FSNn.js +28 -0
  51. package/dist/wire-2QpU1EtJ.js +62 -0
  52. package/docs/01-quickstart.mdx +214 -0
  53. package/docs/concepts/01-contracts.mdx +138 -0
  54. package/docs/concepts/02-handlers.mdx +146 -0
  55. package/docs/concepts/03-durability.mdx +230 -0
  56. package/docs/concepts/04-state.mdx +133 -0
  57. package/docs/guides/01-timers.mdx +85 -0
  58. package/docs/guides/02-cancellation.mdx +107 -0
  59. package/docs/guides/03-react.mdx +234 -0
  60. package/docs/guides/04-local-first.mdx +88 -0
  61. package/docs/guides/05-production.mdx +179 -0
  62. package/docs/guides/06-ai-agents.mdx +659 -0
  63. package/docs/guides/07-devtools.mdx +101 -0
  64. package/docs/guides/08-application-data.mdx +114 -0
  65. package/docs/index.mdx +282 -0
  66. package/docs/reference/01-api.mdx +637 -0
  67. package/docs/reference/02-errors.mdx +77 -0
  68. package/package.json +111 -0
@@ -0,0 +1,101 @@
1
+ ---
2
+ title: Devtools
3
+ description: Mount one read-only handler to inspect sessions, event lifecycles, failures, and snapshots from the durable log.
4
+ ---
5
+
6
+ ## Mount the dashboard
7
+
8
+ The devtools application is one HTTP handler. Give it every A2 server you want
9
+ to inspect, then mount it on a route that matches the root and its child paths.
10
+ In Next.js, an optional catch-all route does both:
11
+
12
+ ```ts app/api/a2/devtools/[[...path]]/route.ts
13
+ import { createDevtools } from 'experimental-a2/devtools/server'
14
+ import { billingServer, ordersServer } from '@/server'
15
+
16
+ const devtools = createDevtools({
17
+ servers: [ordersServer, billingServer],
18
+ authorize: async (_request) => {
19
+ // Return true after your auth or any other checks.
20
+ return process.env.NODE_ENV !== 'production'
21
+ },
22
+ })
23
+
24
+ export const GET = devtools.handler()
25
+ ```
26
+
27
+ Open `/api/a2/devtools`. There is no client factory, provider, React component,
28
+ or separate datastore. The handler serves the browser application itself and
29
+ reads the same log as the servers.
30
+
31
+ The initial HTML contains the first page of the selected contract's sessions
32
+ and the selected session's durable detail. The browser starts from that
33
+ bootstrap data without a loading screen or an initial API request waterfall,
34
+ then takes over navigation and live updates.
35
+
36
+ The selected contract and session stay in the URL. Refreshing preserves the
37
+ view, links open the same session, and browser back and forward navigation work.
38
+
39
+ Omit `authorize` for the same development-only behavior shown above. An
40
+ unguarded handler returns 404 in production. In a deployed dashboard, provide
41
+ your application's access check. Returning `false` hides the route with a 404;
42
+ returning a `Response` supports a redirect or authentication challenge.
43
+
44
+ ## Causal forest
45
+
46
+ Every stored `cause` connects a child to the event index and handler attempt
47
+ whose `ctx.append` first persisted it. The dashboard builds that forest
48
+ directly from the session log. It does not need a trace table or another write
49
+ on the append path.
50
+
51
+ A null cause appears as **origin unknown**. New top-level appends write null,
52
+ but events written before causal metadata also read as null. The dashboard does
53
+ not guess which one it is. If a cause points outside the visible log, the row
54
+ keeps the stored edge and marks its parent unavailable.
55
+
56
+ ## What the timeline means
57
+
58
+ Each row is one durable event. Its bar begins at `createdAt`, when the event
59
+ entered the log, and ends at `processedAt` or `failedAt`. A pending event stays
60
+ open. Markers show the operation boundaries A2 stores:
61
+
62
+ - first and latest dispatch from `firstClaimedAt` and `lastClaimedAt`
63
+ - latest caught failure from `lastFailedAt` and `lastFailedAttempt`
64
+ - completion from `processedAt` and `processedByAttempt`
65
+ - dead-lettering from `failedAt`
66
+
67
+ `attemptCount` counts every durable dispatch claim. `failureCount` counts only
68
+ caught handler failures. A hard crash can increase the first without
69
+ increasing the second. The dashboard calls that difference a dispatch without
70
+ an outcome because it may be active work, a hard crash, or a superseded
71
+ attempt. It does not claim to know which one occurred.
72
+
73
+ Migrated events can have an `attemptCount` without a `firstClaimedAt`. The
74
+ dashboard shows the first dispatch time as unknown instead of moving it to
75
+ `createdAt` or `lastClaimedAt`.
76
+
77
+ The bar is event lifetime, not handler execution time. A2 stores lifecycle
78
+ summaries, not one span row for every attempt. Time spent waiting, running a
79
+ handler, coordinating a lease, or waiting for recovery remains one honest
80
+ interval.
81
+
82
+ Snapshots appear as their reducer name, event frontier, and last update time.
83
+ The dashboard does not send cached snapshot state to the browser.
84
+
85
+ ## Live updates
86
+
87
+ The selected session opens an SSE connection to the same handler. The stream
88
+ sends invalidations, not a second copy of state. On each invalidation the
89
+ browser reads the durable session again. A disconnected browser can miss every
90
+ frame and still become correct immediately after reconnecting.
91
+
92
+ While work is pending, the handler checks processing metadata quickly. It
93
+ backs off after the session settles and emits heartbeats for intermediaries.
94
+ Only sessions someone is viewing have a live inspection loop.
95
+
96
+ ## Custom logs
97
+
98
+ The memory, SQLite, Postgres, and Redis adapters provide the optional read-only
99
+ inspection interface. A custom `A2Log` can implement `inspect.listSessions`
100
+ and `inspect.listSnapshots` to appear in the dashboard. Logs without inspection
101
+ continue to work normally; their devtools endpoint returns 501.
@@ -0,0 +1,114 @@
1
+ ---
2
+ title: A2 and your database
3
+ description: Use A2 for things with a lifecycle. Keep collections, relationships, and queries in your application database.
4
+ ---
5
+
6
+ ## Start with ownership
7
+
8
+ A2 does not replace your application database. It synchronizes one known
9
+ session at a time: its events, its derived state, and optimistic writes from
10
+ the browser. It does not synchronize tables or database queries.
11
+
12
+ Use your database for the records and relationships that the rest of the
13
+ application queries.
14
+
15
+ Decide which system owns each fact:
16
+
17
+ - If A2 owns it, append an event and derive the session's state with a reducer.
18
+ - If your database owns it, write it with the database's normal transaction.
19
+
20
+ Do not make both paths authoritative for the same fact.
21
+
22
+ ## What belongs in A2
23
+
24
+ A2 fits something with a lifecycle: an order moving through fulfillment, an
25
+ agent producing a response, an approval waiting for a decision, or an import
26
+ that can fail and resume.
27
+
28
+ A **contract** names the events in one kind of lifecycle. A **session** is one
29
+ instance of it:
30
+
31
+ ```text
32
+ orders / order-42
33
+ orders / order-43
34
+ agents / run-abc
35
+ ```
36
+
37
+ The session is the boundary A2 orders, recovers, folds, and streams. Put events
38
+ in the same session when they must be ordered or viewed together. Split work
39
+ that should progress independently.
40
+
41
+ A contract is not a table. A session is not a table either. One contract
42
+ usually serves many sessions, and one session may describe changes that touch
43
+ several records elsewhere in an application.
44
+
45
+ ## What belongs in the database
46
+
47
+ Use your application database for collections and relationships: accounts,
48
+ projects, issue lists, labels, permissions, search, joins, pagination, and
49
+ reporting. Use its transactions for constraints that must hold across several
50
+ rows before a request returns.
51
+
52
+ The Postgres adapter in `experimental-a2/log-postgres` stores A2's own logs. It does not
53
+ watch your application tables, turn their changes into events, or combine an
54
+ A2 append with your SQL transaction. The A2 log and your tables may share one
55
+ Postgres database, but they remain separate data models and transaction
56
+ boundaries.
57
+
58
+ ## An issue tracker
59
+
60
+ An issue tracker is mostly a collection and query product. Postgres should
61
+ usually own its core data:
62
+
63
+ | Part | Owner |
64
+ | --- | --- |
65
+ | issues, projects, labels, comments | Postgres |
66
+ | permissions, board filters, search | Postgres |
67
+ | an AI investigation attached to an issue | one A2 session |
68
+ | an import or automation run | one A2 session |
69
+
70
+ The A2 session can store the investigation's messages, progress, tool calls,
71
+ approval, and result. It refers to the issue id, but it does not become a
72
+ second owner of the issue.
73
+
74
+ An individual issue also has a lifecycle, so A2 can own it when the issue's
75
+ ordered history and live state are the center of the product. That choice has
76
+ a cost: boards and search span many sessions and need another query model. If
77
+ those collection views are the center of the product, let the database lead.
78
+
79
+ Do not put an entire workspace in one session just to make cross-issue queries
80
+ possible. Every event in one session is ordered together, and every client of
81
+ that session follows the same growing log.
82
+
83
+ ## Database writes from handlers
84
+
85
+ Handlers may call your database like any other side effect. They are
86
+ at-least-once, so make the write idempotent with `event.id`. The event is
87
+ durable before the handler runs, and the SQL write may happen later or be
88
+ retried.
89
+
90
+ That is a good fit for a reaction to a lifecycle event. It is not one atomic
91
+ write across A2 and your tables. If a relational change must commit before the
92
+ request succeeds, make that change through the database's write path.
93
+
94
+ ## Cross-session indexes
95
+
96
+ Sometimes an A2-owned lifecycle needs a small database index, such as a list of
97
+ active imports. A handler can maintain that index. Treat it as an advanced
98
+ integration:
99
+
100
+ - it is eventually consistent with the session logs;
101
+ - writes must be idempotent;
102
+ - A2 orders events within one session, not across sessions;
103
+ - A2 does not expose one API that scans and replays every session to rebuild
104
+ the index.
105
+
106
+ Back up and migrate that index like application data. If search, joins, and
107
+ cross-session reporting are central rather than incidental, the database
108
+ should probably own that domain.
109
+
110
+ ## The short version
111
+
112
+ Use A2 when people open something by id and watch it progress. Use your
113
+ database when people list, filter, join, and edit many records. In an
114
+ application that needs both, split ownership along that line.
package/docs/index.mdx ADDED
@@ -0,0 +1,282 @@
1
+ ---
2
+ title: Introduction
3
+ description: A2 is durable sync and reactions for things with a lifecycle. Append events, run handlers, and keep one live view from server to browser.
4
+ ---
5
+
6
+ ```bash
7
+ pnpm add experimental-a2
8
+ ```
9
+
10
+ A2 is durable sync and reactions for things with a lifecycle.
11
+
12
+ An order, agent run, approval, or import gets a session. You append events.
13
+ Handlers react to them, and usually append 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
+ There is no declared state machine. Events record facts, reducers compute the
18
+ current view, and handlers perform the reactions.
19
+
20
+ ## Define a contract
21
+
22
+ Events are the vocabulary. A contract names them once, as a plain
23
+ importable value. The server implements it, and the browser types itself
24
+ off it later.
25
+
26
+ ```ts contracts.ts
27
+ import { z } from 'zod'
28
+ import * as a2 from 'experimental-a2'
29
+
30
+ export const orders = a2.contract({
31
+ name: 'orders',
32
+ events: {
33
+ created: z.object({ items: z.array(z.string()) }),
34
+ 'shop.notified': z.object({}),
35
+ cancelled: z.object({}),
36
+ },
37
+ })
38
+ ```
39
+
40
+ ## React to events
41
+
42
+ Handlers are plain async functions: no determinism rules, no replay, no
43
+ wrappers around side effects. Each one reacts to a fact and usually
44
+ appends the next one.
45
+
46
+ ```ts server/orders.ts
47
+ import { createServer } from 'experimental-a2/server'
48
+ import { orders } from '@/contracts'
49
+
50
+ export const ordersServer = createServer({
51
+ contract: orders,
52
+ handlers: {
53
+ created: async ({ event, append }) => {
54
+ // your side effect; event.id makes a stable idempotency key:
55
+ // await notifyShop(event.payload, { idempotencyKey: event.id })
56
+ await append({ type: 'shop.notified', payload: {} })
57
+ },
58
+ },
59
+ })
60
+ ```
61
+
62
+ ## Open a session
63
+
64
+ A session is one instance of the contract, with its own log and its own
65
+ ordering. It is the thing A2 keeps durable and live. The id is yours:
66
+
67
+ - `session('order-8194')`: an order moving through fulfillment
68
+ - `session('chat-a8c1')`: an agent chat streaming its reply
69
+ - `session('cart-julian')`: a shopping cart
70
+ - `session('doc-roadmap')`: a document with three people typing in it
71
+
72
+ Anything whose story spans more than one request. Put events in the same
73
+ session when they must be ordered or viewed together. Any server code can open
74
+ one and append: a route handler, a webhook, a cron job.
75
+
76
+ ```ts app/api/orders/route.ts
77
+ import { ordersServer } from '@/server/orders'
78
+
79
+ export async function POST(req: Request) {
80
+ const body = await req.json()
81
+ await ordersServer.session(body.orderId).append({
82
+ type: 'created',
83
+ payload: { items: body.items },
84
+ })
85
+ return Response.json({ ok: true })
86
+ }
87
+ ```
88
+
89
+ That append is durable. The event hits the log before anything else happens,
90
+ then A2 starts its handler inline. In production, queue-backed recovery wakes
91
+ pending work after a crash or timeout.
92
+
93
+ ## State is a fold
94
+
95
+ There is no separate state record for the session to keep in sync. Fold its log
96
+ through a reducer, a pure function derived from the contract. It imports
97
+ nothing server-only, which is about to matter.
98
+
99
+ ```ts reducer.ts
100
+ import { orders } from '@/contracts'
101
+
102
+ export const orderStatus = orders
103
+ .reducer({ name: 'order-status', initialState: { status: 'new' } })
104
+ .fold((state, event) => {
105
+ switch (event.type) {
106
+ case 'created':
107
+ return { status: 'created' }
108
+ case 'shop.notified':
109
+ return { status: 'notified' }
110
+ case 'cancelled':
111
+ return { status: 'cancelled' }
112
+ default:
113
+ return state
114
+ }
115
+ })
116
+ ```
117
+
118
+ ```ts
119
+ // anywhere on the server:
120
+ import { ordersServer } from '@/server/orders'
121
+ import { orderStatus } from '@/reducer'
122
+
123
+ const { state } = await ordersServer.session(orderId).state(orderStatus)
124
+ // state → { status: 'notified' }
125
+ ```
126
+
127
+ ## The browser joins the session
128
+
129
+ The reducer is isomorphic, so the browser runs the same fold the server
130
+ does. A session streams to the client live and takes optimistic writes,
131
+ all typed by the one contract:
132
+
133
+ ```ts app/orders/[orderId]/session.ts
134
+ 'use client'
135
+ import { createReact } from 'experimental-a2/react'
136
+ import { orderStatus } from '@/reducer'
137
+
138
+ export const { SessionProvider, useSession } = createReact({
139
+ reducer: orderStatus,
140
+ })
141
+ ```
142
+
143
+ ```tsx app/orders/[orderId]/order-client.tsx
144
+ 'use client'
145
+ import { useSession } from './session'
146
+
147
+ export function OrderStatus() {
148
+ const { state, connection, push } = useSession()
149
+ return (
150
+ <div>
151
+ {state.status}
152
+ {connection.status === 'live' ? ' · live' : ''}
153
+ <button onClick={() => void push({ type: 'cancelled', payload: {} })}>
154
+ Cancel order
155
+ </button>
156
+ </div>
157
+ )
158
+ }
159
+ ```
160
+
161
+ `push` applies instantly, validates against the contract before anything
162
+ leaves the browser, and rolls back if the server says no. Mounting
163
+ `SessionProvider` and the small stream route it talks to takes about
164
+ twenty lines; [Live UI](/guides/react) wires it end to end.
165
+
166
+ ## The properties
167
+
168
+ - **Handlers are plain async functions.** If it runs on your laptop, it
169
+ runs in production.
170
+ - **History is real.** [`history()`](/concepts/state) returns what
171
+ actually happened, in order. Debugging is reading, not reconstructing.
172
+ - **Nothing inside a session is sacred except its log.** Snapshots, leases, queue
173
+ messages: all disposable caches. Delete them and A2 rebuilds
174
+ everything from the log.
175
+
176
+ ## FAQ
177
+
178
+ <details>
179
+ <summary>Why not a workflow engine?</summary>
180
+
181
+ Workflow engines replay your code from the top on every wake-up. So the
182
+ code has to be deterministic, so every side effect gets wrapped in a step
183
+ function, and `sleep()` becomes something magical instead of something
184
+ you'd never call in a serverless function.
185
+
186
+ A2's answer is older and simpler: write everything down. Every meaningful
187
+ thing that happens is an event in a log. Handlers are stateless functions
188
+ that react to one event at a time. State isn't stored. It's computed, by
189
+ folding over the log whenever you need it. There's no orchestrator to
190
+ operate.
191
+
192
+ </details>
193
+
194
+ <details>
195
+ <summary>Isn't this just event sourcing?</summary>
196
+
197
+ It's the useful core of it. A log of facts, state as a fold: the idea is
198
+ decades old, and it's a good one. A2 cuts the ceremony that made it a big
199
+ commitment. No command bus, no projection cluster, no upcasting
200
+ framework. A contract, a log, handlers, reducers.
201
+
202
+ </details>
203
+
204
+ <details>
205
+ <summary>How do I wait five days?</summary>
206
+
207
+ You don't sleep. You schedule an event. Anything that can deliver an
208
+ HTTP call later (QStash, a cron, a payment provider's webhook) hits a
209
+ route that appends. The scheduled thing is data, a session id plus an
210
+ event, not a suspended function. See [Timers](/guides/timers).
211
+
212
+ </details>
213
+
214
+ <details>
215
+ <summary>How do I cancel work that's already running?</summary>
216
+
217
+ Cancellation is an event too: same route, same auth, same log. Handlers
218
+ opt in with `abortOn` and receive a live `AbortSignal`; reducers cut the
219
+ derived view exactly. See [Cancellation](/guides/cancellation).
220
+
221
+ </details>
222
+
223
+ <details>
224
+ <summary>What happens when a handler keeps failing?</summary>
225
+
226
+ It retries with backoff, then dead-letters after ten caught failures, and the
227
+ session stalls at that event, visibly, instead of silently skipping it.
228
+ [Durability](/concepts/durability) has the exact guarantees and the
229
+ recovery story behind them.
230
+
231
+ </details>
232
+
233
+ <details>
234
+ <summary>What does production need?</summary>
235
+
236
+ Two decisions: a log backend (Postgres or Redis) and queue-backed
237
+ recovery. Development needs neither. SQLite appears under `.a2/` and
238
+ state survives restarts. See [Going to production](/guides/production).
239
+
240
+ </details>
241
+
242
+ <details>
243
+ <summary>What does A2 sync?</summary>
244
+
245
+ One session whose id the application already knows. The browser follows its
246
+ events, folds them into live state, and can push changes optimistically. A2
247
+ does not replicate database tables or subscribe to queries across sessions.
248
+
249
+ </details>
250
+
251
+ <details>
252
+ <summary>Where does my application database fit?</summary>
253
+
254
+ A2 does not replace it. Use A2 for things with a lifecycle. Keep collections,
255
+ relationships, constraints, search, and reporting in your application
256
+ database. Split ownership instead of writing the same fact through both paths.
257
+ See [A2 and your database](/guides/application-data).
258
+
259
+ </details>
260
+
261
+ ## Where to next
262
+
263
+ <CardGroup cols={2}>
264
+ <Card title="Quickstart" href="/quickstart" icon="rocket">
265
+ A contract, a handler, and a durable append in five minutes.
266
+ </Card>
267
+ <Card title="Contracts and sessions" href="/concepts/contracts" icon="folder">
268
+ The two nouns you'll use everywhere.
269
+ </Card>
270
+ <Card title="Durability" href="/concepts/durability" icon="wrench">
271
+ What A2 guarantees, and exactly how.
272
+ </Card>
273
+ <Card title="Live UI with React" href="/guides/react" icon="lightbulb">
274
+ Stream a session to the browser with optimistic writes.
275
+ </Card>
276
+ <Card title="Durable AI agents" href="/guides/ai-agents" icon="bot">
277
+ Run AI SDK agents with durable messages, progress, tools, and approvals.
278
+ </Card>
279
+ <Card title="A2 and your database" href="/guides/application-data" icon="database">
280
+ Decide what belongs in a session and what belongs in ordinary tables.
281
+ </Card>
282
+ </CardGroup>