experimental-a2 0.19.0 → 0.20.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 +16 -0
- package/dist/actor.js +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +34 -4
- package/dist/ai-fx.js.map +1 -1
- package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -1
- package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-Clbg5ei7.js} +11 -2
- package/dist/ai-harness-Clbg5ei7.js.map +1 -0
- package/dist/ai-pi.js +2 -2
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +6 -4
- package/dist/ai-server.js.map +1 -1
- package/dist/internal-CscSY7K_.js.map +1 -1
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +5 -5
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +4 -3
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/server-DYAATFuT.d.ts.map +1 -1
- package/dist/{server-D-Y4pqwu.js → server-qyb5aJJG.js} +39 -25
- package/dist/server-qyb5aJJG.js.map +1 -0
- package/dist/server.js +1 -1
- package/dist/session-clear-DGI4jTQ5.js.map +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +38 -136
- package/dist/store-postgres.js.map +1 -1
- package/docs/01-quickstart.mdx +7 -4
- package/docs/actors/01-introduction.mdx +1 -1
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/02-tools.mdx +1 -1
- package/docs/agents/03-conversations.mdx +1 -1
- package/docs/agents/meta.ts +1 -1
- package/docs/client/meta.ts +1 -1
- package/docs/contract/03-sessions.mdx +1 -1
- package/docs/index.mdx +2 -2
- package/docs/reference/01-core.mdx +8 -8
- package/docs/reference/02-server.mdx +3 -3
- package/docs/reference/04-ai.mdx +9 -1
- package/docs/reference/05-telemetry.mdx +132 -0
- package/docs/reference/06-errors.mdx +1 -1
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +112 -0
- package/docs/schedulers/02-vercel-queues.mdx +87 -0
- package/docs/schedulers/03-qstash.mdx +123 -0
- package/docs/schedulers/meta.ts +1 -0
- package/docs/server/01-handlers.mdx +29 -2
- package/docs/server/02-timers.mdx +2 -2
- package/docs/server/06-guarantees.mdx +6 -3
- package/docs/stores/01-overview.mdx +37 -0
- package/docs/stores/02-redis.mdx +49 -0
- package/docs/stores/03-redis-http.mdx +56 -0
- package/docs/stores/04-postgres.mdx +121 -0
- package/docs/stores/05-sqlite.mdx +34 -0
- package/docs/stores/06-memory.mdx +22 -0
- package/docs/stores/meta.ts +1 -0
- package/examples/playground/DEPLOY.md +12 -7
- package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
- package/examples/playground/app/a0/chat.test.tsx +421 -330
- package/examples/playground/app/a0/shell.tsx +1 -1
- package/examples/playground/app/a0/uploads/route.test.ts +59 -0
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -90
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
- package/examples/playground/app/agent/ingress.test.ts +41 -17
- package/examples/playground/app/agent/model-input.test.tsx +92 -43
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
- package/examples/playground/app/chat/server.test.ts +38 -65
- package/examples/playground/app/components/activity-history.test.tsx +60 -18
- package/examples/playground/app/components/fast-link.test.tsx +3 -5
- package/examples/playground/app/initial-state.test.tsx +86 -45
- package/examples/playground/package.json +2 -1
- package/examples/playground/test/agent.ts +35 -0
- package/examples/playground/test/model.ts +149 -0
- package/examples/playground/test/serve.ts +184 -0
- package/examples/playground/test/server-only.ts +1 -0
- package/examples/playground/vitest.config.ts +27 -0
- package/package.json +3 -1
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +137 -0
- package/src/ai-agent.ts +5 -4
- package/src/ai-fx.ts +37 -3
- package/src/ai-harness.ts +18 -0
- package/src/errors.ts +1 -1
- package/src/internal.ts +2 -1
- package/src/postgres-notifications.ts +0 -22
- package/src/postgres-resources.ts +6 -9
- package/src/postgres-schema.ts +80 -0
- package/src/scheduler-qstash.ts +12 -5
- package/src/scheduler-vercel.ts +10 -4
- package/src/server.ts +66 -32
- package/src/store-postgres.ts +5 -123
- package/dist/ai-harness-BuuC-gJ3.js.map +0 -1
- package/dist/server-D-Y4pqwu.js.map +0 -1
- package/docs/reference/05-adapters.mdx +0 -351
- package/docs/server/05-production.mdx +0 -323
|
@@ -24,7 +24,7 @@ database, hit an API, or stream a model response. A2 claims the event while it
|
|
|
24
24
|
runs and retries it after a crash.
|
|
25
25
|
|
|
26
26
|
`createServer` from `experimental-a2/server` binds the contract to a
|
|
27
|
-
[store](/
|
|
27
|
+
[store](/stores/overview) and one complete handler table,
|
|
28
28
|
so a handler can't go missing because its module was never imported. Compose
|
|
29
29
|
the table across files by spreading objects into `handlers`. It is the only
|
|
30
30
|
entry point that touches a store, and it fails loudly in a browser bundle.
|
|
@@ -194,7 +194,34 @@ records completion runs the handler again.
|
|
|
194
194
|
Throwing means "retry me." A2 records the failure and retries with backoff.
|
|
195
195
|
The failed event blocks only its lane. After ten failed attempts it
|
|
196
196
|
dead-letters and keeps blocking its lane until
|
|
197
|
-
[you retry or skip it](
|
|
197
|
+
[you retry or skip it](#resolve-a-dead-letter).
|
|
198
198
|
|
|
199
199
|
When someone presses stop, catch the abort and return normally instead. See
|
|
200
200
|
[Cancellation](/server/cancellation).
|
|
201
|
+
|
|
202
|
+
## Resolve a dead letter
|
|
203
|
+
|
|
204
|
+
A [dead-lettered](/server/guarantees#when-a-handler-keeps-failing) event waits
|
|
205
|
+
for you. List a session's dead letters, then retry or skip each one:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
// anywhere on the server, behind your own operator checks:
|
|
209
|
+
import { ordersServer } from '@/server'
|
|
210
|
+
|
|
211
|
+
const order = ordersServer.session(orderId)
|
|
212
|
+
const [dead] = await order.deadLetters()
|
|
213
|
+
if (dead) {
|
|
214
|
+
// dead.lastError says why (null if every attempt timed out or crashed);
|
|
215
|
+
// deploy the handler fix first, then:
|
|
216
|
+
await order.redrive(dead.index, { action: 'retry' })
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
- **Retry.** `{ action: 'retry' }` runs the handler again with a fresh
|
|
221
|
+
ten-failure budget. Once it completes, its lane continues.
|
|
222
|
+
- **Skip.** `{ action: 'skip' }` marks the event processed without running it,
|
|
223
|
+
accepting that its effects never happen. The next event in its lane runs.
|
|
224
|
+
|
|
225
|
+
Each redrive is one atomic store operation, and it wakes the session like an
|
|
226
|
+
append, so the lane resumes without another write. An event that isn't
|
|
227
|
+
dead-lettered anymore is left alone: `redrive` reports `{ redriven: false }`.
|
|
@@ -34,7 +34,7 @@ export const ordersServer = createServer({
|
|
|
34
34
|
|
|
35
35
|
Configure a scheduler, then call `session.schedule(name, timing, ...events)`.
|
|
36
36
|
The scheduler route that handles recovery delivers timers too;
|
|
37
|
-
[mount it once](/
|
|
37
|
+
[mount it once](/schedulers/overview).
|
|
38
38
|
|
|
39
39
|
`timing` is `{ delay }` (units `ms`, `s`, `m`, `h`, `d`, checked by the type)
|
|
40
40
|
or `{ at: Date }`. A delay must be a finite, unsigned base-10 decimal above
|
|
@@ -103,7 +103,7 @@ If the receiving deployment changed or removed the event's schema, delivery
|
|
|
103
103
|
can fail validation and then keeps retrying as a scheduler error. The Vercel
|
|
104
104
|
adapter pins timers to their publishing deployment by default; with
|
|
105
105
|
`deploymentId: null` or a stable URL like QStash's, keep schemas compatible.
|
|
106
|
-
See [
|
|
106
|
+
See [Deploys and rollbacks](/schedulers/overview#deploys-and-rollbacks).
|
|
107
107
|
|
|
108
108
|
## Stale timers
|
|
109
109
|
|
|
@@ -71,7 +71,7 @@ up to two seconds for the watchdog send, never for handlers. A failed arm
|
|
|
71
71
|
records `a2.append.armed = false` on the trace. Handler appends and returned
|
|
72
72
|
events join the running drain and add no arm.
|
|
73
73
|
|
|
74
|
-
[`append.dispatch`](/
|
|
74
|
+
[`append.dispatch`](/schedulers/overview#dispatch-in-a-fresh-invocation) skips
|
|
75
75
|
the inline drain and hands the work to the scheduler instead.
|
|
76
76
|
|
|
77
77
|
An event type without a handler settles in the append transaction. It starts
|
|
@@ -108,8 +108,11 @@ attempt takes its claim over. Ten failed attempts dead-letter the event: later
|
|
|
108
108
|
events in its lane stay blocked, while unlaned events and other lanes continue.
|
|
109
109
|
A crash costs every event it interrupts one attempt, so a session whose work
|
|
110
110
|
keeps outrunning its function dead-letters: bound its fan-out, or put work that
|
|
111
|
-
waits on the same resource in one lane.
|
|
112
|
-
|
|
111
|
+
waits on the same resource in one lane. A `NonRetriableError`, or a returned
|
|
112
|
+
batch the store refuses because it reuses an event id, dead-letters the event
|
|
113
|
+
on that first attempt instead: a retry would fail the same way. The event
|
|
114
|
+
waits until you
|
|
115
|
+
[retry or skip it](/server/handlers#resolve-a-dead-letter) with
|
|
113
116
|
`session.redrive()`.
|
|
114
117
|
|
|
115
118
|
## Limits
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Overview
|
|
3
|
+
description: Where A2 keeps your session histories.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { redis } from 'experimental-a2/store-redis'
|
|
9
|
+
import { orders } from '@/contracts'
|
|
10
|
+
|
|
11
|
+
export const ordersServer = createServer({
|
|
12
|
+
contract: orders,
|
|
13
|
+
store: redis({ url: process.env.REDIS_URL }),
|
|
14
|
+
handlers: {
|
|
15
|
+
/* ... */
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| Store | Use it for |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| [Redis](/stores/redis) | Production (recommended) |
|
|
23
|
+
| [Redis over HTTP](/stores/redis-http) | Production, when your provider only offers a REST API |
|
|
24
|
+
| [Postgres](/stores/postgres) | Production |
|
|
25
|
+
| [SQLite](/stores/sqlite) | Development: the default |
|
|
26
|
+
| [Memory](/stores/memory) | Tests: the default when `NODE_ENV=test` |
|
|
27
|
+
|
|
28
|
+
When you omit `store`, development gets SQLite, so state survives `next dev`
|
|
29
|
+
restarts, and tests get memory. Production has no default, on purpose: the
|
|
30
|
+
server throws `STORE_NOT_CONFIGURED` at startup rather than write events to a
|
|
31
|
+
filesystem that evaporates.
|
|
32
|
+
|
|
33
|
+
A store holds A2's session histories. It doesn't connect A2 to your
|
|
34
|
+
application tables; see
|
|
35
|
+
[Sessions and your database](/contract/sessions#sessions-and-your-database).
|
|
36
|
+
|
|
37
|
+
Servers with handlers or timers also need a [scheduler](/schedulers/overview).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Redis
|
|
3
|
+
description: The recommended production store. Live streams are pushed, not polled.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { redis } from 'experimental-a2/store-redis'
|
|
9
|
+
import { orders } from '@/contracts'
|
|
10
|
+
|
|
11
|
+
export const ordersServer = createServer({
|
|
12
|
+
contract: orders,
|
|
13
|
+
store: redis({ url: process.env.REDIS_URL }),
|
|
14
|
+
handlers: {
|
|
15
|
+
/* ... */
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```package-install
|
|
21
|
+
npm i ioredis
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Works on a single Redis instance and on non-cluster providers such as Upstash,
|
|
25
|
+
which is durable by default (use its TCP `rediss://` URL). There's nothing to
|
|
26
|
+
set up: A2 creates its keys as it writes.
|
|
27
|
+
|
|
28
|
+
## Options
|
|
29
|
+
|
|
30
|
+
| Option | |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `url` | creates an `ioredis` client |
|
|
33
|
+
| `client` | your own `ioredis` client instead |
|
|
34
|
+
| `keyPrefix` | isolates several apps on one Redis. Default `'a2'` |
|
|
35
|
+
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
- Each session is a Redis Stream.
|
|
40
|
+
- Every write publishes a wake-up, so live streams are push-driven, and
|
|
41
|
+
connections scale with your processes, not with your viewers.
|
|
42
|
+
- A wake-up is disposable: a safety re-read recovers a lost one within ten
|
|
43
|
+
seconds.
|
|
44
|
+
|
|
45
|
+
## Cost per command
|
|
46
|
+
|
|
47
|
+
On providers that bill per command, such as Upstash, appending an event with a
|
|
48
|
+
handler and running it takes 24 commands, and an idle live stream takes 6 a
|
|
49
|
+
minute.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Redis over HTTP
|
|
3
|
+
description: The same Redis store over fetch, for providers with only a REST API.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { redisHttp } from 'experimental-a2/store-redis-http'
|
|
9
|
+
import { orders } from '@/contracts'
|
|
10
|
+
|
|
11
|
+
const url = process.env.UPSTASH_REDIS_REST_URL
|
|
12
|
+
const token = process.env.UPSTASH_REDIS_REST_TOKEN
|
|
13
|
+
if (!url || !token) throw new Error('set the Upstash REST URL and token')
|
|
14
|
+
|
|
15
|
+
export const ordersServer = createServer({
|
|
16
|
+
contract: orders,
|
|
17
|
+
store: redisHttp({ url, token }),
|
|
18
|
+
handlers: {
|
|
19
|
+
/* ... */
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
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.
|
|
26
|
+
|
|
27
|
+
:::warning
|
|
28
|
+
Live streams trust the messages on the store's `:notify` channels. Browsers
|
|
29
|
+
never hold Redis credentials, so they can't reach those channels. The risk is a
|
|
30
|
+
credential that can publish but not write, such as a token another service
|
|
31
|
+
uses: it could add or hide events in live views. Grant publish access only to
|
|
32
|
+
credentials that can also write the store.
|
|
33
|
+
:::
|
|
34
|
+
|
|
35
|
+
## Options
|
|
36
|
+
|
|
37
|
+
| Option | |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `url`, `token` | your provider's REST endpoint and token |
|
|
40
|
+
| `keyPrefix` | isolates several apps on one Redis. Default `'a2'` |
|
|
41
|
+
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
42
|
+
|
|
43
|
+
## Live updates
|
|
44
|
+
|
|
45
|
+
- Live events and presence use Upstash's HTTP `SUBSCRIBE` endpoint, with the
|
|
46
|
+
same URL and token. Each active channel holds one outbound HTTP subscription
|
|
47
|
+
per store instance, shared by its viewers and closed when the last one
|
|
48
|
+
leaves.
|
|
49
|
+
- Writes publish their wake-ups in the same Redis script as the append. A
|
|
50
|
+
rejected notification doesn't fail the write.
|
|
51
|
+
- Idle streams re-read every ten seconds to recover a missed notification.
|
|
52
|
+
Subscriptions reconnect on their own and catch up from the event cursor.
|
|
53
|
+
- After a reconnect, A2 re-reads presence and clears fields deleted during the
|
|
54
|
+
gap, so presence may repeat its latest values.
|
|
55
|
+
- If `SUBSCRIBE` answers 404, 405, or 501, the store polls instead, every 25ms
|
|
56
|
+
while busy and up to every two seconds when idle. Other errors stay errors.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Postgres
|
|
3
|
+
description: Any Postgres database. You create A2's tables; A2 never changes your schema.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { postgres } from 'experimental-a2/store-postgres'
|
|
9
|
+
import { orders } from '@/contracts'
|
|
10
|
+
|
|
11
|
+
export const ordersServer = createServer({
|
|
12
|
+
contract: orders,
|
|
13
|
+
store: postgres({ connectionString: process.env.DATABASE_URL }),
|
|
14
|
+
handlers: {
|
|
15
|
+
/* ... */
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```package-install
|
|
21
|
+
npm i pg
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Works with Neon, Supabase, RDS, or your own server, including transaction-mode
|
|
25
|
+
poolers.
|
|
26
|
+
|
|
27
|
+
## Create the tables
|
|
28
|
+
|
|
29
|
+
A2 doesn't create tables in your database. This is the SQL it needs:
|
|
30
|
+
[`schema.sql`](https://github.com/vercel-labs/a2/blob/main/packages/a2/sql/postgres/schema.sql),
|
|
31
|
+
also in the package at `node_modules/experimental-a2/sql/postgres/schema.sql`.
|
|
32
|
+
Apply it once before your first deploy, however you run SQL against your
|
|
33
|
+
database, as the role your app connects with.
|
|
34
|
+
|
|
35
|
+
## Upgrade
|
|
36
|
+
|
|
37
|
+
When a release changes the schema, its changelog says so. Apply that
|
|
38
|
+
release's `schema.sql` before you deploy it. The version you're running keeps
|
|
39
|
+
working against the new schema.
|
|
40
|
+
|
|
41
|
+
If the tables are missing or out of date, the store's first query throws
|
|
42
|
+
`STORE_NOT_CONFIGURED` naming the file to apply. Once it's applied, the next
|
|
43
|
+
request works, with no redeploy. The store checks with one read before its
|
|
44
|
+
first query, which never waits on or blocks writes.
|
|
45
|
+
|
|
46
|
+
## Live updates (polling)
|
|
47
|
+
|
|
48
|
+
By default, live streams poll every 25ms while a session is busy, backing off
|
|
49
|
+
to every two seconds when it's quiet, so an update from another client can take
|
|
50
|
+
up to two seconds, plus query latency, to reach a quiet stream. A client's own
|
|
51
|
+
`push()` refreshes its stream immediately, even when the write and the stream
|
|
52
|
+
run in different invocations.
|
|
53
|
+
|
|
54
|
+
## Live updates (notifications)
|
|
55
|
+
|
|
56
|
+
For instant updates, pass `listenConnectionString`, a direct connection to the
|
|
57
|
+
same database (not through a pooler), and apply
|
|
58
|
+
[`notifications.sql`](https://github.com/vercel-labs/a2/blob/main/packages/a2/sql/postgres/notifications.sql)
|
|
59
|
+
once, the same way as `schema.sql`.
|
|
60
|
+
|
|
61
|
+
Its triggers notify listeners of every write, including writes from stores
|
|
62
|
+
without the option. Notifications arrive after commit, a reconnect catches up
|
|
63
|
+
from the cursor, and subscriptions also reconcile every ten seconds. Every
|
|
64
|
+
serverless instance opens its own direct connection, so leave connection
|
|
65
|
+
headroom.
|
|
66
|
+
|
|
67
|
+
## Use postgres.js
|
|
68
|
+
|
|
69
|
+
`connectionString` creates a `pg` pool. To use
|
|
70
|
+
[postgres.js](https://github.com/porsager/postgres) instead, wrap it as
|
|
71
|
+
`client`:
|
|
72
|
+
|
|
73
|
+
```ts server/store.ts
|
|
74
|
+
import postgresJs from 'postgres'
|
|
75
|
+
import { postgres } from 'experimental-a2/store-postgres'
|
|
76
|
+
|
|
77
|
+
const url = process.env.DATABASE_URL
|
|
78
|
+
if (!url) throw new Error('set DATABASE_URL')
|
|
79
|
+
|
|
80
|
+
const sql = postgresJs(url, {
|
|
81
|
+
// A2 sends JSON as text; this keeps postgres.js from encoding it again.
|
|
82
|
+
types: {
|
|
83
|
+
json: {
|
|
84
|
+
to: 114,
|
|
85
|
+
from: [114, 3802],
|
|
86
|
+
serialize: (x: unknown) => (typeof x === 'string' ? x : JSON.stringify(x)),
|
|
87
|
+
parse: (x: string) => JSON.parse(x),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
export const store = postgres({
|
|
93
|
+
client: {
|
|
94
|
+
query: async (text, values = []) => ({
|
|
95
|
+
rows: [...(await sql.unsafe(text, values))],
|
|
96
|
+
}),
|
|
97
|
+
connect: async () => {
|
|
98
|
+
const connection = await sql.reserve()
|
|
99
|
+
return {
|
|
100
|
+
query: async (text, values = []) => ({
|
|
101
|
+
rows: [...(await connection.unsafe(text, values))],
|
|
102
|
+
}),
|
|
103
|
+
release: () => connection.release(),
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
end: () => sql.end(),
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`listenConnectionString` still opens its listener with `pg`, so live
|
|
112
|
+
notifications need `pg` installed too.
|
|
113
|
+
|
|
114
|
+
## Options
|
|
115
|
+
|
|
116
|
+
| Option | |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `connectionString` | creates a `pg` pool, shared by stores with the same settings |
|
|
119
|
+
| `client` | your own pool instead: a `pg` `Pool`, PGlite, or a pooler wrapper. Its lifecycle is yours |
|
|
120
|
+
| `listenConnectionString` | a direct connection for notifications |
|
|
121
|
+
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SQLite
|
|
3
|
+
description: The development default, in a file at `.a2/dev.db`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { sqlite } from 'experimental-a2/store-sqlite'
|
|
9
|
+
import { orders } from '@/contracts'
|
|
10
|
+
|
|
11
|
+
export const ordersServer = createServer({
|
|
12
|
+
contract: orders,
|
|
13
|
+
store: sqlite({ path: '.a2/orders.db' }),
|
|
14
|
+
handlers: {
|
|
15
|
+
/* ... */
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
In development, a server without a `store` uses SQLite at `.a2/dev.db`, so
|
|
21
|
+
your sessions survive `next dev` restarts. Pass it yourself to pick another
|
|
22
|
+
file.
|
|
23
|
+
|
|
24
|
+
It runs on `node:sqlite`, with no dependencies, and creates its tables when it
|
|
25
|
+
opens the file. It's for development: a serverless filesystem doesn't last or
|
|
26
|
+
share one file across instances, so production needs another
|
|
27
|
+
[store](/stores/overview).
|
|
28
|
+
|
|
29
|
+
## Options
|
|
30
|
+
|
|
31
|
+
| Option | |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `path` | the database file. Default `.a2/dev.db`; `:memory:` works |
|
|
34
|
+
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Memory
|
|
3
|
+
description: An in-process store for tests, the default when `NODE_ENV=test`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
// in a test:
|
|
8
|
+
import { createServer } from 'experimental-a2/server'
|
|
9
|
+
import { memory } from 'experimental-a2/store-memory'
|
|
10
|
+
import { orders } from '@/contracts'
|
|
11
|
+
|
|
12
|
+
const ordersServer = createServer({ contract: orders, store: memory() })
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Sessions live in the process and vanish when it exits. Under `NODE_ENV=test`, a
|
|
16
|
+
server without a `store` already uses memory.
|
|
17
|
+
|
|
18
|
+
## Options
|
|
19
|
+
|
|
20
|
+
| Option | |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { order: 4 }
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
The event log runs with zero setup in development (SQLite at `.a2/dev.db`,
|
|
4
4
|
no scheduler). The Agent demo and A0 also need AI Gateway and Vercel Sandbox
|
|
5
|
-
credentials. Production needs
|
|
6
|
-
`node_modules/experimental-a2/docs/
|
|
5
|
+
credentials. Production needs a store
|
|
6
|
+
(`node_modules/experimental-a2/docs/stores/01-overview.mdx`) and a scheduler
|
|
7
|
+
(`node_modules/experimental-a2/docs/schedulers/01-overview.mdx`).
|
|
7
8
|
|
|
8
9
|
## 1. A store
|
|
9
10
|
|
|
@@ -23,7 +24,12 @@ Redis, poll cadence elsewhere. Upstash is durable by default; use its TCP
|
|
|
23
24
|
`rediss://` URL, not the REST one. Each feature creates its own server with
|
|
24
25
|
the shared store; storage remains namespaced per contract. Without either variable, a
|
|
25
26
|
production boot fails fast with `STORE_NOT_CONFIGURED` (by design).
|
|
26
|
-
|
|
27
|
+
Redis needs no setup. With Postgres, create A2's tables once before the
|
|
28
|
+
first deploy (A2 never creates them at runtime):
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f node_modules/experimental-a2/sql/postgres/schema.sql
|
|
32
|
+
```
|
|
27
33
|
|
|
28
34
|
To verify a provider before leaning on it, run the full conformance
|
|
29
35
|
kit against it (throwaway database; the suite isolates by key prefix
|
|
@@ -105,8 +111,8 @@ A sandbox stays up while it's used: each tool call keeps it running at least
|
|
|
105
111
|
10 more minutes, and so does the preview while it's on screen. Otherwise it
|
|
106
112
|
stops, and resumes with its files the next time it's needed. A new or resumed
|
|
107
113
|
sandbox starts `next dev` in the background, so commands don't wait for it;
|
|
108
|
-
the preview does. Deleting an app
|
|
109
|
-
|
|
114
|
+
the preview does. Deleting an app clears its conversation from the log and
|
|
115
|
+
deletes its sandbox. An app nobody uses for 14 days loses its files.
|
|
110
116
|
|
|
111
117
|
App ids are the only access control: anyone with an A0 URL can open that app,
|
|
112
118
|
and anyone who can reach the deployment can start new apps, each with its own
|
|
@@ -122,8 +128,7 @@ the project, which sets `BLOB_READ_WRITE_TOKEN`; `vercel env pull` brings it
|
|
|
122
128
|
to local runs. Images go to the model with the message; other files go into
|
|
123
129
|
its text as links, which the agent downloads in its sandbox. Each lives at an
|
|
124
130
|
unguessable public URL under `a0/<app id>/`, which anyone with the URL can
|
|
125
|
-
read. Nothing deletes them
|
|
126
|
-
them.
|
|
131
|
+
read. Nothing deletes them, not even deleting their app.
|
|
127
132
|
|
|
128
133
|
A0's dev tools have a Crash button, which ends the function instance running
|
|
129
134
|
an app's agent, mid-turn, so the scheduler resumes the turn in a fresh one.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { inputs } from 'experimental-a2/ai'
|
|
2
1
|
import { isAppId } from '../../apps'
|
|
3
2
|
import { deleteAppSandbox } from '../../sandbox'
|
|
4
3
|
import { agentServers } from '../../server'
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Deletes the app: its conversation, then its sandbox. A clear reads no
|
|
7
|
+
* state, so it also deletes a log this A2 can no longer fold, and no pending
|
|
8
|
+
* step of the old conversation runs to bring the sandbox back.
|
|
9
9
|
*/
|
|
10
10
|
export async function DELETE(
|
|
11
11
|
_request: Request,
|
|
@@ -14,14 +14,10 @@ export async function DELETE(
|
|
|
14
14
|
const { appId } = await context.params
|
|
15
15
|
if (!isAppId(appId))
|
|
16
16
|
return Response.json({ error: 'Unknown app.' }, { status: 404 })
|
|
17
|
-
//
|
|
17
|
+
// Only one harness's agent has the app's log; clearing the others writes
|
|
18
|
+
// nothing.
|
|
18
19
|
await Promise.all(
|
|
19
|
-
Object.values(agentServers).map(
|
|
20
|
-
const session = server.session(appId)
|
|
21
|
-
const { state } = await session.state()
|
|
22
|
-
if (state.active)
|
|
23
|
-
await session.append(...inputs.stop({ turnId: state.active.turnId }))
|
|
24
|
-
}),
|
|
20
|
+
Object.values(agentServers).map((server) => server.session(appId).clear()),
|
|
25
21
|
)
|
|
26
22
|
await deleteAppSandbox({ appId })
|
|
27
23
|
return Response.json({ ok: true })
|