experimental-a2 0.16.1 → 0.18.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 +373 -0
- package/README.md +1 -1
- package/dist/{actor-ohPC-81x.d.ts → actor-Bjce9_5U.d.ts} +21 -11
- package/dist/actor-Bjce9_5U.d.ts.map +1 -0
- package/dist/actor-client.d.ts +2 -5
- package/dist/actor-client.d.ts.map +1 -1
- package/dist/actor-client.js +13 -4
- package/dist/actor-client.js.map +1 -1
- package/dist/actor-react.d.ts +5 -3
- package/dist/actor-react.d.ts.map +1 -1
- package/dist/actor-react.js +20 -10
- package/dist/actor-react.js.map +1 -1
- package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
- package/dist/actor-shared-t7w8xIKP.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +42 -23
- package/dist/actor.js.map +1 -1
- package/dist/{ai-DgOBltJ_.js → ai-BpOGm-zB.js} +621 -216
- package/dist/ai-BpOGm-zB.js.map +1 -0
- package/dist/{ai-CrEf6p_W.d.ts → ai-DrUaMyRt.d.ts} +114 -100
- package/dist/ai-DrUaMyRt.d.ts.map +1 -0
- package/dist/ai-client.d.ts +27 -0
- package/dist/ai-client.d.ts.map +1 -0
- package/dist/ai-client.js +29 -0
- package/dist/ai-client.js.map +1 -0
- package/dist/ai-fx.browser.d.ts +1 -0
- package/dist/ai-fx.browser.js +6 -0
- package/dist/ai-fx.browser.js.map +1 -0
- package/dist/ai-fx.d.ts +10 -0
- package/dist/ai-fx.d.ts.map +1 -0
- package/dist/ai-fx.js +634 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-BuLh_EYi.js +273 -0
- package/dist/ai-harness-BuLh_EYi.js.map +1 -0
- package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
- package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
- package/dist/ai-pi.browser.d.ts +1 -0
- package/dist/ai-pi.browser.js +6 -0
- package/dist/ai-pi.browser.js.map +1 -0
- package/dist/ai-pi.d.ts +17 -0
- package/dist/ai-pi.d.ts.map +1 -0
- package/dist/ai-pi.js +904 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +96 -0
- package/dist/ai-react.d.ts.map +1 -0
- package/dist/ai-react.js +142 -0
- package/dist/ai-react.js.map +1 -0
- package/dist/ai-server.browser.js +2 -2
- package/dist/ai-server.browser.js.map +1 -1
- package/dist/ai-server.d.ts +109 -43
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +526 -283
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-BYzHjkwU.js → client-BReKqwZC.js} +447 -80
- package/dist/client-BReKqwZC.js.map +1 -0
- package/dist/{client-CzyacQpJ.d.ts → client-h_DKFAjj.d.ts} +14 -13
- package/dist/client-h_DKFAjj.d.ts.map +1 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.js +1 -1
- package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
- package/dist/contract-BYKASlCV.js.map +1 -0
- package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
- package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +7 -3
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +1 -0
- package/dist/{internal-Dq2qYxou.js → internal-B2T3U4Rq.js} +29 -2
- package/dist/internal-B2T3U4Rq.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/react-BBXgHWFc.d.ts +86 -0
- package/dist/react-BBXgHWFc.d.ts.map +1 -0
- package/dist/react.d.ts +2 -122
- package/dist/react.js +107 -100
- package/dist/react.js.map +1 -1
- package/dist/reducer-BAXc8JyF.js +87 -0
- package/dist/reducer-BAXc8JyF.js.map +1 -0
- package/dist/{reducer-otzHjuJj.d.ts → reducer-CqA59Q6U.d.ts} +46 -30
- package/dist/reducer-CqA59Q6U.d.ts.map +1 -0
- package/dist/reducer-runtime-DQwz_J59.js +28 -0
- package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +33 -10
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -9
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +25 -44
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-BD5ckxZb.js → server-Dw_k_7kO.js} +904 -219
- package/dist/server-Dw_k_7kO.js.map +1 -0
- package/dist/{server-DbD7IJVK.d.ts → server-En7cbi3v.d.ts} +116 -26
- package/dist/server-En7cbi3v.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/session-clear-Bo-XTFEy.d.ts +12 -0
- package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
- package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
- package/dist/session-clear-DGI4jTQ5.js.map +1 -0
- package/dist/{store-DyZM6fS5.d.ts → store-Djdv0bnZ.d.ts} +73 -3
- package/dist/store-Djdv0bnZ.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +205 -66
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-CmxUbV93.js → store-polling-B5FcwJfo.js} +21 -4
- package/dist/store-polling-B5FcwJfo.js.map +1 -0
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +450 -108
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-BAwS8RWn.js} +3 -3
- package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.d.ts.map +1 -1
- package/dist/store-redis-http.js +293 -11
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis-notify-BRITti_o.js +2493 -0
- package/dist/store-redis-notify-BRITti_o.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +11 -11
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +197 -33
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-Cx7AAPGq.js +23 -0
- package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
- package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-BO5wWCb1.js → wire-rSUglYo8.js} +132 -10
- package/dist/wire-rSUglYo8.js.map +1 -0
- package/docs/01-quickstart.mdx +50 -84
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -82
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -137
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +257 -0
- package/docs/agents/02-tools.mdx +247 -0
- package/docs/agents/03-conversations.mdx +538 -0
- package/docs/agents/04-harnesses.mdx +136 -0
- package/docs/agents/meta.ts +1 -0
- package/docs/client/01-react.mdx +294 -0
- package/docs/client/02-transports.mdx +86 -0
- package/docs/client/03-presence.mdx +224 -0
- package/docs/client/meta.ts +1 -0
- package/docs/contract/01-events.mdx +57 -0
- package/docs/contract/02-state.mdx +142 -0
- package/docs/contract/03-sessions.mdx +175 -0
- package/docs/index.mdx +112 -156
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +855 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +722 -0
- package/docs/reference/05-adapters.mdx +351 -0
- package/docs/reference/06-errors.mdx +138 -0
- package/docs/reference/meta.ts +1 -1
- package/docs/server/01-handlers.mdx +200 -0
- package/docs/server/02-timers.mdx +130 -0
- package/docs/server/03-cancellation.mdx +141 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +322 -0
- package/docs/server/06-guarantees.mdx +123 -0
- package/examples/playground/DEPLOY.md +53 -7
- package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
- package/examples/playground/app/a0/[appId]/page.tsx +53 -0
- package/examples/playground/app/a0/a0.css +818 -0
- package/examples/playground/app/a0/app-list.tsx +218 -0
- package/examples/playground/app/a0/app-view.tsx +63 -0
- package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
- package/examples/playground/app/a0/apps/[appId]/route.ts +28 -0
- package/examples/playground/app/a0/apps.test.ts +128 -0
- package/examples/playground/app/a0/apps.ts +102 -0
- package/examples/playground/app/a0/chat.tsx +410 -0
- package/examples/playground/app/a0/columns.tsx +109 -0
- package/examples/playground/app/a0/context-menu.tsx +116 -0
- package/examples/playground/app/a0/cookies.ts +46 -0
- package/examples/playground/app/a0/crash-on-request.ts +72 -0
- package/examples/playground/app/a0/crashes/route.ts +3 -0
- package/examples/playground/app/a0/crashes.test.ts +33 -0
- package/examples/playground/app/a0/crashes.ts +28 -0
- package/examples/playground/app/a0/devtools.tsx +164 -0
- package/examples/playground/app/a0/events/route.ts +6 -0
- package/examples/playground/app/a0/flags.ts +30 -0
- package/examples/playground/app/a0/fx/events/route.ts +9 -0
- package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
- package/examples/playground/app/a0/layout.tsx +38 -0
- package/examples/playground/app/a0/list/route.ts +10 -0
- package/examples/playground/app/a0/model.test.ts +73 -0
- package/examples/playground/app/a0/model.ts +95 -0
- package/examples/playground/app/a0/no-photon.cjs +4 -0
- package/examples/playground/app/a0/no-quickjs.cjs +3 -0
- package/examples/playground/app/a0/page.tsx +14 -0
- package/examples/playground/app/a0/pi/events/route.ts +9 -0
- package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
- package/examples/playground/app/a0/preview-pane.tsx +145 -0
- package/examples/playground/app/a0/preview.test.ts +53 -0
- package/examples/playground/app/a0/preview.ts +26 -0
- package/examples/playground/app/a0/sandbox-log.ts +75 -0
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/sandbox.ts +191 -0
- package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
- package/examples/playground/app/a0/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/server.ts +213 -0
- package/examples/playground/app/a0/session.ts +59 -0
- package/examples/playground/app/a0/shell.tsx +158 -0
- package/examples/playground/app/a0/short/events/route.ts +9 -0
- package/examples/playground/app/a0/splitter.tsx +120 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
- package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
- package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
- package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
- package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
- package/examples/playground/app/agent/events/route.ts +5 -2
- package/examples/playground/app/agent/ingress.test.ts +101 -0
- package/examples/playground/app/agent/model-input.test.tsx +58 -0
- package/examples/playground/app/agent/model-input.tsx +115 -0
- package/examples/playground/app/agent/model.test.ts +73 -0
- package/examples/playground/app/agent/model.ts +58 -26
- package/examples/playground/app/agent/new-agent-session.tsx +37 -19
- package/examples/playground/app/agent/page.tsx +4 -4
- package/examples/playground/app/agent/server.ts +19 -15
- package/examples/playground/app/agent/session.ts +5 -6
- package/examples/playground/app/agent/telemetry.test.ts +189 -0
- package/examples/playground/app/agent/telemetry.ts +30 -0
- package/examples/playground/app/agent/trace.test.ts +23 -0
- package/examples/playground/app/agent/trace.ts +18 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +12 -12
- package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
- package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
- package/examples/playground/app/canvas/model.ts +53 -53
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
- package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
- package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
- package/examples/playground/app/chat/[chatId]/session.ts +2 -19
- package/examples/playground/app/chat/agent/route.ts +9 -0
- package/examples/playground/app/chat/model.test.ts +46 -119
- package/examples/playground/app/chat/model.ts +82 -128
- package/examples/playground/app/chat/scheduler/route.ts +2 -2
- package/examples/playground/app/chat/server.test.ts +112 -0
- package/examples/playground/app/chat/server.ts +113 -117
- package/examples/playground/app/components/activity-feed.tsx +30 -1
- package/examples/playground/app/components/activity-history.test.tsx +35 -0
- package/examples/playground/app/components/demo-icons.tsx +89 -0
- package/examples/playground/app/components/demos.tsx +33 -0
- package/examples/playground/app/components/fast-link.test.tsx +58 -0
- package/examples/playground/app/components/fast-link.tsx +55 -0
- package/examples/playground/app/components/site-header.tsx +96 -0
- package/examples/playground/app/counter/counter-client.tsx +3 -3
- package/examples/playground/app/counter/model.test.ts +3 -3
- package/examples/playground/app/counter/model.ts +11 -21
- package/examples/playground/app/counter/page.tsx +2 -15
- package/examples/playground/app/counter/session.ts +2 -2
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
- package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
- package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
- package/examples/playground/app/documents/[documentId]/session.ts +2 -2
- package/examples/playground/app/documents/model.ts +27 -27
- package/examples/playground/app/globals.css +420 -207
- package/examples/playground/app/initial-state.test.tsx +54 -0
- package/examples/playground/app/layout.tsx +7 -23
- package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
- package/examples/playground/app/orders/[orderId]/session.ts +2 -2
- package/examples/playground/app/orders/model.ts +49 -49
- package/examples/playground/app/page.tsx +13 -102
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
- package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
- package/examples/playground/app/recovery/model.ts +30 -30
- package/examples/playground/instrumentation.ts +5 -0
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +13 -2
- package/examples/playground/scripts/a0-template.ts +77 -0
- package/examples/playground/vercel.json +36 -0
- package/package.json +29 -3
- package/src/actor-client.ts +16 -4
- package/src/actor-react.ts +25 -11
- package/src/actor-shared.ts +21 -6
- package/src/actor.ts +76 -31
- package/src/ai-agent-actions.ts +219 -0
- package/src/ai-agent.ts +2517 -0
- package/src/ai-app-state.ts +91 -0
- package/src/ai-client-state.ts +6 -3
- package/src/ai-client.ts +62 -0
- package/src/ai-context-schema.ts +25 -5
- package/src/ai-context.ts +90 -40
- package/src/ai-control-server.ts +92 -72
- package/src/ai-coordinator.ts +23 -22
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +833 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-message-base.ts +209 -0
- package/src/ai-message-projection.ts +262 -64
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1263 -0
- package/src/ai-progress-batches.ts +26 -0
- package/src/ai-projector.ts +95 -14
- package/src/ai-react.ts +146 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.browser.ts +2 -2
- package/src/ai-server.ts +23 -1965
- package/src/ai-stored-state.ts +48 -163
- package/src/ai.ts +339 -155
- package/src/bootstrap-stream.ts +72 -0
- package/src/bounded-cache.ts +55 -0
- package/src/client-render-session.ts +49 -0
- package/src/client.ts +688 -151
- package/src/contract.ts +92 -42
- package/src/deterministic-id.ts +37 -0
- package/src/equal-json.ts +32 -0
- package/src/errors.ts +13 -1
- package/src/index.ts +5 -1
- package/src/initial-snapshot.ts +39 -0
- package/src/internal.ts +52 -0
- package/src/libfx.d.ts +34 -0
- package/src/postgres-notification-scope.ts +14 -3
- package/src/react.ts +171 -206
- package/src/redis-http-reply.ts +256 -0
- package/src/redis-http-stream.ts +73 -0
- package/src/redis-http-subscriptions.ts +13 -5
- package/src/reducer-runtime.ts +52 -0
- package/src/reducer.ts +93 -11
- package/src/scheduler-qstash.ts +49 -20
- package/src/scheduler-task.ts +34 -2
- package/src/scheduler-vercel.ts +32 -84
- package/src/server-fetch.ts +139 -35
- package/src/server.ts +1133 -255
- package/src/session-clear.ts +23 -0
- package/src/session-socket.ts +154 -49
- package/src/sse.ts +71 -4
- package/src/store-memory.ts +252 -75
- package/src/store-polling.ts +18 -4
- package/src/store-postgres.ts +541 -114
- package/src/store-redis-core.ts +1450 -493
- package/src/store-redis-http.ts +18 -2
- package/src/store-redis-notify.ts +262 -85
- package/src/store-redis.ts +19 -9
- package/src/store-sqlite.ts +319 -45
- package/src/store.ts +79 -1
- package/src/stream-buffer.ts +43 -0
- package/src/telemetry.ts +14 -1
- package/src/wire.ts +241 -13
- package/dist/actor-ohPC-81x.d.ts.map +0 -1
- package/dist/actor-shared-B5tJfzt-.js.map +0 -1
- package/dist/ai-CrEf6p_W.d.ts.map +0 -1
- package/dist/ai-DgOBltJ_.js.map +0 -1
- package/dist/client-BYzHjkwU.js.map +0 -1
- package/dist/client-CzyacQpJ.d.ts.map +0 -1
- package/dist/contract-CKRg_E4q.js.map +0 -1
- package/dist/errors-DCk6ch5n.js.map +0 -1
- package/dist/errors-DvhSXnxk.d.ts.map +0 -1
- package/dist/internal-Dq2qYxou.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/reducer-DEMjEY_O.js +0 -29
- package/dist/reducer-DEMjEY_O.js.map +0 -1
- package/dist/reducer-otzHjuJj.d.ts.map +0 -1
- package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
- package/dist/server-BD5ckxZb.js.map +0 -1
- package/dist/server-DbD7IJVK.d.ts.map +0 -1
- package/dist/store-DyZM6fS5.d.ts.map +0 -1
- package/dist/store-polling-CmxUbV93.js.map +0 -1
- package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
- package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
- package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
- package/dist/validate-XKT4FSNn.js +0 -30
- package/dist/validate-XKT4FSNn.js.map +0 -1
- package/dist/wire-BO5wWCb1.js.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -142
- package/docs/concepts/02-handlers.mdx +0 -284
- package/docs/concepts/03-durability.mdx +0 -314
- package/docs/concepts/04-state.mdx +0 -209
- package/docs/guides/01-timers.mdx +0 -185
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -384
- package/docs/guides/04-local-first.mdx +0 -88
- package/docs/guides/05-production.mdx +0 -521
- package/docs/guides/06-ai-agents.mdx +0 -1318
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -266
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -1896
- package/docs/reference/02-errors.mdx +0 -149
- package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
- package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
- package/examples/playground/app/agent/compaction-settings.ts +0 -49
- package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
- package/examples/playground/app/agent/compaction-timeline.ts +0 -208
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /package/docs/{guides → server}/meta.ts +0 -0
|
@@ -1,1318 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Durable AI agents
|
|
3
|
-
description: Build an AI SDK agent whose messages, streaming progress, tools, approvals, and recovery live in an A2 event log.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## Install
|
|
7
|
-
|
|
8
|
-
Install A2, the AI SDK, and Zod for the examples below:
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
pnpm add experimental-a2 ai zod
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
The examples use the AI SDK's default global provider, AI Gateway. Add its key
|
|
15
|
-
to your environment:
|
|
16
|
-
|
|
17
|
-
```bash .env.local
|
|
18
|
-
AI_GATEWAY_API_KEY=your_api_key
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Define the agent
|
|
22
|
-
|
|
23
|
-
`agent()` defines an isomorphic event contract, normalized `AIStoredState`,
|
|
24
|
-
and its `AIState` view. The same reducer types server snapshots, hydration,
|
|
25
|
-
and optimistic client updates.
|
|
26
|
-
|
|
27
|
-
```ts assistant.ts
|
|
28
|
-
import { agent } from 'experimental-a2/ai'
|
|
29
|
-
|
|
30
|
-
export const assistant = agent({
|
|
31
|
-
name: 'support-agent',
|
|
32
|
-
})
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Keep this module isomorphic. Model providers, secrets, tools, and storage belong
|
|
36
|
-
in the server module.
|
|
37
|
-
|
|
38
|
-
## Connect the model
|
|
39
|
-
|
|
40
|
-
`createAgentServer()` adds the AI SDK runner and A2's ordinary store,
|
|
41
|
-
scheduler, telemetry, and handler machinery:
|
|
42
|
-
|
|
43
|
-
```ts server/assistant.ts
|
|
44
|
-
import { createAgentServer } from 'experimental-a2/ai/server'
|
|
45
|
-
import { assistant } from '../assistant'
|
|
46
|
-
|
|
47
|
-
export const assistantServer = createAgentServer({
|
|
48
|
-
agent: assistant,
|
|
49
|
-
model: 'openai/gpt-5.6-terra',
|
|
50
|
-
instructions: 'Help the customer. Be concise.',
|
|
51
|
-
generation: {
|
|
52
|
-
temperature: 0.2,
|
|
53
|
-
maxOutputTokens: 2_000,
|
|
54
|
-
},
|
|
55
|
-
})
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
A string model id uses the AI SDK's global provider, which is AI Gateway by
|
|
59
|
-
default. `model` also accepts any AI SDK `LanguageModel`, such as a provider
|
|
60
|
-
model returned by `openai('gpt-5.6-terra')`, or an async resolver that chooses a
|
|
61
|
-
model for each generation.
|
|
62
|
-
|
|
63
|
-
Each durable generation runs one AI SDK `streamText()` step. `generation`
|
|
64
|
-
contains per-step settings such as sampling, token limits, provider options,
|
|
65
|
-
and tool approval policy. A2 owns the stop condition, local tool execution,
|
|
66
|
-
and continuation between steps. Responses have no step limit by default. Set
|
|
67
|
-
`maxSteps` to a positive integer to limit one complete assistant response.
|
|
68
|
-
Individual providers decide which model settings they support. `generation`
|
|
69
|
-
therefore excludes `stopWhen`, tool execution callbacks, tool callers, sandbox
|
|
70
|
-
execution, and the tool approval secret. A2 preserves all other supplied
|
|
71
|
-
settings, including `toolsContext`, when calling the AI SDK. A local tool
|
|
72
|
-
reads its durable execution context later, from the handler that runs it. Model-step
|
|
73
|
-
timeouts are supported; tool-execution timeouts are not.
|
|
74
|
-
|
|
75
|
-
For AI Gateway models, the default generator sets
|
|
76
|
-
`generation.providerOptions.gateway.caching` to `'auto'`. A2 merges this
|
|
77
|
-
default with your provider options, with caller-supplied values taking
|
|
78
|
-
precedence. Other Gateway options and provider namespaces are preserved.
|
|
79
|
-
The same default applies to automatic compaction. A custom `generate`
|
|
80
|
-
function receives your original settings and owns its caching policy.
|
|
81
|
-
|
|
82
|
-
Gateway calls also send an `x-session-affinity` header derived from a SHA-256
|
|
83
|
-
hash of the agent name and session ID. The value stays stable across turns,
|
|
84
|
-
retries, worker restarts, and automatic compaction. No raw session identifier
|
|
85
|
-
is sent in this header. A caller-supplied `generation.headers` entry takes
|
|
86
|
-
precedence, regardless of header casing; an explicit `undefined` suppresses
|
|
87
|
-
A2's default header. Set overrides here, since A2's per-call default can
|
|
88
|
-
override headers configured on the model itself.
|
|
89
|
-
Affinity helps supporting providers reuse their cache; it does not select
|
|
90
|
-
Gateway's provider or guarantee a cache hit.
|
|
91
|
-
See [Gateway automatic caching](https://vercel.com/docs/ai-gateway/models-and-providers/automatic-caching)
|
|
92
|
-
for provider support and cache-write costs.
|
|
93
|
-
|
|
94
|
-
### Store assistant metadata
|
|
95
|
-
|
|
96
|
-
Extend the agent's `UIMessage` type to keep generation status and usage on the
|
|
97
|
-
assistant message:
|
|
98
|
-
|
|
99
|
-
```ts metadata-assistant.ts
|
|
100
|
-
import type { LanguageModelUsage, UIMessage } from 'ai'
|
|
101
|
-
import { agent } from 'experimental-a2/ai'
|
|
102
|
-
|
|
103
|
-
export type MetadataAssistantMessage = UIMessage<{
|
|
104
|
-
status: 'writing' | 'complete'
|
|
105
|
-
usage?: LanguageModelUsage
|
|
106
|
-
}>
|
|
107
|
-
|
|
108
|
-
export const metadataAssistant = agent<MetadataAssistantMessage>({
|
|
109
|
-
name: 'metadata-assistant',
|
|
110
|
-
})
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Map the AI SDK's standard stream parts to that metadata type:
|
|
114
|
-
|
|
115
|
-
```ts server/metadata-assistant.ts
|
|
116
|
-
import { createAgentServer } from 'experimental-a2/ai/server'
|
|
117
|
-
import { metadataAssistant } from '../metadata-assistant'
|
|
118
|
-
|
|
119
|
-
export const metadataAssistantServer = createAgentServer({
|
|
120
|
-
agent: metadataAssistant,
|
|
121
|
-
model: 'openai/gpt-5.6-terra',
|
|
122
|
-
messageMetadata: ({ part }) => {
|
|
123
|
-
if (part.type === 'start') return { status: 'writing' as const }
|
|
124
|
-
if (part.type === 'finish') {
|
|
125
|
-
return { status: 'complete' as const, usage: part.totalUsage }
|
|
126
|
-
}
|
|
127
|
-
return undefined
|
|
128
|
-
},
|
|
129
|
-
})
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
The default generator passes `messageMetadata` to the AI SDK's
|
|
133
|
-
`toUIMessageStream()`. The callback receives the generation context and its
|
|
134
|
-
current `TextStreamPart`. Its return type follows the metadata type in the
|
|
135
|
-
agent's `UIMessage`. Metadata on standard `start` and `finish` chunks enters
|
|
136
|
-
the event log with the rest of the stream. A2 shallow-merges those values onto
|
|
137
|
-
the projected assistant message, so the example exposes `writing` immediately
|
|
138
|
-
and keeps final token usage after completion. The callback is synchronous.
|
|
139
|
-
|
|
140
|
-
`experimental-a2/ai/server` is server-only. The isomorphic
|
|
141
|
-
`experimental-a2/ai` entry point contains the contract, reducer, schemas,
|
|
142
|
-
`handlerContext()`, and pure inputs; it never imports a model provider or
|
|
143
|
-
backend.
|
|
144
|
-
|
|
145
|
-
## Expose the event stream
|
|
146
|
-
|
|
147
|
-
One HTTP route gives the browser a read and write path. `GET` streams events;
|
|
148
|
-
`POST` accepts optimistic pushes:
|
|
149
|
-
|
|
150
|
-
```ts app/api/agent-events/route.ts
|
|
151
|
-
import { assistantServer } from '@/server/assistant'
|
|
152
|
-
|
|
153
|
-
export const GET = assistantServer.fetch
|
|
154
|
-
export const POST = assistantServer.fetch
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
The route never calls the model directly. The browser appends
|
|
158
|
-
`ai.control.requested` commands. The controller admits inputs and schedules
|
|
159
|
-
`ai.generation.requested`; workers run the AI SDK and append progress to the
|
|
160
|
-
same log.
|
|
161
|
-
|
|
162
|
-
## Bind the reducer to React
|
|
163
|
-
|
|
164
|
-
Create the typed provider and hook in a client module:
|
|
165
|
-
|
|
166
|
-
```tsx app/agent/session.ts
|
|
167
|
-
'use client'
|
|
168
|
-
import { createClient } from 'experimental-a2/client'
|
|
169
|
-
import { createReact } from 'experimental-a2/react'
|
|
170
|
-
import { assistant } from '@/assistant'
|
|
171
|
-
|
|
172
|
-
export const assistantClient = createClient({
|
|
173
|
-
reducer: assistant.reducer,
|
|
174
|
-
api: '/api/agent-events',
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
export const { SessionProvider, useSession } = createReact({
|
|
178
|
-
client: assistantClient,
|
|
179
|
-
})
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
Fold the initial state on the server, then let the provider resume the event
|
|
183
|
-
stream from that exact index:
|
|
184
|
-
|
|
185
|
-
```tsx app/agent/[sessionId]/page.tsx
|
|
186
|
-
import { assistant } from '@/assistant'
|
|
187
|
-
import { assistantServer } from '@/server/assistant'
|
|
188
|
-
import { AgentClient } from './agent-client'
|
|
189
|
-
import { SessionProvider } from '../session'
|
|
190
|
-
|
|
191
|
-
export default async function AgentPage({
|
|
192
|
-
params,
|
|
193
|
-
}: {
|
|
194
|
-
params: Promise<{ sessionId: string }>
|
|
195
|
-
}) {
|
|
196
|
-
const { sessionId } = await params
|
|
197
|
-
const session = assistantServer.session(sessionId)
|
|
198
|
-
const { state, index } = await session.state(assistant.reducer)
|
|
199
|
-
|
|
200
|
-
return (
|
|
201
|
-
<SessionProvider
|
|
202
|
-
sessionId={sessionId}
|
|
203
|
-
initialState={state}
|
|
204
|
-
initialIndex={index}
|
|
205
|
-
>
|
|
206
|
-
<AgentClient />
|
|
207
|
-
</SessionProvider>
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
An empty session folds to the initial `AIState`. Its log is created lazily when
|
|
213
|
-
the first message is pushed.
|
|
214
|
-
|
|
215
|
-
Start the session from the previous route, but wait for its durable append
|
|
216
|
-
before navigating. The destination server render then sees the message in its
|
|
217
|
-
folded state instead of racing the `POST`:
|
|
218
|
-
|
|
219
|
-
```tsx app/agent/new-agent-session.tsx
|
|
220
|
-
'use client'
|
|
221
|
-
import type { UIMessage } from 'ai'
|
|
222
|
-
import { inputs } from 'experimental-a2/ai'
|
|
223
|
-
import { assistantClient } from './session'
|
|
224
|
-
|
|
225
|
-
export async function openAgent(
|
|
226
|
-
sessionId: string,
|
|
227
|
-
message: UIMessage,
|
|
228
|
-
router: { push(href: string): void },
|
|
229
|
-
): Promise<void> {
|
|
230
|
-
await assistantClient.session(sessionId).push(...inputs.message(message))
|
|
231
|
-
router.push(`/agent/${sessionId}`)
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
The draft clears immediately while the append is in flight and can be restored
|
|
236
|
-
if it fails. Once navigation begins, SSR already has folded state at a durable
|
|
237
|
-
frontier. When the destination provider mounts, it resolves the same live
|
|
238
|
-
session and reconciles that server render without replacing the object. Idle
|
|
239
|
-
session objects expire after five minutes by default; configure `gcTime` on
|
|
240
|
-
`createClient` when a different lifetime fits the app.
|
|
241
|
-
|
|
242
|
-
## Send messages from the client
|
|
243
|
-
|
|
244
|
-
Build the message inline and pass the resulting events straight to `push()`:
|
|
245
|
-
|
|
246
|
-
```tsx app/agent/[sessionId]/agent-client.tsx
|
|
247
|
-
'use client'
|
|
248
|
-
import type { FormEvent } from 'react'
|
|
249
|
-
import { useState } from 'react'
|
|
250
|
-
import { inputs } from 'experimental-a2/ai'
|
|
251
|
-
import { useSession } from '../session'
|
|
252
|
-
|
|
253
|
-
export function AgentClient() {
|
|
254
|
-
const { state, push } = useSession()
|
|
255
|
-
const [draft, setDraft] = useState('')
|
|
256
|
-
const [error, setError] = useState<string | null>(null)
|
|
257
|
-
|
|
258
|
-
const canSend = state.status !== 'closed'
|
|
259
|
-
const lastMessage = state.messages.at(-1)
|
|
260
|
-
const lastText =
|
|
261
|
-
lastMessage?.parts
|
|
262
|
-
.flatMap((part) => (part.type === 'text' ? [part.text] : []))
|
|
263
|
-
.join('\n') ?? ''
|
|
264
|
-
const showThinking =
|
|
265
|
-
state.status === 'generating' &&
|
|
266
|
-
(lastMessage?.role !== 'assistant' || lastText.length === 0)
|
|
267
|
-
|
|
268
|
-
async function send(event: FormEvent<HTMLFormElement>): Promise<void> {
|
|
269
|
-
event.preventDefault()
|
|
270
|
-
const previousDraft = draft
|
|
271
|
-
const text = previousDraft.trim()
|
|
272
|
-
if (!text || !canSend) return
|
|
273
|
-
|
|
274
|
-
setDraft('')
|
|
275
|
-
setError(null)
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
await push(
|
|
279
|
-
...inputs.message({
|
|
280
|
-
id: crypto.randomUUID(),
|
|
281
|
-
role: 'user',
|
|
282
|
-
parts: [{ type: 'text', text }],
|
|
283
|
-
}),
|
|
284
|
-
)
|
|
285
|
-
} catch (cause) {
|
|
286
|
-
setDraft(previousDraft)
|
|
287
|
-
setError(cause instanceof Error ? cause.message : String(cause))
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return (
|
|
292
|
-
<main>
|
|
293
|
-
<section aria-label="Conversation">
|
|
294
|
-
{state.messages.map((message) => {
|
|
295
|
-
const text = message.parts
|
|
296
|
-
.flatMap((part) => (part.type === 'text' ? [part.text] : []))
|
|
297
|
-
.join('\n')
|
|
298
|
-
if (!text) return null
|
|
299
|
-
|
|
300
|
-
return (
|
|
301
|
-
<article key={message.id}>
|
|
302
|
-
<strong>{message.role === 'user' ? 'You' : 'Agent'}</strong>
|
|
303
|
-
<p>{text}</p>
|
|
304
|
-
</article>
|
|
305
|
-
)
|
|
306
|
-
})}
|
|
307
|
-
|
|
308
|
-
{showThinking ? (
|
|
309
|
-
<article aria-live="polite">
|
|
310
|
-
<strong>Agent</strong>
|
|
311
|
-
<p>Thinking…</p>
|
|
312
|
-
</article>
|
|
313
|
-
) : null}
|
|
314
|
-
</section>
|
|
315
|
-
|
|
316
|
-
<form onSubmit={send}>
|
|
317
|
-
<input
|
|
318
|
-
aria-label="Message"
|
|
319
|
-
value={draft}
|
|
320
|
-
onChange={(event) => setDraft(event.currentTarget.value)}
|
|
321
|
-
/>
|
|
322
|
-
<button disabled={!draft.trim() || !canSend}>Send</button>
|
|
323
|
-
</form>
|
|
324
|
-
|
|
325
|
-
{error ? <p role="alert">{error}</p> : null}
|
|
326
|
-
</main>
|
|
327
|
-
)
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
`push()` persists the command. Its receipt confirms admission to the inbox.
|
|
332
|
-
`state.inbox.items` contains pending inputs; `state.messages` contains the
|
|
333
|
-
admitted conversation. If persistence fails, the component restores the draft.
|
|
334
|
-
|
|
335
|
-
The form gives Enter its normal submit behavior. The thinking row is also
|
|
336
|
-
derived from `AIState`: it appears with the Agent label when the server's
|
|
337
|
-
generation request folds, then the first visible assistant content replaces
|
|
338
|
-
it. Empty stream-start messages never create a blank conversation row.
|
|
339
|
-
|
|
340
|
-
Users can send another message while a response is active. The message enters
|
|
341
|
-
the durable log immediately, but its turn waits until the active assistant
|
|
342
|
-
response, including every tool step and approval, reaches a terminal event.
|
|
343
|
-
Inbox order defaults to arrival order and can be edited explicitly.
|
|
344
|
-
|
|
345
|
-
Pass `{ generate: false }` when a user message should update the conversation
|
|
346
|
-
without starting a model turn:
|
|
347
|
-
|
|
348
|
-
```ts app/agent/passive-message.ts
|
|
349
|
-
import type { UIMessage } from 'ai'
|
|
350
|
-
import { inputs } from 'experimental-a2/ai'
|
|
351
|
-
import { assistantClient } from './session'
|
|
352
|
-
|
|
353
|
-
export async function recordPassiveMessage(
|
|
354
|
-
sessionId: string,
|
|
355
|
-
message: UIMessage,
|
|
356
|
-
): Promise<void> {
|
|
357
|
-
await assistantClient
|
|
358
|
-
.session(sessionId)
|
|
359
|
-
.push(...inputs.message(message, { generate: false }))
|
|
360
|
-
}
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
The passive user message appears in `AIState.messages` when admitted. A later user
|
|
364
|
-
message with the default generation behavior includes passive user messages
|
|
365
|
-
before it in model context. Passive user messages after that trigger remain
|
|
366
|
-
outside its prompt and wait for the next generating message. This ordering
|
|
367
|
-
supports ambient agents without letting new background context leak into an
|
|
368
|
-
already queued turn.
|
|
369
|
-
|
|
370
|
-
## What happens after `push()`
|
|
371
|
-
|
|
372
|
-
One generating user interaction becomes a durable sequence:
|
|
373
|
-
|
|
374
|
-
```text
|
|
375
|
-
browser ai.control.requested durable input command
|
|
376
|
-
server ai.message.created input admitted
|
|
377
|
-
server ai.control.committed accepted state changes
|
|
378
|
-
server ai.control.decided command receipt
|
|
379
|
-
server ai.generation.requested scheduled from durable facts
|
|
380
|
-
server ai.generation.started
|
|
381
|
-
server ai.generation.progress batched UIMessageChunk[]
|
|
382
|
-
server ai.tool.called when present
|
|
383
|
-
server ai.approval.requested when present
|
|
384
|
-
server ai.tool.result after concurrent execution or denial
|
|
385
|
-
server ai.generation.completed closes this model step
|
|
386
|
-
server ai.generation.requested after all sibling tools are terminal
|
|
387
|
-
... another one-step generation
|
|
388
|
-
server ai.message.completed
|
|
389
|
-
server ai.generation.requested next queued user turn, when present
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
Tool results and generation completion may arrive in either order. A fast
|
|
393
|
-
automatic tool can finish while the model stream is still open. The join waits
|
|
394
|
-
for both step closure and every required terminal result.
|
|
395
|
-
|
|
396
|
-
The SSE connection delivers the server events to the same reducer. Progress
|
|
397
|
-
stores every AI SDK chunk once. The reducer and the exported
|
|
398
|
-
`deriveUIMessages()` apply those chunks synchronously to produce the current
|
|
399
|
-
`UIMessage`; cumulative message snapshots are not duplicated in the log.
|
|
400
|
-
|
|
401
|
-
`AIState` exposes `messages`, `status`, `activeGeneration`, `activeRequestId`,
|
|
402
|
-
`activeResponseMessageId`, `activeProjection`, `responseOwners`,
|
|
403
|
-
`terminalRequestIds`, `terminalGenerations`, `pendingApprovals`,
|
|
404
|
-
`pendingInputs`, `tools`, `compaction`, per-generation `usage`, and the last
|
|
405
|
-
generation `error`.
|
|
406
|
-
`activeProjection` is the temporary indexed generation frontier used for exact
|
|
407
|
-
interruption. It remains available after a generation step completes while the
|
|
408
|
-
response waits on tool, approval, or input barriers, and after a generation
|
|
409
|
-
fails while it awaits retry or interruption. It clears when the response
|
|
410
|
-
completes, is interrupted or superseded, a retry begins, or a later generation
|
|
411
|
-
replaces it.
|
|
412
|
-
|
|
413
|
-
`activeRequestId` identifies the server-authorized generation request. It
|
|
414
|
-
prevents a delayed request or recovered attempt from taking ownership from the
|
|
415
|
-
current response before its generation starts.
|
|
416
|
-
|
|
417
|
-
`activeResponseMessageId` identifies that request's response before
|
|
418
|
-
`activeGeneration` exists. It is `null` after the generation starts.
|
|
419
|
-
|
|
420
|
-
`responseOwners` keeps the latest generation ID, request ID, and attempt for each response
|
|
421
|
-
message. Late lifecycle events from a superseded generation stay in raw
|
|
422
|
-
history but cannot alter the projected message, even after its replacement
|
|
423
|
-
finishes.
|
|
424
|
-
|
|
425
|
-
A2 generates fixed-length deterministic IDs in its reserved `a2.ai:` namespace
|
|
426
|
-
for requests, generations, responses, and worker lifecycle events. Treat those
|
|
427
|
-
IDs as opaque. An assistant response uses its first model-request event's ID. Generation requests carry
|
|
428
|
-
`responseMessageId` explicitly; tool continuations also carry `sourceGenerationId`.
|
|
429
|
-
Ownership comes from these fields and the generation's `requestId` and `attempt`,
|
|
430
|
-
so ID strings never embed the preceding tool loop.
|
|
431
|
-
|
|
432
|
-
Approval and application-input events carry both `requestId` and `generationId`.
|
|
433
|
-
Copy both from the pending approval, pending input, or active generation when
|
|
434
|
-
sending a response. `activeRequestReason` and `activeRequestSourceGenerationId`
|
|
435
|
-
describe the current request independently from the message projection.
|
|
436
|
-
|
|
437
|
-
Generation requests share one durable lane, so model calls do not overlap.
|
|
438
|
-
The queued-turn policy is the stronger ordering rule: a later user message is
|
|
439
|
-
not scheduled until the current response's complete tool loop finishes. Lane
|
|
440
|
-
FIFO alone would not prevent a newly appended user turn from overtaking a tool
|
|
441
|
-
continuation that has not been appended yet.
|
|
442
|
-
|
|
443
|
-
## The built-in inputs
|
|
444
|
-
|
|
445
|
-
`inputs` contains pure, isomorphic builders for protocol interactions:
|
|
446
|
-
|
|
447
|
-
| Input | Events |
|
|
448
|
-
| --- | --- |
|
|
449
|
-
| `inputs.message(message, { generate? })` | queues input; `false` queues passive context |
|
|
450
|
-
| `inputs.seed(message)` | records `generate: false`; non-user roles require a trusted append |
|
|
451
|
-
| `inputs.approval(response)` | requests a decision on a pending approval |
|
|
452
|
-
| `inputs.input(response)` | records an application input response fact |
|
|
453
|
-
| `inputs.requestInput(request)` | records a trusted server request for application input |
|
|
454
|
-
| `inputs.retry(options)` | requests a fresh attempt for a failed response |
|
|
455
|
-
| `inputs.stop({ turnId })` | ends the observed turn and advances |
|
|
456
|
-
| `inputs.steer({ turnId, message })` | prioritizes input after the observed turn's current step finishes |
|
|
457
|
-
| `inputs.queue.edit({ message })` | edits a pending input |
|
|
458
|
-
| `inputs.queue.move({ inputId, beforeId })` | reorders pending input; `null` means the end |
|
|
459
|
-
| `inputs.queue.remove({ inputId })` | removes pending input from the inbox |
|
|
460
|
-
| `inputs.queue.sendNow({ inputId, turnId })` | atomically prioritizes queued input and interrupts the observed turn |
|
|
461
|
-
| `inputs.pause({ when })` | pauses now or after the active turn |
|
|
462
|
-
| `inputs.resume()` | resumes retained work and inbox admission |
|
|
463
|
-
| `inputs.toolResult(result)` | records a trusted deferred provider-tool result |
|
|
464
|
-
| `inputs.interrupt(options)` | addresses a specific generation or request |
|
|
465
|
-
|
|
466
|
-
Builders return `ai.control.requested` commands. Message, approval, input, and
|
|
467
|
-
retry builders identify a stable interaction. Other controls use the event ID
|
|
468
|
-
assigned by `push()` or `append()`; transport retries preserve that ID.
|
|
469
|
-
Generation scheduling considers only user-role `ai.message.created` facts.
|
|
470
|
-
Assistant and system message facts remain context regardless of the `generate`
|
|
471
|
-
field, and model output is recorded as generation progress rather than a new
|
|
472
|
-
message fact, so an assistant reply cannot recursively schedule another turn.
|
|
473
|
-
Browser `push()` accepts user messages, approval and input responses,
|
|
474
|
-
interruptions, and explicit retries. `inputs.requestInput()` and non-user
|
|
475
|
-
messages built with `inputs.seed()` are for trusted server appends. Browser
|
|
476
|
-
inputs never append server scheduling, seeded non-user messages, or lifecycle
|
|
477
|
-
events.
|
|
478
|
-
Approval and input request/response payloads carry the active `generationId`;
|
|
479
|
-
clients copy it from the pending request so a stale interaction cannot satisfy
|
|
480
|
-
a later model step.
|
|
481
|
-
|
|
482
|
-
`inputs` deliberately has no session lifecycle methods. The A2 log begins with
|
|
483
|
-
the first append. Apps that use explicit `ai.session.created` or
|
|
484
|
-
`ai.session.closed` lifecycle events append those trusted server facts
|
|
485
|
-
directly.
|
|
486
|
-
|
|
487
|
-
## Add tools and approval
|
|
488
|
-
|
|
489
|
-
Tools are ordinary AI SDK `tool()` definitions. A2 passes them through to
|
|
490
|
-
the AI SDK without interpreting or restricting them. Approval policy
|
|
491
|
-
belongs in `generation`, beside the other per-step settings:
|
|
492
|
-
|
|
493
|
-
```ts server/with-tools.ts
|
|
494
|
-
import { tool } from 'ai'
|
|
495
|
-
import { z } from 'zod'
|
|
496
|
-
import { createAgentServer } from 'experimental-a2/ai/server'
|
|
497
|
-
import { assistant } from '../assistant'
|
|
498
|
-
|
|
499
|
-
const tools = {
|
|
500
|
-
closeTicket: tool({
|
|
501
|
-
description: 'Close a resolved support ticket',
|
|
502
|
-
inputSchema: z.object({ ticketId: z.string() }),
|
|
503
|
-
execute: async ({ ticketId }, { toolCallId }) => {
|
|
504
|
-
// your side effect; toolCallId makes a stable idempotency key:
|
|
505
|
-
// await closeTicket(ticketId, { idempotencyKey: toolCallId })
|
|
506
|
-
return { ticketId, closed: true }
|
|
507
|
-
},
|
|
508
|
-
}),
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
export const assistantServerWithTools = createAgentServer({
|
|
512
|
-
agent: assistant,
|
|
513
|
-
model: 'openai/gpt-5.6-terra',
|
|
514
|
-
tools,
|
|
515
|
-
generation: {
|
|
516
|
-
toolApproval: {
|
|
517
|
-
closeTicket: 'user-approval',
|
|
518
|
-
},
|
|
519
|
-
},
|
|
520
|
-
})
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
The AI SDK infers the tool input and output from its schemas and
|
|
524
|
-
`execute`. A2 additionally runs each local `execute` inside an ambient
|
|
525
|
-
scope carrying the current durable handler attempt. Read it with
|
|
526
|
-
`handlerContext(agent)` from `experimental-a2/ai`: it returns the same
|
|
527
|
-
typed context bag an event handler receives, with `event`, `attempt`,
|
|
528
|
-
`session`, and `signal`. Tools execute an `ai.tool.execution.requested`
|
|
529
|
-
event after authorization. Its payload contains the admitted `call`, its
|
|
530
|
-
`generation`, and its turn and execution version. The agent argument carries the types; A2 verifies it against the
|
|
531
|
-
server executing the tool and throws when a tool written for one agent
|
|
532
|
-
runs under another, or when `handlerContext()` is called outside a tool
|
|
533
|
-
execution. The scope survives awaited helpers and async iteration, so
|
|
534
|
-
any code below `execute` can read it.
|
|
535
|
-
|
|
536
|
-
`generation.toolApproval` is evaluated during the model step, before A2 has a
|
|
537
|
-
durable tool handler. Static policies such as the example above are simplest.
|
|
538
|
-
An input-dependent policy may be a function that returns `user-approval` or
|
|
539
|
-
`approved` from the validated tool input. Approval predicates are
|
|
540
|
-
re-evaluated on recovery for the same durable tool call, so they must be
|
|
541
|
-
deterministic over the tool input; an impure predicate can fork one
|
|
542
|
-
durable call's flow. Model-phase options such as `toolsContext` and
|
|
543
|
-
per-tool `contextSchema` are yours and pass through to the AI SDK
|
|
544
|
-
untouched. Any model-phase hook that receives tool definitions sees
|
|
545
|
-
them without local `execute` functions. The durable A2 context is
|
|
546
|
-
ambient only inside `execute`.
|
|
547
|
-
|
|
548
|
-
The agent argument supplies types, not infrastructure. The context's
|
|
549
|
-
`session` belongs to the server executing the tool, so its `schedule()`
|
|
550
|
-
method uses that server's scheduler. For example, this tool schedules a
|
|
551
|
-
reminder as a new user message:
|
|
552
|
-
|
|
553
|
-
```ts server/set-reminder.ts
|
|
554
|
-
import { tool } from 'ai'
|
|
555
|
-
import { handlerContext, inputs } from 'experimental-a2/ai'
|
|
556
|
-
import { z } from 'zod'
|
|
557
|
-
import { assistant } from '../assistant'
|
|
558
|
-
|
|
559
|
-
export const setReminder = tool({
|
|
560
|
-
description: 'Set a reminder for yourself',
|
|
561
|
-
inputSchema: z.object({
|
|
562
|
-
delaySeconds: z.number().int().min(1).max(6 * 24 * 60 * 60),
|
|
563
|
-
reminder: z.string(),
|
|
564
|
-
}),
|
|
565
|
-
execute: async ({ delaySeconds, reminder }, { toolCallId }) => {
|
|
566
|
-
const ctx = handlerContext(assistant)
|
|
567
|
-
await ctx.session.schedule(
|
|
568
|
-
`reminder:${toolCallId}`,
|
|
569
|
-
{ delay: `${delaySeconds}s` },
|
|
570
|
-
...inputs.message({
|
|
571
|
-
id: `reminder:${ctx.event.id}`,
|
|
572
|
-
role: 'user',
|
|
573
|
-
parts: [{ type: 'text', text: `Reminder: ${reminder}` }],
|
|
574
|
-
}),
|
|
575
|
-
)
|
|
576
|
-
|
|
577
|
-
return { scheduled: true, delaySeconds }
|
|
578
|
-
},
|
|
579
|
-
})
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
Add `setReminder` to the server's `tools` and configure a
|
|
583
|
-
[scheduler](/guides/production).
|
|
584
|
-
The numeric schema lets the model choose any whole-second delay in the
|
|
585
|
-
provider's supported range instead of selecting from a fixed application enum.
|
|
586
|
-
The example uses A2's six-day Vercel Queues limit. The seventh retention day is
|
|
587
|
-
reserved for delivery and retries. Adjust the maximum to match your scheduler
|
|
588
|
-
and plan.
|
|
589
|
-
The schedule name uses `toolCallId`, and the message id uses the durable
|
|
590
|
-
triggering event id. A retry therefore converges on the same timer and message,
|
|
591
|
-
even if a provider reuses tool-call ids in a later generation. When the timer
|
|
592
|
-
arrives, `inputs.message()` queues a durable input command. The
|
|
593
|
-
queued-turn policy starts a fresh response after the active response completes
|
|
594
|
-
or is interrupted. A failed response must be retried or interrupted first.
|
|
595
|
-
If the scheduler send fails ambiguously or transiently, A2 retries the same
|
|
596
|
-
durable tool handler so that identity and target time stay fixed. Deterministic
|
|
597
|
-
adapter rejections are terminal tool errors. A missing scheduler is also a
|
|
598
|
-
normal tool error, so configure one before exposing the tool.
|
|
599
|
-
|
|
600
|
-
A tool that never reads `handlerContext()` is just an AI SDK tool;
|
|
601
|
-
nothing else changes. To test a context-reading tool directly, wrap the
|
|
602
|
-
call with `runWithHandlerContext(ctx, fn)` from
|
|
603
|
-
`experimental-a2/ai/server`: inside `fn`, `handlerContext()` returns
|
|
604
|
-
`ctx` for any agent.
|
|
605
|
-
|
|
606
|
-
The AI SDK emits an approval request instead of executing the tool. A2 records
|
|
607
|
-
it as `ai.approval.requested`, and the reducer adds it to
|
|
608
|
-
`state.pendingApprovals`. Respond from the same optimistic client path:
|
|
609
|
-
|
|
610
|
-
```tsx app/agent/[sessionId]/approval-controls.tsx
|
|
611
|
-
'use client'
|
|
612
|
-
import { inputs } from 'experimental-a2/ai'
|
|
613
|
-
import { useSession } from '../session'
|
|
614
|
-
|
|
615
|
-
export function ApprovalControls() {
|
|
616
|
-
const { state, push } = useSession()
|
|
617
|
-
|
|
618
|
-
return state.pendingApprovals.map((approval) => (
|
|
619
|
-
<div key={approval.approvalId}>
|
|
620
|
-
<span>Allow tool call {approval.toolCallId}?</span>
|
|
621
|
-
<button
|
|
622
|
-
onClick={() =>
|
|
623
|
-
void push(
|
|
624
|
-
...inputs.approval({
|
|
625
|
-
requestId: approval.requestId,
|
|
626
|
-
messageId: approval.messageId,
|
|
627
|
-
generationId: approval.generationId,
|
|
628
|
-
approvalId: approval.approvalId,
|
|
629
|
-
approved: true,
|
|
630
|
-
}),
|
|
631
|
-
)
|
|
632
|
-
}
|
|
633
|
-
>
|
|
634
|
-
Allow
|
|
635
|
-
</button>
|
|
636
|
-
<button
|
|
637
|
-
onClick={() =>
|
|
638
|
-
void push(
|
|
639
|
-
...inputs.approval({
|
|
640
|
-
requestId: approval.requestId,
|
|
641
|
-
messageId: approval.messageId,
|
|
642
|
-
generationId: approval.generationId,
|
|
643
|
-
approvalId: approval.approvalId,
|
|
644
|
-
approved: false,
|
|
645
|
-
reason: 'Denied by the user',
|
|
646
|
-
}),
|
|
647
|
-
)
|
|
648
|
-
}
|
|
649
|
-
>
|
|
650
|
-
Deny
|
|
651
|
-
</button>
|
|
652
|
-
</div>
|
|
653
|
-
))
|
|
654
|
-
}
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
`inputs.approval()` records only the decision and updates the approval part in
|
|
658
|
-
the projected `UIMessage`. A built-in handler executes the approved tool or
|
|
659
|
-
records a denied result. It never bypasses the same sibling join used by
|
|
660
|
-
automatic tools.
|
|
661
|
-
|
|
662
|
-
Tool activity follows the full AI SDK chunk lifecycle. Input validation errors,
|
|
663
|
-
execution errors, denials, and preliminary and final outputs become durable
|
|
664
|
-
`ai.tool.result` events. Preliminary outputs keep the tool `running`; a final
|
|
665
|
-
output completes it. Each result has its own event id, so a preliminary result
|
|
666
|
-
cannot deduplicate the final one. Dynamic-tool, provider-executed, provider
|
|
667
|
-
metadata, and tool metadata fields are preserved in the projection.
|
|
668
|
-
|
|
669
|
-
Independent tool handlers run concurrently as soon as their durable inputs and
|
|
670
|
-
authorization are ready. There is no fixed tool concurrency limit. A private
|
|
671
|
-
coordinator reducer tracks generation closure, cancellation, calls, approvals,
|
|
672
|
-
and terminal results for the active response. Its retained state is bounded;
|
|
673
|
-
completed responses do not accumulate in the coordinator. Each join reads a
|
|
674
|
-
durable reducer snapshot plus the log tail through
|
|
675
|
-
`ctx.session.state(coordinator, { through: 'latest' })`. The snapshot
|
|
676
|
-
is only a cache. Recovery can rebuild the same coordinator state from the event
|
|
677
|
-
log after process death.
|
|
678
|
-
|
|
679
|
-
A continuation needs the generation to close and every sibling tool to have a
|
|
680
|
-
terminal success, error, or denied result. Racing join handlers call
|
|
681
|
-
`ctx.session.append()` with the same deterministic continuation ID, so append
|
|
682
|
-
idempotency resolves the race. A handler's single-owner terminal result is
|
|
683
|
-
returned and committed atomically with that handler's completion. Preliminary
|
|
684
|
-
streaming tool outputs use immediate appends. An approved provider-executed
|
|
685
|
-
call can satisfy the join through its response. A provider tool that declares
|
|
686
|
-
deferred-result support can wait for its provider result instead of a local
|
|
687
|
-
executor. The authenticated provider callback appends that `ai.tool.result`
|
|
688
|
-
through the trusted server session API. Browser `push()` rejects lifecycle
|
|
689
|
-
events, including provider results.
|
|
690
|
-
|
|
691
|
-
`inputs.input()` provides the same durable request/response shape for
|
|
692
|
-
application-defined input. Its value stays in raw history and
|
|
693
|
-
`state.pendingInputs`; resolve instructions dynamically or replace `generate`
|
|
694
|
-
when the value should become model context.
|
|
695
|
-
|
|
696
|
-
Approval and application-input request/response facts carry the active
|
|
697
|
-
`generationId`. Echo that value from the pending request so a delayed UI action
|
|
698
|
-
cannot authorize or resume a later model step.
|
|
699
|
-
|
|
700
|
-
## Interrupt and retry
|
|
701
|
-
|
|
702
|
-
Interrupt the active response from the client:
|
|
703
|
-
|
|
704
|
-
```tsx app/agent/[sessionId]/stop-button.tsx
|
|
705
|
-
'use client'
|
|
706
|
-
import { inputs } from 'experimental-a2/ai'
|
|
707
|
-
import { useSession } from '../session'
|
|
708
|
-
|
|
709
|
-
export function StopButton() {
|
|
710
|
-
const { state, push, index } = useSession()
|
|
711
|
-
const active = state.active
|
|
712
|
-
if (!active) return null
|
|
713
|
-
|
|
714
|
-
return (
|
|
715
|
-
<button
|
|
716
|
-
onClick={() =>
|
|
717
|
-
void push(
|
|
718
|
-
...inputs.stop({
|
|
719
|
-
turnId: active.turnId,
|
|
720
|
-
lastSeenIndex: index,
|
|
721
|
-
}),
|
|
722
|
-
)
|
|
723
|
-
}
|
|
724
|
-
>
|
|
725
|
-
Stop
|
|
726
|
-
</button>
|
|
727
|
-
)
|
|
728
|
-
}
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
The client immediately previews Stop for the observed turn. `lastSeenIndex` is the exact confirmed log frontier visible when the
|
|
732
|
-
user clicked. While that generation's indexed projection is retained, the client freezes the response at that
|
|
733
|
-
frontier through the request echo and controller decision. A rejected push or decision removes the preview.
|
|
734
|
-
The reducer rewinds the indexed generation projection to that
|
|
735
|
-
frontier and keeps the partial text the user actually saw. Tool parts whose
|
|
736
|
-
arguments are still streaming are drafts: interruption removes them from the
|
|
737
|
-
conversation and subsequent model prompts, even if their current JSON parses.
|
|
738
|
-
Their progress remains in the raw log. Accepted tools still awaiting completion
|
|
739
|
-
become `output-error`. An accepted interruption
|
|
740
|
-
terminally fences that request or generation. Later generation, tool, approval,
|
|
741
|
-
input, and compaction events remain in raw history but cannot reactivate it or
|
|
742
|
-
alter the projection.
|
|
743
|
-
Completed tool results at or before the visible frontier remain completed.
|
|
744
|
-
|
|
745
|
-
Once Stop is persisted, A2's existing cancellation stream aborts running model
|
|
746
|
-
and tool work for that turn without waiting for the control handler. Startup
|
|
747
|
-
checks also skip matching jobs created while Stop is pending. Cancellation is
|
|
748
|
-
cooperative: tools must honor their abort signal. The control handler remains
|
|
749
|
-
responsible for recording the interruption and advancing the inbox; a Stop for
|
|
750
|
-
an older turn cannot cancel its successor.
|
|
751
|
-
|
|
752
|
-
For the lower-level `inputs.interrupt()` builder, between
|
|
753
|
-
`ai.generation.requested` and `ai.generation.started`, copy
|
|
754
|
-
`activeRequestId` as `requestId` and `activeResponseMessageId` as `messageId`.
|
|
755
|
-
Once `activeGeneration` exists, send its `generationId` and
|
|
756
|
-
`responseMessageId` instead. A request-owned interruption remains valid if
|
|
757
|
-
that same request starts before the append commits. Every interruption has
|
|
758
|
-
exactly one of `requestId` or `generationId`, so a delayed action cannot stop
|
|
759
|
-
later work that reuses the response message. After a generation completes,
|
|
760
|
-
copy the owner from a pending approval, pending input, or running tool while
|
|
761
|
-
the response is still waiting.
|
|
762
|
-
|
|
763
|
-
After a failed model call, `inputs.retry({ messageId, responseMessageId,
|
|
764
|
-
retryId })` records a retry request. The server reruns that failed model call.
|
|
765
|
-
Earlier completed model steps and their tool results remain, including steps
|
|
766
|
-
completed after the latest compaction. The committed summary also remains. For
|
|
767
|
-
example, if a tool succeeds after compaction and the next model call fails, retry
|
|
768
|
-
uses the summary and that tool's result.
|
|
769
|
-
`retryId` identifies the user's action, so resending one retry is safe while a
|
|
770
|
-
later retry remains distinct. The failed model call's uncommitted step remains in
|
|
771
|
-
raw history but is excluded from the new prompt and response. Later user messages remain queued while
|
|
772
|
-
the failed response awaits an explicit retry or interruption.
|
|
773
|
-
|
|
774
|
-
Applications own conversation forks and edit-and-resend flows for previously
|
|
775
|
-
admitted messages. A2's retry operation continues the current response.
|
|
776
|
-
|
|
777
|
-
## Append from the server
|
|
778
|
-
|
|
779
|
-
Browser interactions should use `push()` for instant state. Server-originated
|
|
780
|
-
turns use the same inputs with `append()`:
|
|
781
|
-
|
|
782
|
-
```ts server/append-message.ts
|
|
783
|
-
import { inputs } from 'experimental-a2/ai'
|
|
784
|
-
import { assistantServer } from './assistant'
|
|
785
|
-
|
|
786
|
-
export async function appendServerMessage(sessionId: string, text: string) {
|
|
787
|
-
await assistantServer.session(sessionId).append(
|
|
788
|
-
...inputs.message({
|
|
789
|
-
id: crypto.randomUUID(),
|
|
790
|
-
role: 'user',
|
|
791
|
-
parts: [{ type: 'text', text }],
|
|
792
|
-
}),
|
|
793
|
-
)
|
|
794
|
-
}
|
|
795
|
-
```
|
|
796
|
-
|
|
797
|
-
This path fits webhooks, jobs, and other backend sources. A connected browser
|
|
798
|
-
still receives the events over SSE, but it has no optimistic overlay for an
|
|
799
|
-
interaction that began elsewhere.
|
|
800
|
-
|
|
801
|
-
## Compact long conversations
|
|
802
|
-
|
|
803
|
-
Compaction is automatic for supported Gateway models. A2 uses the active model
|
|
804
|
-
with the same instructions, tool definitions, and provider settings, and appends
|
|
805
|
-
an internal user message asking for a summary. This preserves the request prefix
|
|
806
|
-
for prompt caching when the provider has a matching cache entry.
|
|
807
|
-
|
|
808
|
-
```ts server/compacted.ts
|
|
809
|
-
import { createAgentServer } from 'experimental-a2/ai/server'
|
|
810
|
-
import { assistant } from '../assistant'
|
|
811
|
-
|
|
812
|
-
export const compactedAssistantServer = createAgentServer({
|
|
813
|
-
agent: assistant,
|
|
814
|
-
model: 'openai/gpt-5.6-terra',
|
|
815
|
-
})
|
|
816
|
-
```
|
|
817
|
-
|
|
818
|
-
On first use of each Gateway model in a session, A2 includes
|
|
819
|
-
`ai.model.metadata.requested` in the existing generation-start append. A separate
|
|
820
|
-
handler fetches the public Gateway model catalog and records the selected limits
|
|
821
|
-
in `ai.model.metadata.resolved`. It runs outside the AI turn lane. The first model
|
|
822
|
-
request proceeds immediately; metadata that arrives afterward is available to
|
|
823
|
-
later steps. Returning to a previously used model reuses its durable metadata.
|
|
824
|
-
The catalog is also cached in memory for one hour across agent servers, with one
|
|
825
|
-
shared in-flight request and a five-second timeout. A failed lookup follows A2's
|
|
826
|
-
handler retry policy without failing the model generation. A model absent from
|
|
827
|
-
the catalog is recorded as unavailable. A pending entry means no result has been
|
|
828
|
-
recorded, including when the metadata handler exhausts its retry budget. A2 does
|
|
829
|
-
not start a fresh lookup on every turn after exhaustion. Inspect the handler
|
|
830
|
-
failure or use an explicit threshold in that case.
|
|
831
|
-
|
|
832
|
-
The default threshold is 75% of the context window. A larger configured
|
|
833
|
-
`generation.maxOutputTokens` lowers the threshold further to reserve that output
|
|
834
|
-
allowance. This is an input-context policy, not a new output limit. Limits and
|
|
835
|
-
metadata status are available through an explicit transcript reducer read.
|
|
836
|
-
|
|
837
|
-
`compaction: { thresholdTokens: 80_000 }` overrides discovery with an explicit
|
|
838
|
-
positive safe integer. `compaction: { instructions: 'Preserve exact IDs.' }`
|
|
839
|
-
adds guidance to the built-in summary prompt while keeping the derived threshold.
|
|
840
|
-
`compaction: false` disables both compaction and metadata lookup. The `compaction`
|
|
841
|
-
option can also be a resolver receiving the same
|
|
842
|
-
`{ event, messages, session, signal }` context as `model` and `instructions`. It
|
|
843
|
-
returns `false`, automatic options, or a custom policy once per generation and
|
|
844
|
-
can await `session.state(settingsReducer)` to read application settings.
|
|
845
|
-
Use this to select a threshold from application settings at the prompt boundary.
|
|
846
|
-
Changes after resolution apply to a later model step. Disabling compaction
|
|
847
|
-
preserves summaries that were already recorded. Custom provider
|
|
848
|
-
models and custom global SDK providers need an explicit threshold. A custom
|
|
849
|
-
`generate` callback keeps compaction disabled unless explicitly configured. Gateway
|
|
850
|
-
fallback model lists also need an explicit threshold appropriate for every model
|
|
851
|
-
they may use.
|
|
852
|
-
|
|
853
|
-
The input estimate starts from serialized UTF-8 bytes divided by four, including
|
|
854
|
-
model messages, instructions, and tool schemas. When a preceding model call has
|
|
855
|
-
reported token usage for the same model and context, A2 uses that measurement as
|
|
856
|
-
an anchor and estimates subsequent growth. It is not a provider tokenizer and
|
|
857
|
-
does not precisely measure new image or audio tokens. A2 performs no remote token
|
|
858
|
-
counting.
|
|
859
|
-
|
|
860
|
-
On a cold session without recorded metadata or an explicit threshold, automatic
|
|
861
|
-
compaction waits for discovery. An oversized first prompt is sent normally and
|
|
862
|
-
its provider error becomes a generation failure. A2 does not silently truncate
|
|
863
|
-
it or attempt automatic recovery from a context-limit error. Use an explicit threshold for
|
|
864
|
-
applications that import a large initial conversation. The estimate is a
|
|
865
|
-
compaction trigger, not a definitive overflow check. Provider context-limit
|
|
866
|
-
errors during generation or summarization remain generation failures.
|
|
867
|
-
|
|
868
|
-
Crossing the threshold adds one summary model request. Application generation
|
|
869
|
-
callbacks, stream transforms, and tool hooks do not run for the summary, and it
|
|
870
|
-
never executes local tools. Provider-executed tools, structured output, and forced
|
|
871
|
-
tool choices disable implicit automatic compaction; explicitly configuring
|
|
872
|
-
automatic compaction for those requests throws. Use a custom policy for them.
|
|
873
|
-
An empty summary or a summary tool call fails the generation instead of replacing
|
|
874
|
-
its context.
|
|
875
|
-
|
|
876
|
-
A2 records `ai.compaction.requested` before the call and
|
|
877
|
-
`ai.compaction.completed` after a successful summary. The completed event stores
|
|
878
|
-
the summary, its usage, and the exact event frontier it covers. Later model
|
|
879
|
-
requests receive the summary plus everything produced after that frontier,
|
|
880
|
-
including later tool steps in the same assistant message. Queued user messages
|
|
881
|
-
remain queued. Compaction waits until earlier tool calls have terminal results.
|
|
882
|
-
The raw log and transcript from `agent.reducer` remain available in full.
|
|
883
|
-
Server execution uses a separate model-context reducer and checkpoint. Successful
|
|
884
|
-
compaction permanently replaces covered context. Callbacks receive their prompt
|
|
885
|
-
messages and request information, not reducer state or execution internals.
|
|
886
|
-
Application reducers remain explicit reads through `session.state(reducer)`.
|
|
887
|
-
To read the full transcript, load `session.state(agent.reducer)` and pass its
|
|
888
|
-
`state` to `agent.reducer.view()`. Client hooks continue to expose `AIState`.
|
|
889
|
-
|
|
890
|
-
Custom `shouldCompact(context)` and `compact(context)` callbacks remain
|
|
891
|
-
available for deterministic replacement messages or application-specific
|
|
892
|
-
retention. Both receive the typed UI messages and `modelMessages`, the model-ready
|
|
893
|
-
context including any existing summary. Custom generation callbacks also receive
|
|
894
|
-
`modelMessages`; use it when forwarding context to a provider. A2 stores built-in
|
|
895
|
-
summaries separately from application-typed UI messages, so a summary does not
|
|
896
|
-
need your message metadata or schema.
|
|
897
|
-
|
|
898
|
-
Render compaction as activity alongside the conversation:
|
|
899
|
-
|
|
900
|
-
```tsx app/agent/compaction-status.tsx
|
|
901
|
-
'use client'
|
|
902
|
-
import { useSession } from './session'
|
|
903
|
-
|
|
904
|
-
export function CompactionStatus() {
|
|
905
|
-
const { state } = useSession()
|
|
906
|
-
return state.compaction?.status === 'running' ? (
|
|
907
|
-
<p role="status">Compacting…</p>
|
|
908
|
-
) : null
|
|
909
|
-
}
|
|
910
|
-
```
|
|
911
|
-
|
|
912
|
-
Successful compaction is a permanent boundary. Failure, interruption, retry, and
|
|
913
|
-
worker recovery never restore the messages it replaced. Retry reruns the failed
|
|
914
|
-
model step from the latest committed context and preserves earlier completed
|
|
915
|
-
steps. A failed summary attempt leaves the previous committed context intact.
|
|
916
|
-
The synthetic summary request is not a user turn or an assistant reply in
|
|
917
|
-
`state.messages`.
|
|
918
|
-
|
|
919
|
-
Compaction runs only when retained tool exchanges have final results and all
|
|
920
|
-
A2-owned tool work has settled, including work from stopped or superseded turns.
|
|
921
|
-
A2 checks a fresh coordinator checkpoint before starting a candidate compaction.
|
|
922
|
-
This adds one state read on that path. Unresolved dead-lettered tool work keeps
|
|
923
|
-
compaction deferred until repair and settlement or session closure.
|
|
924
|
-
|
|
925
|
-
## Extend the assembly
|
|
926
|
-
|
|
927
|
-
The convenience API is ordinary A2 parts:
|
|
928
|
-
|
|
929
|
-
- `events` and `createEvents()` export the built-in schemas.
|
|
930
|
-
- `createReducer({ contract })` builds the normalized state and `AIState` view for a
|
|
931
|
-
compatible contract.
|
|
932
|
-
- `agent()` combines the built-in protocol, application events, and reducer.
|
|
933
|
-
- `tool()` binds an AI SDK function-tool definition to an agent's typed A2
|
|
934
|
-
handler context.
|
|
935
|
-
- `createHandlers({ agent, ... })` returns the AI handler table.
|
|
936
|
-
- `createAgentServer()` combines those handlers with `createServer()`.
|
|
937
|
-
|
|
938
|
-
Use the lower-level pieces when an application needs more handlers or a
|
|
939
|
-
different server assembly:
|
|
940
|
-
|
|
941
|
-
```ts server/custom.ts
|
|
942
|
-
import { z } from 'zod'
|
|
943
|
-
import { agent } from 'experimental-a2/ai'
|
|
944
|
-
import { createHandlers } from 'experimental-a2/ai/server'
|
|
945
|
-
import { createServer } from 'experimental-a2/server'
|
|
946
|
-
|
|
947
|
-
const supportAgent = agent({
|
|
948
|
-
name: 'extended-support-agent',
|
|
949
|
-
events: {
|
|
950
|
-
'ticket.linked': z.object({ ticketId: z.string() }),
|
|
951
|
-
},
|
|
952
|
-
})
|
|
953
|
-
|
|
954
|
-
const aiHandlers = createHandlers({
|
|
955
|
-
agent: supportAgent,
|
|
956
|
-
model: 'openai/gpt-5.6-terra',
|
|
957
|
-
})
|
|
958
|
-
|
|
959
|
-
export const customAssistantServer = createServer({
|
|
960
|
-
contract: supportAgent.contract,
|
|
961
|
-
handlers: {
|
|
962
|
-
...aiHandlers,
|
|
963
|
-
'ticket.linked': async ({ event }) => {
|
|
964
|
-
// your side effect; event.id makes a stable idempotency key:
|
|
965
|
-
// await indexTicket(event.payload.ticketId, { idempotencyKey: event.id })
|
|
966
|
-
},
|
|
967
|
-
},
|
|
968
|
-
})
|
|
969
|
-
```
|
|
970
|
-
|
|
971
|
-
Application events stay fully typed. The standard AI reducer ignores unknown
|
|
972
|
-
events, so another reducer can project application state without forking the AI
|
|
973
|
-
protocol. A custom `createServer()` assembly owns its browser ingress policy.
|
|
974
|
-
Use `server.fetch(request, { authorize })` to authorize operations, and reject
|
|
975
|
-
server-only AI event names there before they reach the custom server.
|
|
976
|
-
`createAgentServer()` applies its stricter built-in ingress allowlist
|
|
977
|
-
automatically.
|
|
978
|
-
|
|
979
|
-
### Replace generation, not durability
|
|
980
|
-
|
|
981
|
-
The default generator uses AI SDK `streamText()` for one model step. Replace
|
|
982
|
-
only that model-facing step for fixtures, provider routing, or a different AI
|
|
983
|
-
SDK assembly:
|
|
984
|
-
|
|
985
|
-
```ts server/custom-generation.ts
|
|
986
|
-
import { convertToModelMessages, streamText } from 'ai'
|
|
987
|
-
import { createAgentServer } from 'experimental-a2/ai/server'
|
|
988
|
-
import { assistant } from '../assistant'
|
|
989
|
-
|
|
990
|
-
export const customGenerationServer = createAgentServer({
|
|
991
|
-
agent: assistant,
|
|
992
|
-
model: 'openai/gpt-5.6-terra',
|
|
993
|
-
generate: async ({
|
|
994
|
-
messages,
|
|
995
|
-
model,
|
|
996
|
-
instructions,
|
|
997
|
-
responseMessageId,
|
|
998
|
-
signal,
|
|
999
|
-
}) => {
|
|
1000
|
-
const result = streamText({
|
|
1001
|
-
model,
|
|
1002
|
-
messages: await convertToModelMessages(messages),
|
|
1003
|
-
abortSignal: signal,
|
|
1004
|
-
...(instructions === undefined ? {} : { instructions }),
|
|
1005
|
-
})
|
|
1006
|
-
|
|
1007
|
-
return result.toUIMessageStream({
|
|
1008
|
-
originalMessages: messages,
|
|
1009
|
-
generateMessageId: () => responseMessageId,
|
|
1010
|
-
})
|
|
1011
|
-
},
|
|
1012
|
-
})
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
`generate` receives compacted messages, resolved model and instructions, tools,
|
|
1016
|
-
generation settings, durable request identity, current state, and
|
|
1017
|
-
the abort signal. It returns exactly one model step as a
|
|
1018
|
-
`ReadableStream<UIMessageChunk>`. A tool-aware replacement sends definitions to
|
|
1019
|
-
the model without running local `execute` functions. A2 still owns durable
|
|
1020
|
-
progress, tool execution, approval, continuation, interruption, and failure.
|
|
1021
|
-
`messageMetadata` applies only to the default generator. A custom `generate`
|
|
1022
|
-
function owns its metadata and can pass a callback to `toUIMessageStream()` or
|
|
1023
|
-
emit typed metadata chunks itself.
|
|
1024
|
-
|
|
1025
|
-
## Checkpoints and recovery reads
|
|
1026
|
-
|
|
1027
|
-
The AI reducer stores normalized `AIStoredState`. `session.state(agent.reducer)`
|
|
1028
|
-
returns that state and its log index, ready to pass to `SessionProvider` or client
|
|
1029
|
-
hydration. Client and React snapshots derive the familiar `AIState` view
|
|
1030
|
-
immediately, including streamed message parts and tool activity.
|
|
1031
|
-
|
|
1032
|
-
For server-side inspection, use the reducer's view:
|
|
1033
|
-
|
|
1034
|
-
```ts server/read-agent.ts
|
|
1035
|
-
import type { UIMessage } from 'ai'
|
|
1036
|
-
import { assistantServer } from './assistant'
|
|
1037
|
-
import { assistant } from '../assistant'
|
|
1038
|
-
|
|
1039
|
-
export async function readMessages(sessionId: string): Promise<UIMessage[]> {
|
|
1040
|
-
const snapshot = await assistantServer.session(sessionId).state(assistant.reducer)
|
|
1041
|
-
return assistant.reducer.view(snapshot.state).messages
|
|
1042
|
-
}
|
|
1043
|
-
```
|
|
1044
|
-
|
|
1045
|
-
Each snapshot stores repeated payloads once and keeps only the changes needed
|
|
1046
|
-
to restore the current generation's prompt and projection. The live view is cached while
|
|
1047
|
-
events fold; normalization does not run for every token. Reading or serializing
|
|
1048
|
-
the stored fields materializes the records. The log and displayed conversation
|
|
1049
|
-
retain their full content.
|
|
1050
|
-
|
|
1051
|
-
Model steps read the model-context reducer and coordinator at a fixed log
|
|
1052
|
-
boundary. Model, instruction, compaction, and custom-generation callbacks
|
|
1053
|
-
receive prompt messages and request information without reducer state. Custom transcript folds and
|
|
1054
|
-
schemas apply only to the transcript; read application state explicitly with
|
|
1055
|
-
`session.state(yourReducer)` when a callback needs it.
|
|
1056
|
-
They do not receive a raw history array. Their `session.state(reducer)` reads
|
|
1057
|
-
application reducers at the captured prompt boundary by default; explicit
|
|
1058
|
-
`through` options follow the ordinary state-read contract. Compaction selection
|
|
1059
|
-
can be asynchronous when it needs a settings-state read.
|
|
1060
|
-
|
|
1061
|
-
Ordinary generation uses the model-context checkpoint without reading the
|
|
1062
|
-
transcript or replaying history to apply an existing compaction. The context
|
|
1063
|
-
checkpoint has its own identity and fold. Recovery and explicit retry discard
|
|
1064
|
-
only the unfinished model step. Completed compactions remain committed, so these
|
|
1065
|
-
paths never reconstruct pre-compaction prompts or retain rollback copies.
|
|
1066
|
-
|
|
1067
|
-
Tools recovering in a fresh process read context at their tool-admission index.
|
|
1068
|
-
The saved generation projection restores their original prompt, preserving the
|
|
1069
|
-
committed summary and excluding later arrivals. No separate history read is
|
|
1070
|
-
needed. A2 core can rebuild a missing or invalid reducer snapshot from the log.
|
|
1071
|
-
|
|
1072
|
-
Compaction immediately releases covered message and tool content from model
|
|
1073
|
-
context. Execution retains the active turn, queued inputs, unsettled tool work
|
|
1074
|
-
IDs, and token calibration. Model metadata is cached per distinct model.
|
|
1075
|
-
Historical input uniqueness comes from core event deduplication. The full
|
|
1076
|
-
transcript remains available independently.
|
|
1077
|
-
|
|
1078
|
-
Streaming progress updates the current message incrementally. Snapshots preserve
|
|
1079
|
-
open text and reasoning parts and partial tool inputs. Indexed progress remains
|
|
1080
|
-
available for exact interruption cutoffs; reordered events rebuild the projection.
|
|
1081
|
-
|
|
1082
|
-
While tool arguments arrive, `input-streaming` parts expose a best-effort partial
|
|
1083
|
-
JSON value in `input`. A command string can appear and grow before its closing
|
|
1084
|
-
quote arrives. This is a display draft, not validated execution input.
|
|
1085
|
-
`tool-input-available` supplies the completed input. Interruption still discards
|
|
1086
|
-
streaming drafts even when their partial JSON is readable.
|
|
1087
|
-
|
|
1088
|
-
## Delivery semantics
|
|
1089
|
-
|
|
1090
|
-
Model steps and tools run inside at-least-once A2 handlers. The returned-event
|
|
1091
|
-
transaction makes each successful handler completion and its terminal events
|
|
1092
|
-
one atomic durable operation. Deterministic ids make scheduling and lifecycle
|
|
1093
|
-
appends effectively once, and A2 marks an incomplete model attempt
|
|
1094
|
-
`superseded` before starting its replacement.
|
|
1095
|
-
|
|
1096
|
-
An expired claim or superseded attempt preserves its abort reason through model,
|
|
1097
|
-
resolver, compaction-policy, and tool calls. A2 retries that attempt without
|
|
1098
|
-
spending its failure budget. Explicit user interruption remains terminal.
|
|
1099
|
-
Streaming tool progress belongs to an execution attempt, so a retry can produce
|
|
1100
|
-
different preliminary output while preserving the tool-call id for idempotency.
|
|
1101
|
-
|
|
1102
|
-
Provider calls and external tool side effects remain outside that transaction.
|
|
1103
|
-
A process can die after an external effect succeeds and before its handler
|
|
1104
|
-
completion commits, so recovery may run the call again. Give side-effecting
|
|
1105
|
-
tools their own idempotency strategy, normally keyed by the AI SDK tool call
|
|
1106
|
-
id. This is the same at-least-once boundary as every other A2 handler. A fast
|
|
1107
|
-
tool may also start before its model step later fails. The failure prevents a
|
|
1108
|
-
continuation, but it cannot roll back that external effect.
|
|
1109
|
-
|
|
1110
|
-
Configure a [production scheduler](/guides/production) exactly as for any other A2
|
|
1111
|
-
server. It recovers an incomplete generation attempt after the original
|
|
1112
|
-
serverless invocation disappears.
|
|
1113
|
-
|
|
1114
|
-
## Inbox and turn controls
|
|
1115
|
-
|
|
1116
|
-
This input protocol requires matching client and server versions. Start a new
|
|
1117
|
-
agent session when adopting it. Existing event logs remain readable; finish
|
|
1118
|
-
pending work with the runtime that created it before upgrading.
|
|
1119
|
-
|
|
1120
|
-
`inputs.message(message)` places a message in the durable inbox. The controller
|
|
1121
|
-
admits one input at a time, freezes its content, and adds it to `state.messages`.
|
|
1122
|
-
Pending inputs appear in `state.inbox.items`; they do not enter the model prompt.
|
|
1123
|
-
Each item has an `id`, `message`, and `generate` flag.
|
|
1124
|
-
|
|
1125
|
-
New-input commands from `inputs.message()`, `inputs.seed()`, and `inputs.steer()`
|
|
1126
|
-
use the message ID as their event ID. Those IDs must match on hand-written
|
|
1127
|
-
commands too. Caller IDs cannot start with the reserved `a2.ai:` prefix. IDs must
|
|
1128
|
-
be unique within the store; include contract/session scope when deriving them
|
|
1129
|
-
with `idempotentId()`.
|
|
1130
|
-
|
|
1131
|
-
An identical submission replays its original event without creating more work,
|
|
1132
|
-
even after removal or compaction. Reusing the ID with different content, a
|
|
1133
|
-
different command action, or another session rejects at append with
|
|
1134
|
-
`PARTIAL_DUPLICATE_BATCH`. Use `queue.edit()` to change a pending input and a fresh
|
|
1135
|
-
message ID to create another input. Edit, move, remove, and other commands have
|
|
1136
|
-
their own event IDs. The controller stores no historical input-ID registry.
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
`state.active` identifies the logical turn, its frozen `input`, and its `phase`.
|
|
1140
|
-
The turn ID stays the same across model steps, tools, and a pause. Stop ends that
|
|
1141
|
-
turn and admits the next queued input when admission is running.
|
|
1142
|
-
|
|
1143
|
-
```ts client/turn-controls.ts
|
|
1144
|
-
import { inputs, type AIState } from 'experimental-a2/ai'
|
|
1145
|
-
import type { UIMessage } from 'ai'
|
|
1146
|
-
|
|
1147
|
-
export function editPending(message: UIMessage) {
|
|
1148
|
-
return inputs.queue.edit({ message })
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
export function movePending(inputId: string, beforeId: string | null) {
|
|
1152
|
-
return inputs.queue.move({ inputId, beforeId })
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
export function removePending(inputId: string) {
|
|
1156
|
-
return inputs.queue.remove({ inputId })
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
export function sendPendingNow(state: AIState, inputId: string) {
|
|
1160
|
-
return inputs.queue.sendNow({ inputId, turnId: state.active?.turnId ?? null })
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
export function stopCurrent(state: AIState) {
|
|
1164
|
-
return state.active ? inputs.stop({ turnId: state.active.turnId }) : []
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
export function steerCurrent(state: AIState, message: UIMessage) {
|
|
1168
|
-
return state.active
|
|
1169
|
-
? inputs.steer({ turnId: state.active.turnId, message })
|
|
1170
|
-
: inputs.message(message)
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
export const pauseAfterTurn = () => inputs.pause({ when: 'after-turn' })
|
|
1174
|
-
export const pauseNow = () => inputs.pause({ when: 'now' })
|
|
1175
|
-
export const resume = () => inputs.resume()
|
|
1176
|
-
```
|
|
1177
|
-
|
|
1178
|
-
Pass these arrays to `push(...events)` from `useSession()`, or to a server
|
|
1179
|
-
session's `append(...events)`. `beforeId: null` moves an item to the end.
|
|
1180
|
-
Removal only applies to pending input. It preserves the immutable log and keeps
|
|
1181
|
-
its original submission event, so replay cannot recreate a removed prompt.
|
|
1182
|
-
Removing an input that is no longer pending returns `not-pending`, whether it was
|
|
1183
|
-
admitted, removed, or never queued.
|
|
1184
|
-
|
|
1185
|
-
Edits to pending input use last-write-wins in session log order. Clients can send
|
|
1186
|
-
another edit before an earlier one is confirmed. Editing preserves the message's
|
|
1187
|
-
identity and role; changing its role returns `role-mismatch`. An edit that loses
|
|
1188
|
-
to admission returns `not-pending`. A Stop aimed at a completed turn returns
|
|
1189
|
-
`stale-turn`.
|
|
1190
|
-
|
|
1191
|
-
Steer validates the new input and places it ahead of the inbox in one decision.
|
|
1192
|
-
If the observed turn is still active, it finishes its current model response and
|
|
1193
|
-
the tools produced by that response before handing off. Parallel tools all
|
|
1194
|
-
settle, and steering runs before the old turn starts another model step.
|
|
1195
|
-
Text and tool results produced after the click remain in the conversation.
|
|
1196
|
-
`lastSeenIndex` is not used to truncate output for steering or Send now.
|
|
1197
|
-
|
|
1198
|
-
When running work settles, unanswered approval or application-input requests
|
|
1199
|
-
are closed so the steering message can take over. The unapproved tool never
|
|
1200
|
-
executes. Provider-executed deferred results still count as running work when
|
|
1201
|
-
execution is authorized; a denied approval cannot block handoff.
|
|
1202
|
-
If the targeted turn has already finished, the input starts when idle or waits
|
|
1203
|
-
first behind a different active turn. Stop remains an immediate interruption.
|
|
1204
|
-
|
|
1205
|
-
Steering preserves the admission gate: steering while paused does not resume
|
|
1206
|
-
execution. Duplicate input identities and closed sessions still reject the command.
|
|
1207
|
-
|
|
1208
|
-
Send now uses the selected queued message's existing identity and latest
|
|
1209
|
-
accepted content. It validates the selected input and the observed `turnId`,
|
|
1210
|
-
then moves the input first for the same handoff after the current step. Use
|
|
1211
|
-
`turnId: null` when no turn is active. If that expectation is stale, the command
|
|
1212
|
-
returns `stale-turn` without changing the queue.
|
|
1213
|
-
|
|
1214
|
-
Send now preserves pause just like steering. A paused queue keeps the selected
|
|
1215
|
-
input first until Resume. It accepts user-role inputs, including passive user
|
|
1216
|
-
input, which it marks for generation. Non-user context returns `not-user-input`;
|
|
1217
|
-
reserved internal IDs are rejected before append. Unsaved editor text
|
|
1218
|
-
is separate from the accepted queue content; save it before sending.
|
|
1219
|
-
|
|
1220
|
-
A queued item's optional `afterStepOf` records the observed turn for steering.
|
|
1221
|
-
A matching active turn hands off after its current step.
|
|
1222
|
-
Editing preserves this intent; removing the item cancels it. Reordering the
|
|
1223
|
-
queue controls which input is admitted at that boundary.
|
|
1224
|
-
|
|
1225
|
-
### Optimistic conversation display
|
|
1226
|
-
|
|
1227
|
-
With `agent().reducer` or `createReducer()`, the client state is already optimistic.
|
|
1228
|
-
An idle, unpaused send appears in `state.messages` immediately, with
|
|
1229
|
-
`state.starting: true`. Another send waits in `state.inbox.items` while that
|
|
1230
|
-
optimistic turn or a confirmed turn is active. Paused sessions keep new sends in
|
|
1231
|
-
the inbox. Render the conversation and queue from `state`. A pending Stop clears
|
|
1232
|
-
the displayed `state.active` turn immediately; rejection restores it. New active
|
|
1233
|
-
turn identities still come from accepted control decisions.
|
|
1234
|
-
Steering appears immediately in `state.messages`, after the current response,
|
|
1235
|
-
and stays there while that step finishes. Send now has the same display when
|
|
1236
|
-
an active turn is targeted. These inputs are omitted from the client inbox,
|
|
1237
|
-
including while paused, but remain pending on the server until admission.
|
|
1238
|
-
Accepted steering survives receipt and hydration in this display. Multiple
|
|
1239
|
-
steering inputs appear in admission order, with the most recent first.
|
|
1240
|
-
No additional request, subscription, or component-owned queue is needed.
|
|
1241
|
-
|
|
1242
|
-
```tsx app/agent/[sessionId]/conversation.tsx
|
|
1243
|
-
'use client'
|
|
1244
|
-
import { useSession } from '../session'
|
|
1245
|
-
|
|
1246
|
-
export function Conversation() {
|
|
1247
|
-
const { state } = useSession()
|
|
1248
|
-
const { messages, inbox, starting } = state
|
|
1249
|
-
const text = (message: (typeof messages)[number]) =>
|
|
1250
|
-
message.parts.filter((part) => part.type === 'text').map((part) => part.text).join(' ')
|
|
1251
|
-
return (
|
|
1252
|
-
<section>
|
|
1253
|
-
<div aria-label="Conversation">
|
|
1254
|
-
{messages.map((message) => <p key={message.id}>{text(message)}</p>)}
|
|
1255
|
-
{starting ? <p role="status">Starting…</p> : null}
|
|
1256
|
-
</div>
|
|
1257
|
-
<ol aria-label="Queued messages">
|
|
1258
|
-
{inbox.items.map((item) => <li key={item.id}>{text(item.message)}</li>)}
|
|
1259
|
-
</ol>
|
|
1260
|
-
</section>
|
|
1261
|
-
)
|
|
1262
|
-
}
|
|
1263
|
-
```
|
|
1264
|
-
|
|
1265
|
-
The request echo keeps the optimistic display until `ai.control.decided` arrives. Accepted
|
|
1266
|
-
commits replace the confirmed base; rejected receipts remove the pending intent.
|
|
1267
|
-
A failed POST rolls back through the ordinary client optimistic overlay.
|
|
1268
|
-
Another client's accepted turn can move a speculative ordinary send back into
|
|
1269
|
-
the queue. Steering stays visible as a message while waiting behind that turn. `state.starting` describes an optimistic turn awaiting acceptance; it does
|
|
1270
|
-
not indicate a running model.
|
|
1271
|
-
|
|
1272
|
-
Server `session.state(reducer)`, reducer folds, checkpoints, and model prompts
|
|
1273
|
-
contain confirmed messages and inbox entries, with `starting: false`. The client
|
|
1274
|
-
applies pending intent and displays accepted steering only when returning its
|
|
1275
|
-
state, after folding events.
|
|
1276
|
-
A custom reducer can keep normalized AI state by nesting `agent.reducer.fold()`
|
|
1277
|
-
and derive its view with `agent.reducer.view()`. It does not inherit the built-in
|
|
1278
|
-
optimistic client display. `reduceAIState()` remains a pure transition over the
|
|
1279
|
-
confirmed `AIState` view for applications that project raw event histories.
|
|
1280
|
-
`state.pendingQueueCommands` tracks unresolved queue commands and Stops incrementally, so
|
|
1281
|
-
the client does not scan raw history. Pending edits apply in order, so the latest
|
|
1282
|
-
local edit stays visible while earlier commands are confirmed. Editing and Send
|
|
1283
|
-
now can remain available while an edit awaits its receipt. Close an editor as
|
|
1284
|
-
soon as its save is pushed; retain the draft if the push fails.
|
|
1285
|
-
|
|
1286
|
-
### Persistence and application are separate
|
|
1287
|
-
|
|
1288
|
-
`push()` confirms that the command is persisted. The `ai.control.decided` event
|
|
1289
|
-
confirms its application, using the submitted event's ID as `commandId`. Its
|
|
1290
|
-
`outcome` is `applied` or `rejected`; a rejection includes a `reason`.
|
|
1291
|
-
`state.receipt` exposes the most recent user command receipt. Applications that
|
|
1292
|
-
have several commands in flight correlate receipts through the existing event
|
|
1293
|
-
stream. A rejection is a normal result and does not spend a handler retry.
|
|
1294
|
-
|
|
1295
|
-
### Pause and resume
|
|
1296
|
-
|
|
1297
|
-
Pause after the turn closes inbox admission and lets the active turn finish.
|
|
1298
|
-
Pause now also cancels current model and tool execution. `state.active.phase`
|
|
1299
|
-
becomes `pausing` while old work settles, then `paused`. Resume reopens admission
|
|
1300
|
-
and resumes retained work before admitting another input.
|
|
1301
|
-
|
|
1302
|
-
Resume starts a fresh model request from durable state. It does not resume the
|
|
1303
|
-
provider's old stream or undo completed tool effects. The controller retains
|
|
1304
|
-
accepted tool calls and records their outcomes while paused. A cancelled tool
|
|
1305
|
-
without a terminal result can execute again after resume; use the stable SDK
|
|
1306
|
-
`toolCallId` for external idempotency. No model or tool claim is held merely to
|
|
1307
|
-
keep a turn paused.
|
|
1308
|
-
|
|
1309
|
-
The controller runs on a short lane. Each decision commits accepted facts,
|
|
1310
|
-
compact state changes, work requests, and its receipt atomically. Workers use
|
|
1311
|
-
separate claims, report lifecycle batches, and wait for acceptance at model-start
|
|
1312
|
-
and compaction boundaries. Token progress appends directly. Explicit AI history
|
|
1313
|
-
reads remain bounded to a relevant generation suffix; core checkpoint rebuilding
|
|
1314
|
-
can replay older events when a snapshot is missing.
|
|
1315
|
-
|
|
1316
|
-
For a deferred provider-executed tool, trusted server code submits
|
|
1317
|
-
`inputs.toolResult(result)`. The controller checks its admitted call and records
|
|
1318
|
-
the result before continuing. Browser ingress rejects this server-only input.
|