experimental-a2 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/{actor-DJi3RsNu.d.ts → actor-BfQSE0KC.d.ts} +4 -4
  3. package/dist/{actor-DJi3RsNu.d.ts.map → actor-BfQSE0KC.d.ts.map} +1 -1
  4. package/dist/actor-client.d.ts +1 -1
  5. package/dist/actor-client.js +1 -1
  6. package/dist/actor-react.d.ts +3 -3
  7. package/dist/actor-react.js +2 -2
  8. package/dist/{actor-shared-DI7J5upy.js → actor-shared-B5tJfzt-.js} +2 -2
  9. package/dist/{actor-shared-DI7J5upy.js.map → actor-shared-B5tJfzt-.js.map} +1 -1
  10. package/dist/actor.d.ts +1 -1
  11. package/dist/actor.js +3 -3
  12. package/dist/ai-Cai-lCbj.d.ts +580 -0
  13. package/dist/ai-Cai-lCbj.d.ts.map +1 -0
  14. package/dist/ai-control-CcD4hh3y.js +119 -0
  15. package/dist/ai-control-CcD4hh3y.js.map +1 -0
  16. package/dist/ai-server.d.ts +6 -6
  17. package/dist/ai-server.d.ts.map +1 -1
  18. package/dist/ai-server.js +1014 -501
  19. package/dist/ai-server.js.map +1 -1
  20. package/dist/ai.d.ts +2 -365
  21. package/dist/ai.js +801 -80
  22. package/dist/ai.js.map +1 -1
  23. package/dist/{client-P_NNNRM-.d.ts → client-BAEABRZB.d.ts} +2 -2
  24. package/dist/{client-P_NNNRM-.d.ts.map → client-BAEABRZB.d.ts.map} +1 -1
  25. package/dist/{client-Bf6uSEAk.js → client-BYzHjkwU.js} +21 -6
  26. package/dist/client-BYzHjkwU.js.map +1 -0
  27. package/dist/client.d.ts +1 -1
  28. package/dist/client.js +1 -1
  29. package/dist/{contract-48bUMgcL.js → contract-CKRg_E4q.js} +3 -26
  30. package/dist/contract-CKRg_E4q.js.map +1 -0
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +1 -1
  33. package/dist/react.d.ts +2 -2
  34. package/dist/react.js +1 -1
  35. package/dist/{reducer-DJKWm3cp.d.ts → reducer-BcS9VDKC.d.ts} +4 -1
  36. package/dist/{reducer-DJKWm3cp.d.ts.map → reducer-BcS9VDKC.d.ts.map} +1 -1
  37. package/dist/reducer-DEMjEY_O.js +29 -0
  38. package/dist/reducer-DEMjEY_O.js.map +1 -0
  39. package/dist/scheduler-qstash.d.ts +2 -2
  40. package/dist/scheduler-qstash.js +1 -1
  41. package/dist/scheduler-vercel.d.ts +2 -2
  42. package/dist/scheduler-vercel.js +1 -1
  43. package/dist/{server-DjZZa1wr.d.ts → server-Bp5Nd1pF.d.ts} +3 -3
  44. package/dist/{server-DjZZa1wr.d.ts.map → server-Bp5Nd1pF.d.ts.map} +1 -1
  45. package/dist/{server-BeNADlCI.js → server-CjJSGcF7.js} +4 -3
  46. package/dist/server-CjJSGcF7.js.map +1 -0
  47. package/dist/server.d.ts +3 -3
  48. package/dist/server.js +1 -1
  49. package/dist/{store-DtDOWLSn.d.ts → store-D_yhNdPz.d.ts} +7 -2
  50. package/dist/{store-DtDOWLSn.d.ts.map → store-D_yhNdPz.d.ts.map} +1 -1
  51. package/dist/store-N8PXxDAS.js.map +1 -1
  52. package/dist/store-memory.d.ts +1 -1
  53. package/dist/store-postgres.d.ts +1 -1
  54. package/dist/store-postgres.js +19 -0
  55. package/dist/store-postgres.js.map +1 -1
  56. package/dist/store-redis-http.d.ts +1 -1
  57. package/dist/store-redis-http.js +1 -1
  58. package/dist/{store-redis-notify-BUCyXOn0.js → store-redis-notify-D2EI6gwX.js} +27 -2
  59. package/dist/store-redis-notify-D2EI6gwX.js.map +1 -0
  60. package/dist/store-redis.d.ts +1 -1
  61. package/dist/store-redis.js +1 -1
  62. package/dist/store-sqlite.d.ts +1 -1
  63. package/docs/guides/06-ai-agents.mdx +265 -62
  64. package/docs/reference/01-api.mdx +122 -27
  65. package/examples/playground/app/agent/[agentId]/agent-client.tsx +118 -21
  66. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +289 -0
  67. package/examples/playground/app/agent/compaction-settings.test.ts +22 -6
  68. package/examples/playground/app/agent/model.ts +11 -2
  69. package/examples/playground/app/agent/server.ts +8 -2
  70. package/examples/playground/app/chat/[chatId]/chat-client.tsx +3 -13
  71. package/examples/playground/app/chat/model.ts +2 -2
  72. package/examples/playground/app/chat/server.ts +24 -17
  73. package/examples/playground/app/globals.css +179 -0
  74. package/examples/playground/package.json +1 -1
  75. package/package.json +1 -1
  76. package/src/ai-client-state.ts +185 -0
  77. package/src/ai-control-server.ts +829 -0
  78. package/src/ai-control-state.ts +152 -0
  79. package/src/ai-control.ts +139 -0
  80. package/src/ai-coordinator.ts +99 -32
  81. package/src/ai-progress-batches.ts +68 -0
  82. package/src/ai-projector.ts +76 -15
  83. package/src/ai-sdk-step.ts +0 -1
  84. package/src/ai-server.ts +381 -608
  85. package/src/ai.ts +553 -108
  86. package/src/client.ts +31 -9
  87. package/src/licenses/Apache-2.0.txt +55 -0
  88. package/src/parse-partial-json.ts +441 -0
  89. package/src/reducer.ts +6 -0
  90. package/src/server.ts +8 -4
  91. package/src/store-postgres.ts +27 -0
  92. package/src/store-redis-core.ts +53 -1
  93. package/src/store-redis-notify.ts +1 -0
  94. package/src/store.ts +6 -0
  95. package/dist/ai.d.ts.map +0 -1
  96. package/dist/client-Bf6uSEAk.js.map +0 -1
  97. package/dist/contract-48bUMgcL.js.map +0 -1
  98. package/dist/server-BeNADlCI.js.map +0 -1
  99. package/dist/store-redis-notify-BUCyXOn0.js.map +0 -1
package/dist/ai-server.js CHANGED
@@ -1,150 +1,10 @@
1
+ import { n as validateSync } from "./validate-XKT4FSNn.js";
1
2
  import { t as A2Error } from "./errors-DCk6ch5n.js";
2
3
  import { f as consumeSchedulerSendFailure, p as installAmbientToolScopeStorage } from "./internal-Dq2qYxou.js";
3
- import { i as setServerFetchHooks, t as createServer } from "./server-BeNADlCI.js";
4
+ import { i as setServerFetchHooks, t as createServer } from "./server-CjJSGcF7.js";
5
+ import { n as applyControlChanges, r as controlCommit, t as initialControlState } from "./ai-control-CcD4hh3y.js";
4
6
  import { AsyncLocalStorage } from "node:async_hooks";
5
7
  import { asSchema, convertToModelMessages, stepCountIs, streamText, toUIMessageStream } from "ai";
6
- //#region src/ai-sdk-step.ts
7
- const CONTROLLED_SETTINGS = [
8
- "_internal",
9
- "abortSignal",
10
- "instructions",
11
- "messages",
12
- "model",
13
- "onToolExecutionEnd",
14
- "onToolExecutionStart",
15
- "experimental_onToolCallFinish",
16
- "experimental_onToolCallStart",
17
- "experimental_sandbox",
18
- "experimental_toolCallers",
19
- "experimental_toolApprovalSecret",
20
- "prompt",
21
- "prepareStep",
22
- "stopWhen",
23
- "system",
24
- "tools"
25
- ];
26
- const safeSettings = (settings) => {
27
- const result = { ...settings };
28
- for (const key of CONTROLLED_SETTINGS) Reflect.deleteProperty(result, key);
29
- if (typeof result.timeout === "object" && result.timeout !== null) {
30
- const timeout = { ...result.timeout };
31
- Reflect.deleteProperty(timeout, "toolMs");
32
- Reflect.deleteProperty(timeout, "tools");
33
- result.timeout = timeout;
34
- }
35
- return result;
36
- };
37
- const modelToolSet = (tools) => Object.fromEntries(Object.entries(tools).map(([name, tool]) => {
38
- if (tool.type === "provider" && tool.isProviderExecuted === true) return [name, tool];
39
- const definition = { ...tool };
40
- Reflect.deleteProperty(definition, "execute");
41
- return [name, definition];
42
- }));
43
- const errorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
44
- /** Run exactly one AI SDK model step while leaving local tool execution to A2. */
45
- async function generateAISDKStep(input) {
46
- const settings = safeSettings(input.settings);
47
- const tools = modelToolSet(input.tools);
48
- const messages = input.modelMessages ?? await convertToModelMessages(input.messages, { tools });
49
- const streamOptions = {
50
- ...settings,
51
- model: input.model,
52
- tools,
53
- messages,
54
- abortSignal: input.abortSignal,
55
- ...input.instructions === void 0 ? {} : { instructions: input.instructions },
56
- stopWhen: stepCountIs(1),
57
- onError: settings.onError ?? (() => {})
58
- };
59
- const result = streamText(streamOptions);
60
- let completion;
61
- let fatalError;
62
- const observed = result.stream.pipeThrough(new TransformStream({ transform(part, controller) {
63
- if (part.type === "error" && fatalError === void 0) fatalError = { value: part.error };
64
- else if (part.type === "finish") completion = {
65
- finishReason: part.finishReason,
66
- usage: part.totalUsage
67
- };
68
- controller.enqueue(part);
69
- } }));
70
- return {
71
- stream: toUIMessageStream({
72
- stream: observed,
73
- tools,
74
- originalMessages: input.messages,
75
- generateMessageId: () => input.responseMessageId,
76
- onError: errorMessage$1,
77
- ...input.messageMetadata === void 0 ? {} : { messageMetadata: input.messageMetadata }
78
- }),
79
- completion() {
80
- if (fatalError !== void 0) throw fatalError.value;
81
- if (completion === void 0) throw new Error("AI SDK step ended without a finish part");
82
- if (completion.finishReason === "error") throw new Error("AI SDK step finished with an error");
83
- return completion;
84
- }
85
- };
86
- }
87
- //#endregion
88
- //#region src/ai-model-metadata.ts
89
- const catalogUrl = "https://ai-gateway.vercel.sh/v1/models";
90
- const catalogLifetimeMs = 36e5;
91
- const catalogTimeoutMs = 5e3;
92
- let catalog;
93
- const gatewayModelId = (model) => {
94
- if (typeof model === "string") return globalThis.AI_SDK_DEFAULT_PROVIDER === void 0 ? model : void 0;
95
- return model.provider === "gateway" ? model.modelId : void 0;
96
- };
97
- const readCatalog = () => {
98
- if (catalog && catalog.expiresAt > Date.now()) return catalog.promise;
99
- const entry = {
100
- expiresAt: Number.POSITIVE_INFINITY,
101
- promise: (async () => {
102
- const controller = new AbortController();
103
- const timeout = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("model catalog request timed out")), catalogTimeoutMs);
104
- timeout.unref?.();
105
- try {
106
- const response = await fetch(catalogUrl, { signal: controller.signal });
107
- if (!response.ok) throw new Error(`model catalog request failed: HTTP ${response.status}`);
108
- const body = await response.json();
109
- if (typeof body !== "object" || body === null || !("data" in body) || !Array.isArray(body.data)) throw new Error("model catalog response must contain a data array");
110
- const models = /* @__PURE__ */ new Map();
111
- for (const row of body.data) {
112
- if (typeof row !== "object" || row === null || !("type" in row) || !("id" in row) || !("context_window" in row) || !("max_tokens" in row) || row.type !== "language" || typeof row.id !== "string" || typeof row.context_window !== "number" || typeof row.max_tokens !== "number" || !Number.isSafeInteger(row.context_window) || row.context_window <= 0 || !Number.isSafeInteger(row.max_tokens) || row.max_tokens <= 0) continue;
113
- models.set(row.id, {
114
- contextWindow: row.context_window,
115
- maxOutputTokens: row.max_tokens
116
- });
117
- }
118
- return models;
119
- } finally {
120
- clearTimeout(timeout);
121
- }
122
- })()
123
- };
124
- catalog = entry;
125
- entry.promise = entry.promise.then((models) => {
126
- entry.expiresAt = Date.now() + catalogLifetimeMs;
127
- return models;
128
- }, (error) => {
129
- if (catalog === entry) catalog = void 0;
130
- throw error;
131
- });
132
- return entry.promise;
133
- };
134
- const readModelLimits = async (options) => {
135
- options.signal.throwIfAborted();
136
- let onAbort;
137
- const aborted = new Promise((_resolve, reject) => {
138
- onAbort = () => reject(options.signal.reason);
139
- options.signal.addEventListener("abort", onAbort, { once: true });
140
- });
141
- try {
142
- return (await Promise.race([readCatalog(), aborted])).get(options.modelId) ?? null;
143
- } finally {
144
- options.signal.removeEventListener("abort", onAbort);
145
- }
146
- };
147
- //#endregion
148
8
  //#region src/ai-coordinator.ts
149
9
  const updateResponse = (state, update) => state.response === void 0 ? state : {
150
10
  ...state,
@@ -156,6 +16,11 @@ const foldCoordinator = (state, event) => {
156
16
  closed: true,
157
17
  queued: []
158
18
  };
19
+ case "ai.retry.requested":
20
+ case "ai.compaction.completed": {
21
+ const { calibration: _, ...rest } = state;
22
+ return rest;
23
+ }
159
24
  case "ai.message.created": {
160
25
  const payload = event.payload;
161
26
  const message = payload.message;
@@ -163,24 +28,24 @@ const foldCoordinator = (state, event) => {
163
28
  return {
164
29
  ...state,
165
30
  queued: [...state.queued.filter((item) => item.messageId !== message.id), {
166
- index: event.index,
167
31
  messageId: message.id,
168
32
  generate: payload.generate !== false
169
- }].toSorted((left, right) => left.index - right.index)
33
+ }]
170
34
  };
171
35
  }
172
36
  case "ai.generation.requested": {
173
37
  const request = event.payload;
174
38
  if (request.reason === "message") {
175
39
  const responseMessageId = request.responseMessageId ?? `${request.messageId}:assistant`;
176
- const requested = state.queued.find((item) => item.messageId === request.messageId);
40
+ const requested = state.queued.findIndex((item) => item.messageId === request.messageId);
177
41
  return {
178
42
  ...state,
179
- queued: requested === void 0 ? state.queued.filter((item) => item.messageId !== request.messageId) : state.queued.filter((item) => item.index > requested.index),
43
+ queued: requested < 0 ? state.queued.filter((item) => item.messageId !== request.messageId) : state.queued.slice(requested + 1),
180
44
  response: {
181
45
  rootMessageId: request.messageId,
182
46
  responseMessageId,
183
47
  status: "requested",
48
+ stepCount: 0,
184
49
  activeRequestId: event.id,
185
50
  calls: [],
186
51
  inputs: []
@@ -189,14 +54,25 @@ const foldCoordinator = (state, event) => {
189
54
  }
190
55
  if (state.response === void 0 || request.responseMessageId !== state.response.responseMessageId) return state;
191
56
  if (request.reason === "tool" && (!continuationReady(state) || event.id !== `ai.generate:tools:${state.response.generation?.generationId}`) || request.reason === "retry" && state.response.status !== "failed" || request.reason === "input" && state.response.inputResponse === void 0) return state;
192
- return updateResponse(state, (response) => ({
193
- rootMessageId: response.rootMessageId,
194
- responseMessageId: response.responseMessageId,
195
- status: "requested",
196
- activeRequestId: event.id,
197
- calls: [],
198
- inputs: []
199
- }));
57
+ return updateResponse(state, (response) => {
58
+ const generation = response.generation ?? response.source?.generation;
59
+ const promptThroughIndex = response.promptThroughIndex ?? response.source?.promptThroughIndex;
60
+ return {
61
+ rootMessageId: response.rootMessageId,
62
+ responseMessageId: response.responseMessageId,
63
+ status: "requested",
64
+ stepCount: response.stepCount,
65
+ activeRequestId: event.id,
66
+ ...generation === void 0 || promptThroughIndex === void 0 ? {} : { source: {
67
+ generation,
68
+ promptThroughIndex,
69
+ failed: response.failure?.generationId === generation.generationId,
70
+ canCompact: response.calls.every((call) => call.terminal)
71
+ } },
72
+ calls: [],
73
+ inputs: []
74
+ };
75
+ });
200
76
  }
201
77
  case "ai.generation.started": {
202
78
  const generation = event.payload;
@@ -205,7 +81,10 @@ const foldCoordinator = (state, event) => {
205
81
  rootMessageId: response.rootMessageId,
206
82
  responseMessageId: response.responseMessageId,
207
83
  status: "generating",
84
+ stepCount: response.stepCount,
208
85
  generation,
86
+ ...generation.promptThroughIndex === void 0 ? {} : { promptThroughIndex: generation.promptThroughIndex },
87
+ ...response.source === void 0 ? {} : { source: response.source },
209
88
  activeRequestId: generation.requestId,
210
89
  calls: [],
211
90
  inputs: []
@@ -214,11 +93,22 @@ const foldCoordinator = (state, event) => {
214
93
  case "ai.generation.completed": {
215
94
  const completion = event.payload;
216
95
  if (state.response?.generation?.generationId !== completion.generationId) return state;
217
- return updateResponse(state, (response) => ({
96
+ const { calibration: _, ...uncalibrated } = updateResponse(state, (response) => ({
218
97
  ...response,
219
98
  status: "waiting",
220
- completion
99
+ completion,
100
+ stepCount: response.stepCount + (response.completion === void 0 ? 1 : 0)
221
101
  }));
102
+ const inputTokens = completion.usage?.inputTokens;
103
+ const estimate = completion.inputTokenEstimate;
104
+ return inputTokens !== void 0 && Number.isFinite(inputTokens) && inputTokens >= 0 && estimate !== void 0 ? {
105
+ ...uncalibrated,
106
+ calibration: {
107
+ model: state.response.generation.model,
108
+ inputTokens,
109
+ estimate
110
+ }
111
+ } : uncalibrated;
222
112
  }
223
113
  case "ai.generation.failed": {
224
114
  const failure = event.payload;
@@ -234,18 +124,16 @@ const foldCoordinator = (state, event) => {
234
124
  }
235
125
  case "ai.message.completed": {
236
126
  const messageId = event.payload.messageId;
237
- return state.response?.responseMessageId === messageId ? {
238
- closed: state.closed,
239
- queued: state.queued
240
- } : state;
127
+ if (state.response?.responseMessageId !== messageId) return state;
128
+ const { response: _, ...rest } = state;
129
+ return rest;
241
130
  }
242
131
  case "ai.message.interrupted": {
243
132
  const interruption = event.payload;
244
133
  const response = state.response;
245
- return response?.responseMessageId === interruption.messageId && (interruption.generationId === response.generation?.generationId || interruption.requestId === response.activeRequestId) ? {
246
- closed: state.closed,
247
- queued: state.queued
248
- } : state;
134
+ if (response?.responseMessageId !== interruption.messageId || interruption.generationId !== response.generation?.generationId && interruption.requestId !== response.activeRequestId) return state;
135
+ const { response: _, ...rest } = state;
136
+ return rest;
249
137
  }
250
138
  case "ai.tool.called": {
251
139
  const call = event.payload;
@@ -253,7 +141,6 @@ const foldCoordinator = (state, event) => {
253
141
  return updateResponse(state, (response) => ({
254
142
  ...response,
255
143
  calls: [...response.calls.filter((candidate) => candidate.call.toolCallId !== call.toolCallId), {
256
- index: event.index,
257
144
  call,
258
145
  terminal: false
259
146
  }]
@@ -277,8 +164,7 @@ const foldCoordinator = (state, event) => {
277
164
  ...response,
278
165
  calls: response.calls.map((candidate) => candidate.approval?.approvalId === approval.approvalId ? {
279
166
  ...candidate,
280
- response: approval,
281
- responseIndex: event.index
167
+ response: approval
282
168
  } : candidate)
283
169
  }));
284
170
  }
@@ -301,34 +187,701 @@ const foldCoordinator = (state, event) => {
301
187
  inputs: [...response.inputs.filter((candidate) => candidate.messageId !== input.messageId || candidate.generationId !== input.generationId || candidate.inputId !== input.inputId), input]
302
188
  }));
303
189
  }
304
- case "ai.input.responded": {
305
- const input = event.payload;
306
- const requested = state.response?.inputs.find((candidate) => candidate.messageId === input.messageId && candidate.generationId === input.generationId && candidate.inputId === input.inputId && candidate.name === input.name);
307
- if (state.response?.responseMessageId !== input.messageId || state.response.generation?.generationId !== input.generationId || requested === void 0) return state;
308
- return updateResponse(state, (response) => ({
309
- ...response,
310
- inputs: response.inputs.filter((candidate) => candidate.inputId !== input.inputId),
311
- inputResponse: {
312
- index: event.index,
313
- generationId: input.generationId,
314
- inputId: input.inputId,
315
- name: requested.name
190
+ case "ai.input.responded": {
191
+ const input = event.payload;
192
+ const requested = state.response?.inputs.find((candidate) => candidate.messageId === input.messageId && candidate.generationId === input.generationId && candidate.inputId === input.inputId && candidate.name === input.name);
193
+ if (state.response?.responseMessageId !== input.messageId || state.response.generation?.generationId !== input.generationId || requested === void 0) return state;
194
+ return updateResponse(state, (response) => ({
195
+ ...response,
196
+ inputs: response.inputs.filter((candidate) => candidate.inputId !== input.inputId),
197
+ inputResponse: {
198
+ generationId: input.generationId,
199
+ inputId: input.inputId,
200
+ name: requested.name
201
+ }
202
+ }));
203
+ }
204
+ default: return state;
205
+ }
206
+ };
207
+ const aiCoordinatorReducer = (contract) => contract.reducer({
208
+ name: "a2.ai.coordinator.v5",
209
+ initialState: {
210
+ closed: false,
211
+ queued: []
212
+ }
213
+ }).fold((state, event) => event.type === "ai.control.committed" ? applyControlChanges({
214
+ state,
215
+ changes: event.payload.changes.filter((change) => change.path[0] === "coordinator").map((change) => Object.assign({}, change, { path: change.path.slice(1) }))
216
+ }) : state);
217
+ const continuationReady = (state) => {
218
+ const response = state.response;
219
+ return !state.closed && response?.completion?.finishReason === "tool-calls" && response.generation?.generationId === response.completion.generationId && response.failure === void 0 && response.calls.length > 0 && response.calls.every((call) => call.terminal || call.call.providerExecuted === true && call.call.supportsDeferredResults !== true && call.approval !== void 0 && call.response !== void 0);
220
+ };
221
+ //#endregion
222
+ //#region src/ai-control-server.ts
223
+ var ControlCancelled = class extends Error {};
224
+ const callReadyForModel = (call) => call.terminal || call.call.providerExecuted === true && !call.call.supportsDeferredResults && call.response !== void 0;
225
+ const approvalBlocksExecution = (call) => call.approval !== void 0 && call.response?.approved !== true;
226
+ const createControlRuntime = (options) => {
227
+ const reducer = options.agent.contract.reducer({
228
+ name: "a2.ai.control.v1",
229
+ initialState: initialControlState()
230
+ }).fold((state, event) => event.type === "ai.control.committed" ? applyControlChanges({
231
+ state,
232
+ changes: event.payload.changes
233
+ }) : state);
234
+ const coordinator = aiCoordinatorReducer(options.agent.contract);
235
+ const handler = async (ctx) => {
236
+ const snapshot = await ctx.session.state(reducer, { through: "latest" });
237
+ const before = structuredClone(snapshot.state);
238
+ const moves = [];
239
+ const state = structuredClone(snapshot.state);
240
+ const facts = [];
241
+ const jobs = [];
242
+ const emit = ({ type, payload, id = `${ctx.event.id}:fact:${facts.length}` }) => {
243
+ const fact = {
244
+ type,
245
+ payload,
246
+ id
247
+ };
248
+ facts.push(fact);
249
+ state.coordinator = foldCoordinator(state.coordinator, {
250
+ ...ctx.event,
251
+ ...fact
252
+ });
253
+ };
254
+ let reason;
255
+ const end = ({ lastSeenIndex, reason: interruptionReason }) => {
256
+ const response = state.coordinator.response;
257
+ if (response === void 0) return;
258
+ const owner = response.generation === void 0 ? { requestId: response.activeRequestId } : { generationId: response.generation.generationId };
259
+ emit({
260
+ type: "ai.message.interrupted",
261
+ payload: {
262
+ messageId: response.responseMessageId,
263
+ ...owner,
264
+ ...interruptionReason === void 0 ? {} : { reason: interruptionReason },
265
+ ...lastSeenIndex === void 0 ? {} : { lastSeenIndex }
266
+ }
267
+ });
268
+ state.active = null;
269
+ };
270
+ const knownInput = (id) => state.inbox.items.some((candidate) => candidate.id === id) || Object.hasOwn(state.reservedIds, id);
271
+ const missingInputReason = (id) => state.reservedIds[id] === "removed" || !knownInput(id) ? "not-found" : "already-active";
272
+ const inputTaken = ({ message, generate = true }) => knownInput(message.id) || generate && message.role === "user" && knownInput(`${message.id}:assistant`);
273
+ const insert = ({ message, generate = true, first = false, afterStepOf }) => {
274
+ const candidate = {
275
+ id: message.id,
276
+ revision: 0,
277
+ message,
278
+ generate: generate && message.role === "user",
279
+ ...afterStepOf === void 0 ? {} : { afterStepOf }
280
+ };
281
+ state.reservedIds = {
282
+ ...state.reservedIds,
283
+ [candidate.id]: true,
284
+ ...candidate.generate ? { [`${candidate.id}:assistant`]: true } : {}
285
+ };
286
+ if (first) state.inbox.items.unshift(candidate);
287
+ else state.inbox.items.push(candidate);
288
+ };
289
+ const moveInput = ({ from, to }) => {
290
+ if (from === to) return;
291
+ const [moved] = state.inbox.items.splice(from, 1);
292
+ state.inbox.items.splice(to, 0, moved);
293
+ const [previous] = before.inbox.items.splice(from, 1);
294
+ before.inbox.items.splice(to, 0, previous);
295
+ moves.push({
296
+ path: ["inbox", "items"],
297
+ move: {
298
+ from,
299
+ to
300
+ }
301
+ });
302
+ };
303
+ const requestModel = ({ request, id }) => {
304
+ const active = state.active;
305
+ const payload = {
306
+ ...request,
307
+ control: {
308
+ turnId: active.turnId,
309
+ version: active.version
310
+ }
311
+ };
312
+ state.coordinator = foldCoordinator(state.coordinator, {
313
+ ...ctx.event,
314
+ type: "ai.generation.requested",
315
+ id,
316
+ payload
317
+ });
318
+ active.model = {
319
+ id,
320
+ version: active.version,
321
+ settled: false
322
+ };
323
+ active.phase = "starting";
324
+ jobs.push({
325
+ type: "ai.generation.requested",
326
+ id,
327
+ payload
328
+ });
329
+ };
330
+ if (ctx.event.type === "ai.session.closed") {
331
+ state.coordinator = {
332
+ closed: true,
333
+ queued: []
334
+ };
335
+ state.active = null;
336
+ state.inbox.items = [];
337
+ } else if (ctx.event.type === "ai.control.requested") {
338
+ const command = ctx.event.payload;
339
+ if (state.coordinator.closed) reason = "closed";
340
+ else switch (command.action) {
341
+ case "send":
342
+ if (inputTaken({
343
+ message: command.message,
344
+ generate: command.generate !== false
345
+ })) reason = "duplicate-input";
346
+ else insert({
347
+ message: command.message,
348
+ generate: command.generate !== false
349
+ });
350
+ break;
351
+ case "edit": {
352
+ const item = state.inbox.items.find((candidate) => candidate.id === command.message.id);
353
+ if (item === void 0) reason = missingInputReason(command.message.id);
354
+ else if (item.revision !== command.expectedRevision) reason = "revision-conflict";
355
+ else if (item.message.role !== command.message.role) reason = "revision-conflict";
356
+ else {
357
+ item.message = command.message;
358
+ item.revision += 1;
359
+ }
360
+ break;
361
+ }
362
+ case "remove": {
363
+ const position = state.inbox.items.findIndex((item) => item.id === command.inputId);
364
+ if (position < 0) reason = missingInputReason(command.inputId);
365
+ else {
366
+ state.inbox.items.splice(position, 1);
367
+ state.reservedIds = {
368
+ ...state.reservedIds,
369
+ [command.inputId]: "removed"
370
+ };
371
+ }
372
+ break;
373
+ }
374
+ case "move": {
375
+ const index = state.inbox.items.findIndex((candidate) => candidate.id === command.inputId);
376
+ if (index < 0) reason = missingInputReason(command.inputId);
377
+ else if (command.beforeId !== null && !state.inbox.items.some((candidate) => candidate.id === command.beforeId)) reason = "not-found";
378
+ else {
379
+ const destination = command.beforeId === null ? state.inbox.items.length : state.inbox.items.findIndex((item) => item.id === command.beforeId);
380
+ moveInput({
381
+ from: index,
382
+ to: destination > index ? destination - 1 : destination
383
+ });
384
+ }
385
+ break;
386
+ }
387
+ case "send-now": {
388
+ const position = state.inbox.items.findIndex((item) => item.id === command.inputId);
389
+ const selected = state.inbox.items[position];
390
+ if (!selected) reason = missingInputReason(command.inputId);
391
+ else if ((state.active?.turnId ?? null) !== command.turnId) reason = "stale-turn";
392
+ else if (selected.message.role !== "user") reason = "not-user-input";
393
+ else if (!selected.generate && knownInput(`${selected.id}:assistant`)) reason = "duplicate-input";
394
+ else {
395
+ if (!selected.generate) {
396
+ state.reservedIds = {
397
+ ...state.reservedIds,
398
+ [`${selected.id}:assistant`]: true
399
+ };
400
+ selected.generate = true;
401
+ }
402
+ if (state.active) selected.afterStepOf = state.active.turnId;
403
+ else delete selected.afterStepOf;
404
+ moveInput({
405
+ from: position,
406
+ to: 0
407
+ });
408
+ }
409
+ break;
410
+ }
411
+ case "stop":
412
+ if (state.active?.turnId !== command.turnId) reason = "stale-turn";
413
+ else end({
414
+ lastSeenIndex: command.lastSeenIndex,
415
+ reason: void 0
416
+ });
417
+ break;
418
+ case "steer":
419
+ if (inputTaken({ message: command.message })) reason = "duplicate-input";
420
+ else insert({
421
+ message: command.message,
422
+ generate: true,
423
+ first: true,
424
+ afterStepOf: command.turnId
425
+ });
426
+ break;
427
+ case "interrupt": {
428
+ const response = state.coordinator.response;
429
+ if (!response || response.responseMessageId !== command.request.messageId || command.request.generationId !== response.generation?.generationId && command.request.requestId !== response.activeRequestId) reason = "stale-turn";
430
+ else end({
431
+ lastSeenIndex: command.request.lastSeenIndex,
432
+ reason: command.request.reason
433
+ });
434
+ break;
435
+ }
436
+ case "pause": {
437
+ state.inbox.paused = true;
438
+ const active = state.active;
439
+ if (command.when === "now" && active !== null && !active.suspended) {
440
+ active.suspended = true;
441
+ active.version += 1;
442
+ const generation = state.coordinator.response?.generation;
443
+ if (generation) emit({
444
+ type: "ai.generation.paused",
445
+ payload: {
446
+ generationId: generation.generationId,
447
+ messageId: generation.responseMessageId
448
+ }
449
+ });
450
+ }
451
+ break;
452
+ }
453
+ case "resume":
454
+ state.inbox.paused = false;
455
+ break;
456
+ case "tool-result":
457
+ if (!state.coordinator.response?.calls.find((candidate) => candidate.call.generationId === command.result.generationId && candidate.call.messageId === command.result.messageId && candidate.call.toolCallId === command.result.toolCallId && candidate.call.providerExecuted === true && !candidate.terminal)) reason = "not-found";
458
+ else emit({
459
+ type: "ai.tool.result",
460
+ payload: command.result
461
+ });
462
+ break;
463
+ case "approval": {
464
+ const response = state.coordinator.response;
465
+ const call = response?.calls.find((candidate) => candidate.approval?.approvalId === command.response.approvalId && candidate.approval.generationId === command.response.generationId && candidate.approval.messageId === command.response.messageId);
466
+ if (response?.failure) reason = "stale-turn";
467
+ else if (!call || call.response || call.terminal) reason = "not-found";
468
+ else emit({
469
+ type: "ai.approval.responded",
470
+ payload: command.response
471
+ });
472
+ break;
473
+ }
474
+ case "request-input":
475
+ case "input": {
476
+ const payload = command.action === "input" ? command.response : command.request;
477
+ const response = state.coordinator.response;
478
+ if (response?.failure !== void 0 || response?.generation?.generationId !== payload.generationId || response.responseMessageId !== payload.messageId || command.action === "input" && !response.inputs.some((input) => input.inputId === payload.inputId && input.name === payload.name)) reason = "not-found";
479
+ else emit({
480
+ type: command.action === "input" ? "ai.input.responded" : "ai.input.requested",
481
+ payload
482
+ });
483
+ break;
484
+ }
485
+ case "retry": {
486
+ const response = state.coordinator.response;
487
+ if (!state.active || response?.status !== "failed" || response.rootMessageId !== command.request.messageId || response.responseMessageId !== command.request.responseMessageId) reason = "stale-turn";
488
+ else if (!state.inbox.paused && !state.active.suspended) {
489
+ emit({
490
+ type: "ai.retry.requested",
491
+ payload: command.request
492
+ });
493
+ requestModel({
494
+ request: {
495
+ messageId: response.rootMessageId,
496
+ responseMessageId: response.responseMessageId,
497
+ reason: "retry"
498
+ },
499
+ id: `ai.generate:retry:${command.request.retryId}`
500
+ });
501
+ } else reason = "stale-turn";
502
+ break;
503
+ }
504
+ }
505
+ } else {
506
+ const report = ctx.event.payload;
507
+ const active = state.active;
508
+ const call = state.coordinator.response?.calls.find((candidate) => candidate.work?.id === report.workId);
509
+ const work = report.kind === "model" ? active?.model : call?.work;
510
+ if (!(active?.turnId === report.turnId && work?.id === report.workId && work.version === report.version && !work.settled && report.attempt >= (work.attempt ?? 0))) reason = "stale-work";
511
+ else {
512
+ work.attempt = report.attempt;
513
+ const authorized = !active.suspended && active.version === report.version;
514
+ for (const event of report.events) {
515
+ if (work.facts?.includes(event.id)) continue;
516
+ if (authorized || report.kind === "tool" && event.type === "ai.tool.result") {
517
+ emit({
518
+ type: event.type,
519
+ payload: event.payload,
520
+ id: event.id
521
+ });
522
+ work.facts = [...work.facts ?? [], event.id];
523
+ }
524
+ }
525
+ if (report.settled) work.settled = true;
526
+ if (!authorized) reason = "stale-work";
527
+ }
528
+ }
529
+ let active = state.active;
530
+ let response = state.coordinator.response;
531
+ if (active && response === void 0) {
532
+ state.active = null;
533
+ active = null;
534
+ }
535
+ if (active && response) {
536
+ const handoff = state.inbox.items.some((item) => item.afterStepOf === active.turnId);
537
+ const outstanding = active.model !== null && !active.model.settled || response.calls.some((candidate) => candidate.work !== void 0 && !candidate.work.settled);
538
+ if (active.suspended) {
539
+ active.phase = outstanding ? "pausing" : "paused";
540
+ if (!state.inbox.paused && !outstanding && !handoff) active.suspended = false;
541
+ }
542
+ if (!active.suspended && response.failure) active.phase = "failed";
543
+ if (!active.suspended && response.failure === void 0) for (const call of response.calls) {
544
+ if (call.terminal || call.work !== void 0 && (!call.work.settled || call.work.version === active.version) || call.call.providerExecuted === true || call.approval && !call.response) continue;
545
+ if (call.response?.approved === false) {
546
+ emit({
547
+ type: "ai.tool.result",
548
+ payload: {
549
+ ...call.call,
550
+ denied: true,
551
+ phase: "execution"
552
+ }
553
+ });
554
+ continue;
555
+ }
556
+ const id = `${ctx.event.id}:tool:${call.call.toolCallId}`;
557
+ call.work = {
558
+ id,
559
+ version: active.version,
560
+ settled: false
561
+ };
562
+ jobs.push({
563
+ type: "ai.tool.execution.requested",
564
+ id,
565
+ payload: {
566
+ turnId: active.turnId,
567
+ version: active.version,
568
+ call: call.call,
569
+ generation: response.generation
570
+ }
571
+ });
572
+ }
573
+ response = state.coordinator.response;
574
+ const callsDone = response.calls.every(callReadyForModel);
575
+ const modelDone = active.model === null || active.model.settled;
576
+ const workSettled = modelDone && response.calls.every((call) => call.work === void 0 || call.work.settled);
577
+ const externalPending = response.calls.some((call) => call.call.providerExecuted && call.call.supportsDeferredResults && !call.terminal && !approvalBlocksExecution(call));
578
+ const handoffReady = handoff && workSettled && !externalPending && (active.suspended || response.failure !== void 0 || response.calls.every((call) => callReadyForModel(call) || approvalBlocksExecution(call)));
579
+ const readyForModel = !active.suspended && response.failure === void 0 && modelDone && callsDone && response.inputs.length === 0;
580
+ const naturalCompletion = readyForModel && response.completion !== void 0 && response.completion.finishReason !== "tool-calls";
581
+ if (handoffReady && !naturalCompletion) end({
582
+ lastSeenIndex: void 0,
583
+ reason: "steered after step"
584
+ });
585
+ else if (readyForModel && (!handoff || handoffReady)) {
586
+ const resume = active.model !== null && active.model.version !== active.version;
587
+ const input = response.inputResponse;
588
+ if (!handoff && (resume || continuationReady(state.coordinator) || response.completion && input)) {
589
+ const nextReason = resume ? "resume" : input ? "input" : "tool";
590
+ const id = nextReason === "tool" ? `ai.generate:tools:${response.generation.generationId}` : `${ctx.event.id}:generate:${nextReason}`;
591
+ requestModel({
592
+ request: {
593
+ messageId: response.responseMessageId,
594
+ responseMessageId: response.responseMessageId,
595
+ reason: nextReason
596
+ },
597
+ id
598
+ });
599
+ } else if (naturalCompletion) {
600
+ emit({
601
+ type: "ai.message.completed",
602
+ payload: { messageId: response.responseMessageId }
603
+ });
604
+ state.active = null;
605
+ }
606
+ }
607
+ if (!active.suspended && state.active && jobs.every((job) => job.type !== "ai.generation.requested")) state.active.phase = response.failure ? "failed" : !modelDone ? "generating" : !callsDone ? "tools" : "waiting";
608
+ }
609
+ if (state.active === null && !state.inbox.paused && !state.coordinator.closed) while (state.inbox.items.length > 0) {
610
+ const item = state.inbox.items.shift();
611
+ delete item.afterStepOf;
612
+ emit({
613
+ type: "ai.message.created",
614
+ payload: {
615
+ message: item.message,
616
+ generate: item.generate
617
+ },
618
+ id: `ai.admitted:${item.id}`
619
+ });
620
+ if (!item.generate) continue;
621
+ state.active = {
622
+ turnId: item.id,
623
+ input: item,
624
+ phase: "starting",
625
+ version: 0,
626
+ suspended: false,
627
+ model: null
628
+ };
629
+ requestModel({
630
+ request: {
631
+ messageId: item.id,
632
+ reason: "message"
633
+ },
634
+ id: `ai.generate:message:${item.id}`
635
+ });
636
+ break;
637
+ }
638
+ const receipt = {
639
+ commandId: ctx.event.id,
640
+ ...ctx.event.type === "ai.work.reported" ? { work: true } : {},
641
+ outcome: reason === void 0 ? "applied" : "rejected",
642
+ ...reason === void 0 ? {} : { reason }
643
+ };
644
+ return [
645
+ ...facts,
646
+ {
647
+ type: "ai.control.committed",
648
+ payload: controlCommit({
649
+ before,
650
+ after: state,
651
+ moves
652
+ })
653
+ },
654
+ ...jobs,
655
+ {
656
+ type: "ai.control.decided",
657
+ payload: receipt
658
+ }
659
+ ];
660
+ };
661
+ const report = ({ ctx, events, settled, name }) => {
662
+ const payload = ctx.event.payload;
663
+ const { turnId, version } = "turnId" in payload ? payload : payload.control;
664
+ for (const event of events) {
665
+ const schema = options.agent.contract.events[event.type];
666
+ const result = validateSync(schema, event.payload, `AI worker ${event.type}`);
667
+ if (result.issues) throw new TypeError(result.issues.map((issue) => issue.message).join("; "));
668
+ }
669
+ return {
670
+ type: "ai.work.reported",
671
+ id: `${ctx.event.id}:report:${ctx.attempt}:${name}`,
672
+ payload: {
673
+ workId: ctx.event.id,
674
+ attempt: ctx.attempt,
675
+ turnId,
676
+ version,
677
+ kind: ctx.event.type === "ai.generation.requested" ? "model" : "tool",
678
+ settled,
679
+ events: events.filter((event) => event.type !== "ai.message.completed" && event.type !== "ai.message.interrupted").map((event, index) => ({
680
+ type: event.type,
681
+ payload: event.payload,
682
+ id: event.id ?? `${ctx.event.id}:fact:${ctx.attempt}:${name}:${index}`
683
+ }))
684
+ }
685
+ };
686
+ };
687
+ const append = async ({ ctx, name, events }) => {
688
+ const progress = events.filter((event) => event.type === "ai.generation.progress");
689
+ const lifecycle = events.filter((event) => event.type !== "ai.generation.progress");
690
+ if (lifecycle.length === 0) return ctx.session.append(name, ...progress);
691
+ const input = report({
692
+ ctx,
693
+ events: lifecycle,
694
+ settled: false,
695
+ name
696
+ });
697
+ const persisted = await ctx.session.append(name, ...progress, input);
698
+ const index = persisted.at(-1).index;
699
+ const stream = ctx.session.stream({ startAfter: index })[Symbol.asyncIterator]();
700
+ const canonical = [];
701
+ const wanted = new Set(lifecycle.map((event) => event.id));
702
+ let rejectAbort;
703
+ const aborted = new Promise((_, reject) => {
704
+ rejectAbort = reject;
705
+ });
706
+ const onAbort = () => {
707
+ rejectAbort(ctx.signal.reason);
708
+ };
709
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
710
+ try {
711
+ ctx.signal.throwIfAborted();
712
+ for (;;) {
713
+ const item = await Promise.race([stream.next(), aborted]);
714
+ if (item.done) throw new Error("AI control acknowledgement stream ended");
715
+ if (wanted.has(item.value.id)) canonical.push(item.value);
716
+ if (item.value.type === "ai.control.decided" && item.value.payload.commandId === input.id) {
717
+ if (item.value.payload.outcome === "rejected") throw new ControlCancelled("stale AI work");
718
+ return [...persisted.filter((event) => event.type !== "ai.work.reported"), ...canonical];
719
+ }
720
+ }
721
+ } finally {
722
+ ctx.signal.removeEventListener("abort", onAbort);
723
+ await stream.return?.();
724
+ }
725
+ };
726
+ const settled = ({ ctx, events }) => report({
727
+ ctx,
728
+ events: events === void 0 ? [] : Array.isArray(events) ? events : [events],
729
+ settled: true,
730
+ name: "settled"
731
+ });
732
+ const cancelled = ({ error, signal }) => {
733
+ if (signal.reason instanceof A2Error) throw signal.reason;
734
+ return error instanceof ControlCancelled || signal.aborted;
735
+ };
736
+ return {
737
+ handler,
738
+ append,
739
+ settled,
740
+ cancelled,
741
+ reducer,
742
+ coordinator
743
+ };
744
+ };
745
+ //#endregion
746
+ //#region src/ai-sdk-step.ts
747
+ const CONTROLLED_SETTINGS = [
748
+ "_internal",
749
+ "abortSignal",
750
+ "instructions",
751
+ "messages",
752
+ "model",
753
+ "onToolExecutionEnd",
754
+ "onToolExecutionStart",
755
+ "experimental_onToolCallFinish",
756
+ "experimental_onToolCallStart",
757
+ "experimental_sandbox",
758
+ "experimental_toolCallers",
759
+ "experimental_toolApprovalSecret",
760
+ "prompt",
761
+ "prepareStep",
762
+ "stopWhen",
763
+ "system",
764
+ "tools"
765
+ ];
766
+ const safeSettings = (settings) => {
767
+ const result = { ...settings };
768
+ for (const key of CONTROLLED_SETTINGS) Reflect.deleteProperty(result, key);
769
+ if (typeof result.timeout === "object" && result.timeout !== null) {
770
+ const timeout = { ...result.timeout };
771
+ Reflect.deleteProperty(timeout, "toolMs");
772
+ Reflect.deleteProperty(timeout, "tools");
773
+ result.timeout = timeout;
774
+ }
775
+ return result;
776
+ };
777
+ const modelToolSet = (tools) => Object.fromEntries(Object.entries(tools).map(([name, tool]) => {
778
+ if (tool.type === "provider" && tool.isProviderExecuted === true) return [name, tool];
779
+ const definition = { ...tool };
780
+ Reflect.deleteProperty(definition, "execute");
781
+ return [name, definition];
782
+ }));
783
+ const errorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
784
+ /** Run exactly one AI SDK model step while leaving local tool execution to A2. */
785
+ async function generateAISDKStep(input) {
786
+ const settings = safeSettings(input.settings);
787
+ const tools = modelToolSet(input.tools);
788
+ const messages = input.modelMessages ?? await convertToModelMessages(input.messages, { tools });
789
+ const streamOptions = {
790
+ ...settings,
791
+ model: input.model,
792
+ tools,
793
+ messages,
794
+ abortSignal: input.abortSignal,
795
+ ...input.instructions === void 0 ? {} : { instructions: input.instructions },
796
+ stopWhen: stepCountIs(1),
797
+ onError: settings.onError ?? (() => {})
798
+ };
799
+ const result = streamText(streamOptions);
800
+ let completion;
801
+ let fatalError;
802
+ const observed = result.stream.pipeThrough(new TransformStream({ transform(part, controller) {
803
+ if (part.type === "error" && fatalError === void 0) fatalError = { value: part.error };
804
+ else if (part.type === "finish") completion = {
805
+ finishReason: part.finishReason,
806
+ usage: part.totalUsage
807
+ };
808
+ controller.enqueue(part);
809
+ } }));
810
+ return {
811
+ stream: toUIMessageStream({
812
+ stream: observed,
813
+ tools,
814
+ generateMessageId: () => input.responseMessageId,
815
+ onError: errorMessage$1,
816
+ ...input.messageMetadata === void 0 ? {} : { messageMetadata: input.messageMetadata }
817
+ }),
818
+ completion() {
819
+ if (fatalError !== void 0) throw fatalError.value;
820
+ if (completion === void 0) throw new Error("AI SDK step ended without a finish part");
821
+ if (completion.finishReason === "error") throw new Error("AI SDK step finished with an error");
822
+ return completion;
823
+ }
824
+ };
825
+ }
826
+ //#endregion
827
+ //#region src/ai-model-metadata.ts
828
+ const catalogUrl = "https://ai-gateway.vercel.sh/v1/models";
829
+ const catalogLifetimeMs = 36e5;
830
+ const catalogTimeoutMs = 5e3;
831
+ let catalog;
832
+ const gatewayModelId = (model) => {
833
+ if (typeof model === "string") return globalThis.AI_SDK_DEFAULT_PROVIDER === void 0 ? model : void 0;
834
+ return model.provider === "gateway" ? model.modelId : void 0;
835
+ };
836
+ const readCatalog = () => {
837
+ if (catalog && catalog.expiresAt > Date.now()) return catalog.promise;
838
+ const entry = {
839
+ expiresAt: Number.POSITIVE_INFINITY,
840
+ promise: (async () => {
841
+ const controller = new AbortController();
842
+ const timeout = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("model catalog request timed out")), catalogTimeoutMs);
843
+ timeout.unref?.();
844
+ try {
845
+ const response = await fetch(catalogUrl, { signal: controller.signal });
846
+ if (!response.ok) throw new Error(`model catalog request failed: HTTP ${response.status}`);
847
+ const body = await response.json();
848
+ if (typeof body !== "object" || body === null || !("data" in body) || !Array.isArray(body.data)) throw new Error("model catalog response must contain a data array");
849
+ const models = /* @__PURE__ */ new Map();
850
+ for (const row of body.data) {
851
+ if (typeof row !== "object" || row === null || !("type" in row) || !("id" in row) || !("context_window" in row) || !("max_tokens" in row) || row.type !== "language" || typeof row.id !== "string" || typeof row.context_window !== "number" || typeof row.max_tokens !== "number" || !Number.isSafeInteger(row.context_window) || row.context_window <= 0 || !Number.isSafeInteger(row.max_tokens) || row.max_tokens <= 0) continue;
852
+ models.set(row.id, {
853
+ contextWindow: row.context_window,
854
+ maxOutputTokens: row.max_tokens
855
+ });
316
856
  }
317
- }));
318
- }
319
- default: return state;
320
- }
857
+ return models;
858
+ } finally {
859
+ clearTimeout(timeout);
860
+ }
861
+ })()
862
+ };
863
+ catalog = entry;
864
+ entry.promise = entry.promise.then((models) => {
865
+ entry.expiresAt = Date.now() + catalogLifetimeMs;
866
+ return models;
867
+ }, (error) => {
868
+ if (catalog === entry) catalog = void 0;
869
+ throw error;
870
+ });
871
+ return entry.promise;
321
872
  };
322
- const aiCoordinatorReducer = (contract) => contract.reducer({
323
- name: "a2.ai.coordinator.v3",
324
- initialState: {
325
- closed: false,
326
- queued: []
873
+ const readModelLimits = async (options) => {
874
+ options.signal.throwIfAborted();
875
+ let onAbort;
876
+ const aborted = new Promise((_resolve, reject) => {
877
+ onAbort = () => reject(options.signal.reason);
878
+ options.signal.addEventListener("abort", onAbort, { once: true });
879
+ });
880
+ try {
881
+ return (await Promise.race([readCatalog(), aborted])).get(options.modelId) ?? null;
882
+ } finally {
883
+ options.signal.removeEventListener("abort", onAbort);
327
884
  }
328
- }).fold((state, event) => foldCoordinator(state, event));
329
- const continuationReady = (state) => {
330
- const response = state.response;
331
- return !state.closed && response?.completion?.finishReason === "tool-calls" && response.generation?.generationId === response.completion.generationId && response.failure === void 0 && response.calls.length > 0 && response.calls.every((call) => call.terminal || call.call.providerExecuted === true && call.call.supportsDeferredResults !== true && call.approval !== void 0 && call.response !== void 0);
332
885
  };
333
886
  //#endregion
334
887
  //#region src/ai-server.ts
@@ -340,9 +893,11 @@ const continuationReady = (state) => {
340
893
  */
341
894
  function validateAgentIngress(context) {
342
895
  const rejected = context.events.find((event) => {
343
- if (event.type !== "ai.message.created") return !(event.type === "ai.approval.responded" || event.type === "ai.input.responded" || event.type === "ai.message.interrupted" || event.type === "ai.retry.requested");
344
- const payload = event.payload;
345
- return typeof payload !== "object" || payload === null || !("message" in payload) || typeof payload.message !== "object" || payload.message === null || !("role" in payload.message) || payload.message.role !== "user";
896
+ if (event.type !== "ai.control.requested") return true;
897
+ const command = event.payload;
898
+ if (command["action"] === "request-input" || command["action"] === "tool-result") return true;
899
+ if (command["action"] === "send" || command["action"] === "edit" || command["action"] === "steer") return command["message"]?.role !== "user";
900
+ return false;
346
901
  });
347
902
  if (rejected !== void 0) throw new A2Error("INVALID_PAYLOAD", `event '${rejected.type}' is server-only for an AI agent`);
348
903
  }
@@ -491,9 +1046,9 @@ async function* consumeGeneration(options) {
491
1046
  ...completion?.usage === void 0 ? {} : { usage: completion.usage }
492
1047
  };
493
1048
  }
494
- const replay = (definition, history) => {
495
- let state = definition.reducer.initialState;
496
- for (const event of history) state = definition.reducer.fold(state, event);
1049
+ const foldAIEvents = (options) => {
1050
+ let state = options.state;
1051
+ for (const event of options.events) state = options.agent.reducer.fold(state, event);
497
1052
  return state;
498
1053
  };
499
1054
  const summarize = async (options) => {
@@ -558,35 +1113,50 @@ const activeCompaction = (state) => {
558
1113
  const compaction = state.compaction;
559
1114
  return compaction?.status === "completed" && compaction.messages !== void 0 && state.messages.some((message) => message.id === compaction.throughMessageId) ? compaction : null;
560
1115
  };
561
- const contextMessages = (options) => {
562
- const { agent, state, history } = options;
1116
+ const contextMessages = async (options) => {
1117
+ const { agent, session, snapshot } = options;
1118
+ const appended = options.appended ?? [];
1119
+ const state = foldAIEvents({
1120
+ agent,
1121
+ events: appended,
1122
+ state: snapshot.state
1123
+ });
563
1124
  const compaction = activeCompaction(state);
564
- if (compaction === null) return state.messages;
1125
+ const queued = new Set(options.coordinator.queued.map((item) => item.messageId));
1126
+ const visible = (messages) => messages.filter((message) => !queued.has(message.id));
1127
+ if (compaction === null) return visible(state.messages);
565
1128
  const retained = new Set(compaction.retainedMessageIds ?? []);
566
- const startedIndex = history.find((event) => event.type === "ai.generation.started" && event.payload.generationId === compaction.generationId)?.index;
567
- const throughIndex = compaction.throughIndex ?? (startedIndex === void 0 ? void 0 : startedIndex - 1);
1129
+ const throughIndex = compaction.throughIndex;
568
1130
  if (throughIndex !== void 0) {
569
- const prefix = history.filter((event) => event.index <= throughIndex);
570
- for (const queued of queuedMessagesAt(prefix)) retained.add(queued.messageId);
571
- let context = replay(agent, prefix);
572
- context = {
573
- ...context,
574
- messages: [...compaction.messages, ...context.messages.filter((message) => retained.has(message.id))],
575
- activeProjection: null
576
- };
577
- for (const event of history) if (event.index > throughIndex) context = agent.reducer.fold(context, event);
578
- return context.messages;
1131
+ const [prefix, priorCoordinator, tail] = await Promise.all([
1132
+ throughIndex === snapshot.index ? Promise.resolve(snapshot) : session.state(agent.reducer, { through: throughIndex }),
1133
+ session.state(aiCoordinatorReducer(agent.contract), { through: throughIndex }),
1134
+ throughIndex >= snapshot.index ? Promise.resolve([]) : session.history({
1135
+ gte: throughIndex + 1,
1136
+ lte: snapshot.index
1137
+ })
1138
+ ]);
1139
+ for (const item of priorCoordinator.state.queued) retained.add(item.messageId);
1140
+ const events = options.excluded === void 0 ? tail : withoutGenerationLifecycle(tail, options.excluded);
1141
+ const context = foldAIEvents({
1142
+ agent,
1143
+ events: [...events, ...appended],
1144
+ state: {
1145
+ ...prefix.state,
1146
+ messages: [...compaction.messages, ...prefix.state.messages.filter((message) => retained.has(message.id))],
1147
+ activeProjection: null
1148
+ }
1149
+ });
1150
+ const positions = /* @__PURE__ */ new Map();
1151
+ for (const message of [...compaction.messages, ...state.messages]) if (!positions.has(message.id)) positions.set(message.id, positions.size);
1152
+ return visible(context.messages.toSorted((left, right) => (positions.get(left.id) ?? positions.size) - (positions.get(right.id) ?? positions.size)));
579
1153
  }
580
1154
  const boundary = state.messages.findIndex((message) => message.id === compaction.throughMessageId);
581
- return [
1155
+ return visible([
582
1156
  ...compaction.messages,
583
1157
  ...state.messages.slice(0, boundary + 1).filter((message) => retained.has(message.id)),
584
1158
  ...state.messages.slice(boundary + 1)
585
- ];
586
- };
587
- const activeContextMessages = (options) => {
588
- const queued = new Set(options.coordinator.queued.map((item) => item.messageId));
589
- return contextMessages(options).filter((message) => !queued.has(message.id));
1159
+ ]);
590
1160
  };
591
1161
  const modelContext = async (options) => {
592
1162
  const messages = await convertToModelMessages(options.messages, { tools: options.tools });
@@ -609,22 +1179,8 @@ const estimateInputTokens = async (options) => {
609
1179
  })).byteLength / 4);
610
1180
  };
611
1181
  const measuredInputTokens = (options) => {
612
- for (const event of options.history.toReversed()) {
613
- if (event.type === "ai.compaction.completed" || event.type === "ai.retry.requested") break;
614
- if (event.type !== "ai.generation.completed") continue;
615
- const completed = event.payload;
616
- const started = options.history.find((candidate) => candidate.type === "ai.generation.started" && candidate.payload.generationId === completed.generationId);
617
- const input = completed.usage?.inputTokens;
618
- const estimate = completed.inputTokenEstimate;
619
- if ((started?.payload)?.model !== options.model) break;
620
- if (input === void 0 || estimate === void 0 || !Number.isFinite(input) || input < 0 || options.estimate < estimate) break;
621
- return Math.max(options.estimate, Math.ceil(input + options.estimate - estimate));
622
- }
623
- return options.estimate;
624
- };
625
- const generationRequestId = (generationId) => {
626
- const markerIndex = generationId.lastIndexOf(":generation:");
627
- return markerIndex === -1 ? void 0 : generationId.slice(0, markerIndex);
1182
+ const previous = options.calibration;
1183
+ return previous !== void 0 && previous.model === options.model && options.estimate >= previous.estimate ? Math.max(options.estimate, Math.ceil(previous.inputTokens + options.estimate - previous.estimate)) : options.estimate;
628
1184
  };
629
1185
  const toolCalledEvent = (payload) => ({
630
1186
  type: "ai.tool.called",
@@ -793,29 +1349,6 @@ const lifecycleEvents = (options) => {
793
1349
  pending
794
1350
  };
795
1351
  };
796
- function queuedMessagesAt(history) {
797
- const queued = [];
798
- for (const event of history) {
799
- if (event.type === "ai.message.created") {
800
- const payload = event.payload;
801
- if (payload.message.role !== "user") continue;
802
- const duplicate = queued.findIndex((item) => item.messageId === payload.message.id);
803
- if (duplicate !== -1) queued.splice(duplicate, 1);
804
- queued.push({
805
- index: event.index,
806
- messageId: payload.message.id,
807
- generate: payload.generate !== false
808
- });
809
- continue;
810
- }
811
- if (event.type !== "ai.generation.requested") continue;
812
- const request = event.payload;
813
- if (request.reason !== "message") continue;
814
- const requested = queued.findIndex((item) => item.messageId === request.messageId);
815
- if (requested !== -1) queued.splice(0, requested + 1);
816
- }
817
- return queued;
818
- }
819
1352
  function withoutGenerationLifecycle(history, generationIds) {
820
1353
  if (generationIds.size === 0) return history;
821
1354
  const approvalKeys = /* @__PURE__ */ new Set();
@@ -839,7 +1372,7 @@ const validateCompaction = (options) => {
839
1372
  if (compaction !== false && (typeof compaction !== "object" || compaction === null)) throw new TypeError("compaction must resolve to false or a policy object");
840
1373
  if (compaction !== false && "then" in compaction) {
841
1374
  Promise.resolve(compaction).catch(() => {});
842
- throw new TypeError("compaction options must resolve synchronously");
1375
+ throw new TypeError("compaction options must be a policy or a resolver");
843
1376
  }
844
1377
  if (compaction !== false && !("shouldCompact" in compaction)) {
845
1378
  if (compaction.thresholdTokens !== void 0 && (!Number.isSafeInteger(compaction.thresholdTokens) || compaction.thresholdTokens < 1)) throw new TypeError("compaction.thresholdTokens must be a positive safe integer");
@@ -871,58 +1404,9 @@ function createHandlers(options) {
871
1404
  const tools = options.tools ?? {};
872
1405
  const generation = options.generation ?? {};
873
1406
  const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY;
874
- const coordinator = aiCoordinatorReducer(options.agent.contract);
1407
+ const control = createControlRuntime({ agent: options.agent });
1408
+ const coordinator = control.coordinator;
875
1409
  const promptCache = /* @__PURE__ */ new Map();
876
- const coordinatorStateAt = (history, frontier = Number.POSITIVE_INFINITY) => {
877
- let state = coordinator.initialState;
878
- for (const event of history) {
879
- if (event.index >= frontier) break;
880
- state = coordinator.fold(state, event);
881
- }
882
- return state;
883
- };
884
- const requestForNextMessage = (state) => {
885
- if (state.closed || state.response !== void 0) return void 0;
886
- const next = state.queued.find((item) => item.generate !== false);
887
- if (next === void 0) return void 0;
888
- return {
889
- type: "ai.generation.requested",
890
- id: `ai.generate:message:${next.messageId}`,
891
- payload: {
892
- messageId: next.messageId,
893
- reason: "message"
894
- }
895
- };
896
- };
897
- const scheduleNext = async (ctx) => requestForNextMessage((await ctx.session.state(coordinator, { through: "latest" })).state);
898
- const continueIfReady = async (ctx, generationId) => {
899
- const state = (await ctx.session.state(coordinator, { through: "latest" })).state;
900
- const response = state.response;
901
- if (response?.generation?.generationId !== generationId) return;
902
- const input = response.inputResponse;
903
- if (response.completion?.generationId === generationId && response.failure === void 0 && input !== void 0 && response.inputs.length === 0 && response.calls.every((call) => call.terminal || call.call.providerExecuted === true && call.call.supportsDeferredResults !== true && call.approval !== void 0 && call.response !== void 0)) {
904
- await ctx.session.append("continue-after-input", {
905
- type: "ai.generation.requested",
906
- id: `ai.generate:input:${encodeURIComponent(response.responseMessageId)}:${encodeURIComponent(input.generationId)}:${encodeURIComponent(input.inputId)}`,
907
- payload: {
908
- messageId: response.responseMessageId,
909
- responseMessageId: response.responseMessageId,
910
- reason: "input"
911
- }
912
- });
913
- return;
914
- }
915
- if (!continuationReady(state)) return;
916
- await ctx.session.append("continue-after-tools", {
917
- type: "ai.generation.requested",
918
- id: `ai.generate:tools:${generationId}`,
919
- payload: {
920
- messageId: response.responseMessageId,
921
- responseMessageId: response.responseMessageId,
922
- reason: "tool"
923
- }
924
- });
925
- };
926
1410
  const resultEvent = (call, suffix, result) => ({
927
1411
  type: "ai.tool.result",
928
1412
  id: `${call.generationId}:tool:${call.toolCallId}:${suffix}`,
@@ -943,22 +1427,35 @@ function createHandlers(options) {
943
1427
  ...call.toolMetadata === void 0 ? {} : { toolMetadata: call.toolMetadata }
944
1428
  }
945
1429
  });
946
- const promptMessages = (sessionId, call, readHistory) => {
947
- const key = promptCacheKey(sessionId, call.generationId);
1430
+ const promptMessages = (input) => {
1431
+ const { ctx, call, generation: owner } = input;
1432
+ const key = promptCacheKey(ctx.event.sessionId, call.generationId);
948
1433
  const cached = promptCache.get(key);
949
1434
  if (cached) return cached;
950
1435
  const computation = (async () => {
951
- const history = await readHistory();
952
- const started = history.find((event) => event.type === "ai.generation.started" && event.payload.generationId === call.generationId);
953
- const frontier = (started?.payload)?.promptThroughIndex ?? (started?.index ?? Number.POSITIVE_INFINITY) - 1;
954
- const promptHistory = history.filter((event) => event.index <= frontier || (event.type === "ai.generation.started" || event.type === "ai.compaction.requested" || event.type === "ai.compaction.completed") && event.payload.generationId === call.generationId);
955
- const state = replay(options.agent, promptHistory);
1436
+ const frontier = owner.promptThroughIndex;
1437
+ if (frontier === void 0) throw new TypeError("AI work requires a prompt checkpoint");
1438
+ const [snapshot, atPrompt, tail] = await Promise.all([
1439
+ ctx.session.state(options.agent.reducer, { through: frontier }),
1440
+ ctx.session.state(coordinator, { through: frontier }),
1441
+ ctx.session.history({
1442
+ gte: frontier + 1,
1443
+ lte: ctx.event.index
1444
+ })
1445
+ ]);
1446
+ const appended = tail.filter((event) => (event.type === "ai.generation.started" || event.type === "ai.compaction.requested" || event.type === "ai.compaction.completed") && event.payload.generationId === call.generationId);
1447
+ const state = foldAIEvents({
1448
+ agent: options.agent,
1449
+ events: appended,
1450
+ state: snapshot.state
1451
+ });
956
1452
  return modelContext({
957
- messages: activeContextMessages({
1453
+ messages: await contextMessages({
958
1454
  agent: options.agent,
959
- state,
960
- history: promptHistory,
961
- coordinator: coordinatorStateAt(promptHistory)
1455
+ session: ctx.session,
1456
+ snapshot,
1457
+ coordinator: atPrompt.state,
1458
+ appended
962
1459
  }),
963
1460
  state,
964
1461
  tools
@@ -974,7 +1471,7 @@ function createHandlers(options) {
974
1471
  const schedulerFailure = consumeSchedulerSendFailure(error);
975
1472
  if (checkAbort(ctx.signal)) return;
976
1473
  if (schedulerFailure === "retryable") throw error;
977
- return resultEvent(call, "execution:error", { error: errorMessage(error) });
1474
+ return resultEvent(call, `${ctx.event.id}:execution:error`, { error: errorMessage(error) });
978
1475
  };
979
1476
  const runToolExecution = async (ctx, call, execute, messages) => {
980
1477
  let output;
@@ -993,7 +1490,7 @@ function createHandlers(options) {
993
1490
  } catch (error) {
994
1491
  return toolExecutionFailure(ctx, call, error);
995
1492
  }
996
- if (iterator === void 0) return resultEvent(call, "execution:0", { output });
1493
+ if (iterator === void 0) return resultEvent(call, `${ctx.event.id}:execution:0`, { output });
997
1494
  let last;
998
1495
  let sequence = 0;
999
1496
  let done = false;
@@ -1005,16 +1502,21 @@ function createHandlers(options) {
1005
1502
  } catch (error) {
1006
1503
  return toolExecutionFailure(ctx, call, error);
1007
1504
  }
1008
- if (checkAbort(ctx.signal)) return;
1009
1505
  if (result.done) {
1506
+ if (ctx.signal.reason instanceof A2Error) throw ctx.signal.reason;
1010
1507
  done = true;
1011
1508
  break;
1012
1509
  }
1510
+ if (checkAbort(ctx.signal)) return;
1013
1511
  last = result.value;
1014
- await ctx.session.append(`tool:${call.toolCallId}:preliminary:${sequence}`, resultEvent(call, `execution:${ctx.attempt}:${sequence}:preliminary`, {
1015
- output: result.value,
1016
- preliminary: true
1017
- }));
1512
+ await control.append({
1513
+ ctx,
1514
+ name: `tool:${call.toolCallId}:preliminary:${sequence}`,
1515
+ events: [resultEvent(call, `${ctx.event.id}:execution:${ctx.attempt}:${sequence}:preliminary`, {
1516
+ output: result.value,
1517
+ preliminary: true
1518
+ })]
1519
+ });
1018
1520
  sequence += 1;
1019
1521
  }
1020
1522
  } finally {
@@ -1022,12 +1524,13 @@ function createHandlers(options) {
1022
1524
  await iterator.return?.();
1023
1525
  } catch {}
1024
1526
  }
1025
- return resultEvent(call, `execution:${sequence}:final`, sequence === 0 ? {} : { output: last });
1527
+ return resultEvent(call, `${ctx.event.id}:execution:${sequence}:final`, sequence === 0 ? {} : { output: last });
1026
1528
  };
1027
- const executeTool = async (ctx, call) => {
1529
+ const executeTool = async (input) => {
1530
+ const { ctx, call } = input;
1028
1531
  const execute = tools[call.toolName]?.execute;
1029
- if (execute === void 0) return resultEvent(call, "execution:error", { error: `Tool '${call.toolName}' has no server executor` });
1030
- const messages = await promptMessages(ctx.event.sessionId, call, ctx.session.history);
1532
+ if (execute === void 0) return resultEvent(call, `${ctx.event.id}:execution:error`, { error: `Tool '${call.toolName}' has no server executor` });
1533
+ const messages = await promptMessages(input);
1031
1534
  if (checkAbort(ctx.signal)) return;
1032
1535
  const scope = {
1033
1536
  contract: options.agent.contract,
@@ -1035,62 +1538,82 @@ function createHandlers(options) {
1035
1538
  };
1036
1539
  return toolScopeStorage.run(scope, () => runToolExecution(ctx, call, execute, messages));
1037
1540
  };
1038
- const handleToolCall = async (ctx) => {
1039
- const response = (await ctx.session.state(coordinator, { through: "latest" })).state.response;
1040
- const current = response?.calls.find((candidate) => candidate.index === ctx.event.index);
1041
- if (current === void 0 || response?.generation?.generationId !== ctx.event.payload.generationId || response.failure !== void 0 || current.terminal || current.approval !== void 0) return;
1042
- if (current.call.providerExecuted === true) {
1043
- await continueIfReady(ctx, current.call.generationId);
1044
- return;
1045
- }
1046
- return executeTool(ctx, current.call);
1047
- };
1048
- const handleApproval = async (ctx) => {
1049
- const response = (await ctx.session.state(coordinator, { through: "latest" })).state.response;
1050
- const current = response?.calls.find((candidate) => candidate.approval?.approvalId === ctx.event.payload.approvalId && candidate.approval.messageId === ctx.event.payload.messageId && candidate.approval.generationId === ctx.event.payload.generationId && candidate.responseIndex === ctx.event.index);
1051
- if (current === void 0 || response?.generation?.generationId !== current.call.generationId || response.failure !== void 0 || current.terminal) return;
1052
- if (current.call.providerExecuted === true) {
1053
- await continueIfReady(ctx, current.call.generationId);
1054
- return;
1541
+ const handleToolExecution = async (ctx) => {
1542
+ try {
1543
+ const state = (await ctx.session.state(control.reducer, { through: "latest" })).state;
1544
+ const request = ctx.event.payload;
1545
+ const call = state.coordinator.response?.calls.find((candidate) => candidate.work?.id === ctx.event.id);
1546
+ if (state.active?.turnId !== request.turnId || state.active.suspended || state.active.version !== request.version || state.coordinator.response?.failure !== void 0 || !call || call.terminal || call.work?.settled) return control.settled({
1547
+ ctx,
1548
+ events: void 0
1549
+ });
1550
+ return control.settled({
1551
+ ctx,
1552
+ events: await executeTool({
1553
+ ctx,
1554
+ call: request.call,
1555
+ generation: request.generation
1556
+ })
1557
+ });
1558
+ } catch (error) {
1559
+ if (control.cancelled({
1560
+ error,
1561
+ signal: ctx.signal
1562
+ })) return control.settled({
1563
+ ctx,
1564
+ events: void 0
1565
+ });
1566
+ throw error;
1055
1567
  }
1056
- if (!ctx.event.payload.approved) return resultEvent(current.call, "execution:denied", { denied: true });
1057
- return executeTool(ctx, current.call);
1058
1568
  };
1059
1569
  const generationHandler = async (ctx) => {
1060
- const history = await ctx.session.history();
1570
+ if (checkAbort(ctx.signal)) return;
1061
1571
  const requestId = ctx.event.id;
1062
1572
  const request = ctx.event.payload;
1063
- const coordinatorState = (await ctx.session.state(coordinator, { through: "latest" })).state;
1573
+ const snapshot = await ctx.session.state(options.agent.reducer, { through: "latest" });
1574
+ if (snapshot.state.active?.phase === "paused" || snapshot.state.active?.phase === "pausing") return;
1575
+ const coordinatorState = (await ctx.session.state(coordinator, { through: snapshot.index })).state;
1064
1576
  if (coordinatorState.closed || coordinatorState.response?.activeRequestId !== requestId) return;
1065
- if (history.some((event) => {
1066
- if (event.type === "ai.generation.completed") return event.payload.requestId === requestId;
1067
- if (event.type !== "ai.generation.failed") return false;
1068
- const payload = event.payload;
1069
- return payload.requestId === requestId && payload.superseded !== true;
1070
- })) return;
1071
- const previousStarts = history.filter((event) => event.type === "ai.generation.started" && event.payload.requestId === requestId);
1072
- if (previousStarts.map((event) => event.payload.attempt).some((priorAttempt) => priorAttempt >= ctx.attempt)) return;
1577
+ const response = coordinatorState.response;
1578
+ const current = response.generation?.requestId === requestId ? response.generation : void 0;
1579
+ if (current !== void 0 && (response.completion !== void 0 || response.failure !== void 0 && response.failure.superseded !== true)) return;
1580
+ if (current !== void 0 && current.attempt >= ctx.attempt) return;
1073
1581
  const attempt = ctx.attempt;
1074
1582
  const generationId = `${requestId}:generation:${attempt}`;
1075
1583
  const responseMessageId = request.responseMessageId ?? (request.reason === "message" ? `${request.messageId}:assistant` : request.messageId);
1076
- const responseStepCount = history.filter((event) => event.type === "ai.generation.completed" && event.payload.responseMessageId === responseMessageId).length;
1077
- const sourceCoordinatorState = coordinatorStateAt(history, ctx.event.index);
1078
- if (request.reason === "tool") {
1079
- if (!requestId.startsWith("ai.generate:tools:")) return;
1080
- const sourceGenerationId = requestId.slice(18);
1081
- if (!continuationReady(sourceCoordinatorState) || sourceCoordinatorState.response?.generation?.generationId !== sourceGenerationId || sourceCoordinatorState.response.responseMessageId !== request.messageId || sourceCoordinatorState.response.responseMessageId !== request.responseMessageId) return;
1584
+ const responseStepCount = response.stepCount;
1585
+ if (request.reason === "tool" && (requestId !== `ai.generate:tools:${response.source?.generation.generationId}` || response.responseMessageId !== request.messageId || response.responseMessageId !== request.responseMessageId)) return;
1586
+ const previous = current;
1587
+ const replaced = previous === void 0 ? [] : [{
1588
+ generation: previous,
1589
+ frontier: response.promptThroughIndex
1590
+ }];
1591
+ if (request.reason === "retry" && response.source?.failed) replaced.push({
1592
+ generation: response.source.generation,
1593
+ frontier: response.source.promptThroughIndex
1594
+ });
1595
+ const replacedGenerationIds = new Set(replaced.map(({ generation: owner }) => owner.generationId));
1596
+ let state = snapshot.state;
1597
+ if (replaced.length > 0) {
1598
+ const through = Math.min(...replaced.map(({ frontier }) => frontier));
1599
+ const baseline = await ctx.session.state(options.agent.reducer, { through });
1600
+ const tail = await ctx.session.history({
1601
+ gte: through + 1,
1602
+ lte: snapshot.index
1603
+ });
1604
+ state = foldAIEvents({
1605
+ agent: options.agent,
1606
+ events: withoutGenerationLifecycle(tail, replacedGenerationIds),
1607
+ state: baseline.state
1608
+ });
1082
1609
  }
1083
- const previous = previousStarts.filter((event) => event.payload.attempt < attempt).toSorted((left, right) => right.payload.attempt - left.payload.attempt)[0];
1084
- const incompleteId = previous ? previous.payload.generationId : void 0;
1085
- const replacedGenerationIds = /* @__PURE__ */ new Set();
1086
- if (incompleteId !== void 0) replacedGenerationIds.add(incompleteId);
1087
- if (request.reason === "retry" && sourceCoordinatorState.response?.failure?.generationId !== void 0) replacedGenerationIds.add(sourceCoordinatorState.response.failure.generationId);
1088
- const promptHistory = withoutGenerationLifecycle(history, replacedGenerationIds);
1089
- const state = replay(options.agent, promptHistory);
1090
1610
  const resolverContext = {
1091
1611
  event: ctx.event,
1092
1612
  state,
1093
- history,
1613
+ session: { state: (reducer, readOptions) => ctx.session.state(reducer, {
1614
+ ...readOptions,
1615
+ through: readOptions?.through ?? snapshot.index
1616
+ }) },
1094
1617
  signal: ctx.signal
1095
1618
  };
1096
1619
  let generationStarted = false;
@@ -1101,14 +1624,14 @@ function createHandlers(options) {
1101
1624
  const resolvedInstructions = options.instructions === void 0 ? void 0 : await resolve(options.instructions, resolverContext);
1102
1625
  if (checkAbort(ctx.signal)) return;
1103
1626
  const compaction = typeof configuredCompaction === "function" ? validateCompaction({
1104
- compaction: configuredCompaction(resolverContext),
1627
+ compaction: await resolve(configuredCompaction, resolverContext),
1105
1628
  explicit: true,
1106
1629
  generation: options.generation,
1107
1630
  tools: options.tools
1108
1631
  }) : staticCompaction;
1109
1632
  if (checkAbort(ctx.signal)) return;
1110
1633
  if (request.reason === "tool" && responseStepCount >= maxSteps) {
1111
- const source = sourceCoordinatorState.response?.generation;
1634
+ const source = response.source?.generation;
1112
1635
  if (source === void 0) return;
1113
1636
  return {
1114
1637
  type: "ai.generation.failed",
@@ -1130,7 +1653,7 @@ function createHandlers(options) {
1130
1653
  responseMessageId,
1131
1654
  attempt,
1132
1655
  model: modelName(resolvedModel),
1133
- promptThroughIndex: history.at(-1)?.index ?? ctx.event.index
1656
+ promptThroughIndex: snapshot.index
1134
1657
  };
1135
1658
  const catalogModelId = gatewayModelId(resolvedModel);
1136
1659
  const gatewayOptions = options.generation?.providerOptions?.["gateway"];
@@ -1143,7 +1666,7 @@ function createHandlers(options) {
1143
1666
  payload: { modelId: catalogModelId }
1144
1667
  });
1145
1668
  if (previous) {
1146
- const payload = previous.payload;
1669
+ const payload = previous;
1147
1670
  const superseded = {
1148
1671
  requestId,
1149
1672
  messageId: payload.messageId,
@@ -1163,20 +1686,25 @@ function createHandlers(options) {
1163
1686
  id: generationId,
1164
1687
  payload: started
1165
1688
  });
1166
- const generationHistory = [...history, ...await ctx.session.append("generation-start", ...startEvents)];
1689
+ const generationEvents = await control.append({
1690
+ ctx,
1691
+ name: "generation-start",
1692
+ events: startEvents
1693
+ });
1167
1694
  generationStarted = true;
1168
- const startedCoordinatorState = (await ctx.session.state(coordinator, { through: "latest" })).state;
1169
- if (startedCoordinatorState.response?.activeRequestId !== requestId || startedCoordinatorState.response.generation?.generationId !== generationId) return;
1170
- const promptCoordinatorState = {
1171
- ...coordinatorStateAt(history),
1172
- queued: queuedMessagesAt(history)
1173
- };
1174
- const messages = activeContextMessages({
1695
+ if (checkAbort(ctx.signal)) return;
1696
+ const promptCoordinatorState = coordinatorState;
1697
+ const messages = await contextMessages({
1175
1698
  agent: options.agent,
1176
- state,
1177
- history: promptHistory,
1178
- coordinator: promptCoordinatorState
1699
+ session: ctx.session,
1700
+ snapshot: {
1701
+ state,
1702
+ index: snapshot.index
1703
+ },
1704
+ coordinator: promptCoordinatorState,
1705
+ excluded: replacedGenerationIds
1179
1706
  });
1707
+ let generationMessages = messages;
1180
1708
  let modelMessages = await modelContext({
1181
1709
  messages,
1182
1710
  state,
@@ -1190,7 +1718,7 @@ function createHandlers(options) {
1190
1718
  messages,
1191
1719
  modelMessages,
1192
1720
  state,
1193
- history,
1721
+ session: resolverContext.session,
1194
1722
  signal: ctx.signal,
1195
1723
  model: resolvedModel,
1196
1724
  tools,
@@ -1203,7 +1731,7 @@ function createHandlers(options) {
1203
1731
  const limits = metadata?.status === "resolved" && !usesFallbackModels ? metadata.limits : void 0;
1204
1732
  let inputTokenEstimate;
1205
1733
  let compacted = false;
1206
- const canCompact = sourceCoordinatorState.response?.calls.every((call) => call.terminal) ?? true;
1734
+ const canCompact = response.source?.canCompact ?? true;
1207
1735
  if (policy && canCompact) {
1208
1736
  const compactionContext = {
1209
1737
  ...resolverContext,
@@ -1221,7 +1749,7 @@ function createHandlers(options) {
1221
1749
  const inputTokens = measuredInputTokens({
1222
1750
  estimate: inputTokenEstimate,
1223
1751
  model: modelName(resolvedModel),
1224
- history
1752
+ calibration: coordinatorState.calibration
1225
1753
  });
1226
1754
  const threshold = policy.thresholdTokens ?? (limits === void 0 ? void 0 : Math.floor(Math.min(limits.contextWindow * .75, limits.contextWindow - (generation.maxOutputTokens ?? 0))));
1227
1755
  if (limits !== void 0 && threshold !== void 0 && threshold <= 0) throw new Error(`output allowance exhausts the ${limits.contextWindow}-token context window`);
@@ -1230,15 +1758,19 @@ function createHandlers(options) {
1230
1758
  if (checkAbort(ctx.signal)) return;
1231
1759
  if (shouldCompact) {
1232
1760
  const throughMessageId = state.messages.findLast((message) => !promptCoordinatorState.queued.some((queued) => queued.messageId === message.id))?.id ?? request.messageId;
1233
- const throughIndex = history.at(-1)?.index ?? ctx.event.index;
1234
- generationHistory.push(...await ctx.session.append("compaction-requested", {
1235
- type: "ai.compaction.requested",
1236
- id: `${generationId}:compaction:requested`,
1237
- payload: {
1238
- generationId,
1239
- throughMessageId,
1240
- throughIndex
1241
- }
1761
+ const throughIndex = snapshot.index;
1762
+ generationEvents.push(...await control.append({
1763
+ ctx,
1764
+ name: "compaction-requested",
1765
+ events: [{
1766
+ type: "ai.compaction.requested",
1767
+ id: `${generationId}:compaction:requested`,
1768
+ payload: {
1769
+ generationId,
1770
+ throughMessageId,
1771
+ throughIndex
1772
+ }
1773
+ }]
1242
1774
  }));
1243
1775
  const result = !("shouldCompact" in policy) ? {
1244
1776
  messages: [],
@@ -1255,23 +1787,33 @@ function createHandlers(options) {
1255
1787
  ...result,
1256
1788
  ...promptCoordinatorState.queued.length === 0 ? {} : { retainedMessageIds: promptCoordinatorState.queued.map((item) => item.messageId) }
1257
1789
  };
1258
- generationHistory.push(...await ctx.session.append("compaction-completed", {
1259
- type: "ai.compaction.completed",
1260
- id: `${generationId}:compaction:completed`,
1261
- payload: completed
1790
+ generationEvents.push(...await control.append({
1791
+ ctx,
1792
+ name: "compaction-completed",
1793
+ events: [{
1794
+ type: "ai.compaction.completed",
1795
+ id: `${generationId}:compaction:completed`,
1796
+ payload: completed
1797
+ }]
1262
1798
  }));
1799
+ const queued = new Set(promptCoordinatorState.queued.map((item) => item.messageId));
1800
+ generationMessages = result.messages.filter((message) => !queued.has(message.id));
1263
1801
  compacted = true;
1264
1802
  }
1265
1803
  }
1266
- const currentState = replay(options.agent, generationHistory);
1267
- const currentCoordinatorState = coordinatorStateAt(generationHistory);
1268
- const generationMessages = activeContextMessages({
1804
+ const currentState = foldAIEvents({
1269
1805
  agent: options.agent,
1270
- state: currentState,
1271
- history: generationHistory,
1272
- coordinator: currentCoordinatorState
1806
+ events: generationEvents,
1807
+ state: snapshot.state
1273
1808
  });
1274
- modelMessages = await modelContext({
1809
+ if (request.reason === "retry" || previous !== void 0) generationMessages = await contextMessages({
1810
+ agent: options.agent,
1811
+ session: ctx.session,
1812
+ snapshot,
1813
+ coordinator: promptCoordinatorState,
1814
+ appended: generationEvents
1815
+ });
1816
+ if (policy && "shouldCompact" in policy || generationMessages !== messages || activeCompaction(currentState)?.summary !== activeCompaction(state)?.summary) modelMessages = await modelContext({
1275
1817
  messages: generationMessages,
1276
1818
  state: currentState,
1277
1819
  tools
@@ -1292,7 +1834,7 @@ function createHandlers(options) {
1292
1834
  messages: generationMessages,
1293
1835
  modelMessages,
1294
1836
  state: currentState,
1295
- history,
1837
+ session: resolverContext.session,
1296
1838
  signal: ctx.signal,
1297
1839
  model: resolvedModel,
1298
1840
  tools,
@@ -1332,11 +1874,15 @@ function createHandlers(options) {
1332
1874
  custom
1333
1875
  });
1334
1876
  pendingToolCalls = lifecycle.pending;
1335
- await ctx.session.append(`generation-progress:${sequence}`, {
1336
- type: "ai.generation.progress",
1337
- id: `${generationId}:progress:${sequence}`,
1338
- payload: progress
1339
- }, ...lifecycle.events);
1877
+ await control.append({
1878
+ ctx,
1879
+ name: `generation-progress:${sequence}`,
1880
+ events: [{
1881
+ type: "ai.generation.progress",
1882
+ id: `${generationId}:progress:${sequence}`,
1883
+ payload: progress
1884
+ }, ...lifecycle.events]
1885
+ });
1340
1886
  sequence += 1;
1341
1887
  }
1342
1888
  if (!finish) throw new Error("agent generation finished without output");
@@ -1368,22 +1914,12 @@ function createHandlers(options) {
1368
1914
  }
1369
1915
  ];
1370
1916
  } catch (error) {
1917
+ if (error instanceof ControlCancelled) throw error;
1371
1918
  if (error instanceof A2Error) {
1372
1919
  checkAbort(ctx.signal);
1373
1920
  throw error;
1374
1921
  }
1375
- if (checkAbort(ctx.signal)) {
1376
- const interrupted = {
1377
- messageId: responseMessageId,
1378
- generationId,
1379
- reason: "aborted"
1380
- };
1381
- return {
1382
- type: "ai.message.interrupted",
1383
- id: `${generationId}:interrupted`,
1384
- payload: interrupted
1385
- };
1386
- }
1922
+ if (checkAbort(ctx.signal)) return;
1387
1923
  if (!generationStarted) throw error;
1388
1924
  const failed = {
1389
1925
  requestId,
@@ -1399,36 +1935,10 @@ function createHandlers(options) {
1399
1935
  };
1400
1936
  }
1401
1937
  };
1402
- const handleMessageCreated = async (ctx) => {
1403
- if (ctx.event.payload.message.role !== "user" || ctx.event.payload.generate === false) return;
1404
- return scheduleNext(ctx);
1405
- };
1406
- const handleRetry = async (ctx) => {
1407
- const response = (await ctx.session.state(coordinator, { through: "latest" })).state.response;
1408
- if (response?.status !== "failed" || response.rootMessageId !== ctx.event.payload.messageId || response.responseMessageId !== ctx.event.payload.responseMessageId) return;
1409
- return {
1410
- type: "ai.generation.requested",
1411
- id: `ai.generate:retry:${ctx.event.payload.retryId}`,
1412
- payload: {
1413
- messageId: response.rootMessageId,
1414
- responseMessageId: response.responseMessageId,
1415
- reason: "retry"
1416
- }
1417
- };
1418
- };
1419
- const handleInputResponse = async (ctx) => {
1420
- const response = (await ctx.session.state(coordinator, { through: "latest" })).state.response;
1421
- if (response?.responseMessageId !== ctx.event.payload.messageId || response.inputResponse?.index !== ctx.event.index || response.inputResponse.generationId !== ctx.event.payload.generationId || response.inputResponse.inputId !== ctx.event.payload.inputId) return;
1422
- await continueIfReady(ctx, ctx.event.payload.generationId);
1423
- };
1424
1938
  const clearPromptCache = (sessionId) => {
1425
1939
  const prefix = `${sessionId}\u001f`;
1426
1940
  for (const key of promptCache.keys()) if (key.startsWith(prefix)) promptCache.delete(key);
1427
1941
  };
1428
- const handleResponseEnded = async (ctx) => {
1429
- clearPromptCache(ctx.event.sessionId);
1430
- return scheduleNext(ctx);
1431
- };
1432
1942
  return {
1433
1943
  "ai.model.metadata.requested": { handler: async (ctx) => {
1434
1944
  const limits = await readModelLimits({
@@ -1445,66 +1955,69 @@ function createHandlers(options) {
1445
1955
  }
1446
1956
  };
1447
1957
  } },
1448
- "ai.message.created": {
1449
- lane: "a2.ai.turn",
1450
- handler: handleMessageCreated
1451
- },
1452
- "ai.retry.requested": {
1453
- lane: "a2.ai.turn",
1454
- handler: handleRetry
1958
+ "ai.control.requested": {
1959
+ lane: "a2.ai.control",
1960
+ handler: control.handler
1455
1961
  },
1456
- "ai.input.responded": {
1457
- lane: "a2.ai.turn",
1458
- handler: handleInputResponse
1962
+ "ai.work.reported": {
1963
+ lane: "a2.ai.control",
1964
+ handler: control.handler
1459
1965
  },
1460
- "ai.message.completed": {
1461
- lane: "a2.ai.turn",
1462
- handler: handleResponseEnded
1463
- },
1464
- "ai.message.interrupted": {
1465
- lane: "a2.ai.turn",
1466
- handler: handleResponseEnded
1467
- },
1468
- "ai.session.closed": { handler: (ctx) => {
1966
+ "ai.message.completed": { handler: async (ctx) => {
1967
+ clearPromptCache(ctx.event.sessionId);
1968
+ } },
1969
+ "ai.message.interrupted": { handler: async (ctx) => {
1469
1970
  clearPromptCache(ctx.event.sessionId);
1470
- return Promise.resolve();
1471
1971
  } },
1972
+ "ai.session.closed": {
1973
+ lane: "a2.ai.control",
1974
+ handler: async (ctx) => {
1975
+ clearPromptCache(ctx.event.sessionId);
1976
+ return control.handler(ctx);
1977
+ }
1978
+ },
1472
1979
  "ai.generation.failed": { handler: (ctx) => {
1473
1980
  promptCache.delete(promptCacheKey(ctx.event.sessionId, ctx.event.payload.generationId));
1474
1981
  return Promise.resolve();
1475
1982
  } },
1476
1983
  "ai.generation.requested": {
1477
- lane: "a2.ai.turn",
1984
+ lane: "a2.ai.model",
1478
1985
  abortOn: {
1479
- "ai.message.interrupted": (event, trigger, context) => {
1480
- const responseMessageId = trigger.payload.responseMessageId ?? (trigger.payload.reason === "message" ? `${trigger.payload.messageId}:assistant` : trigger.payload.messageId);
1481
- return event.payload.messageId === responseMessageId && (event.payload.requestId === trigger.id || event.payload.generationId === `${trigger.id}:generation:${context.attempt}`);
1986
+ "ai.control.committed": (event, trigger) => {
1987
+ const active = event.payload.view;
1988
+ return active?.turnId !== trigger.payload.control?.turnId || active?.version !== trigger.payload.control?.version;
1482
1989
  },
1483
1990
  "ai.session.closed": true
1484
1991
  },
1485
- handler: generationHandler
1486
- },
1487
- "ai.generation.completed": { handler: async (ctx) => continueIfReady(ctx, ctx.event.payload.generationId) },
1488
- "ai.tool.called": {
1489
- abortOn: {
1490
- "ai.generation.failed": (event, trigger) => event.payload.generationId === trigger.payload.generationId,
1491
- "ai.message.interrupted": (event, trigger) => event.payload.messageId === trigger.payload.messageId && (event.payload.requestId === trigger.payload.requestId || event.payload.generationId === trigger.payload.generationId),
1492
- "ai.session.closed": true
1493
- },
1494
- handler: handleToolCall
1992
+ handler: async (ctx) => {
1993
+ try {
1994
+ return control.settled({
1995
+ ctx,
1996
+ events: await generationHandler(ctx)
1997
+ });
1998
+ } catch (error) {
1999
+ if (control.cancelled({
2000
+ error,
2001
+ signal: ctx.signal
2002
+ })) return control.settled({
2003
+ ctx,
2004
+ events: void 0
2005
+ });
2006
+ throw error;
2007
+ }
2008
+ }
1495
2009
  },
1496
- "ai.approval.responded": {
2010
+ "ai.tool.execution.requested": {
1497
2011
  abortOn: {
1498
- "ai.generation.failed": (event, trigger) => event.payload.responseMessageId === trigger.payload.messageId && event.payload.generationId === trigger.payload.generationId,
1499
- "ai.message.interrupted": (event, trigger) => event.payload.messageId === trigger.payload.messageId && (event.payload.requestId === generationRequestId(trigger.payload.generationId) || event.payload.generationId === trigger.payload.generationId),
2012
+ "ai.generation.failed": (event, trigger) => event.payload.generationId === trigger.payload.call.generationId,
2013
+ "ai.control.committed": (event, trigger) => {
2014
+ const active = event.payload.view;
2015
+ return active?.turnId !== trigger.payload.turnId || active?.version !== trigger.payload.version;
2016
+ },
1500
2017
  "ai.session.closed": true
1501
2018
  },
1502
- handler: handleApproval
1503
- },
1504
- "ai.tool.result": { handler: async (ctx) => {
1505
- if (ctx.event.payload.preliminary === true) return;
1506
- await continueIfReady(ctx, ctx.event.payload.generationId);
1507
- } }
2019
+ handler: handleToolExecution
2020
+ }
1508
2021
  };
1509
2022
  }
1510
2023
  /** Assemble an A2 server with the built-in agent handlers and app extensions. */