experimental-a2 0.19.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +1 -1
- package/dist/{actor-C7b-1arn.d.ts → actor-HnGmrOZZ.d.ts} +5 -5
- package/dist/{actor-C7b-1arn.d.ts.map → actor-HnGmrOZZ.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +2 -2
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +5 -5
- package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
- package/dist/{actor-shared-t7w8xIKP.js.map → actor-shared-DFET5q16.js.map} +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +54 -43
- package/dist/actor.js.map +1 -1
- package/dist/{ai-Cz3hqVhk.d.ts → ai-CvEnqVfo.d.ts} +8 -8
- package/dist/{ai-Cz3hqVhk.d.ts.map → ai-CvEnqVfo.d.ts.map} +1 -1
- package/dist/{ai-mMiWul1T.js → ai-CzE672Q3.js} +9 -9
- package/dist/ai-CzE672Q3.js.map +1 -0
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +36 -9
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-Bfk-Lycn.d.ts} +2 -2
- package/dist/ai-harness-Bfk-Lycn.d.ts.map +1 -0
- package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-dQeJaR1t.js} +17 -4
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +4 -4
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +14 -17
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +84 -75
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BhHLoNKF.js → client-9IeTsiAs.js} +5 -5
- package/dist/{client-BhHLoNKF.js.map → client-9IeTsiAs.js.map} +1 -1
- package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
- package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
- package/dist/contract-CWukt01d.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
- package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
- package/dist/{internal-CscSY7K_.js → internal-Bc4PvfHk.js} +10 -3
- package/dist/internal-Bc4PvfHk.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js +1 -1
- package/dist/otel.js.map +1 -1
- package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
- package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
- package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
- package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
- package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
- package/dist/runtime-consumer-BWmht60z.js +12 -0
- package/dist/runtime-consumer-BWmht60z.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +7 -7
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +5 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-DYAATFuT.d.ts → server-BshFZ0eL.d.ts} +108 -41
- package/dist/server-BshFZ0eL.d.ts.map +1 -0
- package/dist/{server-D-Y4pqwu.js → server-DvIB8kj5.js} +423 -135
- package/dist/server-DvIB8kj5.js.map +1 -0
- package/dist/server.browser.js +1 -1
- package/dist/server.browser.js.map +1 -1
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-Bk_29eEw.d.ts} +2 -2
- package/dist/session-clear-Bk_29eEw.d.ts.map +1 -0
- package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
- package/dist/session-clear-wHd5GCpZ.js.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-Z2qjw50n.d.ts +523 -0
- package/dist/store-Z2qjw50n.d.ts.map +1 -0
- package/dist/store-codec-DwrWlgQx.js +65 -0
- package/dist/store-codec-DwrWlgQx.js.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +393 -210
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-7WJV0YWP.js → store-polling-DYNyxCev.js} +2 -2
- package/dist/{store-polling-7WJV0YWP.js.map → store-polling-DYNyxCev.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +1595 -673
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-B1ixeqIS.js → store-presence-polling-CzQ0wdtM.js} +3 -3
- package/dist/{store-presence-polling-B1ixeqIS.js.map → store-presence-polling-CzQ0wdtM.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +4 -4
- package/dist/store-redis-notify-IiVdlYHc.js +2919 -0
- package/dist/store-redis-notify-IiVdlYHc.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +2 -2
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +517 -287
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
- package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
- package/dist/{wire-BKxH1vbP.js → wire-CZGqNZRx.js} +5 -4
- package/dist/wire-CZGqNZRx.js.map +1 -0
- package/docs/01-quickstart.mdx +28 -19
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +9 -1
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +13 -12
- package/docs/agents/03-conversations.mdx +31 -21
- package/docs/agents/04-harnesses.mdx +7 -3
- package/docs/agents/meta.ts +1 -1
- package/docs/client/03-presence.mdx +18 -14
- package/docs/client/meta.ts +1 -1
- package/docs/contract/01-events.mdx +2 -2
- package/docs/contract/02-state.mdx +18 -14
- package/docs/contract/03-sessions.mdx +32 -25
- package/docs/index.mdx +31 -26
- package/docs/reference/01-core.mdx +12 -12
- package/docs/reference/02-server.mdx +247 -192
- package/docs/reference/04-ai.mdx +29 -20
- package/docs/reference/05-telemetry.mdx +146 -0
- package/docs/reference/06-errors.mdx +24 -18
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +112 -0
- package/docs/schedulers/02-vercel-queues.mdx +87 -0
- package/docs/schedulers/03-qstash.mdx +123 -0
- package/docs/schedulers/meta.ts +1 -0
- package/docs/server/01-consumers.mdx +415 -0
- package/docs/server/02-timers.mdx +33 -23
- package/docs/server/03-cancellation.mdx +52 -44
- package/docs/server/04-routes.mdx +4 -1
- package/docs/server/05-sql-projections.mdx +391 -0
- package/docs/server/06-guarantees.mdx +60 -50
- package/docs/stores/01-overview.mdx +37 -0
- package/docs/stores/02-redis.mdx +57 -0
- package/docs/stores/03-redis-http.mdx +56 -0
- package/docs/stores/04-postgres.mdx +172 -0
- package/docs/stores/05-sqlite.mdx +36 -0
- package/docs/stores/06-memory.mdx +22 -0
- package/docs/stores/meta.ts +1 -0
- package/docs/upgrades/01-overview.mdx +30 -0
- package/docs/upgrades/79-0-21.mdx +165 -0
- package/docs/upgrades/80-0-20.mdx +43 -0
- package/docs/upgrades/81-0-19.mdx +33 -0
- package/docs/upgrades/82-0-18.mdx +33 -0
- package/docs/upgrades/83-0-17.mdx +69 -0
- package/docs/upgrades/meta.ts +3 -0
- package/examples/playground/DEPLOY.md +48 -11
- package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
- package/examples/playground/app/a0/chat.test.tsx +421 -330
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/server.ts +4 -4
- package/examples/playground/app/a0/shell.tsx +1 -1
- package/examples/playground/app/a0/uploads/route.test.ts +59 -0
- package/examples/playground/app/a2slack/a2slack.css +1067 -0
- package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
- package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
- package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
- package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
- package/examples/playground/app/a2slack/consumers/search.ts +37 -0
- package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
- package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
- package/examples/playground/app/a2slack/db/a2.ts +206 -0
- package/examples/playground/app/a2slack/db/index.ts +73 -0
- package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
- package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
- package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
- package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +34 -0
- package/examples/playground/app/a2slack/db/queries.ts +263 -0
- package/examples/playground/app/a2slack/db/schema.ts +145 -0
- package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
- package/examples/playground/app/a2slack/drills/harness.ts +244 -0
- package/examples/playground/app/a2slack/events/route.ts +36 -0
- package/examples/playground/app/a2slack/identity.ts +44 -0
- package/examples/playground/app/a2slack/inbox/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/page.tsx +101 -0
- package/examples/playground/app/a2slack/lag.ts +73 -0
- package/examples/playground/app/a2slack/layout.tsx +43 -0
- package/examples/playground/app/a2slack/messages/route.ts +17 -0
- package/examples/playground/app/a2slack/model.test.ts +208 -0
- package/examples/playground/app/a2slack/model.ts +369 -0
- package/examples/playground/app/a2slack/models.test.ts +72 -0
- package/examples/playground/app/a2slack/page.tsx +10 -0
- package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
- package/examples/playground/app/a2slack/search/route.ts +40 -0
- package/examples/playground/app/a2slack/search-index.ts +108 -0
- package/examples/playground/app/a2slack/server.test.ts +315 -0
- package/examples/playground/app/a2slack/server.ts +55 -0
- package/examples/playground/app/a2slack/sessions.ts +50 -0
- package/examples/playground/app/a2slack/thread/route.ts +8 -0
- package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
- package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
- package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
- package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
- package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
- package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
- package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
- package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
- package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
- package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
- package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
- package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
- package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
- package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
- package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -90
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
- package/examples/playground/app/agent/ingress.test.ts +41 -17
- package/examples/playground/app/agent/model-input.test.tsx +92 -43
- package/examples/playground/app/agent/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +38 -65
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/activity-history.test.tsx +60 -18
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- package/examples/playground/app/components/fast-link.test.tsx +3 -5
- package/examples/playground/app/initial-state.test.tsx +86 -45
- package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
- package/examples/playground/app/orders/page.tsx +1 -1
- package/examples/playground/app/orders/server.ts +49 -30
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
- package/examples/playground/app/recovery/page.tsx +4 -4
- package/examples/playground/app/recovery/server.ts +23 -32
- package/examples/playground/drizzle.config.ts +14 -0
- package/examples/playground/next.config.ts +6 -1
- package/examples/playground/package.json +13 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/test/agent.ts +35 -0
- package/examples/playground/test/model.ts +149 -0
- package/examples/playground/test/serve.ts +184 -0
- package/examples/playground/test/server-only.ts +1 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.config.ts +27 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +7 -2
- package/sql/postgres/drizzle-schema.ts +206 -0
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +276 -0
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +154 -115
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +41 -9
- package/src/ai-harness.ts +28 -2
- package/src/ai-pi.ts +2 -2
- package/src/ai-server.ts +2 -2
- package/src/ai.ts +7 -7
- package/src/contract.ts +1 -1
- package/src/deterministic-id.ts +26 -3
- package/src/errors.ts +9 -6
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +17 -6
- package/src/otel.ts +1 -1
- package/src/postgres-notifications.ts +0 -22
- package/src/postgres-resources.ts +6 -9
- package/src/postgres-schema.ts +121 -0
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-qstash.ts +12 -5
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +10 -4
- package/src/server.browser.ts +1 -1
- package/src/server.ts +839 -238
- package/src/session-socket.ts +1 -1
- package/src/store-codec.ts +66 -0
- package/src/store-memory.ts +581 -255
- package/src/store-postgres.ts +1984 -943
- package/src/store-redis-core.ts +1624 -1152
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +891 -486
- package/src/store.ts +227 -97
- package/src/telemetry.ts +2 -1
- package/src/wire.ts +1 -0
- package/dist/ai-harness-BuuC-gJ3.js.map +0 -1
- package/dist/ai-harness-CV6rXbB3.d.ts.map +0 -1
- package/dist/ai-mMiWul1T.js.map +0 -1
- package/dist/contract-BYKASlCV.js.map +0 -1
- package/dist/idempotent-replay-JdgMQRRm.js +0 -21
- package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
- package/dist/internal-CscSY7K_.js.map +0 -1
- package/dist/server-D-Y4pqwu.js.map +0 -1
- package/dist/server-DYAATFuT.d.ts.map +0 -1
- package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
- package/dist/session-clear-DGI4jTQ5.js.map +0 -1
- package/dist/store-CyLUy86v.d.ts +0 -404
- package/dist/store-CyLUy86v.d.ts.map +0 -1
- package/dist/store-codec-DTG0Ftek.js +0 -10
- package/dist/store-codec-DTG0Ftek.js.map +0 -1
- package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
- package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
- package/dist/wire-BKxH1vbP.js.map +0 -1
- package/docs/reference/05-adapters.mdx +0 -351
- package/docs/server/01-handlers.mdx +0 -200
- package/docs/server/05-production.mdx +0 -323
package/src/ai-harness.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
ReasoningLevel,
|
|
15
15
|
ToolResultPayload,
|
|
16
16
|
} from './ai.ts'
|
|
17
|
+
import { createHash } from 'node:crypto'
|
|
17
18
|
import { STEERED_REASON } from './ai-control.ts'
|
|
18
19
|
import { boundedCache, type BoundedCache } from './bounded-cache.ts'
|
|
19
20
|
import type { ContractEvent, EventDefs } from './contract.ts'
|
|
@@ -322,6 +323,7 @@ export type HarnessInput = { text: string; files: FileUIPart[] }
|
|
|
322
323
|
|
|
323
324
|
/** One harness turn to rebuild: its prompt, completed generations, and end. */
|
|
324
325
|
export type HarnessTurn = {
|
|
326
|
+
/** With any steering that entered before the turn's first completed generation. */
|
|
325
327
|
prompt: HarnessInput
|
|
326
328
|
generations: RecordedGeneration[]
|
|
327
329
|
/** Steering that entered after the generation at that index. */
|
|
@@ -440,8 +442,15 @@ export function harnessTurns(
|
|
|
440
442
|
files: response.prompt.flatMap((prompt) => prompt.files),
|
|
441
443
|
}
|
|
442
444
|
let turn = turns.at(-1)
|
|
443
|
-
if (steered && turn
|
|
444
|
-
|
|
445
|
+
if (steered && turn?.generations.length === 0) {
|
|
446
|
+
// Nothing answered the prompt yet, so the steering joins it, as messages
|
|
447
|
+
// queued before a response join its prompt.
|
|
448
|
+
turn.prompt = {
|
|
449
|
+
text: `${turn.prompt.text}\n\n${input.text}`,
|
|
450
|
+
files: [...turn.prompt.files, ...input.files],
|
|
451
|
+
}
|
|
452
|
+
} else if (steered && turn !== undefined) {
|
|
453
|
+
const at = turn.generations.length - 1
|
|
445
454
|
turn.steers.set(at, [...(turn.steers.get(at) ?? []), input])
|
|
446
455
|
} else {
|
|
447
456
|
turn = { prompt: input, generations: [], steers: new Map(), end: 'live' }
|
|
@@ -598,6 +607,23 @@ export function gatewayCredential(harness: string): GatewayCredential {
|
|
|
598
607
|
throw new Error(`${harness}() needs AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN`)
|
|
599
608
|
}
|
|
600
609
|
|
|
610
|
+
/**
|
|
611
|
+
* The `x-session-affinity` an agent session's Gateway calls carry: stable for
|
|
612
|
+
* the session, so supporting providers can reuse their cache, and free of its
|
|
613
|
+
* raw ID.
|
|
614
|
+
*/
|
|
615
|
+
export function gatewaySessionAffinity({
|
|
616
|
+
agentName,
|
|
617
|
+
sessionId,
|
|
618
|
+
}: {
|
|
619
|
+
agentName: string
|
|
620
|
+
sessionId: string
|
|
621
|
+
}): string {
|
|
622
|
+
return createHash('sha256')
|
|
623
|
+
.update(JSON.stringify([agentName, sessionId]))
|
|
624
|
+
.digest('hex')
|
|
625
|
+
}
|
|
626
|
+
|
|
601
627
|
// Where Vercel Functions keep the current request, as @vercel/oidc reads it.
|
|
602
628
|
const REQUEST_CONTEXT = Symbol.for('@vercel/request-context')
|
|
603
629
|
|
package/src/ai-pi.ts
CHANGED
|
@@ -436,7 +436,7 @@ async function runStep(
|
|
|
436
436
|
const result = results.get(toolCallId)
|
|
437
437
|
if (result !== undefined) return answeredTool(result, shape)
|
|
438
438
|
if (state.plan.end === 'live' && state.liveStep) {
|
|
439
|
-
// Pi now waits on tools that A2's tool
|
|
439
|
+
// Pi now waits on tools that A2's tool consumer run.
|
|
440
440
|
endStep(state)
|
|
441
441
|
return pending(signal)
|
|
442
442
|
}
|
|
@@ -962,7 +962,7 @@ function builtinDefinition(
|
|
|
962
962
|
|
|
963
963
|
/**
|
|
964
964
|
* The tools Pi's session declares: Pi's own, and the agent's. Their results
|
|
965
|
-
* come from A2's log; A2's tool
|
|
965
|
+
* come from A2's log; A2's tool consumer ran them.
|
|
966
966
|
*/
|
|
967
967
|
async function sessionTools(
|
|
968
968
|
modules: PiModules,
|
package/src/ai-server.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* experimental-a2/ai/server — `agent()`: the experimental-a2/ai protocol,
|
|
3
|
-
* its model and tool
|
|
3
|
+
* its model and tool consumers, and A2's store, scheduler, and telemetry,
|
|
4
4
|
* assembled as one ordinary A2 server.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export { agent,
|
|
7
|
+
export { agent, runWithConsumerContext } from './ai-agent.ts'
|
|
8
8
|
export type {
|
|
9
9
|
AISDKHarness,
|
|
10
10
|
Agent,
|
package/src/ai.ts
CHANGED
|
@@ -94,7 +94,7 @@ import {
|
|
|
94
94
|
type ProgressBatches,
|
|
95
95
|
type StoredProgressPayload,
|
|
96
96
|
} from './ai-progress-batches.ts'
|
|
97
|
-
import type {
|
|
97
|
+
import type { ConsumerContext } from './server.ts'
|
|
98
98
|
import type { StandardSchemaV1 } from './standard-schema.ts'
|
|
99
99
|
import { assertSyncSchema, validateSync } from './validate.ts'
|
|
100
100
|
|
|
@@ -462,8 +462,8 @@ export type AgentContract<
|
|
|
462
462
|
export type AgentToolEventDefs = EventDefs &
|
|
463
463
|
Pick<AIEventDefs, 'ai.tool.execution.requested'>
|
|
464
464
|
|
|
465
|
-
/** The durable
|
|
466
|
-
export type AgentToolContext<D extends AgentToolEventDefs> =
|
|
465
|
+
/** The durable consumer context supplied when A2 executes a local AI tool. */
|
|
466
|
+
export type AgentToolContext<D extends AgentToolEventDefs> = ConsumerContext<
|
|
467
467
|
D,
|
|
468
468
|
'ai.tool.execution.requested'
|
|
469
469
|
>
|
|
@@ -2526,20 +2526,20 @@ export const inputs: AIInputs = {
|
|
|
2526
2526
|
}
|
|
2527
2527
|
|
|
2528
2528
|
/**
|
|
2529
|
-
* Read the durable
|
|
2529
|
+
* Read the durable consumer context ambient to the current AI tool
|
|
2530
2530
|
* execution. A2 runs each local tool's `execute` inside this scope, so
|
|
2531
2531
|
* plain AI SDK `tool()` definitions reach their `event`, `attempt`,
|
|
2532
2532
|
* `session`, and `signal` without an A2-specific wrapper. Pass the
|
|
2533
2533
|
* agent's contract to type application events; it is verified against
|
|
2534
2534
|
* the executing agent.
|
|
2535
2535
|
*/
|
|
2536
|
-
export function
|
|
2536
|
+
export function consumerContext<D extends AgentToolEventDefs = AIEventDefs>(
|
|
2537
2537
|
contract?: Contract<D>,
|
|
2538
2538
|
): AgentToolContext<D> {
|
|
2539
2539
|
const scope = ambientToolScopeStorage()?.getStore()
|
|
2540
2540
|
if (scope === undefined) {
|
|
2541
2541
|
throw new TypeError(
|
|
2542
|
-
"
|
|
2542
|
+
"consumerContext() requires an ambient A2 tool execution: call it inside a local tool's execute, or wrap direct tool tests with runWithConsumerContext",
|
|
2543
2543
|
)
|
|
2544
2544
|
}
|
|
2545
2545
|
if (
|
|
@@ -2548,7 +2548,7 @@ export function handlerContext<D extends AgentToolEventDefs = AIEventDefs>(
|
|
|
2548
2548
|
scope.contract.name !== contract.name
|
|
2549
2549
|
) {
|
|
2550
2550
|
throw new TypeError(
|
|
2551
|
-
`
|
|
2551
|
+
`consumerContext('${contract.name}') was called during a tool execution for agent '${scope.contract.name}'`,
|
|
2552
2552
|
)
|
|
2553
2553
|
}
|
|
2554
2554
|
return scope.context as AgentToolContext<D>
|
package/src/contract.ts
CHANGED
|
@@ -123,7 +123,7 @@ export type Contract<
|
|
|
123
123
|
readonly events: Readonly<D>
|
|
124
124
|
/** The presence vocabulary — empty unless declared. */
|
|
125
125
|
readonly presence: Readonly<P>
|
|
126
|
-
/** Preserve literal types for a
|
|
126
|
+
/** Preserve literal types for a consumer-returned event batch. */
|
|
127
127
|
readonly batch: EventBatchFactory<D>
|
|
128
128
|
readonly reducer: R
|
|
129
129
|
}
|
package/src/deterministic-id.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deterministic default ids for
|
|
2
|
+
* Deterministic default ids for consumer `session.append` calls. A consumer
|
|
3
3
|
* re-run after a crash calls `session.append` again; giving
|
|
4
4
|
* each call a deterministic id — derived from the triggering event's id
|
|
5
5
|
* and the caller's stable name — makes the re-run a no-op via the store's
|
|
@@ -32,20 +32,38 @@ async function deterministicId(parts: Array<string | number>): Promise<string> {
|
|
|
32
32
|
return deterministicIdFromInput(parts.join('\u0000'))
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* `consumer` keys the id on the consumer that appends, so two consumers of
|
|
37
|
+
* one event never collide. A2's own consumers pass none: their types are
|
|
38
|
+
* disjoint, so the event alone keys them.
|
|
39
|
+
*/
|
|
35
40
|
export async function deterministicEventId(
|
|
36
41
|
triggerEventId: string,
|
|
37
42
|
name: string,
|
|
38
43
|
itemOrdinal: number,
|
|
44
|
+
consumer?: string,
|
|
39
45
|
): Promise<string> {
|
|
40
46
|
if (name.length === 0) throw new TypeError('append name must not be empty')
|
|
41
|
-
return deterministicId([
|
|
47
|
+
return deterministicId([
|
|
48
|
+
APPEND_NAMESPACE,
|
|
49
|
+
triggerEventId,
|
|
50
|
+
name,
|
|
51
|
+
itemOrdinal,
|
|
52
|
+
...(consumer === undefined ? [] : [consumer]),
|
|
53
|
+
])
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
export async function deterministicReturnedEventId(
|
|
45
57
|
triggerEventId: string,
|
|
46
58
|
itemOrdinal: number,
|
|
59
|
+
consumer?: string,
|
|
47
60
|
): Promise<string> {
|
|
48
|
-
return deterministicId([
|
|
61
|
+
return deterministicId([
|
|
62
|
+
RETURN_NAMESPACE,
|
|
63
|
+
triggerEventId,
|
|
64
|
+
itemOrdinal,
|
|
65
|
+
...(consumer === undefined ? [] : [consumer]),
|
|
66
|
+
])
|
|
49
67
|
}
|
|
50
68
|
|
|
51
69
|
/**
|
|
@@ -60,6 +78,7 @@ export async function deterministicScheduleId(
|
|
|
60
78
|
triggerEventId: string | null,
|
|
61
79
|
name: string,
|
|
62
80
|
clearIndex = 0,
|
|
81
|
+
consumer?: string,
|
|
63
82
|
): Promise<string> {
|
|
64
83
|
return deterministicIdFromInput(
|
|
65
84
|
JSON.stringify([
|
|
@@ -69,6 +88,7 @@ export async function deterministicScheduleId(
|
|
|
69
88
|
triggerEventId,
|
|
70
89
|
name,
|
|
71
90
|
...(clearIndex > 0 ? [clearIndex] : []),
|
|
91
|
+
...(consumer === undefined ? [] : [{ consumer }]),
|
|
72
92
|
]),
|
|
73
93
|
)
|
|
74
94
|
}
|
|
@@ -80,6 +100,7 @@ export async function deterministicScheduleClearId({
|
|
|
80
100
|
clearIndex,
|
|
81
101
|
name,
|
|
82
102
|
ifLastIndex,
|
|
103
|
+
consumer,
|
|
83
104
|
}: {
|
|
84
105
|
contract: string
|
|
85
106
|
sessionId: string
|
|
@@ -87,6 +108,7 @@ export async function deterministicScheduleClearId({
|
|
|
87
108
|
clearIndex: number
|
|
88
109
|
name: string
|
|
89
110
|
ifLastIndex: number
|
|
111
|
+
consumer?: string
|
|
90
112
|
}): Promise<string> {
|
|
91
113
|
return deterministicIdFromInput(
|
|
92
114
|
JSON.stringify([
|
|
@@ -97,6 +119,7 @@ export async function deterministicScheduleClearId({
|
|
|
97
119
|
clearIndex,
|
|
98
120
|
name,
|
|
99
121
|
ifLastIndex,
|
|
122
|
+
...(consumer === undefined ? [] : [{ consumer }]),
|
|
100
123
|
]),
|
|
101
124
|
)
|
|
102
125
|
}
|
package/src/errors.ts
CHANGED
|
@@ -3,13 +3,14 @@ export type A2ServerErrorCode =
|
|
|
3
3
|
| 'FORBIDDEN' // external ingress was denied by server.fetch authorization
|
|
4
4
|
| 'UNKNOWN_EVENT_TYPE' // event type not in the machine's `events` map
|
|
5
5
|
| 'PARTIAL_DUPLICATE_BATCH' // batch mixed already-appended and fresh events
|
|
6
|
-
| 'SUPERSEDED_ATTEMPT' //
|
|
6
|
+
| 'SUPERSEDED_ATTEMPT' // consumer append from an attempt a recovery claim or a clear replaced
|
|
7
7
|
| 'SESSION_CLEARED' // a read or call reached across the session's latest clear
|
|
8
8
|
| 'CLAIM_EXPIRED' // ctx.signal abort reason: the attempt's lease lapsed without renewal
|
|
9
9
|
| 'STORE_UNAVAILABLE' // store backend failure — retryable, nothing was written
|
|
10
|
-
| 'STORE_NOT_CONFIGURED' //
|
|
10
|
+
| 'STORE_NOT_CONFIGURED' // no `store` in production, a missing peer dependency, or Postgres lacks A2's tables
|
|
11
11
|
| 'UNKNOWN_PRESENCE_FIELD' // presence field not in the contract's `presence` map
|
|
12
12
|
| 'PRESENCE_NOT_SUPPORTED' // contract declares presence but the store lacks the capability
|
|
13
|
+
| 'CONSUMER_DEAD_LETTERED' // session.consume reached a head that dead-lettered for that consumer
|
|
13
14
|
|
|
14
15
|
export type A2ErrorCode =
|
|
15
16
|
A2ServerErrorCode | 'TIMEOUT' | 'HTTP_ERROR' | 'CONNECTION_FAILED' | 'UNKNOWN'
|
|
@@ -34,14 +35,16 @@ export class A2Error extends Error {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
/** A
|
|
38
|
+
/** A consumer write whose attempt no longer owns its triggering event. */
|
|
38
39
|
export function supersededAttemptError(
|
|
39
40
|
sessionId: string,
|
|
40
|
-
cause: { index: number; attempt: number },
|
|
41
|
+
cause: { consumer?: string; index: number; attempt: number },
|
|
41
42
|
): A2Error {
|
|
43
|
+
const of =
|
|
44
|
+
cause.consumer === undefined ? '' : ` for consumer '${cause.consumer}'`
|
|
42
45
|
return new A2Error(
|
|
43
46
|
'SUPERSEDED_ATTEMPT',
|
|
44
|
-
`attempt ${cause.attempt} no longer owns event ${cause.index} in session '${sessionId}'`,
|
|
47
|
+
`attempt ${cause.attempt} no longer owns event ${cause.index}${of} in session '${sessionId}'`,
|
|
45
48
|
)
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -58,7 +61,7 @@ export function asStoreUnavailable(err: unknown): A2Error {
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
|
-
* Throw from a
|
|
64
|
+
* Throw from a consumer to reject retrying: the failure is adjudicated
|
|
62
65
|
* terminal immediately (dead-lettered on the first throw) instead of
|
|
63
66
|
* consuming the ten-failure retry budget. For deterministic failures —
|
|
64
67
|
* a guard that will refuse identically on every attempt — retrying is
|
package/src/idempotent-replay.ts
CHANGED
|
@@ -37,7 +37,6 @@ export const idempotentReplay = (
|
|
|
37
37
|
stored === undefined ||
|
|
38
38
|
stored.index !== firstIndex + index ||
|
|
39
39
|
stored.type !== event.type ||
|
|
40
|
-
stored.lane !== (event.lane ?? null) ||
|
|
41
40
|
stored.cause?.batchSize !== event.cause?.batchSize ||
|
|
42
41
|
canonicalJson(stored.payload) !== canonicalJson(event.payload)
|
|
43
42
|
)
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This entry point is isomorphic by construction: the contract (the
|
|
5
5
|
* vocabulary everything shares), reducers derived from it, and the
|
|
6
|
-
* error type. Implementing a contract — storage,
|
|
6
|
+
* error type. Implementing a contract — storage, consumers, sessions —
|
|
7
7
|
* lives in experimental-a2/server, which never enters a browser bundle.
|
|
8
8
|
*/
|
|
9
9
|
|
package/src/internal.ts
CHANGED
|
@@ -7,7 +7,8 @@ import type { ScheduledEvent } from './scheduler-task.ts'
|
|
|
7
7
|
import type { DrainableServer } from './server.ts'
|
|
8
8
|
import type { Event } from './store.ts'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/** `failed`: a scheduler drain threw after arming its successor. */
|
|
11
|
+
export type DrainOutcome = 'busy' | 'stalled' | 'settled' | 'failed'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* A null-prototype record for maps keyed by caller-controlled strings
|
|
@@ -74,7 +75,17 @@ export type ServerInternals = {
|
|
|
74
75
|
export const serverInternals: WeakMap<object, ServerInternals> = new WeakMap()
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
|
-
*
|
|
78
|
+
* Consumers A2's own runtimes define: named under `a2.`, with child ids that
|
|
79
|
+
* leave the name out, since their types are disjoint. Defined through
|
|
80
|
+
* `runtimeConsumer` (runtime-consumer.ts), which the server module installs.
|
|
81
|
+
*/
|
|
82
|
+
export const runtimeConsumers: WeakSet<object> = new WeakSet()
|
|
83
|
+
|
|
84
|
+
export const runtimeConsumerFactory: { define?: (options: never) => unknown } =
|
|
85
|
+
{}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Consumer contexts whose previous attempt ended in a failure the consumer
|
|
78
89
|
* threw, as opposed to a claim that lapsed (a crash, a timeout, a frozen
|
|
79
90
|
* process). Only the attempt's own outcome is known, not how far it got.
|
|
80
91
|
*/
|
|
@@ -83,9 +94,9 @@ export const failedPreviousAttempt: WeakSet<object> = new WeakSet()
|
|
|
83
94
|
const objectLike = (value: unknown): value is object =>
|
|
84
95
|
(typeof value === 'object' && value !== null) || typeof value === 'function'
|
|
85
96
|
|
|
86
|
-
/** The ambient value delivered to `
|
|
97
|
+
/** The ambient value delivered to `consumerContext` during a tool execution. */
|
|
87
98
|
export type AmbientToolScope = {
|
|
88
|
-
/** Executing server identity; a test scope from `
|
|
99
|
+
/** Executing server identity; a test scope from `runWithConsumerContext` has none. */
|
|
89
100
|
contract?: { readonly name: string }
|
|
90
101
|
context: unknown
|
|
91
102
|
}
|
|
@@ -94,7 +105,7 @@ export type AmbientToolScope = {
|
|
|
94
105
|
* Structural AsyncLocalStorage surface for the ambient tool scope. The
|
|
95
106
|
* instance is created by the server-only AI entry point (the only place
|
|
96
107
|
* `node:async_hooks` may be imported) and shared through a global
|
|
97
|
-
* symbol slot, so `
|
|
108
|
+
* symbol slot, so `consumerContext` in the isomorphic AI entry point and
|
|
98
109
|
* independently bundled A2 copies all read the same scope.
|
|
99
110
|
*/
|
|
100
111
|
export type AmbientToolScopeStorage = {
|
|
@@ -266,7 +277,7 @@ export function assertSchedulerTargets(
|
|
|
266
277
|
export const DRAIN_TIMINGS: {
|
|
267
278
|
/** How long a claim lives without renewal. */
|
|
268
279
|
claimTtlMs: number
|
|
269
|
-
/** Heartbeat cadence while a
|
|
280
|
+
/** Heartbeat cadence while a consumer is running. */
|
|
270
281
|
claimHeartbeatMs: number
|
|
271
282
|
/** Base delay before retrying a failed renewal while the lease lives. */
|
|
272
283
|
renewRetryMs: number
|
package/src/otel.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Peer-depends on `@opentelemetry/api` (optional). Spans nest through
|
|
5
5
|
* the active context, so with a context manager registered (any real
|
|
6
6
|
* OTel setup, e.g. `@vercel/otel`) the happy path lands in one trace:
|
|
7
|
-
* request → a2.append → a2.drain → a2.event → the
|
|
7
|
+
* request → a2.append → a2.drain → a2.event → the consumer's own spans.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
@@ -220,25 +220,3 @@ export function postgresNotifications(
|
|
|
220
220
|
}
|
|
221
221
|
return manager
|
|
222
222
|
}
|
|
223
|
-
|
|
224
|
-
export const POSTGRES_NOTIFICATIONS_SQL = `
|
|
225
|
-
create or replace function a2_notify_events() returns trigger language plpgsql as $$
|
|
226
|
-
begin
|
|
227
|
-
perform pg_notify('a2_events_' || md5(new.session_id), '');
|
|
228
|
-
return null;
|
|
229
|
-
end $$;
|
|
230
|
-
create or replace function a2_notify_presence() returns trigger language plpgsql as $$
|
|
231
|
-
begin
|
|
232
|
-
perform pg_notify('a2_presence_' || md5(case when TG_OP = 'DELETE' then old.ns else new.ns end),
|
|
233
|
-
coalesce(current_setting('a2.presence_source', true), ''));
|
|
234
|
-
return null;
|
|
235
|
-
end $$;
|
|
236
|
-
do $$ begin
|
|
237
|
-
if not exists (select 1 from pg_trigger where tgname = 'a2_notify_events' and tgrelid = 'a2_events'::regclass) then
|
|
238
|
-
create trigger a2_notify_events after insert on a2_events for each row execute function a2_notify_events();
|
|
239
|
-
end if;
|
|
240
|
-
if not exists (select 1 from pg_trigger where tgname = 'a2_notify_presence' and tgrelid = 'a2_presence'::regclass) then
|
|
241
|
-
create trigger a2_notify_presence after insert or update or delete on a2_presence for each row execute function a2_notify_presence();
|
|
242
|
-
end if;
|
|
243
|
-
end $$;
|
|
244
|
-
`
|
|
@@ -15,10 +15,7 @@ export type PostgresQueries = {
|
|
|
15
15
|
client: PostgresClient
|
|
16
16
|
raw: PostgresClient
|
|
17
17
|
exclusive<T>(fn: () => Promise<T>): Promise<T>
|
|
18
|
-
initialize(options: {
|
|
19
|
-
schema: string
|
|
20
|
-
run: () => Promise<void>
|
|
21
|
-
}): Promise<void>
|
|
18
|
+
initialize(options: { key: string; run: () => Promise<void> }): Promise<void>
|
|
22
19
|
close(): Promise<void>
|
|
23
20
|
}
|
|
24
21
|
|
|
@@ -128,7 +125,7 @@ export function postgresConnectionKey({
|
|
|
128
125
|
|
|
129
126
|
const queries = (raw: PostgresClient): PostgresQueries => {
|
|
130
127
|
let pending: Promise<unknown> = Promise.resolve()
|
|
131
|
-
const
|
|
128
|
+
const initialized = new Map<string, Promise<void>>()
|
|
132
129
|
const exclusive = <T>(fn: () => Promise<T>): Promise<T> => {
|
|
133
130
|
const result = pending.then(fn)
|
|
134
131
|
pending = result.catch(() => {})
|
|
@@ -140,13 +137,13 @@ const queries = (raw: PostgresClient): PostgresQueries => {
|
|
|
140
137
|
? raw
|
|
141
138
|
: { query: (text, values) => exclusive(() => raw.query(text, values)) },
|
|
142
139
|
exclusive,
|
|
143
|
-
initialize({
|
|
144
|
-
const existing =
|
|
140
|
+
initialize({ key, run }) {
|
|
141
|
+
const existing = initialized.get(key)
|
|
145
142
|
if (existing) return existing
|
|
146
143
|
const attempt = run()
|
|
147
|
-
|
|
144
|
+
initialized.set(key, attempt)
|
|
148
145
|
void attempt.catch(() => {
|
|
149
|
-
if (
|
|
146
|
+
if (initialized.get(key) === attempt) initialized.delete(key)
|
|
150
147
|
})
|
|
151
148
|
return attempt
|
|
152
149
|
},
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Postgres schema contract. A2 never runs DDL in a Postgres database:
|
|
3
|
+
* `sql/postgres/schema.sql`, or the tables in `drizzle-schema.ts` next to it,
|
|
4
|
+
* create
|
|
5
|
+
* the tables, and `notifications.sql` the triggers `listenConnectionString`
|
|
6
|
+
* needs. A store checks nothing up front. A query that finds a table or column
|
|
7
|
+
* missing fails anyway, so the store only turns that failure into one that
|
|
8
|
+
* says what to apply. Missing triggers fail no query, so a listening store
|
|
9
|
+
* looks for them when its listener starts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { A2Error } from './errors.ts'
|
|
13
|
+
import type { PostgresClient, PostgresConnection } from './store-postgres.ts'
|
|
14
|
+
|
|
15
|
+
const PACKAGE = 'node_modules/experimental-a2'
|
|
16
|
+
|
|
17
|
+
const SEE_DOCS = `See ${PACKAGE}/docs/stores/04-postgres.mdx.`
|
|
18
|
+
|
|
19
|
+
/** Postgres error codes a store that hasn't been set up raises. */
|
|
20
|
+
const UNDEFINED_TABLE = '42P01'
|
|
21
|
+
const UNDEFINED_COLUMN = '42703'
|
|
22
|
+
const UNDEFINED_FUNCTION = '42883'
|
|
23
|
+
/** `a2_batch` refuses to run outside read committed. */
|
|
24
|
+
const BATCH_ISOLATION = 'A2002'
|
|
25
|
+
const INSUFFICIENT_PRIVILEGE = '42501'
|
|
26
|
+
/** NUL, which Postgres can't store: in jsonb (`\u0000`), or in text. */
|
|
27
|
+
const UNTRANSLATABLE_CHARACTER = '22P05'
|
|
28
|
+
const CHARACTER_NOT_IN_REPERTOIRE = '22021'
|
|
29
|
+
|
|
30
|
+
/** The Postgres error in `error`'s cause chain, for clients that wrap theirs. */
|
|
31
|
+
const postgresError = (
|
|
32
|
+
error: unknown,
|
|
33
|
+
): { code: string; message: string } | undefined => {
|
|
34
|
+
for (let current = error, depth = 0; depth < 5; depth += 1) {
|
|
35
|
+
if (typeof current !== 'object' || current === null) return undefined
|
|
36
|
+
const { code, message, cause } = current as {
|
|
37
|
+
code?: unknown
|
|
38
|
+
message?: unknown
|
|
39
|
+
cause?: unknown
|
|
40
|
+
}
|
|
41
|
+
if (typeof code === 'string' && /^[0-9A-Z]{5}$/.test(code))
|
|
42
|
+
return { code, message: typeof message === 'string' ? message : code }
|
|
43
|
+
current = cause
|
|
44
|
+
}
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Explains a query that failed because A2's tables aren't set up, or on a NUL, or returns `error`. */
|
|
49
|
+
export const explainPostgresError = (error: unknown): unknown => {
|
|
50
|
+
const found = postgresError(error)
|
|
51
|
+
if (
|
|
52
|
+
found?.code === UNDEFINED_TABLE ||
|
|
53
|
+
found?.code === UNDEFINED_COLUMN ||
|
|
54
|
+
found?.code === UNDEFINED_FUNCTION
|
|
55
|
+
)
|
|
56
|
+
return new A2Error(
|
|
57
|
+
'STORE_NOT_CONFIGURED',
|
|
58
|
+
`${found.message}: A2's tables or its function are missing, or from an earlier release, and A2 doesn't create or change them at runtime. Apply ${PACKAGE}/sql/postgres/schema.sql to this database, or migrate it to the tables in drizzle-schema.ts next to it, with schema.sql's a2_batch function. ${SEE_DOCS}`,
|
|
59
|
+
{ cause: error },
|
|
60
|
+
)
|
|
61
|
+
if (found?.code === INSUFFICIENT_PRIVILEGE)
|
|
62
|
+
return new A2Error(
|
|
63
|
+
'STORE_NOT_CONFIGURED',
|
|
64
|
+
`${found.message}: the role this store connects as needs select, insert, update, and delete on A2's tables, and execute on a2_batch. Create them as that role, or grant it those privileges. ${SEE_DOCS}`,
|
|
65
|
+
{ cause: error },
|
|
66
|
+
)
|
|
67
|
+
// Nothing was written: the payload is the problem, so retrying can't help.
|
|
68
|
+
if (
|
|
69
|
+
found?.code === UNTRANSLATABLE_CHARACTER ||
|
|
70
|
+
found?.code === CHARACTER_NOT_IN_REPERTOIRE
|
|
71
|
+
)
|
|
72
|
+
return new A2Error(
|
|
73
|
+
'INVALID_PAYLOAD',
|
|
74
|
+
`${found.message}: Postgres can't store a NUL character (\\u0000) in text or jsonb, so nothing was written. Remove it before appending.`,
|
|
75
|
+
{ cause: error },
|
|
76
|
+
)
|
|
77
|
+
if (found?.code === BATCH_ISOLATION)
|
|
78
|
+
return new A2Error(
|
|
79
|
+
'STORE_NOT_CONFIGURED',
|
|
80
|
+
`${found.message}: A2 reads what it locks in read committed, Postgres's default, and its connections here default to a stricter level. Set default_transaction_isolation to 'read committed' for the role or database this store connects to. ${SEE_DOCS}`,
|
|
81
|
+
{ cause: error },
|
|
82
|
+
)
|
|
83
|
+
return error
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const explained = <T>(promise: Promise<T>): Promise<T> =>
|
|
87
|
+
promise.catch((error: unknown) => {
|
|
88
|
+
throw explainPostgresError(error)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/** `client`, with every query's setup failures explained. */
|
|
92
|
+
export const explainingPostgresClient = (
|
|
93
|
+
client: PostgresClient,
|
|
94
|
+
): PostgresClient => ({
|
|
95
|
+
query: (text, values) => explained(client.query(text, values)),
|
|
96
|
+
...(client.connect && {
|
|
97
|
+
connect: async (): Promise<PostgresConnection> => {
|
|
98
|
+
const connection = await client.connect!()
|
|
99
|
+
return {
|
|
100
|
+
query: (text, values) => explained(connection.query(text, values)),
|
|
101
|
+
release: () => connection.release(),
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
...(client.end && { end: () => client.end!() }),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
/** Fails when the triggers `listenConnectionString` listens to are missing. */
|
|
109
|
+
export const checkPostgresTriggers = async (
|
|
110
|
+
client: PostgresClient,
|
|
111
|
+
): Promise<void> => {
|
|
112
|
+
const { rows } =
|
|
113
|
+
await client.query(`select count(*) as triggers from pg_trigger
|
|
114
|
+
where (tgname = 'a2_notify_events' and tgrelid = to_regclass('a2_events'))
|
|
115
|
+
or (tgname = 'a2_notify_presence' and tgrelid = to_regclass('a2_presence'))`)
|
|
116
|
+
if (Number(rows[0]?.['triggers']) < 2)
|
|
117
|
+
throw new A2Error(
|
|
118
|
+
'STORE_NOT_CONFIGURED',
|
|
119
|
+
`listenConnectionString needs A2's notification triggers, which aren't in this Postgres database. After schema.sql, apply ${PACKAGE}/sql/postgres/notifications.sql to this database. ${SEE_DOCS}`,
|
|
120
|
+
)
|
|
121
|
+
}
|
package/src/push-envelope.ts
CHANGED
|
@@ -77,6 +77,13 @@ export function parsePresenceSibling(
|
|
|
77
77
|
return out
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** The longest event id a push may carry: ids are indexed, and Postgres's index rows have a limit. */
|
|
81
|
+
const MAX_PUSHED_ID = 256
|
|
82
|
+
|
|
83
|
+
/** A UUIDv5, the shape of the ids A2 derives (deterministic-id.ts). */
|
|
84
|
+
const DERIVED_ID =
|
|
85
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
86
|
+
|
|
80
87
|
/** The events half of the envelope, shared by the POST body and socket
|
|
81
88
|
* `push` frames so INVALID_PAYLOAD shapes have one implementation. */
|
|
82
89
|
export function parsePushEvents(events: unknown): ParsedPushEvent[] {
|
|
@@ -94,6 +101,19 @@ export function parsePushEvents(events: unknown): ParsedPushEvent[] {
|
|
|
94
101
|
if (id !== undefined && typeof id !== 'string') {
|
|
95
102
|
throw invalid(`events[${i}].id must be a string when present`)
|
|
96
103
|
}
|
|
104
|
+
if (id !== undefined && (id.length === 0 || id.length > MAX_PUSHED_ID)) {
|
|
105
|
+
throw invalid(
|
|
106
|
+
`events[${i}].id must be 1-${MAX_PUSHED_ID} characters when present`,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
// A2 derives the ids of what consumers append, return, and schedule as
|
|
110
|
+
// UUIDv5s, from inputs a caller can know: one pushed first would take its
|
|
111
|
+
// place. Callers' own ids are v4 (crypto.randomUUID) or v8 (idempotentId).
|
|
112
|
+
if (id !== undefined && DERIVED_ID.test(id)) {
|
|
113
|
+
throw invalid(
|
|
114
|
+
`events[${i}].id is shaped like the ids A2 derives for consumers (a UUIDv5); use crypto.randomUUID() or idempotentId()`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
97
117
|
const out: { type: string; payload: unknown; id?: string } = {
|
|
98
118
|
type,
|
|
99
119
|
payload,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `consumer()` for A2's own runtimes (actors, the AI runtime): their names
|
|
3
|
+
* sit under the reserved `a2.` prefix, and their child ids leave the name
|
|
4
|
+
* out, as the handler tables they replaced had. Not a public entry point.
|
|
5
|
+
*/
|
|
6
|
+
import { runtimeConsumerFactory } from './internal.ts'
|
|
7
|
+
import type { consumer } from './server.ts'
|
|
8
|
+
|
|
9
|
+
export const runtimeConsumer = ((options: never) =>
|
|
10
|
+
runtimeConsumerFactory.define!(options)) as typeof consumer
|
package/src/scheduler-qstash.ts
CHANGED
|
@@ -497,8 +497,11 @@ const readBoundedBody = async (
|
|
|
497
497
|
return new TextDecoder().decode(bytes)
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
-
|
|
501
|
-
|
|
500
|
+
// A store that isn't set up yet (Postgres before schema.sql, a missing driver)
|
|
501
|
+
// is fixed without changing the task, so its work waits like any outage.
|
|
502
|
+
const storeOutage = (error: unknown): error is A2Error =>
|
|
503
|
+
error instanceof A2Error &&
|
|
504
|
+
(error.code === 'STORE_UNAVAILABLE' || error.code === 'STORE_NOT_CONFIGURED')
|
|
502
505
|
|
|
503
506
|
const response = {
|
|
504
507
|
ok: (): Response => new Response('ok'),
|
|
@@ -888,7 +891,7 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
|
|
|
888
891
|
await deliverSchedulerAppend(server, task)
|
|
889
892
|
else await deliverSchedulerClear(server, task)
|
|
890
893
|
} catch (error) {
|
|
891
|
-
if (!
|
|
894
|
+
if (!storeOutage(error)) throw error
|
|
892
895
|
await sendStoreOutageSuccessor(task, outage)
|
|
893
896
|
}
|
|
894
897
|
return response.ok()
|
|
@@ -906,13 +909,17 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
|
|
|
906
909
|
outcome: 'unknown' as const,
|
|
907
910
|
}
|
|
908
911
|
} catch (error) {
|
|
909
|
-
if (!
|
|
912
|
+
if (!storeOutage(error)) throw error
|
|
910
913
|
await sendStoreOutageSuccessor(task, outage)
|
|
911
914
|
return response.ok()
|
|
912
915
|
}
|
|
913
916
|
if (result.settled) return response.ok()
|
|
914
917
|
|
|
915
|
-
if (
|
|
918
|
+
if (
|
|
919
|
+
result.outcome === 'busy' ||
|
|
920
|
+
result.outcome === 'stalled' ||
|
|
921
|
+
result.outcome === 'failed'
|
|
922
|
+
) {
|
|
916
923
|
if (result.schedulerArm) await result.schedulerArm
|
|
917
924
|
else {
|
|
918
925
|
await sendTask({
|