experimental-a2 0.18.1 → 0.19.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 +28 -0
- package/dist/{ai-BMLlI2Ff.d.ts → ai-Cz3hqVhk.d.ts} +33 -16
- package/dist/ai-Cz3hqVhk.d.ts.map +1 -0
- package/dist/ai-client.d.ts +1 -1
- package/dist/ai-client.js +1 -1
- package/dist/ai-control-CvLbW10B.js +46 -0
- package/dist/ai-control-CvLbW10B.js.map +1 -0
- package/dist/ai-fx.d.ts +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +30 -10
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-BuLh_EYi.js → ai-harness-BuuC-gJ3.js} +79 -15
- package/dist/ai-harness-BuuC-gJ3.js.map +1 -0
- package/dist/{ai-harness-B2suP2Qh.d.ts → ai-harness-CV6rXbB3.d.ts} +2 -2
- package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -0
- package/dist/{ai-D6mGlO8j.js → ai-mMiWul1T.js} +116 -77
- package/dist/ai-mMiWul1T.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.d.ts.map +1 -1
- package/dist/ai-pi.js +55 -10
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +1 -1
- package/dist/ai-server.d.ts +2 -2
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +118 -50
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +1 -1
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +1 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/docs/agents/03-conversations.mdx +25 -13
- package/docs/agents/04-harnesses.mdx +51 -5
- package/docs/contract/03-sessions.mdx +1 -1
- package/docs/reference/04-ai.mdx +27 -18
- package/docs/reference/05-adapters.mdx +2 -2
- package/docs/server/02-timers.mdx +1 -1
- package/docs/server/05-production.mdx +2 -2
- package/examples/playground/DEPLOY.md +9 -0
- package/examples/playground/app/a0/a0.css +244 -5
- package/examples/playground/app/a0/app-view.tsx +2 -1
- package/examples/playground/app/a0/attachments.ts +133 -0
- package/examples/playground/app/a0/chat.test.tsx +504 -0
- package/examples/playground/app/a0/chat.tsx +623 -63
- package/examples/playground/app/a0/context-menu.tsx +38 -11
- package/examples/playground/app/a0/model.test.ts +28 -0
- package/examples/playground/app/a0/model.ts +30 -0
- package/examples/playground/app/a0/server.ts +5 -0
- package/examples/playground/app/a0/uploads/route.ts +29 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +2 -1
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +1 -0
- package/examples/playground/app/agent/model.test.ts +12 -3
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +1 -5
- package/examples/playground/next.config.ts +7 -4
- package/examples/playground/package.json +3 -1
- package/package.json +1 -1
- package/src/ai-agent.ts +4 -0
- package/src/ai-client-state.ts +21 -9
- package/src/ai-context-schema.ts +28 -4
- package/src/ai-context.ts +83 -3
- package/src/ai-control-server.ts +70 -43
- package/src/ai-control-state.ts +3 -4
- package/src/ai-control.ts +59 -10
- package/src/ai-coordinator.ts +7 -6
- package/src/ai-fx.ts +58 -20
- package/src/ai-harness.ts +153 -29
- package/src/ai-message-shape.ts +41 -0
- package/src/ai-pi.ts +88 -32
- package/src/ai-stored-state.ts +1 -3
- package/src/ai.ts +116 -77
- package/src/libfx.d.ts +8 -1
- package/src/scheduler-vercel.ts +1 -12
- package/dist/ai-BMLlI2Ff.d.ts.map +0 -1
- package/dist/ai-D6mGlO8j.js.map +0 -1
- package/dist/ai-harness-B2suP2Qh.d.ts.map +0 -1
- package/dist/ai-harness-BuLh_EYi.js.map +0 -1
- package/examples/playground/app/a0/no-photon.cjs +0 -4
package/dist/ai.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $ as agentContract, A as InputRespondedPayload, B as PendingApproval, C as GenerationFailedPayload, D as GenerationStartedPayload, E as GenerationRequestedPayload, F as MessageInterruptedPayload, G as SessionClosedPayload, H as ReasoningLevel, I as ModelLimits, J as ToolCalledPayload, K as SessionCreatedPayload, L as ModelMetadataRequestedPayload, M as LanguageModelUsage, N as MessageCompletedPayload, O as GenerationUsage, P as MessageCreatedPayload, Q as UIMessageChunk, R as ModelMetadataResolvedPayload, S as GenerationCompletedPayload, T as GenerationReason, U as RetryGenerationOptions, V as PendingInput, W as RetryRequestedPayload, X as TranscriptMessage, Y as ToolResultPayload, Z as UIMessage, _ as CompactionPart, a as AI_STATE_REDUCER, at as reasoningLevels, b as CompactionState, c as AgentContract, ct as PendingQueueCommand, d as AgentStatus, dt as ControlReceipt, et as createEvents, f as AgentToolContext, ft as InboxItem, g as CompactionCompletedPayload, h as ApprovalRespondedPayload, i as AIState, it as inputs, j as JSONValue, k as InputRequestedPayload, l as AgentContractOptions, lt as ActiveTurn, m as ApprovalRequestedPayload, n as AIInputs, nt as events, o as ActiveGeneration, ot as reduceAIState, p as AgentToolEventDefs, q as ToolActivity, r as AIMessageOf, rt as handlerContext, s as ActiveGenerationProjection, st as isCompactionPart, t as AIEventDefs, tt as deriveUIMessages, u as AgentReducer, ut as ControlCommand, v as CompactionPartData, w as GenerationProgressPayload, x as FinishReason, y as CompactionRequestedPayload, z as ModelMetadataState } from "./ai-
|
|
2
|
-
export { AIEventDefs, AIInputs, AIMessageOf, AIState, AI_STATE_REDUCER, ActiveGeneration, ActiveGenerationProjection, type ActiveTurn, AgentContract, AgentContractOptions, AgentReducer, AgentStatus, AgentToolContext, AgentToolEventDefs, ApprovalRequestedPayload, ApprovalRespondedPayload, CompactionCompletedPayload, CompactionPart, CompactionPartData, CompactionRequestedPayload, CompactionState, type ControlCommand, type ControlReceipt, type FinishReason, GenerationCompletedPayload, GenerationFailedPayload, GenerationProgressPayload, GenerationReason, GenerationRequestedPayload, GenerationStartedPayload, GenerationUsage, type InboxItem, InputRequestedPayload, InputRespondedPayload, type JSONValue, type LanguageModelUsage, MessageCompletedPayload, MessageCreatedPayload, MessageInterruptedPayload, ModelLimits, ModelMetadataRequestedPayload, ModelMetadataResolvedPayload, ModelMetadataState, PendingApproval, PendingInput, type PendingQueueCommand, ReasoningLevel, RetryGenerationOptions, RetryRequestedPayload, SessionClosedPayload, SessionCreatedPayload, ToolActivity, ToolCalledPayload, ToolResultPayload, TranscriptMessage, type UIMessage, type UIMessageChunk, agentContract, createEvents, deriveUIMessages, events, handlerContext, inputs, isCompactionPart, reasoningLevels, reduceAIState };
|
|
1
|
+
import { $ as agentContract, A as InputRespondedPayload, B as PendingApproval, C as GenerationFailedPayload, D as GenerationStartedPayload, E as GenerationRequestedPayload, F as MessageInterruptedPayload, G as SessionClosedPayload, H as ReasoningLevel, I as ModelLimits, J as ToolCalledPayload, K as SessionCreatedPayload, L as ModelMetadataRequestedPayload, M as LanguageModelUsage, N as MessageCompletedPayload, O as GenerationUsage, P as MessageCreatedPayload, Q as UIMessageChunk, R as ModelMetadataResolvedPayload, S as GenerationCompletedPayload, T as GenerationReason, U as RetryGenerationOptions, V as PendingInput, W as RetryRequestedPayload, X as TranscriptMessage, Y as ToolResultPayload, Z as UIMessage, _ as CompactionPart, a as AI_STATE_REDUCER, at as reasoningLevels, b as CompactionState, c as AgentContract, ct as PendingQueueCommand, d as AgentStatus, dt as ControlReceipt, et as createEvents, f as AgentToolContext, ft as InboxItem, g as CompactionCompletedPayload, h as ApprovalRespondedPayload, i as AIState, it as inputs, j as JSONValue, k as InputRequestedPayload, l as AgentContractOptions, lt as ActiveTurn, m as ApprovalRequestedPayload, n as AIInputs, nt as events, o as ActiveGeneration, ot as reduceAIState, p as AgentToolEventDefs, pt as InputRef, q as ToolActivity, r as AIMessageOf, rt as handlerContext, s as ActiveGenerationProjection, st as isCompactionPart, t as AIEventDefs, tt as deriveUIMessages, u as AgentReducer, ut as ControlCommand, v as CompactionPartData, w as GenerationProgressPayload, x as FinishReason, y as CompactionRequestedPayload, z as ModelMetadataState } from "./ai-Cz3hqVhk.js";
|
|
2
|
+
export { AIEventDefs, AIInputs, AIMessageOf, AIState, AI_STATE_REDUCER, ActiveGeneration, ActiveGenerationProjection, type ActiveTurn, AgentContract, AgentContractOptions, AgentReducer, AgentStatus, AgentToolContext, AgentToolEventDefs, ApprovalRequestedPayload, ApprovalRespondedPayload, CompactionCompletedPayload, CompactionPart, CompactionPartData, CompactionRequestedPayload, CompactionState, type ControlCommand, type ControlReceipt, type FinishReason, GenerationCompletedPayload, GenerationFailedPayload, GenerationProgressPayload, GenerationReason, GenerationRequestedPayload, GenerationStartedPayload, GenerationUsage, type InboxItem, type InputRef, InputRequestedPayload, InputRespondedPayload, type JSONValue, type LanguageModelUsage, MessageCompletedPayload, MessageCreatedPayload, MessageInterruptedPayload, ModelLimits, ModelMetadataRequestedPayload, ModelMetadataResolvedPayload, ModelMetadataState, PendingApproval, PendingInput, type PendingQueueCommand, ReasoningLevel, RetryGenerationOptions, RetryRequestedPayload, SessionClosedPayload, SessionCreatedPayload, ToolActivity, ToolCalledPayload, ToolResultPayload, TranscriptMessage, type UIMessage, type UIMessageChunk, agentContract, createEvents, deriveUIMessages, events, handlerContext, inputs, isCompactionPart, reasoningLevels, reduceAIState };
|
package/dist/ai.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as events, c as reasoningLevels, i as deriveUIMessages, l as reduceAIState, n as agentContract, o as handlerContext, r as createEvents, s as inputs, t as AI_STATE_REDUCER, x as isCompactionPart } from "./ai-
|
|
1
|
+
import { a as events, c as reasoningLevels, i as deriveUIMessages, l as reduceAIState, n as agentContract, o as handlerContext, r as createEvents, s as inputs, t as AI_STATE_REDUCER, x as isCompactionPart } from "./ai-mMiWul1T.js";
|
|
2
2
|
export { AI_STATE_REDUCER, agentContract, createEvents, deriveUIMessages, events, handlerContext, inputs, isCompactionPart, reasoningLevels, reduceAIState };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler-vercel.d.ts","names":[],"sources":["../src/scheduler-vercel.ts"],"mappings":";;;;;;;;;;;;cAqDa,uBAAuB;KAExB;EACV;EACA;;;KAIU;EACV,KACE,eACA,SAAS,eACT;IACE;IACA;IACA;MAED;;;;;;;;EAQH,eACE,UACE,kBACA,UAAU,0BACP,iBACH,KAAK,YAAY,QAAQ;;EAE7B,YAAY;;KAGF;;EAEV;;EAEA;;EAEA,YAAY;;EAEZ,QAAQ;;
|
|
1
|
+
{"version":3,"file":"scheduler-vercel.d.ts","names":[],"sources":["../src/scheduler-vercel.ts"],"mappings":";;;;;;;;;;;;cAqDa,uBAAuB;KAExB;EACV;EACA;;;KAIU;EACV,KACE,eACA,SAAS,eACT;IACE;IACA;IACA;MAED;;;;;;;;EAQH,eACE,UACE,kBACA,UAAU,0BACP,iBACH,KAAK,YAAY,QAAQ;;EAE7B,YAAY;;KAGF;;EAEV;;EAEA;;EAEA,YAAY;;EAEZ,QAAQ;;iBA2HM,aAAa,UAAS,sBAA2B"}
|
package/dist/scheduler-vercel.js
CHANGED
|
@@ -39,10 +39,8 @@ import { createHash } from "node:crypto";
|
|
|
39
39
|
var DrainUnsettled = class extends Error {};
|
|
40
40
|
/** Vercel Queues rejects shorter visibility changes. */
|
|
41
41
|
const MIN_VISIBILITY_SECONDS = 30;
|
|
42
|
-
/** Vercel Queues' maximum message retention
|
|
42
|
+
/** Vercel Queues' maximum message retention; its default is a day. */
|
|
43
43
|
const MAX_RETENTION_SECONDS = 604800;
|
|
44
|
-
/** Leave the final retention day for delivery and retries. */
|
|
45
|
-
const MAX_DELAY_SECONDS = 518400;
|
|
46
44
|
const TOPIC_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
47
45
|
const RETRYABLE_HTTP_CLIENT_STATUSES = /* @__PURE__ */ new Set([
|
|
48
46
|
408,
|
|
@@ -119,7 +117,6 @@ function vercelQueues(options = {}) {
|
|
|
119
117
|
const dueAt = requestedDueAt <= nowMs ? Math.floor(nowMs / 1e3) * 1e3 : Math.ceil(requestedDueAt / 1e3) * 1e3;
|
|
120
118
|
const deliveryTargetAt = task.kind === "drain" ? dueAt : requestedDueAt;
|
|
121
119
|
const deliveryDelaySeconds = Math.max(0, Math.ceil((deliveryTargetAt - nowMs) / 1e3));
|
|
122
|
-
if (deliveryDelaySeconds > MAX_DELAY_SECONDS) throw classifySchedulerSendFailure(/* @__PURE__ */ new RangeError("vercelQueues() tasks cannot be delayed more than 6 days"), "terminal");
|
|
123
120
|
const message = {
|
|
124
121
|
...task,
|
|
125
122
|
dueAt
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler-vercel.js","names":[],"sources":["../src/scheduler-vercel.ts"],"sourcesContent":["/**\n * experimental-a2/scheduler-vercel — queue-backed scheduler over Vercel Queues.\n *\n * `vercelQueues()` returns an `A2Scheduler` (a2-implementation.md §9):\n *\n * - drain tasks round the requested watchdog time up to a one-second slot.\n * One message per `(topic, contract, session, dueAt)` coalesces appends,\n * claim renewals and racing callbacks. `DuplicateMessageError` is\n * success because that due-time slot is already durable.\n * - append tasks hash their stable task id with the topic for provider\n * deduplication. Their six-day maximum delay leaves a full day for\n * delivery and retries inside the provider's seven-day retention.\n * Clear tasks follow the same identity and delay rules. All tasks use\n * the configured deployment scope for sending and receiving.\n * - `handler(...servers)` is the delivery route: look up the server for\n * the contract named in the message and drain it. Settled drains ack;\n * contention and handler failure arm a later watchdog before acking.\n *\n * `@vercel/queue` is an optional peer dependency, imported lazily. The\n * `transport` option is the injection seam — tests run a real little\n * in-memory queue through the same interface.\n */\n\nimport { createHash } from 'node:crypto'\nimport { SYSTEM_CLOCK, type Clock } from './store.ts'\nimport {\n assertSchedulerTargets,\n classifySchedulerSendFailure,\n DRAIN_TIMINGS,\n serverInternals,\n} from './internal.ts'\nimport { retryableLazy } from './retryable-lazy.ts'\nimport {\n normalizeSchedulerTask,\n type SchedulerDrainTask,\n type SchedulerTask,\n} from './scheduler-task.ts'\nimport {\n deliverSchedulerAppend,\n deliverSchedulerClear,\n type A2Scheduler,\n type DrainableServer,\n} from './server.ts'\n\n/**\n * The \"not settled yet\" signal for a drain whose outcome is unknown — an\n * expected state, not a transport failure. The default transport classifies\n * it in `@vercel/queue`'s `retry` option and reschedules the delivery quietly\n * (`{ afterSeconds }` → visibility change → 200 response, no error log)\n * instead of letting it surface as a thrown 500. Busy and stalled deliveries\n * use a fresh watchdog instead. Custom transports should treat this class the\n * same way when their protocol has a polite reschedule.\n */\nexport class DrainUnsettled extends Error {}\n\nexport type QueueDeliveryMetadata = {\n messageId: string\n deliveryCount: number\n}\n\n/** What this module needs from a Vercel Queues-compatible transport. */\nexport type QueueTransport = {\n send(\n topic: string,\n message: SchedulerTask,\n options?: {\n delaySeconds?: number\n idempotencyKey?: string\n retentionSeconds?: number\n },\n ): Promise<unknown>\n /**\n * Wrap the delivery handler into a route. The handler throws\n * `DrainUnsettled` when the session needs redelivery — transports\n * with a polite reschedule (visibility change) should use it there\n * instead of surfacing an error; anything else thrown is a genuine\n * failure and should stay loud.\n */\n handleCallback(\n handler: (\n message: unknown,\n metadata: QueueDeliveryMetadata,\n ) => Promise<void>,\n ): (req: Request) => Promise<Response>\n /** Is this send error the idempotency-key collision? (= success) */\n isDuplicate(error: unknown): boolean\n}\n\nexport type VercelQueuesOptions = {\n /** Queue topic matching `[A-Za-z0-9_-]+`. Default `'a2'`. */\n topic?: string\n /** Queue SDK deployment scope for all tasks. Default: VERCEL_DEPLOYMENT_ID; null opts out. */\n deploymentId?: string | null\n /** Injectable transport (tests). Default: `@vercel/queue`. */\n transport?: QueueTransport\n /** Injectable clock for due-time calculations (tests). */\n clock?: Clock\n}\n\n/** Vercel Queues rejects shorter visibility changes. */\nconst MIN_VISIBILITY_SECONDS = 30\n\n/** Vercel Queues' maximum message retention and delivery delay. */\nconst MAX_RETENTION_SECONDS = 7 * 24 * 60 * 60\n\n/** Leave the final retention day for delivery and retries. */\nconst MAX_DELAY_SECONDS = 6 * 24 * 60 * 60\n\nconst TOPIC_PATTERN = /^[A-Za-z0-9_-]+$/\nconst RETRYABLE_HTTP_CLIENT_STATUSES = new Set([408, 409, 425, 429])\n/** The SDK's send path maps these statuses to name-only error instances. */\nconst TERMINAL_VERCEL_SEND_ERROR_NAMES = new Set([\n 'BadRequestError',\n 'UnauthorizedError',\n 'ForbiddenError',\n])\n\nconst recordOf = (value: unknown): Record<string, unknown> | null =>\n value !== null && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null\n\nconst httpStatusOf = (error: unknown): number | undefined => {\n try {\n const record = recordOf(error)\n if (!record) return undefined\n const response = recordOf(record['response'])\n const statuses = [\n record['status'],\n record['statusCode'],\n response?.['status'],\n ].filter(\n (value): value is number =>\n typeof value === 'number' &&\n Number.isInteger(value) &&\n value >= 100 &&\n value <= 599,\n )\n const [status] = statuses\n if (status === undefined) return undefined\n return statuses.every((candidate) => candidate === status)\n ? status\n : undefined\n } catch {\n return undefined\n }\n}\n\nconst errorNameOf = (error: unknown): string | undefined => {\n try {\n const name = recordOf(error)?.['name']\n return typeof name === 'string' ? name : undefined\n } catch {\n return undefined\n }\n}\n\nconst classifyVercelSendFailure = (error: unknown): unknown => {\n const status = httpStatusOf(error)\n const terminal =\n status === undefined\n ? TERMINAL_VERCEL_SEND_ERROR_NAMES.has(errorNameOf(error) ?? '')\n : status >= 400 &&\n status < 500 &&\n !RETRYABLE_HTTP_CLIENT_STATUSES.has(status)\n return classifySchedulerSendFailure(\n error,\n terminal ? 'terminal' : 'retryable',\n )\n}\n\nconst digestId = (parts: readonly string[]): string =>\n `a2_${createHash('sha256').update(JSON.stringify(parts)).digest('hex')}`\n\n/** Base of the unsettled redelivery backoff, doubled per delivery. */\nconst UNSETTLED_BACKOFF_BASE_SECONDS = 5\n\n/** Redelivery backoff for retry states: base, 2×, 4×… bounded. */\nconst unsettledBackoffSeconds = (deliveryCount: number): number =>\n Math.min(\n Math.max(\n UNSETTLED_BACKOFF_BASE_SECONDS * 2 ** Math.max(deliveryCount - 1, 0),\n MIN_VISIBILITY_SECONDS,\n ),\n 60,\n )\n\nconst vercelQueueTransport = async (\n clientOptions: Pick<VercelQueuesOptions, 'deploymentId'>,\n): Promise<QueueTransport> => {\n const queue = await import('@vercel/queue').catch(() => {\n throw classifySchedulerSendFailure(\n new Error(\n \"experimental-a2/scheduler-vercel needs the '@vercel/queue' package (optional peer dependency) — install it, or inject a transport\",\n ),\n 'terminal',\n )\n })\n const client = new queue.QueueClient(clientOptions)\n return {\n send: (topic, message, options) => client.send(topic, message, options),\n handleCallback: (handler) =>\n client.handleCallback(handler, {\n // A hard-killed callback normally has a fresh claim watchdog. If\n // its successor send also failed, make the old unacked delivery's\n // fallback as prompt as Vercel Queues permits.\n visibilityTimeoutSeconds: MIN_VISIBILITY_SECONDS,\n // Not-settled-yet is an expected state: reschedule the (still\n // unacked) delivery via a visibility change — 200, no error\n // log. Everything else returns undefined → the SDK's default\n // throw path, which is loud on purpose.\n retry: (error, metadata) =>\n error instanceof DrainUnsettled\n ? {\n afterSeconds: unsettledBackoffSeconds(metadata.deliveryCount),\n }\n : undefined,\n }) as (req: Request) => Promise<Response>,\n isDuplicate: (error) => error instanceof queue.DuplicateMessageError,\n }\n}\n\nexport function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {\n const topic = options.topic ?? 'a2'\n if (typeof topic !== 'string' || !TOPIC_PATTERN.test(topic)) {\n throw new TypeError('vercelQueues() topic must match [A-Za-z0-9_-]+')\n }\n const clock = options.clock ?? SYSTEM_CLOCK\n const clientOptions =\n options.deploymentId === undefined\n ? {}\n : { deploymentId: options.deploymentId }\n\n // Lazy: constructing the scheduler value does no I/O and works without\n // the peer dep until the first task or delivery.\n const transport = retryableLazy(() =>\n options.transport\n ? Promise.resolve(options.transport)\n : vercelQueueTransport(clientOptions),\n ).get\n\n const sendTask = async (task: SchedulerTask): Promise<void> => {\n const nowMs = clock.now().getTime()\n const requestedDueAt = task.dueAt\n if (!Number.isFinite(requestedDueAt)) {\n throw classifySchedulerSendFailure(\n new TypeError('vercel scheduler dueAt must be a finite number'),\n 'terminal',\n )\n }\n const dueAt =\n requestedDueAt <= nowMs\n ? Math.floor(nowMs / 1_000) * 1_000\n : Math.ceil(requestedDueAt / 1_000) * 1_000\n const deliveryTargetAt = task.kind === 'drain' ? dueAt : requestedDueAt\n const deliveryDelaySeconds = Math.max(\n 0,\n Math.ceil((deliveryTargetAt - nowMs) / 1_000),\n )\n if (deliveryDelaySeconds > MAX_DELAY_SECONDS) {\n throw classifySchedulerSendFailure(\n new RangeError(\n 'vercelQueues() tasks cannot be delayed more than 6 days',\n ),\n 'terminal',\n )\n }\n const message: SchedulerTask = { ...task, dueAt }\n const idempotencyKey =\n task.kind === 'drain'\n ? digestId([\n 'a2:scheduler-vercel:drain:v1',\n topic,\n task.contract,\n task.sessionId,\n String(dueAt),\n ])\n : task.kind === 'append'\n ? digestId(['a2:scheduler-vercel:append:v1', topic, task.id])\n : digestId(['a2:scheduler-vercel:clear:v1', topic, task.id])\n const t = await transport()\n try {\n await t.send(topic, message, {\n delaySeconds: deliveryDelaySeconds,\n idempotencyKey,\n retentionSeconds: MAX_RETENTION_SECONDS,\n })\n } catch (err) {\n if (t.isDuplicate(err)) return\n throw classifyVercelSendFailure(err)\n }\n }\n\n const successorDueAt = (message: SchedulerDrainTask): number => {\n const minimumDueAt =\n clock.now().getTime() +\n DRAIN_TIMINGS.claimTtlMs +\n DRAIN_TIMINGS.watchdogGraceMs\n const beats = Math.max(\n 1,\n Math.ceil(\n (minimumDueAt - message.dueAt) / DRAIN_TIMINGS.claimHeartbeatMs,\n ),\n )\n return message.dueAt + beats * DRAIN_TIMINGS.claimHeartbeatMs\n }\n\n return {\n async schedule(task) {\n await sendTask(task)\n },\n\n handler(...servers: DrainableServer[]) {\n assertSchedulerTargets(servers)\n const byContract = new Map<string, DrainableServer>()\n for (const server of servers) {\n byContract.set(server.contract.name, server)\n }\n return async (req: Request): Promise<Response> => {\n const t = await transport()\n return t.handleCallback(async (delivered) => {\n const task = normalizeSchedulerTask(delivered)\n if (!task) {\n throw new TypeError('a2 scheduler received an invalid task')\n }\n const server = byContract.get(task.contract)\n if (!server) {\n // A message for a contract this route doesn't serve is a\n // wiring bug — throw so it redelivers and stays visible in\n // queue observability instead of vanishing on an ack.\n throw new Error(\n `a2 scheduler: no server for contract '${task.contract}' — pass it to scheduler.handler(...)`,\n )\n }\n const internals = serverInternals.get(server)\n if (task.kind === 'append') {\n await deliverSchedulerAppend(server, task)\n return\n }\n if (task.kind === 'clear') {\n await deliverSchedulerClear(server, task)\n return\n }\n\n const nextDueAt = successorDueAt(task)\n const result = internals\n ? await internals.schedulerDrain(task.sessionId, {\n watchdogDueAt: nextDueAt,\n })\n : {\n ...(await server.drain(task.sessionId)),\n outcome: 'unknown' as const,\n }\n if (result.settled) return\n\n if (result.outcome === 'busy' || result.outcome === 'stalled') {\n // The live owner arms its own claim windows, and the store holds\n // a failed event until its retry time. Ensure a later slot here\n // as well before relinquishing this durable delivery. If the queue is down, the\n // throw leaves the current message unacked for visibility-timeout\n // redelivery.\n if (result.schedulerArm) await result.schedulerArm\n else {\n await sendTask({\n version: 1,\n kind: 'drain',\n contract: task.contract,\n sessionId: task.sessionId,\n dueAt: nextDueAt,\n })\n }\n return\n }\n\n // A drain whose outcome is unknown keeps the current durable message\n // and uses the transport retry path.\n throw new DrainUnsettled(\n `a2 scheduler: session '${task.sessionId}' of '${task.contract}' not settled yet (${result.outcome}) — rescheduled for redelivery`,\n )\n })(req)\n }\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,IAAa,iBAAb,cAAoC,MAAM,CAAC;;AA+C3C,MAAM,yBAAyB;;AAG/B,MAAM,wBAAwB;;AAG9B,MAAM,oBAAoB;AAE1B,MAAM,gBAAgB;AACtB,MAAM,iDAAiC,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;AAAG,CAAC;;AAEnE,MAAM,mDAAmC,IAAI,IAAI;CAC/C;CACA;CACA;AACF,CAAC;AAED,MAAM,YAAY,UAChB,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAC9D,QACD;AAEN,MAAM,gBAAgB,UAAuC;CAC3D,IAAI;EACF,MAAM,SAAS,SAAS,KAAK;EAC7B,IAAI,CAAC,QAAQ,OAAO,KAAA;EACpB,MAAM,WAAW,SAAS,OAAO,WAAW;EAC5C,MAAM,WAAW;GACf,OAAO;GACP,OAAO;GACP,WAAW;EACb,CAAC,CAAC,QACC,UACC,OAAO,UAAU,YACjB,OAAO,UAAU,KAAK,KACtB,SAAS,OACT,SAAS,GACb;EACA,MAAM,CAAC,UAAU;EACjB,IAAI,WAAW,KAAA,GAAW,OAAO,KAAA;EACjC,OAAO,SAAS,OAAO,cAAc,cAAc,MAAM,IACrD,SACA,KAAA;CACN,QAAQ;EACN;CACF;AACF;AAEA,MAAM,eAAe,UAAuC;CAC1D,IAAI;EACF,MAAM,OAAO,SAAS,KAAK,CAAC,GAAG;EAC/B,OAAO,OAAO,SAAS,WAAW,OAAO,KAAA;CAC3C,QAAQ;EACN;CACF;AACF;AAEA,MAAM,6BAA6B,UAA4B;CAC7D,MAAM,SAAS,aAAa,KAAK;CACjC,MAAM,WACJ,WAAW,KAAA,IACP,iCAAiC,IAAI,YAAY,KAAK,KAAK,EAAE,IAC7D,UAAU,OACV,SAAS,OACT,CAAC,+BAA+B,IAAI,MAAM;CAChD,OAAO,6BACL,OACA,WAAW,aAAa,WAC1B;AACF;AAEA,MAAM,YAAY,UAChB,MAAM,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,UAAU,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK;;AAGvE,MAAM,iCAAiC;;AAGvC,MAAM,2BAA2B,kBAC/B,KAAK,IACH,KAAK,IACH,iCAAiC,KAAK,KAAK,IAAI,gBAAgB,GAAG,CAAC,GACnE,sBACF,GACA,EACF;AAEF,MAAM,uBAAuB,OAC3B,kBAC4B;CAC5B,MAAM,QAAQ,MAAM,OAAO,gBAAgB,CAAC,YAAY;EACtD,MAAM,6CACJ,IAAI,MACF,mIACF,GACA,UACF;CACF,CAAC;CACD,MAAM,SAAS,IAAI,MAAM,YAAY,aAAa;CAClD,OAAO;EACL,OAAO,OAAO,SAAS,YAAY,OAAO,KAAK,OAAO,SAAS,OAAO;EACtE,iBAAiB,YACf,OAAO,eAAe,SAAS;GAI7B,0BAA0B;GAK1B,QAAQ,OAAO,aACb,iBAAiB,iBACb,EACE,cAAc,wBAAwB,SAAS,aAAa,EAC9D,IACA,KAAA;EACR,CAAC;EACH,cAAc,UAAU,iBAAiB,MAAM;CACjD;AACF;AAEA,SAAgB,aAAa,UAA+B,CAAC,GAAgB;CAC3E,MAAM,QAAQ,QAAQ,SAAS;CAC/B,IAAI,OAAO,UAAU,YAAY,CAAC,cAAc,KAAK,KAAK,GACxD,MAAM,IAAI,UAAU,gDAAgD;CAEtE,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,gBACJ,QAAQ,iBAAiB,KAAA,IACrB,CAAC,IACD,EAAE,cAAc,QAAQ,aAAa;CAI3C,MAAM,YAAY,oBAChB,QAAQ,YACJ,QAAQ,QAAQ,QAAQ,SAAS,IACjC,qBAAqB,aAAa,CACxC,CAAC,CAAC;CAEF,MAAM,WAAW,OAAO,SAAuC;EAC7D,MAAM,QAAQ,MAAM,IAAI,CAAC,CAAC,QAAQ;EAClC,MAAM,iBAAiB,KAAK;EAC5B,IAAI,CAAC,OAAO,SAAS,cAAc,GACjC,MAAM,6CACJ,IAAI,UAAU,gDAAgD,GAC9D,UACF;EAEF,MAAM,QACJ,kBAAkB,QACd,KAAK,MAAM,QAAQ,GAAK,IAAI,MAC5B,KAAK,KAAK,iBAAiB,GAAK,IAAI;EAC1C,MAAM,mBAAmB,KAAK,SAAS,UAAU,QAAQ;EACzD,MAAM,uBAAuB,KAAK,IAChC,GACA,KAAK,MAAM,mBAAmB,SAAS,GAAK,CAC9C;EACA,IAAI,uBAAuB,mBACzB,MAAM,6CACJ,IAAI,WACF,yDACF,GACA,UACF;EAEF,MAAM,UAAyB;GAAE,GAAG;GAAM;EAAM;EAChD,MAAM,iBACJ,KAAK,SAAS,UACV,SAAS;GACP;GACA;GACA,KAAK;GACL,KAAK;GACL,OAAO,KAAK;EACd,CAAC,IACD,KAAK,SAAS,WACZ,SAAS;GAAC;GAAiC;GAAO,KAAK;EAAE,CAAC,IAC1D,SAAS;GAAC;GAAgC;GAAO,KAAK;EAAE,CAAC;EACjE,MAAM,IAAI,MAAM,UAAU;EAC1B,IAAI;GACF,MAAM,EAAE,KAAK,OAAO,SAAS;IAC3B,cAAc;IACd;IACA,kBAAkB;GACpB,CAAC;EACH,SAAS,KAAK;GACZ,IAAI,EAAE,YAAY,GAAG,GAAG;GACxB,MAAM,0BAA0B,GAAG;EACrC;CACF;CAEA,MAAM,kBAAkB,YAAwC;EAC9D,MAAM,eACJ,MAAM,IAAI,CAAC,CAAC,QAAQ,IACpB,cAAc,aACd,cAAc;EAChB,MAAM,QAAQ,KAAK,IACjB,GACA,KAAK,MACF,eAAe,QAAQ,SAAS,cAAc,gBACjD,CACF;EACA,OAAO,QAAQ,QAAQ,QAAQ,cAAc;CAC/C;CAEA,OAAO;EACL,MAAM,SAAS,MAAM;GACnB,MAAM,SAAS,IAAI;EACrB;EAEA,QAAQ,GAAG,SAA4B;GACrC,uBAAuB,OAAO;GAC9B,MAAM,6BAAa,IAAI,IAA6B;GACpD,KAAK,MAAM,UAAU,SACnB,WAAW,IAAI,OAAO,SAAS,MAAM,MAAM;GAE7C,OAAO,OAAO,QAAoC;IAEhD,QAAO,MADS,UAAU,EAAA,CACjB,eAAe,OAAO,cAAc;KAC3C,MAAM,OAAO,uBAAuB,SAAS;KAC7C,IAAI,CAAC,MACH,MAAM,IAAI,UAAU,uCAAuC;KAE7D,MAAM,SAAS,WAAW,IAAI,KAAK,QAAQ;KAC3C,IAAI,CAAC,QAIH,MAAM,IAAI,MACR,yCAAyC,KAAK,SAAS,sCACzD;KAEF,MAAM,YAAY,gBAAgB,IAAI,MAAM;KAC5C,IAAI,KAAK,SAAS,UAAU;MAC1B,MAAM,uBAAuB,QAAQ,IAAI;MACzC;KACF;KACA,IAAI,KAAK,SAAS,SAAS;MACzB,MAAM,sBAAsB,QAAQ,IAAI;MACxC;KACF;KAEA,MAAM,YAAY,eAAe,IAAI;KACrC,MAAM,SAAS,YACX,MAAM,UAAU,eAAe,KAAK,WAAW,EAC7C,eAAe,UACjB,CAAC,IACD;MACE,GAAI,MAAM,OAAO,MAAM,KAAK,SAAS;MACrC,SAAS;KACX;KACJ,IAAI,OAAO,SAAS;KAEpB,IAAI,OAAO,YAAY,UAAU,OAAO,YAAY,WAAW;MAM7D,IAAI,OAAO,cAAc,MAAM,OAAO;WAEpC,MAAM,SAAS;OACb,SAAS;OACT,MAAM;OACN,UAAU,KAAK;OACf,WAAW,KAAK;OAChB,OAAO;MACT,CAAC;MAEH;KACF;KAIA,MAAM,IAAI,eACR,0BAA0B,KAAK,UAAU,QAAQ,KAAK,SAAS,qBAAqB,OAAO,QAAQ,+BACrG;IACF,CAAC,CAAC,CAAC,GAAG;GACR;EACF;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"scheduler-vercel.js","names":[],"sources":["../src/scheduler-vercel.ts"],"sourcesContent":["/**\n * experimental-a2/scheduler-vercel — queue-backed scheduler over Vercel Queues.\n *\n * `vercelQueues()` returns an `A2Scheduler` (a2-implementation.md §9):\n *\n * - drain tasks round the requested watchdog time up to a one-second slot.\n * One message per `(topic, contract, session, dueAt)` coalesces appends,\n * claim renewals and racing callbacks. `DuplicateMessageError` is\n * success because that due-time slot is already durable.\n * - append tasks hash their stable task id with the topic for provider\n * deduplication. Their six-day maximum delay leaves a full day for\n * delivery and retries inside the provider's seven-day retention.\n * Clear tasks follow the same identity and delay rules. All tasks use\n * the configured deployment scope for sending and receiving.\n * - `handler(...servers)` is the delivery route: look up the server for\n * the contract named in the message and drain it. Settled drains ack;\n * contention and handler failure arm a later watchdog before acking.\n *\n * `@vercel/queue` is an optional peer dependency, imported lazily. The\n * `transport` option is the injection seam — tests run a real little\n * in-memory queue through the same interface.\n */\n\nimport { createHash } from 'node:crypto'\nimport { SYSTEM_CLOCK, type Clock } from './store.ts'\nimport {\n assertSchedulerTargets,\n classifySchedulerSendFailure,\n DRAIN_TIMINGS,\n serverInternals,\n} from './internal.ts'\nimport { retryableLazy } from './retryable-lazy.ts'\nimport {\n normalizeSchedulerTask,\n type SchedulerDrainTask,\n type SchedulerTask,\n} from './scheduler-task.ts'\nimport {\n deliverSchedulerAppend,\n deliverSchedulerClear,\n type A2Scheduler,\n type DrainableServer,\n} from './server.ts'\n\n/**\n * The \"not settled yet\" signal for a drain whose outcome is unknown — an\n * expected state, not a transport failure. The default transport classifies\n * it in `@vercel/queue`'s `retry` option and reschedules the delivery quietly\n * (`{ afterSeconds }` → visibility change → 200 response, no error log)\n * instead of letting it surface as a thrown 500. Busy and stalled deliveries\n * use a fresh watchdog instead. Custom transports should treat this class the\n * same way when their protocol has a polite reschedule.\n */\nexport class DrainUnsettled extends Error {}\n\nexport type QueueDeliveryMetadata = {\n messageId: string\n deliveryCount: number\n}\n\n/** What this module needs from a Vercel Queues-compatible transport. */\nexport type QueueTransport = {\n send(\n topic: string,\n message: SchedulerTask,\n options?: {\n delaySeconds?: number\n idempotencyKey?: string\n retentionSeconds?: number\n },\n ): Promise<unknown>\n /**\n * Wrap the delivery handler into a route. The handler throws\n * `DrainUnsettled` when the session needs redelivery — transports\n * with a polite reschedule (visibility change) should use it there\n * instead of surfacing an error; anything else thrown is a genuine\n * failure and should stay loud.\n */\n handleCallback(\n handler: (\n message: unknown,\n metadata: QueueDeliveryMetadata,\n ) => Promise<void>,\n ): (req: Request) => Promise<Response>\n /** Is this send error the idempotency-key collision? (= success) */\n isDuplicate(error: unknown): boolean\n}\n\nexport type VercelQueuesOptions = {\n /** Queue topic matching `[A-Za-z0-9_-]+`. Default `'a2'`. */\n topic?: string\n /** Queue SDK deployment scope for all tasks. Default: VERCEL_DEPLOYMENT_ID; null opts out. */\n deploymentId?: string | null\n /** Injectable transport (tests). Default: `@vercel/queue`. */\n transport?: QueueTransport\n /** Injectable clock for due-time calculations (tests). */\n clock?: Clock\n}\n\n/** Vercel Queues rejects shorter visibility changes. */\nconst MIN_VISIBILITY_SECONDS = 30\n\n/** Vercel Queues' maximum message retention; its default is a day. */\nconst MAX_RETENTION_SECONDS = 7 * 24 * 60 * 60\n\nconst TOPIC_PATTERN = /^[A-Za-z0-9_-]+$/\nconst RETRYABLE_HTTP_CLIENT_STATUSES = new Set([408, 409, 425, 429])\n/** The SDK's send path maps these statuses to name-only error instances. */\nconst TERMINAL_VERCEL_SEND_ERROR_NAMES = new Set([\n 'BadRequestError',\n 'UnauthorizedError',\n 'ForbiddenError',\n])\n\nconst recordOf = (value: unknown): Record<string, unknown> | null =>\n value !== null && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null\n\nconst httpStatusOf = (error: unknown): number | undefined => {\n try {\n const record = recordOf(error)\n if (!record) return undefined\n const response = recordOf(record['response'])\n const statuses = [\n record['status'],\n record['statusCode'],\n response?.['status'],\n ].filter(\n (value): value is number =>\n typeof value === 'number' &&\n Number.isInteger(value) &&\n value >= 100 &&\n value <= 599,\n )\n const [status] = statuses\n if (status === undefined) return undefined\n return statuses.every((candidate) => candidate === status)\n ? status\n : undefined\n } catch {\n return undefined\n }\n}\n\nconst errorNameOf = (error: unknown): string | undefined => {\n try {\n const name = recordOf(error)?.['name']\n return typeof name === 'string' ? name : undefined\n } catch {\n return undefined\n }\n}\n\nconst classifyVercelSendFailure = (error: unknown): unknown => {\n const status = httpStatusOf(error)\n const terminal =\n status === undefined\n ? TERMINAL_VERCEL_SEND_ERROR_NAMES.has(errorNameOf(error) ?? '')\n : status >= 400 &&\n status < 500 &&\n !RETRYABLE_HTTP_CLIENT_STATUSES.has(status)\n return classifySchedulerSendFailure(\n error,\n terminal ? 'terminal' : 'retryable',\n )\n}\n\nconst digestId = (parts: readonly string[]): string =>\n `a2_${createHash('sha256').update(JSON.stringify(parts)).digest('hex')}`\n\n/** Base of the unsettled redelivery backoff, doubled per delivery. */\nconst UNSETTLED_BACKOFF_BASE_SECONDS = 5\n\n/** Redelivery backoff for retry states: base, 2×, 4×… bounded. */\nconst unsettledBackoffSeconds = (deliveryCount: number): number =>\n Math.min(\n Math.max(\n UNSETTLED_BACKOFF_BASE_SECONDS * 2 ** Math.max(deliveryCount - 1, 0),\n MIN_VISIBILITY_SECONDS,\n ),\n 60,\n )\n\nconst vercelQueueTransport = async (\n clientOptions: Pick<VercelQueuesOptions, 'deploymentId'>,\n): Promise<QueueTransport> => {\n const queue = await import('@vercel/queue').catch(() => {\n throw classifySchedulerSendFailure(\n new Error(\n \"experimental-a2/scheduler-vercel needs the '@vercel/queue' package (optional peer dependency) — install it, or inject a transport\",\n ),\n 'terminal',\n )\n })\n const client = new queue.QueueClient(clientOptions)\n return {\n send: (topic, message, options) => client.send(topic, message, options),\n handleCallback: (handler) =>\n client.handleCallback(handler, {\n // A hard-killed callback normally has a fresh claim watchdog. If\n // its successor send also failed, make the old unacked delivery's\n // fallback as prompt as Vercel Queues permits.\n visibilityTimeoutSeconds: MIN_VISIBILITY_SECONDS,\n // Not-settled-yet is an expected state: reschedule the (still\n // unacked) delivery via a visibility change — 200, no error\n // log. Everything else returns undefined → the SDK's default\n // throw path, which is loud on purpose.\n retry: (error, metadata) =>\n error instanceof DrainUnsettled\n ? {\n afterSeconds: unsettledBackoffSeconds(metadata.deliveryCount),\n }\n : undefined,\n }) as (req: Request) => Promise<Response>,\n isDuplicate: (error) => error instanceof queue.DuplicateMessageError,\n }\n}\n\nexport function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {\n const topic = options.topic ?? 'a2'\n if (typeof topic !== 'string' || !TOPIC_PATTERN.test(topic)) {\n throw new TypeError('vercelQueues() topic must match [A-Za-z0-9_-]+')\n }\n const clock = options.clock ?? SYSTEM_CLOCK\n const clientOptions =\n options.deploymentId === undefined\n ? {}\n : { deploymentId: options.deploymentId }\n\n // Lazy: constructing the scheduler value does no I/O and works without\n // the peer dep until the first task or delivery.\n const transport = retryableLazy(() =>\n options.transport\n ? Promise.resolve(options.transport)\n : vercelQueueTransport(clientOptions),\n ).get\n\n const sendTask = async (task: SchedulerTask): Promise<void> => {\n const nowMs = clock.now().getTime()\n const requestedDueAt = task.dueAt\n if (!Number.isFinite(requestedDueAt)) {\n throw classifySchedulerSendFailure(\n new TypeError('vercel scheduler dueAt must be a finite number'),\n 'terminal',\n )\n }\n const dueAt =\n requestedDueAt <= nowMs\n ? Math.floor(nowMs / 1_000) * 1_000\n : Math.ceil(requestedDueAt / 1_000) * 1_000\n const deliveryTargetAt = task.kind === 'drain' ? dueAt : requestedDueAt\n const deliveryDelaySeconds = Math.max(\n 0,\n Math.ceil((deliveryTargetAt - nowMs) / 1_000),\n )\n const message: SchedulerTask = { ...task, dueAt }\n const idempotencyKey =\n task.kind === 'drain'\n ? digestId([\n 'a2:scheduler-vercel:drain:v1',\n topic,\n task.contract,\n task.sessionId,\n String(dueAt),\n ])\n : task.kind === 'append'\n ? digestId(['a2:scheduler-vercel:append:v1', topic, task.id])\n : digestId(['a2:scheduler-vercel:clear:v1', topic, task.id])\n const t = await transport()\n try {\n await t.send(topic, message, {\n delaySeconds: deliveryDelaySeconds,\n idempotencyKey,\n retentionSeconds: MAX_RETENTION_SECONDS,\n })\n } catch (err) {\n if (t.isDuplicate(err)) return\n throw classifyVercelSendFailure(err)\n }\n }\n\n const successorDueAt = (message: SchedulerDrainTask): number => {\n const minimumDueAt =\n clock.now().getTime() +\n DRAIN_TIMINGS.claimTtlMs +\n DRAIN_TIMINGS.watchdogGraceMs\n const beats = Math.max(\n 1,\n Math.ceil(\n (minimumDueAt - message.dueAt) / DRAIN_TIMINGS.claimHeartbeatMs,\n ),\n )\n return message.dueAt + beats * DRAIN_TIMINGS.claimHeartbeatMs\n }\n\n return {\n async schedule(task) {\n await sendTask(task)\n },\n\n handler(...servers: DrainableServer[]) {\n assertSchedulerTargets(servers)\n const byContract = new Map<string, DrainableServer>()\n for (const server of servers) {\n byContract.set(server.contract.name, server)\n }\n return async (req: Request): Promise<Response> => {\n const t = await transport()\n return t.handleCallback(async (delivered) => {\n const task = normalizeSchedulerTask(delivered)\n if (!task) {\n throw new TypeError('a2 scheduler received an invalid task')\n }\n const server = byContract.get(task.contract)\n if (!server) {\n // A message for a contract this route doesn't serve is a\n // wiring bug — throw so it redelivers and stays visible in\n // queue observability instead of vanishing on an ack.\n throw new Error(\n `a2 scheduler: no server for contract '${task.contract}' — pass it to scheduler.handler(...)`,\n )\n }\n const internals = serverInternals.get(server)\n if (task.kind === 'append') {\n await deliverSchedulerAppend(server, task)\n return\n }\n if (task.kind === 'clear') {\n await deliverSchedulerClear(server, task)\n return\n }\n\n const nextDueAt = successorDueAt(task)\n const result = internals\n ? await internals.schedulerDrain(task.sessionId, {\n watchdogDueAt: nextDueAt,\n })\n : {\n ...(await server.drain(task.sessionId)),\n outcome: 'unknown' as const,\n }\n if (result.settled) return\n\n if (result.outcome === 'busy' || result.outcome === 'stalled') {\n // The live owner arms its own claim windows, and the store holds\n // a failed event until its retry time. Ensure a later slot here\n // as well before relinquishing this durable delivery. If the queue is down, the\n // throw leaves the current message unacked for visibility-timeout\n // redelivery.\n if (result.schedulerArm) await result.schedulerArm\n else {\n await sendTask({\n version: 1,\n kind: 'drain',\n contract: task.contract,\n sessionId: task.sessionId,\n dueAt: nextDueAt,\n })\n }\n return\n }\n\n // A drain whose outcome is unknown keeps the current durable message\n // and uses the transport retry path.\n throw new DrainUnsettled(\n `a2 scheduler: session '${task.sessionId}' of '${task.contract}' not settled yet (${result.outcome}) — rescheduled for redelivery`,\n )\n })(req)\n }\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -446,9 +446,11 @@ export function Queue({ sessionId }: { sessionId: string }) {
|
|
|
446
446
|
```
|
|
447
447
|
|
|
448
448
|
`sendMessage()` puts a message in the durable inbox, `state.inbox.items`. The
|
|
449
|
-
controller admits one input at a time
|
|
450
|
-
|
|
451
|
-
|
|
449
|
+
controller admits one input at a time and adds it to `state.messages`; pending
|
|
450
|
+
inputs never enter the model prompt, and an edit after admission is rejected.
|
|
451
|
+
The log stores a message once, in the command that sent it or the edit that
|
|
452
|
+
last changed it. `state.active` is
|
|
453
|
+
the logical turn: its ID, its `input`, and `phase`. The turn ID holds
|
|
452
454
|
across model steps, tools, and a pause. Stop ends the turn and admits the next
|
|
453
455
|
input when admission is running.
|
|
454
456
|
|
|
@@ -466,15 +468,20 @@ replay cannot recreate the prompt. Removing an input that is no longer pending
|
|
|
466
468
|
(admitted, removed, or never queued) returns `not-pending`, as does an edit that
|
|
467
469
|
loses to admission; a Stop aimed at a finished turn returns `stale-turn`.
|
|
468
470
|
|
|
469
|
-
`steer()` puts new input
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
471
|
+
`steer()` puts new input right after the turn's earlier steering, or first
|
|
472
|
+
when there is none, so steering hands off in the order it was sent. If the observed turn
|
|
473
|
+
is still active, it finishes its current model response and that response's
|
|
474
|
+
tools, including authorized deferred provider results, then hands off before
|
|
475
|
+
another model step. Everything steering that turn enters together as one new
|
|
476
|
+
turn: the earlier messages as context and the last one generating, so the model
|
|
477
|
+
answers them once. Output produced after the click stays, and unanswered
|
|
478
|
+
approval and input requests close, so the unapproved tool never runs; a denied
|
|
479
|
+
approval never blocks the handoff. With no active turn, `steer()` sends like
|
|
480
|
+
`sendMessage()`.
|
|
481
|
+
|
|
482
|
+
`queue.sendNow()` makes the same handoff with a queued message, after the
|
|
483
|
+
steering sent before it, keeping its ID and latest accepted content and
|
|
484
|
+
promoting passive user input to generation.
|
|
478
485
|
Non-user context returns `not-user-input`, and a stale observed turn returns
|
|
479
486
|
`stale-turn` without changing the queue. Send now uses the accepted content, so
|
|
480
487
|
save editor text first. `steer()` and `queue.sendNow()` both preserve pause: a
|
|
@@ -483,7 +490,8 @@ immediate interruption.
|
|
|
483
490
|
|
|
484
491
|
A queued item's optional `afterStepOf` records the observed turn for steering.
|
|
485
492
|
Editing keeps it, removing the item cancels it, and queue order decides which
|
|
486
|
-
|
|
493
|
+
inputs are admitted at the handoff: the run at the front that steers the same
|
|
494
|
+
turn. See [`inputs`](/reference/ai#inputs) for the
|
|
487
495
|
full handoff rules.
|
|
488
496
|
|
|
489
497
|
### Input identity
|
|
@@ -531,6 +539,10 @@ finish. `pause({ when: 'now' })` also cancels current model and tool work;
|
|
|
531
539
|
`state.active.phase` is `pausing` while old work settles, then `paused`.
|
|
532
540
|
`resume()` resumes retained work before admitting another input.
|
|
533
541
|
|
|
542
|
+
A turn paused with Pause now keeps its steering until Resume. Every
|
|
543
|
+
`steer()` or `queue.sendNow()` sent while it's paused targets it, and on Resume
|
|
544
|
+
they enter together as one turn; the paused turn's own work does not resume.
|
|
545
|
+
|
|
534
546
|
Resume starts a fresh model request from durable state; it does not resume the
|
|
535
547
|
provider's old stream or undo completed tool effects. While paused, accepted
|
|
536
548
|
tool calls are retained with their outcomes recorded, and the turn holds no
|
|
@@ -110,17 +110,63 @@ Both harnesses call models through Vercel AI Gateway. `model` is a Gateway
|
|
|
110
110
|
model ID, or [`ModelSettings`](/reference/ai#modelsettings) with one, resolved
|
|
111
111
|
for each step like any agent's model; `pi()` takes the Gateway models Pi's own
|
|
112
112
|
catalog lists. `reasoning` becomes FX's effort or Pi's thinking level, and
|
|
113
|
-
`speed: 'fast'` asks
|
|
114
|
-
`AI_GATEWAY_API_KEY`, or from Vercel's OIDC
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
`speed: 'fast'` asks AI Gateway for the model's faster tier under either
|
|
114
|
+
harness. Credentials come from `AI_GATEWAY_API_KEY`, or from Vercel's OIDC
|
|
115
|
+
token on Vercel. Seeded assistant messages do not reach the harness.
|
|
116
|
+
|
|
117
|
+
## Images and files
|
|
118
|
+
|
|
119
|
+
The harness reads each user message's text and its image files. Neither FX nor
|
|
120
|
+
Pi takes URLs, so A2 fetches each image from its `https:` or `data:` URL and
|
|
121
|
+
hands the harness its bytes. The fetch goes through the
|
|
122
|
+
AI SDK's download, which refuses private addresses, checks each redirect, and
|
|
123
|
+
stops past 32 MiB of one message's files. Each step of a response replays its
|
|
124
|
+
turn, so a process keeps the images it fetched instead of fetching them again.
|
|
125
|
+
|
|
126
|
+
A file that isn't an image, or that A2 can't fetch, fails the turn with an
|
|
127
|
+
error that names it. So does a steer with files under FX, which steers with
|
|
128
|
+
text only. The harness applies its own rules to the images it gets: FX fails a
|
|
129
|
+
turn whose model doesn't take images, and Pi resizes them. Every step sends the
|
|
130
|
+
conversation so far, files and all, so a file that fails one turn fails the
|
|
131
|
+
turns after it too.
|
|
132
|
+
|
|
133
|
+
A2 fetches whatever URL a user message names, so check file URLs on the way
|
|
134
|
+
in. An agent's `messageSchema` can require them to point at your storage:
|
|
135
|
+
|
|
136
|
+
```ts server/reviewer.ts
|
|
137
|
+
import type { UIMessage } from 'ai'
|
|
138
|
+
import { agent } from 'experimental-a2/ai/server'
|
|
139
|
+
import { fx } from 'experimental-a2/ai/fx'
|
|
140
|
+
import { z } from 'zod'
|
|
141
|
+
|
|
142
|
+
// your storage's public URL:
|
|
143
|
+
const uploads = 'https://uploads.example.com/'
|
|
144
|
+
|
|
145
|
+
export const reviewer = agent({
|
|
146
|
+
name: 'reviewer',
|
|
147
|
+
harness: fx(),
|
|
148
|
+
model: 'anthropic/claude-sonnet-5.5',
|
|
149
|
+
instructions: 'Review the screenshots the user sends. Keep replies short.',
|
|
150
|
+
messageSchema: z
|
|
151
|
+
.custom<UIMessage>()
|
|
152
|
+
.refine(
|
|
153
|
+
(message) =>
|
|
154
|
+
message.role !== 'user' ||
|
|
155
|
+
message.parts.every(
|
|
156
|
+
(part) => part.type !== 'file' || part.url.startsWith(uploads),
|
|
157
|
+
),
|
|
158
|
+
"A user's files must be uploads",
|
|
159
|
+
),
|
|
160
|
+
})
|
|
161
|
+
```
|
|
117
162
|
|
|
118
163
|
## What the harness owns
|
|
119
164
|
|
|
120
165
|
The harness owns its loop, its prompt, and its compaction, so `generation`,
|
|
121
166
|
`generate`, `compaction`, and `messageMetadata` exist only in
|
|
122
167
|
`{ type: 'ai-sdk' }`. A harness runs its tools without approval, so a tool that
|
|
123
|
-
sets `needsApproval` is rejected.
|
|
168
|
+
sets `needsApproval` is rejected. It hands the engine only tools A2 runs, so a
|
|
169
|
+
provider-defined tool, such as a provider's web search, is rejected too. `maxSteps` limits the model calls in one
|
|
124
170
|
response under every harness. Steering enters at the next step, as with any
|
|
125
171
|
agent: the harness sees the steered message before its next model call.
|
|
126
172
|
|
|
@@ -109,7 +109,7 @@ append returned.
|
|
|
109
109
|
Delivery runs the same conditional clear and finishes the deletion before it
|
|
110
110
|
acknowledges, so provider retries complete an interrupted delete. No event is
|
|
111
111
|
appended. The clear happens at delivery, not at an exact deadline, within
|
|
112
|
-
provider limits: Vercel Queues accepts delays up to
|
|
112
|
+
provider limits: Vercel Queues accepts delays up to seven days.
|
|
113
113
|
|
|
114
114
|
## Sessions and your database
|
|
115
115
|
|
package/docs/reference/04-ai.mdx
CHANGED
|
@@ -39,8 +39,8 @@ state: serve it with `agent({ contract })` and use it with
|
|
|
39
39
|
through.
|
|
40
40
|
- An `initialState` key that `AIState` also has throws a `TypeError` and fails
|
|
41
41
|
to typecheck.
|
|
42
|
-
- The reducer name is `AI_STATE_REDUCER` (`a2.ai.state.
|
|
43
|
-
`reducer`, else `${reducer.name}+a2.ai.state.
|
|
42
|
+
- The reducer name is `AI_STATE_REDUCER` (`a2.ai.state.v27`) without
|
|
43
|
+
`reducer`, else `${reducer.name}+a2.ai.state.v27`, so checkpoints advance when
|
|
44
44
|
either fold changes. `AgentReducer` types that name (so
|
|
45
45
|
[`createAgentClient()`](#createagentclientoptions) accepts a plain
|
|
46
46
|
`agentContract()`) and carries `X` to `agent()`'s resolvers.
|
|
@@ -61,9 +61,9 @@ even when `M` declares its own data parts.
|
|
|
61
61
|
|
|
62
62
|
### `AIMessageOf<Events>`
|
|
63
63
|
|
|
64
|
-
The `UIMessage` type a contract's `ai.
|
|
65
|
-
`AIMessageOf<typeof contract.events>`; `UIMessage` otherwise.
|
|
66
|
-
messages with it.
|
|
64
|
+
The `UIMessage` type a contract's input commands (`ai.control.requested`)
|
|
65
|
+
carry, such as `AIMessageOf<typeof contract.events>`; `UIMessage` otherwise.
|
|
66
|
+
`agent()` types messages with it.
|
|
67
67
|
|
|
68
68
|
### `handlerContext(contract?)`
|
|
69
69
|
|
|
@@ -105,13 +105,13 @@ Pure typed builders of `ai.control.requested` commands, for `append()` and
|
|
|
105
105
|
| `inputs.requestInput(request)` | requests application input from trusted server code |
|
|
106
106
|
| `inputs.retry(options)` | retries a failed response |
|
|
107
107
|
| `inputs.stop({ turnId, lastSeenIndex? })` | ends the observed turn and advances when admission is open |
|
|
108
|
-
| `inputs.steer({ turnId, message, lastSeenIndex? })` |
|
|
108
|
+
| `inputs.steer({ turnId, message, lastSeenIndex? })` | queues the new input right after the observed turn's earlier steering, or first, for a handoff after its current step; the turn's steering enters together, its last generating input starting the turn |
|
|
109
109
|
| `inputs.queue.edit({ message })` | changes a pending input; its ID stays fixed |
|
|
110
110
|
| `inputs.queue.move({ inputId, beforeId })` | moves pending input before an ID; `null` means the end |
|
|
111
111
|
| `inputs.queue.remove({ inputId })` | removes pending input; its submission stays in the log |
|
|
112
|
-
| `inputs.queue.sendNow({ inputId, turnId, lastSeenIndex? })` | promotes an existing queued input for a handoff after the current step; preserves pause |
|
|
112
|
+
| `inputs.queue.sendNow({ inputId, turnId, lastSeenIndex? })` | promotes an existing queued input, after the observed turn's earlier steering, for a handoff after the current step; preserves pause |
|
|
113
113
|
| `inputs.pause({ when: 'now' \| 'after-turn' })` | pauses execution now or inbox admission after the turn |
|
|
114
|
-
| `inputs.resume()` | resumes retained work, then inbox admission |
|
|
114
|
+
| `inputs.resume()` | resumes retained work, then inbox admission; a turn paused now with steering hands it off instead |
|
|
115
115
|
| `inputs.toolResult(result)` | records a deferred provider-executed tool result from trusted server code |
|
|
116
116
|
| `inputs.interrupt(options)` | interrupts a specific generation/request owner |
|
|
117
117
|
|
|
@@ -174,7 +174,9 @@ Pure typed builders of `ai.control.requested` commands, for `append()` and
|
|
|
174
174
|
- **Interruption.** `ai.message.interrupted` is `{ messageId, requestId,
|
|
175
175
|
reason?, lastSeenIndex? }` or `{ messageId, generationId, reason?,
|
|
176
176
|
lastSeenIndex? }`: copy `activeRequestId` before the generation starts,
|
|
177
|
-
`activeGeneration.generationId` after.
|
|
177
|
+
`activeGeneration.generationId` after. A2 writes the reason
|
|
178
|
+
`'steered after step'` itself when steering ends a response, so an
|
|
179
|
+
interruption you send can't use it. A request-owned interruption survives
|
|
178
180
|
that request starting first. An older or mismatched message/owner pair is a
|
|
179
181
|
no-op. An accepted one fences its owner for good: later events stay in raw
|
|
180
182
|
history but cannot alter `AIState`, satisfy pending work, or reactivate the
|
|
@@ -249,7 +251,7 @@ extra I/O or raw-history reads.
|
|
|
249
251
|
win.
|
|
250
252
|
- Steer, and Send now against an active turn, show in `state.messages` after
|
|
251
253
|
the current response, not the inbox, until admission; they stay out of the
|
|
252
|
-
prompt. Several steering inputs show in
|
|
254
|
+
prompt. Several steering inputs show in the order they were sent,
|
|
253
255
|
and the display survives receipt and hydration, including while paused.
|
|
254
256
|
- A pending Stop clears the displayed `state.active`, generation, and pending
|
|
255
257
|
approvals and inputs, previews an idle status (paused when the inbox is), and
|
|
@@ -274,8 +276,8 @@ events. Encoding is internal; no event or tool payload is truncated.
|
|
|
274
276
|
| Field | What it holds |
|
|
275
277
|
| --- | --- |
|
|
276
278
|
| `messages` | [`TranscriptMessage<M>`](#transcriptmessagem) values; server-side, without pending inputs |
|
|
277
|
-
| `inbox` | `{ paused, items }`; items are `{ id, message, generate, afterStepOf? }`, where `afterStepOf` is an observed steering turn that hands off after its current step (removal cancels it, admission consumes it) |
|
|
278
|
-
| `active` | `null` or `{ turnId, input, phase }
|
|
279
|
+
| `inbox` | `{ paused, items }`; items are `{ id, role, message, generate, afterStepOf? }`, where `afterStepOf` is an observed steering turn that hands off after its current step (removal cancels it, admission consumes it) |
|
|
280
|
+
| `active` | `null` or `{ turnId, input, phase }`, where `input` is the admitted input's `{ id, role, generate }` |
|
|
279
281
|
| `status` | generation status |
|
|
280
282
|
| `starting` | a speculative optimistic turn |
|
|
281
283
|
| `receipt` | the latest user command receipt |
|
|
@@ -311,7 +313,10 @@ See [What happens after a send](/agents/introduction#what-happens-after-a-send).
|
|
|
311
313
|
|
|
312
314
|
`deriveUIMessages()` folds a raw event history into `TranscriptMessage<M>[]`.
|
|
313
315
|
`reduceAIState()` is the event-at-a-time form the standard reducer uses. Both
|
|
314
|
-
project the same chunk-only protocol synchronously.
|
|
316
|
+
project the same chunk-only protocol synchronously. A message's content is in
|
|
317
|
+
the command that brought it in, so a fold needs each `ai.message.created`'s
|
|
318
|
+
command before it; one without throws. Only A2's control lane writes
|
|
319
|
+
`ai.message.created`.
|
|
315
320
|
|
|
316
321
|
## `experimental-a2/ai/server`
|
|
317
322
|
|
|
@@ -525,7 +530,11 @@ name the harness's own tools, such as Pi's `bash`. `model` is resolved per step
|
|
|
525
530
|
as for any agent. `instructions` and `tools` reach the harness; how they
|
|
526
531
|
combine with its own prompt and tools depends on the harness. Tool approval
|
|
527
532
|
policy lives in `harness.generation`, so a harness agent's tools run without
|
|
528
|
-
approval, and a tool that sets `needsApproval` is rejected.
|
|
533
|
+
approval, and a tool that sets `needsApproval` is rejected. So is a
|
|
534
|
+
provider-defined tool (`type: 'provider'`), which the engine can't run. A user
|
|
535
|
+
message reaches the harness as its text and its images, which A2 fetches from
|
|
536
|
+
their `https:` or `data:` URLs; any other file fails the turn. See
|
|
537
|
+
[Images and files](/agents/harnesses#images-and-files).
|
|
529
538
|
|
|
530
539
|
A harness resumes after a crash at the interrupted step. Finished model calls
|
|
531
540
|
are not made again and finished tool calls do not run again. A2 rebuilds the
|
|
@@ -589,8 +598,8 @@ continue a response, A2 restores FX's latest checkpoint and replays the
|
|
|
589
598
|
response so far from the log: recorded model responses and tool results answer
|
|
590
599
|
the kernel without network calls or tool runs, then FX makes its next model
|
|
591
600
|
call. Replay grows with the response, roughly 0.1 to 0.5 seconds for one of 80
|
|
592
|
-
to 200 tool calls.
|
|
593
|
-
|
|
601
|
+
to 200 tool calls. When libfx can't take a checkpoint, A2 replays from an
|
|
602
|
+
earlier one.
|
|
594
603
|
|
|
595
604
|
Credentials come from `AI_GATEWAY_API_KEY`, or Vercel's OIDC token on Vercel.
|
|
596
605
|
This entry point is server-only.
|
|
@@ -617,8 +626,8 @@ forbids, such as a property it does not list. A call to one of the agent's tools
|
|
|
617
626
|
then goes through the tool's schema, as under `{ type: 'ai-sdk' }`: `execute`
|
|
618
627
|
receives the schema's output, defaults included. Input either check rejects goes
|
|
619
628
|
back to the model as an error, without running the tool. `reasoning` becomes
|
|
620
|
-
Pi's thinking level
|
|
621
|
-
lists. Pi works in the sandbox's working directory; its `bash` runs with the
|
|
629
|
+
Pi's thinking level and `speed: 'fast'` asks AI Gateway for the model's fast
|
|
630
|
+
tier; `model` is one of the Gateway models Pi's catalog lists. Pi works in the sandbox's working directory; its `bash` runs with the
|
|
622
631
|
sandbox's environment, never the server's. Pi compacts its own context; a
|
|
623
632
|
compaction appears in the transcript as a `data-a2-compaction` part with its
|
|
624
633
|
summary.
|
|
@@ -189,8 +189,8 @@ export const scheduler = vercelQueues()
|
|
|
189
189
|
Deploymentless delivery lets Vercel choose an eligible consumer, not the
|
|
190
190
|
latest production deployment; every eligible consumer must use
|
|
191
191
|
`deploymentId: null` and stay compatible with pending tasks and session work.
|
|
192
|
-
- Tasks are retained for seven days
|
|
193
|
-
|
|
192
|
+
- Tasks are retained for seven days, Vercel Queues' maximum. A delay goes out
|
|
193
|
+
as asked; Vercel Queues refuses one longer than the retention.
|
|
194
194
|
- `transport` and `clock` are advanced injection seams for tests and custom
|
|
195
195
|
delivery. An injected transport receives the topic, an exact
|
|
196
196
|
`SchedulerTask`, and send options, and owns its provider configuration;
|
|
@@ -119,7 +119,7 @@ handler schedule the next one.
|
|
|
119
119
|
|
|
120
120
|
| Adapter | Maximum one-shot delay |
|
|
121
121
|
| --- | --- |
|
|
122
|
-
| Vercel Queues |
|
|
122
|
+
| Vercel Queues | 7 days |
|
|
123
123
|
| QStash free | 7 days |
|
|
124
124
|
| QStash usage-based | 1 year |
|
|
125
125
|
| QStash fixed | no fixed maximum |
|
|
@@ -143,8 +143,8 @@ The trigger is required. It connects the route to Vercel Queues and makes the
|
|
|
143
143
|
function private, so only queue infrastructure can invoke it. Don't expose the
|
|
144
144
|
callback as a public route or wrap it in a second authentication scheme.
|
|
145
145
|
|
|
146
|
-
The adapter keeps messages for 7 days (Vercel Queues' default is 24 hours)
|
|
147
|
-
|
|
146
|
+
The adapter keeps messages for 7 days (Vercel Queues' default is 24 hours), so
|
|
147
|
+
scheduled appends and clears can be up to 7 days out.
|
|
148
148
|
|
|
149
149
|
### QStash
|
|
150
150
|
|
|
@@ -116,6 +116,15 @@ of a public deployment. A0's sandboxes are named `a2-a0-*`, which is where to
|
|
|
116
116
|
look when cleaning up. The sidebar lists the apps this browser opened, from a
|
|
117
117
|
cookie.
|
|
118
118
|
|
|
119
|
+
Files attached in A0's chat upload from the browser straight to a public
|
|
120
|
+
Vercel Blob store, through a token from `/a0/uploads`. Connect a Blob store to
|
|
121
|
+
the project, which sets `BLOB_READ_WRITE_TOKEN`; `vercel env pull` brings it
|
|
122
|
+
to local runs. Images go to the model with the message; other files go into
|
|
123
|
+
its text as links, which the agent downloads in its sandbox. Each lives at an
|
|
124
|
+
unguessable public URL under `a0/<app id>/`, which anyone with the URL can
|
|
125
|
+
read. Nothing deletes them: the conversation that stays in the log still links
|
|
126
|
+
them.
|
|
127
|
+
|
|
119
128
|
A0's dev tools have a Crash button, which ends the function instance running
|
|
120
129
|
an app's agent, mid-turn, so the scheduler resumes the turn in a fresh one.
|
|
121
130
|
Crashing ends everything else that instance is serving, and under `next dev`
|