experimental-a2 0.20.0 → 0.21.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 +34 -0
- package/README.md +1 -1
- package/dist/{actor-C7b-1arn.d.ts → actor-HnGmrOZZ.d.ts} +5 -5
- package/dist/{actor-C7b-1arn.d.ts.map → actor-HnGmrOZZ.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +2 -2
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +5 -5
- package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
- package/dist/{actor-shared-t7w8xIKP.js.map → actor-shared-DFET5q16.js.map} +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +54 -43
- package/dist/actor.js.map +1 -1
- package/dist/{ai-Cz3hqVhk.d.ts → ai-CvEnqVfo.d.ts} +8 -8
- package/dist/{ai-Cz3hqVhk.d.ts.map → ai-CvEnqVfo.d.ts.map} +1 -1
- package/dist/{ai-mMiWul1T.js → ai-CzE672Q3.js} +9 -9
- package/dist/ai-CzE672Q3.js.map +1 -0
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +1 -1
- package/dist/ai-fx.js +3 -6
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-Bfk-Lycn.d.ts} +2 -2
- package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-Bfk-Lycn.d.ts.map} +1 -1
- package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +3 -3
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +14 -17
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +80 -73
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BhHLoNKF.js → client-9IeTsiAs.js} +5 -5
- package/dist/{client-BhHLoNKF.js.map → client-9IeTsiAs.js.map} +1 -1
- package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
- package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
- package/dist/contract-CWukt01d.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
- package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
- package/dist/{internal-CscSY7K_.js → internal-Bc4PvfHk.js} +10 -3
- package/dist/internal-Bc4PvfHk.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js +1 -1
- package/dist/otel.js.map +1 -1
- package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
- package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
- package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
- package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
- package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
- package/dist/runtime-consumer-BWmht60z.js +12 -0
- package/dist/runtime-consumer-BWmht60z.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +3 -3
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-DYAATFuT.d.ts → server-BshFZ0eL.d.ts} +108 -41
- package/dist/server-BshFZ0eL.d.ts.map +1 -0
- package/dist/{server-qyb5aJJG.js → server-DvIB8kj5.js} +387 -113
- package/dist/server-DvIB8kj5.js.map +1 -0
- package/dist/server.browser.js +1 -1
- package/dist/server.browser.js.map +1 -1
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-Bk_29eEw.d.ts} +2 -2
- package/dist/session-clear-Bk_29eEw.d.ts.map +1 -0
- package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
- package/dist/session-clear-wHd5GCpZ.js.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-Z2qjw50n.d.ts +523 -0
- package/dist/store-Z2qjw50n.d.ts.map +1 -0
- package/dist/store-codec-DwrWlgQx.js +65 -0
- package/dist/store-codec-DwrWlgQx.js.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +393 -210
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-7WJV0YWP.js → store-polling-DYNyxCev.js} +2 -2
- package/dist/{store-polling-7WJV0YWP.js.map → store-polling-DYNyxCev.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 +1586 -566
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-B1ixeqIS.js → store-presence-polling-CzQ0wdtM.js} +3 -3
- package/dist/{store-presence-polling-B1ixeqIS.js.map → store-presence-polling-CzQ0wdtM.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +4 -4
- package/dist/store-redis-notify-IiVdlYHc.js +2919 -0
- package/dist/store-redis-notify-IiVdlYHc.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +2 -2
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +517 -287
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
- package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
- package/dist/{wire-BKxH1vbP.js → wire-CZGqNZRx.js} +5 -4
- package/dist/wire-CZGqNZRx.js.map +1 -0
- package/docs/01-quickstart.mdx +21 -15
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +8 -0
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +12 -11
- package/docs/agents/03-conversations.mdx +30 -20
- package/docs/agents/04-harnesses.mdx +7 -3
- package/docs/client/03-presence.mdx +18 -14
- package/docs/contract/01-events.mdx +2 -2
- package/docs/contract/02-state.mdx +18 -14
- package/docs/contract/03-sessions.mdx +32 -25
- package/docs/index.mdx +29 -24
- package/docs/reference/01-core.mdx +4 -4
- package/docs/reference/02-server.mdx +245 -190
- package/docs/reference/04-ai.mdx +20 -19
- package/docs/reference/05-telemetry.mdx +27 -13
- package/docs/reference/06-errors.mdx +24 -18
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +8 -8
- package/docs/schedulers/02-vercel-queues.mdx +2 -2
- package/docs/schedulers/03-qstash.mdx +4 -4
- package/docs/server/01-consumers.mdx +415 -0
- package/docs/server/02-timers.mdx +31 -21
- package/docs/server/03-cancellation.mdx +52 -44
- package/docs/server/04-routes.mdx +4 -1
- package/docs/server/05-sql-projections.mdx +391 -0
- package/docs/server/06-guarantees.mdx +55 -48
- package/docs/stores/01-overview.mdx +3 -3
- package/docs/stores/02-redis.mdx +11 -3
- package/docs/stores/03-redis-http.mdx +2 -2
- package/docs/stores/04-postgres.mdx +65 -14
- package/docs/stores/05-sqlite.mdx +5 -3
- package/docs/upgrades/01-overview.mdx +30 -0
- package/docs/upgrades/79-0-21.mdx +165 -0
- package/docs/upgrades/80-0-20.mdx +43 -0
- package/docs/upgrades/81-0-19.mdx +33 -0
- package/docs/upgrades/82-0-18.mdx +33 -0
- package/docs/upgrades/83-0-17.mdx +69 -0
- package/docs/upgrades/meta.ts +3 -0
- package/examples/playground/DEPLOY.md +39 -7
- package/examples/playground/app/a0/chat.test.tsx +2 -2
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/server.ts +4 -4
- package/examples/playground/app/a2slack/a2slack.css +1067 -0
- package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
- package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
- package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
- package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
- package/examples/playground/app/a2slack/consumers/search.ts +37 -0
- package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
- package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
- package/examples/playground/app/a2slack/db/a2.ts +206 -0
- package/examples/playground/app/a2slack/db/index.ts +73 -0
- package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
- package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
- package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
- package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +34 -0
- package/examples/playground/app/a2slack/db/queries.ts +263 -0
- package/examples/playground/app/a2slack/db/schema.ts +145 -0
- package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
- package/examples/playground/app/a2slack/drills/harness.ts +244 -0
- package/examples/playground/app/a2slack/events/route.ts +36 -0
- package/examples/playground/app/a2slack/identity.ts +44 -0
- package/examples/playground/app/a2slack/inbox/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/page.tsx +101 -0
- package/examples/playground/app/a2slack/lag.ts +73 -0
- package/examples/playground/app/a2slack/layout.tsx +43 -0
- package/examples/playground/app/a2slack/messages/route.ts +17 -0
- package/examples/playground/app/a2slack/model.test.ts +208 -0
- package/examples/playground/app/a2slack/model.ts +369 -0
- package/examples/playground/app/a2slack/models.test.ts +72 -0
- package/examples/playground/app/a2slack/page.tsx +10 -0
- package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
- package/examples/playground/app/a2slack/search/route.ts +40 -0
- package/examples/playground/app/a2slack/search-index.ts +108 -0
- package/examples/playground/app/a2slack/server.test.ts +315 -0
- package/examples/playground/app/a2slack/server.ts +55 -0
- package/examples/playground/app/a2slack/sessions.ts +50 -0
- package/examples/playground/app/a2slack/thread/route.ts +8 -0
- package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
- package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
- package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
- package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
- package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
- package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
- package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
- package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
- package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
- package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
- package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
- package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
- package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
- package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
- package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
- package/examples/playground/app/agent/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
- package/examples/playground/app/orders/page.tsx +1 -1
- package/examples/playground/app/orders/server.ts +49 -30
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
- package/examples/playground/app/recovery/page.tsx +4 -4
- package/examples/playground/app/recovery/server.ts +23 -32
- package/examples/playground/drizzle.config.ts +14 -0
- package/examples/playground/next.config.ts +6 -1
- package/examples/playground/package.json +12 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +5 -2
- package/sql/postgres/drizzle-schema.ts +206 -0
- package/sql/postgres/schema.sql +206 -67
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +149 -111
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +4 -6
- package/src/ai-harness.ts +10 -2
- package/src/ai-pi.ts +2 -2
- package/src/ai-server.ts +2 -2
- package/src/ai.ts +7 -7
- package/src/contract.ts +1 -1
- package/src/deterministic-id.ts +26 -3
- package/src/errors.ts +8 -5
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +15 -5
- package/src/otel.ts +1 -1
- package/src/postgres-schema.ts +98 -57
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +1 -1
- package/src/server.browser.ts +1 -1
- package/src/server.ts +774 -207
- package/src/session-socket.ts +1 -1
- package/src/store-codec.ts +66 -0
- package/src/store-memory.ts +581 -255
- package/src/store-postgres.ts +1984 -825
- package/src/store-redis-core.ts +1624 -1152
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +891 -486
- package/src/store.ts +227 -97
- package/src/telemetry.ts +2 -1
- package/src/wire.ts +1 -0
- package/dist/ai-harness-Clbg5ei7.js.map +0 -1
- package/dist/ai-mMiWul1T.js.map +0 -1
- package/dist/contract-BYKASlCV.js.map +0 -1
- package/dist/idempotent-replay-JdgMQRRm.js +0 -21
- package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
- package/dist/internal-CscSY7K_.js.map +0 -1
- package/dist/server-DYAATFuT.d.ts.map +0 -1
- package/dist/server-qyb5aJJG.js.map +0 -1
- package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
- package/dist/session-clear-DGI4jTQ5.js.map +0 -1
- package/dist/store-CyLUy86v.d.ts +0 -404
- package/dist/store-CyLUy86v.d.ts.map +0 -1
- package/dist/store-codec-DTG0Ftek.js +0 -10
- package/dist/store-codec-DTG0Ftek.js.map +0 -1
- package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
- package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
- package/dist/wire-BKxH1vbP.js.map +0 -1
- package/docs/server/01-handlers.mdx +0 -227
|
@@ -5,7 +5,7 @@ description: "Each instance processes one message at a time. Slow work goes in c
|
|
|
5
5
|
|
|
6
6
|
## One message at a time
|
|
7
7
|
|
|
8
|
-
Every message runs on the instance's [lane](/server/
|
|
8
|
+
Every message runs on the instance's [lane](/server/consumers#lanes):
|
|
9
9
|
one handler at a time, in arrival order. A handler sees the state every
|
|
10
10
|
previous message produced, no other message can race its checks, and
|
|
11
11
|
its changes commit before the next message runs.
|
|
@@ -22,7 +22,7 @@ export const assistant = agent({
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
`agent()` is the whole server: the AI protocol, the agent's loop, and A2's
|
|
25
|
-
store, scheduler, and
|
|
25
|
+
store, scheduler, and consumers. `name` is its identity. The loop is the
|
|
26
26
|
agent's `harness`. By default it's `{ type: 'ai-sdk' }`: each response runs as
|
|
27
27
|
durable one-step `streamText()` generations, tuned with `harness.generation`
|
|
28
28
|
and `maxSteps`, listed under [`agent()`](/reference/ai#agentoptions). The same
|
package/docs/agents/02-tools.mdx
CHANGED
|
@@ -42,11 +42,11 @@ Approval policy lives in `harness.generation.toolApproval`, beside the other
|
|
|
42
42
|
per-step settings. Later examples that import `assistant` use the agent from
|
|
43
43
|
[Agents](/agents/introduction#define-the-agent).
|
|
44
44
|
|
|
45
|
-
### Read the
|
|
45
|
+
### Read the tool's context
|
|
46
46
|
|
|
47
47
|
```ts server/set-reminder.ts
|
|
48
48
|
import { tool } from 'ai'
|
|
49
|
-
import {
|
|
49
|
+
import { consumerContext, inputs } from 'experimental-a2/ai'
|
|
50
50
|
import { z } from 'zod'
|
|
51
51
|
|
|
52
52
|
export const setReminder = tool({
|
|
@@ -56,7 +56,7 @@ export const setReminder = tool({
|
|
|
56
56
|
reminder: z.string(),
|
|
57
57
|
}),
|
|
58
58
|
execute: async ({ delaySeconds, reminder }, { toolCallId }) => {
|
|
59
|
-
const ctx =
|
|
59
|
+
const ctx = consumerContext()
|
|
60
60
|
await ctx.session.schedule(
|
|
61
61
|
`reminder:${toolCallId}`,
|
|
62
62
|
{ delay: `${delaySeconds}s` },
|
|
@@ -72,11 +72,12 @@ export const setReminder = tool({
|
|
|
72
72
|
})
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
`
|
|
76
|
-
|
|
75
|
+
`consumerContext()` returns the running tool's
|
|
76
|
+
[consumer context](/server/consumers#the-context): `event`, `attempt`,
|
|
77
|
+
`session`, and `signal`. It works anywhere below `execute`, including
|
|
77
78
|
awaited helpers, and throws outside a tool execution. Pass a shared
|
|
78
79
|
`agentContract()` to type application events; A2 checks it against the
|
|
79
|
-
executing agent. See [`
|
|
80
|
+
executing agent. See [`consumerContext()`](/reference/ai#consumercontextcontract)
|
|
80
81
|
for the event payload.
|
|
81
82
|
|
|
82
83
|
This tool schedules a reminder as a new user message. `ctx.session` belongs to
|
|
@@ -86,12 +87,12 @@ Naming the timer after `toolCallId` and the message after the triggering
|
|
|
86
87
|
event's ID makes a retry converge on the same timer and message, even if a
|
|
87
88
|
provider reuses tool-call IDs. The six-day maximum is Vercel Queues'
|
|
88
89
|
[delay limit](/server/timers#provider-limits); adjust it for your scheduler. A
|
|
89
|
-
transient or ambiguous scheduler failure retries the
|
|
90
|
+
transient or ambiguous scheduler failure retries the tool's run with the same
|
|
90
91
|
identity and target time; a deterministic rejection or a missing scheduler is
|
|
91
92
|
an ordinary tool error.
|
|
92
93
|
|
|
93
|
-
To test a context-reading tool, call it inside `
|
|
94
|
-
from `experimental-a2/ai/server`; within `fn`, `
|
|
94
|
+
To test a context-reading tool, call it inside `runWithConsumerContext(ctx, fn)`
|
|
95
|
+
from `experimental-a2/ai/server`; within `fn`, `consumerContext()` returns `ctx`.
|
|
95
96
|
|
|
96
97
|
### Answer approvals
|
|
97
98
|
|
|
@@ -125,8 +126,8 @@ request as `ai.approval.requested`, and the reducer adds it to
|
|
|
125
126
|
`state.pendingApprovals`. `approve()` and `deny()` copy
|
|
126
127
|
the request, message, generation, and approval IDs from it, so a delayed click
|
|
127
128
|
cannot authorize a later model step. The decision updates the approval part in
|
|
128
|
-
the message, and
|
|
129
|
-
|
|
129
|
+
the message, and A2 then runs the approved tool or records the denied
|
|
130
|
+
result.
|
|
130
131
|
|
|
131
132
|
A policy can also be a function that returns `'user-approval'` or
|
|
132
133
|
`'approved'` from the validated input. It runs during the model step and again
|
|
@@ -50,12 +50,12 @@ Provider-executed tools, structured output, and forced tool choices skip
|
|
|
50
50
|
automatic compaction, and configuring it for them throws; use custom callbacks.
|
|
51
51
|
|
|
52
52
|
The first generation with each Gateway model in a session records
|
|
53
|
-
`ai.model.metadata.requested`;
|
|
54
|
-
records `ai.model.metadata.resolved`, without delaying or failing
|
|
55
|
-
generation. `state.modelMetadata` holds each model's limits and status:
|
|
53
|
+
`ai.model.metadata.requested`; A2's `a2.ai.metadata` consumer fetches the
|
|
54
|
+
catalog and records `ai.model.metadata.resolved`, without delaying or failing
|
|
55
|
+
the generation. `state.modelMetadata` holds each model's limits and status:
|
|
56
56
|
pending, resolved, or unavailable (absent from the catalog). A lookup that
|
|
57
|
-
exhausted its retries stays pending without a new lookup each turn, so
|
|
58
|
-
|
|
57
|
+
exhausted its retries stays pending without a new lookup each turn, so check
|
|
58
|
+
the session's dead letters for `a2.ai.metadata` or set a threshold. The estimate is serialized UTF-8
|
|
59
59
|
bytes divided by four, calibrated by the previous same-model call's usage. It is
|
|
60
60
|
not a tokenizer, makes no remote calls, and does not precisely measure new image
|
|
61
61
|
or audio tokens.
|
|
@@ -191,11 +191,13 @@ transcript. The session's state is `AIState` merged with yours, so a key that
|
|
|
191
191
|
shadows one of `AIState`'s is a type error. The reducer's `name` is its snapshot
|
|
192
192
|
identity: rename it when `fold` changes.
|
|
193
193
|
|
|
194
|
-
Serve it with `agent({ contract })
|
|
194
|
+
Serve it with `agent({ contract })`, and react to your own events with
|
|
195
|
+
`consumers`, next to the agent's. Every resolver receives your state as
|
|
195
196
|
`state`:
|
|
196
197
|
|
|
197
198
|
```ts server/support-desk.ts
|
|
198
199
|
import { agent } from 'experimental-a2/ai/server'
|
|
200
|
+
import { consumer } from 'experimental-a2/server'
|
|
199
201
|
import { supportDesk } from '../support-desk'
|
|
200
202
|
|
|
201
203
|
export const supportDeskAgent = agent({
|
|
@@ -204,12 +206,16 @@ export const supportDeskAgent = agent({
|
|
|
204
206
|
state.plan === 'pro'
|
|
205
207
|
? { model: 'openai/gpt-5.6-terra', reasoning: 'high' }
|
|
206
208
|
: { model: 'openai/gpt-5.6-luna', speed: 'fast' },
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
consumers: [
|
|
210
|
+
{
|
|
211
|
+
name: 'page-on-call',
|
|
212
|
+
types: ['ticket.flagged'],
|
|
213
|
+
run: async ({ event }) => {
|
|
214
|
+
// your side effect; event.id makes a stable idempotency key:
|
|
215
|
+
// await notifyOnCall(event.payload.ticketId, { idempotencyKey: event.id })
|
|
216
|
+
},
|
|
211
217
|
},
|
|
212
|
-
|
|
218
|
+
],
|
|
213
219
|
})
|
|
214
220
|
```
|
|
215
221
|
|
|
@@ -277,7 +283,7 @@ export const POST = handle
|
|
|
277
283
|
Server appends, such as
|
|
278
284
|
`supportDeskAgent.session(id).append({ type: 'plan.changed', payload })`, are
|
|
279
285
|
trusted and skip `authorize`. `push()` also accepts any `inputs` builder, and
|
|
280
|
-
`
|
|
286
|
+
`consumerContext(supportDesk)` types application events inside tools.
|
|
281
287
|
|
|
282
288
|
The contract-free client, `createAgentClient()` or `useAgent({ api | client })`
|
|
283
289
|
with a type-only import, folds only the built-in reducer. It accepts
|
|
@@ -285,10 +291,10 @@ with a type-only import, folds only the built-in reducer. It accepts
|
|
|
285
291
|
no `reducer`; the types enforce this through `AI_STATE_REDUCER`.
|
|
286
292
|
|
|
287
293
|
When the agent belongs inside something larger, such as a shared chat room,
|
|
288
|
-
keep two sessions.
|
|
289
|
-
to the agent's session with `inputs.message()`. Event IDs are unique across
|
|
290
|
-
store's sessions, so derive the message ID from the room event's ID with
|
|
291
|
-
`idempotentId()`; a retried
|
|
294
|
+
keep two sessions. A consumer of the room appends each message the agent should
|
|
295
|
+
see to the agent's session with `inputs.message()`. Event IDs are unique across
|
|
296
|
+
a store's sessions, so derive the message ID from the room event's ID with
|
|
297
|
+
`idempotentId()`; a retried run then converges on the same message. A tool
|
|
292
298
|
the agent calls to speak appends to the room with an `idempotentId()` of its
|
|
293
299
|
`toolCallId`. Neither fold reads the other's state.
|
|
294
300
|
|
|
@@ -369,8 +375,12 @@ completed input.
|
|
|
369
375
|
|
|
370
376
|
## Delivery semantics
|
|
371
377
|
|
|
372
|
-
|
|
373
|
-
|
|
378
|
+
The agent runs on four of A2's own at-least-once
|
|
379
|
+
[consumers](/server/consumers): `a2.ai.control` applies commands and work
|
|
380
|
+
reports one at a time, `a2.ai.model` runs model steps on one lane, `a2.ai.tools` runs local
|
|
381
|
+
tools, and `a2.ai.metadata` looks up model limits. A dead letter names the one
|
|
382
|
+
that failed. A successful completion commits atomically with its terminal
|
|
383
|
+
events, and deterministic IDs
|
|
374
384
|
make scheduling and lifecycle appends effectively once. An incomplete model
|
|
375
385
|
attempt is marked `superseded` before its replacement starts. An expired claim
|
|
376
386
|
or superseded attempt keeps its abort reason through model, resolver,
|
|
@@ -379,7 +389,7 @@ attempt; an explicit interruption stays terminal. A retried tool can stream
|
|
|
379
389
|
different preliminary output under the same tool-call ID.
|
|
380
390
|
|
|
381
391
|
Provider calls and external side effects sit outside that transaction, so
|
|
382
|
-
recovery may repeat a call whose effect landed before its
|
|
392
|
+
recovery may repeat a call whose effect landed before its run committed.
|
|
383
393
|
Give side-effecting tools their own idempotency, normally keyed by the AI SDK
|
|
384
394
|
`toolCallId`. A fast tool can also start before its model step fails; the
|
|
385
395
|
failure blocks the continuation but cannot undo the effect.
|
|
@@ -530,7 +540,7 @@ application, with `commandId` (the submitted event's ID) and an `outcome` of
|
|
|
530
540
|
in flight, each action resolves to its persisted command events, whose `id` is
|
|
531
541
|
the receipt's `commandId`, or to an empty array when there is nothing to
|
|
532
542
|
address, such as Stop with no active turn. A rejection is a normal result and
|
|
533
|
-
spends no
|
|
543
|
+
spends no retry.
|
|
534
544
|
|
|
535
545
|
### Pause and resume
|
|
536
546
|
|
|
@@ -31,7 +31,7 @@ everything it can call:
|
|
|
31
31
|
import { tool } from 'ai'
|
|
32
32
|
import { Sandbox } from '@vercel/sandbox'
|
|
33
33
|
import { z } from 'zod'
|
|
34
|
-
import {
|
|
34
|
+
import { consumerContext } from 'experimental-a2/ai'
|
|
35
35
|
import { agent } from 'experimental-a2/ai/server'
|
|
36
36
|
import { fx } from 'experimental-a2/ai/fx'
|
|
37
37
|
|
|
@@ -45,7 +45,7 @@ export const coder = agent({
|
|
|
45
45
|
description: 'Run a shell command in the repository.',
|
|
46
46
|
inputSchema: z.object({ command: z.string() }),
|
|
47
47
|
execute: async ({ command }) => {
|
|
48
|
-
const { session, signal } =
|
|
48
|
+
const { session, signal } = consumerContext()
|
|
49
49
|
// One Vercel Sandbox per session, created on first use.
|
|
50
50
|
const sandbox = await Sandbox.getOrCreate({
|
|
51
51
|
name: session.id,
|
|
@@ -125,7 +125,8 @@ turn, so a process keeps the images it fetched instead of fetching them again.
|
|
|
125
125
|
|
|
126
126
|
A file that isn't an image, or that A2 can't fetch, fails the turn with an
|
|
127
127
|
error that names it. So does a steer with files under FX, which steers with
|
|
128
|
-
text only.
|
|
128
|
+
text only. A steer that joins the turn's prompt, as described below, sends its
|
|
129
|
+
files with the prompt. The harness applies its own rules to the images it gets: FX fails a
|
|
129
130
|
turn whose model doesn't take images, and Pi resizes them. Every step sends the
|
|
130
131
|
conversation so far, files and all, so a file that fails one turn fails the
|
|
131
132
|
turns after it too.
|
|
@@ -169,6 +170,9 @@ sets `needsApproval` is rejected. It hands the engine only tools A2 runs, so a
|
|
|
169
170
|
provider-defined tool, such as a provider's web search, is rejected too. `maxSteps` limits the model calls in one
|
|
170
171
|
response under every harness. Steering enters at the next step, as with any
|
|
171
172
|
agent: the harness sees the steered message before its next model call.
|
|
173
|
+
Steering that hands off before any of the turn's model calls finished, as after
|
|
174
|
+
a pause during the first one, joins the turn's prompt instead, so the turn's
|
|
175
|
+
first model call sees it.
|
|
172
176
|
|
|
173
177
|
## Recovery
|
|
174
178
|
|
|
@@ -13,7 +13,7 @@ replay value, and a lost update is repainted by the next one. The test:
|
|
|
13
13
|
> passes? Yes: event, in the log. No: presence.
|
|
14
14
|
|
|
15
15
|
Messages, document updates, cancellations: events. Cursors, selections,
|
|
16
|
-
typing, "who's online", a
|
|
16
|
+
typing, "who's online", a consumer's live progress: presence.
|
|
17
17
|
|
|
18
18
|
Presence rides the session you already have: same contract, route, auth, and
|
|
19
19
|
stream. A2 replicates each participant's latest field values to every
|
|
@@ -167,30 +167,34 @@ render-time rule:
|
|
|
167
167
|
The durable event at index 214 beats any presence stamped 213 or lower. A2
|
|
168
168
|
carries the stamp; the comparison is yours, in view code.
|
|
169
169
|
|
|
170
|
-
##
|
|
170
|
+
## Consumers broadcast too
|
|
171
171
|
|
|
172
|
-
`ctx.session.setPresence` is the same operation, so a
|
|
172
|
+
`ctx.session.setPresence` is the same operation, so a consumer can publish live
|
|
173
173
|
progress without writing non-facts to the log:
|
|
174
174
|
|
|
175
175
|
```ts server/canvas.ts
|
|
176
|
-
import { createServer } from 'experimental-a2/server'
|
|
176
|
+
import { consumer, createServer } from 'experimental-a2/server'
|
|
177
177
|
import { canvas } from '@/contracts'
|
|
178
178
|
|
|
179
|
+
const thumbnails = consumer({
|
|
180
|
+
contract: canvas,
|
|
181
|
+
name: 'thumbnails',
|
|
182
|
+
types: ['shape.created'],
|
|
183
|
+
}).run(async (ctx) => {
|
|
184
|
+
// long work; broadcast progress as it goes:
|
|
185
|
+
// await ctx.session.setPresence({
|
|
186
|
+
// participant: 'system:thumbnails',
|
|
187
|
+
// values: { progress: { step: 2, of: 5 } },
|
|
188
|
+
// })
|
|
189
|
+
})
|
|
190
|
+
|
|
179
191
|
export const canvasServer = createServer({
|
|
180
192
|
contract: canvas,
|
|
181
|
-
|
|
182
|
-
'shape.created': async (ctx) => {
|
|
183
|
-
// long work; broadcast progress as it goes:
|
|
184
|
-
// await ctx.session.setPresence({
|
|
185
|
-
// participant: 'system:thumbnails',
|
|
186
|
-
// values: { progress: { step: 2, of: 5 } },
|
|
187
|
-
// })
|
|
188
|
-
},
|
|
189
|
-
},
|
|
193
|
+
consumers: [thumbnails],
|
|
190
194
|
})
|
|
191
195
|
```
|
|
192
196
|
|
|
193
|
-
The participant id is the
|
|
197
|
+
The participant id is the consumer's choice; a `system:` prefix keeps robots
|
|
194
198
|
apart from humans in the map. When the work completes, append the outcome as
|
|
195
199
|
an event: the progress was never a fact, the result is.
|
|
196
200
|
|
|
@@ -33,7 +33,7 @@ schema for its payload. These docs use Zod, but any
|
|
|
33
33
|
your own.
|
|
34
34
|
|
|
35
35
|
The contract is a plain, importable, **isomorphic** value. The server
|
|
36
|
-
implements it with [
|
|
36
|
+
implements it with [consumers](/server/consumers), [reducers](/contract/state)
|
|
37
37
|
derive the current view from it, and the browser types its pushes off it.
|
|
38
38
|
|
|
39
39
|
## Validation
|
|
@@ -52,6 +52,6 @@ typing, live progress) that is replicated but never stored in the log. See
|
|
|
52
52
|
:::tip
|
|
53
53
|
Events are facts, so name them in past tense: `created`, `expired`,
|
|
54
54
|
`shop.notified`. A name that reads like a command (`notifyShop`) describes
|
|
55
|
-
what a
|
|
55
|
+
what a consumer should do, not what happened. Dots are a naming convention, not
|
|
56
56
|
a namespace.
|
|
57
57
|
:::
|
|
@@ -11,8 +11,8 @@ const events = await ordersServer.session(orderId).history()
|
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Every event in the session, oldest first: always the raw log, never a summary
|
|
14
|
-
or snapshot. Use it for audits, debugging, and
|
|
15
|
-
shop already start?" Pass inclusive `gte` or `lte` indexes to read a slice.
|
|
14
|
+
or snapshot. Use it for audits, debugging, and consumer questions like "did
|
|
15
|
+
the shop already start?" Pass inclusive `gte` or `lte` indexes to read a slice.
|
|
16
16
|
|
|
17
17
|
## Reducers
|
|
18
18
|
|
|
@@ -72,7 +72,7 @@ server-only app can keep the contract next to `createServer` instead.
|
|
|
72
72
|
|
|
73
73
|
## Read boundaries
|
|
74
74
|
|
|
75
|
-
A root session reads the latest committed frontier. A
|
|
75
|
+
A root session reads the latest committed frontier. A consumer's session stops
|
|
76
76
|
at its triggering event by default, stable across concurrent appends and
|
|
77
77
|
retries:
|
|
78
78
|
|
|
@@ -83,23 +83,27 @@ import { ordersReducer } from './reducer'
|
|
|
83
83
|
|
|
84
84
|
export const ordersServer = createServer({
|
|
85
85
|
contract: orders,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
consumers: [
|
|
87
|
+
{
|
|
88
|
+
name: 'read-status',
|
|
89
|
+
types: ['created'],
|
|
90
|
+
run: async ({ event, session }) => {
|
|
91
|
+
const atTrigger = await session.state(ordersReducer)
|
|
92
|
+
// atTrigger.index === event.index
|
|
93
|
+
|
|
94
|
+
const latest = await session.state(ordersReducer, { through: 'latest' })
|
|
95
|
+
// latest also folds events appended after this one
|
|
96
|
+
},
|
|
93
97
|
},
|
|
94
|
-
|
|
98
|
+
],
|
|
95
99
|
})
|
|
96
100
|
```
|
|
97
101
|
|
|
98
102
|
Use `{ through: 'latest' }` for joins or other logic that intentionally
|
|
99
103
|
observes later events. A numeric `{ through: index }` reads any inclusive log
|
|
100
104
|
boundary; `through: 0` returns `initialState`. `state()` is observational: it
|
|
101
|
-
never runs
|
|
102
|
-
history, not
|
|
105
|
+
never runs consumers or waits for pending work, and its index marks committed
|
|
106
|
+
history, not consumer progress.
|
|
103
107
|
|
|
104
108
|
A latest read and a following `ctx.session.append(name, ...events)` are
|
|
105
109
|
separate operations, and concurrent appends and retries can move the frontier
|
|
@@ -125,7 +129,7 @@ telemetry span (or `console.error` by default) without snapshot contents, and
|
|
|
125
129
|
the next read folds the missing tail again.
|
|
126
130
|
|
|
127
131
|
Each session and reducer has one latest snapshot. Older ones exist only while
|
|
128
|
-
|
|
132
|
+
consumer runs that read at their boundary are unfinished (not yet completed
|
|
129
133
|
or dead-lettered). This is automatic: there is no TTL, history limit, or
|
|
130
134
|
retention setting.
|
|
131
135
|
|
|
@@ -4,7 +4,7 @@ description: "A session is one instance of a contract, with its own ordered log,
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
```ts
|
|
7
|
-
// anywhere on the server: a route, a
|
|
7
|
+
// anywhere on the server: a route, a consumer, a script
|
|
8
8
|
const order = ordersServer.session('order-42')
|
|
9
9
|
```
|
|
10
10
|
|
|
@@ -19,7 +19,7 @@ Put events in one session when they form one history, fold, recovery unit, or
|
|
|
19
19
|
live view. Split what should be read and recovered independently.
|
|
20
20
|
|
|
21
21
|
A session is not a table or a general query boundary: one `orders` contract can
|
|
22
|
-
serve millions of order sessions through the same server and
|
|
22
|
+
serve millions of order sessions through the same server and consumers.
|
|
23
23
|
Collections, joins, search, and constraints across many records belong in
|
|
24
24
|
[your database](#sessions-and-your-database).
|
|
25
25
|
|
|
@@ -48,7 +48,7 @@ same atomic step that appends the marker; if another event landed first, the
|
|
|
48
48
|
clear skips and returns `{ cleared: false, index }` with the current head. A
|
|
49
49
|
conditional clear is safe to retry.
|
|
50
50
|
|
|
51
|
-
The old lifetime's pending events are never dispatched, and a
|
|
51
|
+
The old lifetime's pending events are never dispatched, and a consumer still
|
|
52
52
|
running for one can't append, return events, or read the new lifetime's state.
|
|
53
53
|
Clearing can't stop code that is already running or undo its side effects.
|
|
54
54
|
|
|
@@ -70,7 +70,7 @@ authorize your own route before calling it.
|
|
|
70
70
|
|
|
71
71
|
### Scheduling a clear
|
|
72
72
|
|
|
73
|
-
With a [scheduler](/schedulers/overview) configured, a
|
|
73
|
+
With a [scheduler](/schedulers/overview) configured, a consumer can schedule a
|
|
74
74
|
clear. This one clears an idle cart a day after its last activity:
|
|
75
75
|
|
|
76
76
|
```ts server/carts.ts
|
|
@@ -89,20 +89,24 @@ export const carts = a2.contract({
|
|
|
89
89
|
export const cartsServer = createServer({
|
|
90
90
|
contract: carts,
|
|
91
91
|
scheduler: vercelQueues(),
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
consumers: [
|
|
93
|
+
{
|
|
94
|
+
name: 'clear-idle',
|
|
95
|
+
types: ['item.added'],
|
|
96
|
+
run: async ({ event, session }) => {
|
|
97
|
+
await session.schedule.clear('idle', {
|
|
98
|
+
delay: '1d',
|
|
99
|
+
ifLastIndex: event.index,
|
|
100
|
+
})
|
|
101
|
+
},
|
|
98
102
|
},
|
|
99
|
-
|
|
103
|
+
],
|
|
100
104
|
})
|
|
101
105
|
```
|
|
102
106
|
|
|
103
107
|
The delay counts from the event's stored time. A later event moves the head
|
|
104
|
-
past `event.index`, so this delivery skips and that event's
|
|
105
|
-
|
|
108
|
+
past `event.index`, so this delivery skips and that event's run schedules the
|
|
109
|
+
next check; only events the consumer takes extend the policy. A consumer's own
|
|
106
110
|
writes move the head too, so one that appends should pass the index its last
|
|
107
111
|
append returned.
|
|
108
112
|
|
|
@@ -148,27 +152,30 @@ attached to it:
|
|
|
148
152
|
The investigation's session refers to the issue id without becoming a second
|
|
149
153
|
owner of the issue. A2 can own individual issues when their history and live
|
|
150
154
|
state are the center of the product, but boards and search then span many
|
|
151
|
-
sessions and need another query model.
|
|
152
|
-
session
|
|
153
|
-
client
|
|
155
|
+
sessions and need another query model. A whole workspace can live in one
|
|
156
|
+
session when its pages read from [your tables](/server/sql-projections) instead
|
|
157
|
+
of folding the log, but every client following that session's live stream
|
|
158
|
+
receives every event, so weigh that traffic before many people share one.
|
|
154
159
|
|
|
155
|
-
### Database writes from
|
|
160
|
+
### Database writes from consumers
|
|
156
161
|
|
|
157
|
-
|
|
162
|
+
Consumers may call your database like any other side effect. They are
|
|
158
163
|
at-least-once, so make the write idempotent with `event.id`; it runs after the
|
|
159
164
|
event is durable and may be retried. That is not one atomic write across A2
|
|
160
165
|
and your tables: if a relational change must commit before the request
|
|
161
166
|
succeeds, make it through the database's write path.
|
|
162
167
|
|
|
163
|
-
###
|
|
168
|
+
### Tables kept from the logs
|
|
164
169
|
|
|
165
|
-
A
|
|
166
|
-
list of active imports.
|
|
170
|
+
A consumer can keep a table in your database from A2-owned sessions, such as a
|
|
171
|
+
list of active imports. Committing its position in the same transaction as the
|
|
172
|
+
rows applies each event exactly once; [SQL projections](/server/sql-projections)
|
|
173
|
+
shows how. The table:
|
|
167
174
|
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
|
|
175
|
+
- is eventually consistent with the session logs;
|
|
176
|
+
- has each session's events in order, not an order across sessions;
|
|
177
|
+
- fills from history with `backfill`, one session at a time, as each next
|
|
178
|
+
runs: nothing enumerates sessions, so to fill it for idle ones, consume each.
|
|
172
179
|
|
|
173
180
|
Back up and migrate it like application data. If search, joins, and
|
|
174
181
|
cross-session reporting are central, the database should probably own that
|
package/docs/index.mdx
CHANGED
|
@@ -42,8 +42,8 @@ export const orders = a2.contract({
|
|
|
42
42
|
|
|
43
43
|
## Implement the server
|
|
44
44
|
|
|
45
|
-
Bind the contract to a store, a scheduler, and the
|
|
46
|
-
events.
|
|
45
|
+
Bind the contract to a store, a scheduler, and the consumers that react to
|
|
46
|
+
its events.
|
|
47
47
|
|
|
48
48
|
```ts server/orders.ts
|
|
49
49
|
import { createServer } from 'experimental-a2/server'
|
|
@@ -55,14 +55,18 @@ export const ordersServer = createServer({
|
|
|
55
55
|
contract: orders,
|
|
56
56
|
store: redis({ url: process.env.REDIS_URL }),
|
|
57
57
|
scheduler: vercelQueues(),
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
consumers: [
|
|
59
|
+
{
|
|
60
|
+
name: 'notify-shop',
|
|
61
|
+
types: ['order.created'],
|
|
62
|
+
run: async ({ event }) => {
|
|
63
|
+
// your side effect; event.id makes a stable idempotency key:
|
|
64
|
+
// await sendEmailToShop(event.payload, { idempotencyKey: event.id })
|
|
65
|
+
const { shopId } = event.payload
|
|
66
|
+
return { type: 'shop.notified', payload: { shopId } }
|
|
67
|
+
},
|
|
64
68
|
},
|
|
65
|
-
|
|
69
|
+
],
|
|
66
70
|
})
|
|
67
71
|
```
|
|
68
72
|
|
|
@@ -130,18 +134,18 @@ from the log.
|
|
|
130
134
|
|
|
131
135
|
- Appends are effectively-once.
|
|
132
136
|
- The log stays ordered.
|
|
133
|
-
-
|
|
134
|
-
- Returned events commit with the
|
|
135
|
-
-
|
|
137
|
+
- Consumers are at-least-once.
|
|
138
|
+
- Returned events commit with the consumer's completion.
|
|
139
|
+
- Consumers run concurrently, unless they share a lane.
|
|
136
140
|
- Failures stay contained.
|
|
137
|
-
- A failing
|
|
141
|
+
- A failing consumer never makes `append` throw.
|
|
138
142
|
|
|
139
143
|
[Guarantees](/server/guarantees) has the details and the limits.
|
|
140
144
|
|
|
141
145
|
## Go further...
|
|
142
146
|
|
|
143
147
|
<CardGroup cols={2}>
|
|
144
|
-
<Card title="
|
|
148
|
+
<Card title="Consumers" href="/server/consumers" icon="zap">
|
|
145
149
|
Retries, lanes, and idempotency.
|
|
146
150
|
</Card>
|
|
147
151
|
<Card title="State" href="/contract/state" icon="layers">
|
|
@@ -164,14 +168,14 @@ from the log.
|
|
|
164
168
|
## FAQ
|
|
165
169
|
|
|
166
170
|
<details>
|
|
167
|
-
<summary>Can I use A2 without
|
|
171
|
+
<summary>Can I use A2 without consumers?</summary>
|
|
168
172
|
|
|
169
|
-
Yes. Omit `
|
|
173
|
+
Yes. Omit `consumers` and use `append`, `history`, reducers, and live streams as
|
|
170
174
|
a durable event log. Events settle in their append transaction, with no drain,
|
|
171
|
-
claim, or queue message. A contract can also mix
|
|
172
|
-
types. A2 decides per event type.
|
|
175
|
+
claim, or queue message. A contract can also mix consumed and history-only
|
|
176
|
+
event types. A2 decides per event type.
|
|
173
177
|
|
|
174
|
-
See [
|
|
178
|
+
See [Servers without consumers](/server/consumers#servers-without-consumers).
|
|
175
179
|
|
|
176
180
|
</details>
|
|
177
181
|
|
|
@@ -188,18 +192,19 @@ event ids stay stable across retries. See [Timers](/server/timers).
|
|
|
188
192
|
<details>
|
|
189
193
|
<summary>How do I cancel work that's already running?</summary>
|
|
190
194
|
|
|
191
|
-
Cancellation is an event too: same route, same auth, same log.
|
|
195
|
+
Cancellation is an event too: same route, same auth, same log. Consumers
|
|
192
196
|
opt in with `abortOn` and receive a live `AbortSignal`; reducers cut the
|
|
193
197
|
derived view exactly. See [Cancellation](/server/cancellation).
|
|
194
198
|
|
|
195
199
|
</details>
|
|
196
200
|
|
|
197
201
|
<details>
|
|
198
|
-
<summary>What happens when a
|
|
202
|
+
<summary>What happens when a consumer keeps failing?</summary>
|
|
199
203
|
|
|
200
204
|
It retries with backoff, then dead-letters after ten failed attempts, whether
|
|
201
|
-
the
|
|
202
|
-
until you retry or skip it; unlaned events and other
|
|
205
|
+
the consumer threw or its process ended. Later events in the same lane wait
|
|
206
|
+
until you retry or skip it; unlaned events, other lanes, and other consumers
|
|
207
|
+
continue.
|
|
203
208
|
[Guarantees](/server/guarantees) has the exact guarantees and the
|
|
204
209
|
recovery story behind them.
|
|
205
210
|
|
|
@@ -209,7 +214,7 @@ recovery story behind them.
|
|
|
209
214
|
<summary>What does production need?</summary>
|
|
210
215
|
|
|
211
216
|
A durable store backend, such as Postgres or Redis. Add a queue-backed scheduler
|
|
212
|
-
when the
|
|
217
|
+
when the server has consumers or calls `session.schedule`. A history-only
|
|
213
218
|
contract with no timers needs no reaction infrastructure. Development needs
|
|
214
219
|
neither: SQLite appears under `.a2/` and state survives restarts. See
|
|
215
220
|
[Stores](/stores/overview) and [Schedulers](/schedulers/overview).
|
|
@@ -11,7 +11,7 @@ description: "Every entry point, and the isomorphic `experimental-a2` core: cont
|
|
|
11
11
|
| `experimental-a2/server` | `createServer`: implement a contract | none |
|
|
12
12
|
| `experimental-a2/client` | `createClient`: framework-agnostic session client | none |
|
|
13
13
|
| `experimental-a2/react` | `createReact` | `react` |
|
|
14
|
-
| `experimental-a2/ai` | `agentContract`, `
|
|
14
|
+
| `experimental-a2/ai` | `agentContract`, `consumerContext`, `inputs`, and the AI reducer: isomorphic | `ai` |
|
|
15
15
|
| `experimental-a2/ai/server` | `agent`: an agent's loop as an A2 server | `ai` |
|
|
16
16
|
| `experimental-a2/ai/fx` | `fx`: the FX harness for `agent()` | `ai`, `libfx` |
|
|
17
17
|
| `experimental-a2/ai/pi` | `pi`: the Pi harness for `agent()` | `ai`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-ai` |
|
|
@@ -63,8 +63,8 @@ ones are rejected at definition time. The validated output is what's stored.
|
|
|
63
63
|
### `contract.batch(...events)`
|
|
64
64
|
|
|
65
65
|
Returns the same events as a readonly tuple, preserving each literal type in a
|
|
66
|
-
heterogeneous
|
|
67
|
-
completes. See [
|
|
66
|
+
heterogeneous array a consumer returns. A2 validates them when the consumer
|
|
67
|
+
completes. See [Consumers](/server/consumers#return-what-happens-after-success).
|
|
68
68
|
|
|
69
69
|
### `reducer(options)`
|
|
70
70
|
|
|
@@ -120,7 +120,7 @@ export const id = await idempotentId('orders', 'shop-7', 'request-42')
|
|
|
120
120
|
|
|
121
121
|
An `Event` whose `type` is `'a2.cleared'` and whose `payload` is `null`: the
|
|
122
122
|
marker a [clear](/reference/server#sessionclearoptions) leaves in
|
|
123
|
-
`session.stream()`. It never reaches
|
|
123
|
+
`session.stream()`. It never reaches consumers or `history()`. Comparing
|
|
124
124
|
`event.type` with that string narrows a stream item to `SessionCleared`. The
|
|
125
125
|
session client handles the marker itself, so it matters only when you read the
|
|
126
126
|
server stream yourself.
|