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.
Files changed (95) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/actor.js +1 -1
  3. package/dist/ai-fx.d.ts.map +1 -1
  4. package/dist/ai-fx.js +34 -4
  5. package/dist/ai-fx.js.map +1 -1
  6. package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -1
  7. package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-Clbg5ei7.js} +11 -2
  8. package/dist/ai-harness-Clbg5ei7.js.map +1 -0
  9. package/dist/ai-pi.js +2 -2
  10. package/dist/ai-server.d.ts.map +1 -1
  11. package/dist/ai-server.js +6 -4
  12. package/dist/ai-server.js.map +1 -1
  13. package/dist/internal-CscSY7K_.js.map +1 -1
  14. package/dist/scheduler-qstash.d.ts.map +1 -1
  15. package/dist/scheduler-qstash.js +5 -5
  16. package/dist/scheduler-qstash.js.map +1 -1
  17. package/dist/scheduler-vercel.d.ts.map +1 -1
  18. package/dist/scheduler-vercel.js +4 -3
  19. package/dist/scheduler-vercel.js.map +1 -1
  20. package/dist/server-DYAATFuT.d.ts.map +1 -1
  21. package/dist/{server-D-Y4pqwu.js → server-qyb5aJJG.js} +39 -25
  22. package/dist/server-qyb5aJJG.js.map +1 -0
  23. package/dist/server.js +1 -1
  24. package/dist/session-clear-DGI4jTQ5.js.map +1 -1
  25. package/dist/store-postgres.d.ts.map +1 -1
  26. package/dist/store-postgres.js +38 -136
  27. package/dist/store-postgres.js.map +1 -1
  28. package/docs/01-quickstart.mdx +7 -4
  29. package/docs/actors/01-introduction.mdx +1 -1
  30. package/docs/actors/meta.ts +1 -1
  31. package/docs/agents/02-tools.mdx +1 -1
  32. package/docs/agents/03-conversations.mdx +1 -1
  33. package/docs/agents/meta.ts +1 -1
  34. package/docs/client/meta.ts +1 -1
  35. package/docs/contract/03-sessions.mdx +1 -1
  36. package/docs/index.mdx +2 -2
  37. package/docs/reference/01-core.mdx +8 -8
  38. package/docs/reference/02-server.mdx +3 -3
  39. package/docs/reference/04-ai.mdx +9 -1
  40. package/docs/reference/05-telemetry.mdx +132 -0
  41. package/docs/reference/06-errors.mdx +1 -1
  42. package/docs/reference/meta.ts +1 -1
  43. package/docs/schedulers/01-overview.mdx +112 -0
  44. package/docs/schedulers/02-vercel-queues.mdx +87 -0
  45. package/docs/schedulers/03-qstash.mdx +123 -0
  46. package/docs/schedulers/meta.ts +1 -0
  47. package/docs/server/01-handlers.mdx +29 -2
  48. package/docs/server/02-timers.mdx +2 -2
  49. package/docs/server/06-guarantees.mdx +6 -3
  50. package/docs/stores/01-overview.mdx +37 -0
  51. package/docs/stores/02-redis.mdx +49 -0
  52. package/docs/stores/03-redis-http.mdx +56 -0
  53. package/docs/stores/04-postgres.mdx +121 -0
  54. package/docs/stores/05-sqlite.mdx +34 -0
  55. package/docs/stores/06-memory.mdx +22 -0
  56. package/docs/stores/meta.ts +1 -0
  57. package/examples/playground/DEPLOY.md +12 -7
  58. package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
  59. package/examples/playground/app/a0/chat.test.tsx +421 -330
  60. package/examples/playground/app/a0/shell.tsx +1 -1
  61. package/examples/playground/app/a0/uploads/route.test.ts +59 -0
  62. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -90
  63. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
  64. package/examples/playground/app/agent/ingress.test.ts +41 -17
  65. package/examples/playground/app/agent/model-input.test.tsx +92 -43
  66. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
  67. package/examples/playground/app/chat/server.test.ts +38 -65
  68. package/examples/playground/app/components/activity-history.test.tsx +60 -18
  69. package/examples/playground/app/components/fast-link.test.tsx +3 -5
  70. package/examples/playground/app/initial-state.test.tsx +86 -45
  71. package/examples/playground/package.json +2 -1
  72. package/examples/playground/test/agent.ts +35 -0
  73. package/examples/playground/test/model.ts +149 -0
  74. package/examples/playground/test/serve.ts +184 -0
  75. package/examples/playground/test/server-only.ts +1 -0
  76. package/examples/playground/vitest.config.ts +27 -0
  77. package/package.json +3 -1
  78. package/sql/postgres/notifications.sql +24 -0
  79. package/sql/postgres/schema.sql +137 -0
  80. package/src/ai-agent.ts +5 -4
  81. package/src/ai-fx.ts +37 -3
  82. package/src/ai-harness.ts +18 -0
  83. package/src/errors.ts +1 -1
  84. package/src/internal.ts +2 -1
  85. package/src/postgres-notifications.ts +0 -22
  86. package/src/postgres-resources.ts +6 -9
  87. package/src/postgres-schema.ts +80 -0
  88. package/src/scheduler-qstash.ts +12 -5
  89. package/src/scheduler-vercel.ts +10 -4
  90. package/src/server.ts +66 -32
  91. package/src/store-postgres.ts +5 -123
  92. package/dist/ai-harness-BuuC-gJ3.js.map +0 -1
  93. package/dist/server-D-Y4pqwu.js.map +0 -1
  94. package/docs/reference/05-adapters.mdx +0 -351
  95. 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](/server/production#1-choose-a-store) and one complete handler table,
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](/server/production#3-when-an-event-dead-letters).
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](/server/production#2-add-a-scheduler-for-handlers-and-timers).
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 [Deploy safely](/server/production#4-deploy-safely).
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`](/server/production#dispatch-in-a-fresh-invocation) skips
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. The event waits until you
112
- [retry or skip it](/server/production#3-when-an-event-dead-letters) with
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 the deliberate pieces from
6
- `node_modules/experimental-a2/docs/server/05-production.mdx`.
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
- Schemas/keys are created lazily on first use; no migration step.
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 stops its turn and deletes its sandbox; its
109
- conversation stays in the log. An app nobody uses for 14 days loses its files.
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: the conversation that stays in the log still links
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
- * Stops the agent's current turn, so no later tool call brings the sandbox
8
- * back, then deletes it. The conversation stays in the A2 log.
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
- // The app's agent is whichever harness's agent has its log.
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(async (server) => {
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 })