experimental-a2 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +43 -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 +1053 -513
  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 +287 -65
  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 +429 -619
  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,11 @@
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";
7
+ import { createHash } from "node:crypto";
5
8
  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
9
  //#region src/ai-coordinator.ts
149
10
  const updateResponse = (state, update) => state.response === void 0 ? state : {
150
11
  ...state,
@@ -156,6 +17,11 @@ const foldCoordinator = (state, event) => {
156
17
  closed: true,
157
18
  queued: []
158
19
  };
20
+ case "ai.retry.requested":
21
+ case "ai.compaction.completed": {
22
+ const { calibration: _, ...rest } = state;
23
+ return rest;
24
+ }
159
25
  case "ai.message.created": {
160
26
  const payload = event.payload;
161
27
  const message = payload.message;
@@ -163,24 +29,24 @@ const foldCoordinator = (state, event) => {
163
29
  return {
164
30
  ...state,
165
31
  queued: [...state.queued.filter((item) => item.messageId !== message.id), {
166
- index: event.index,
167
32
  messageId: message.id,
168
33
  generate: payload.generate !== false
169
- }].toSorted((left, right) => left.index - right.index)
34
+ }]
170
35
  };
171
36
  }
172
37
  case "ai.generation.requested": {
173
38
  const request = event.payload;
174
39
  if (request.reason === "message") {
175
40
  const responseMessageId = request.responseMessageId ?? `${request.messageId}:assistant`;
176
- const requested = state.queued.find((item) => item.messageId === request.messageId);
41
+ const requested = state.queued.findIndex((item) => item.messageId === request.messageId);
177
42
  return {
178
43
  ...state,
179
- queued: requested === void 0 ? state.queued.filter((item) => item.messageId !== request.messageId) : state.queued.filter((item) => item.index > requested.index),
44
+ queued: requested < 0 ? state.queued.filter((item) => item.messageId !== request.messageId) : state.queued.slice(requested + 1),
180
45
  response: {
181
46
  rootMessageId: request.messageId,
182
47
  responseMessageId,
183
48
  status: "requested",
49
+ stepCount: 0,
184
50
  activeRequestId: event.id,
185
51
  calls: [],
186
52
  inputs: []
@@ -189,14 +55,25 @@ const foldCoordinator = (state, event) => {
189
55
  }
190
56
  if (state.response === void 0 || request.responseMessageId !== state.response.responseMessageId) return state;
191
57
  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
- }));
58
+ return updateResponse(state, (response) => {
59
+ const generation = response.generation ?? response.source?.generation;
60
+ const promptThroughIndex = response.promptThroughIndex ?? response.source?.promptThroughIndex;
61
+ return {
62
+ rootMessageId: response.rootMessageId,
63
+ responseMessageId: response.responseMessageId,
64
+ status: "requested",
65
+ stepCount: response.stepCount,
66
+ activeRequestId: event.id,
67
+ ...generation === void 0 || promptThroughIndex === void 0 ? {} : { source: {
68
+ generation,
69
+ promptThroughIndex,
70
+ failed: response.failure?.generationId === generation.generationId,
71
+ canCompact: response.calls.every((call) => call.terminal)
72
+ } },
73
+ calls: [],
74
+ inputs: []
75
+ };
76
+ });
200
77
  }
201
78
  case "ai.generation.started": {
202
79
  const generation = event.payload;
@@ -205,7 +82,10 @@ const foldCoordinator = (state, event) => {
205
82
  rootMessageId: response.rootMessageId,
206
83
  responseMessageId: response.responseMessageId,
207
84
  status: "generating",
85
+ stepCount: response.stepCount,
208
86
  generation,
87
+ ...generation.promptThroughIndex === void 0 ? {} : { promptThroughIndex: generation.promptThroughIndex },
88
+ ...response.source === void 0 ? {} : { source: response.source },
209
89
  activeRequestId: generation.requestId,
210
90
  calls: [],
211
91
  inputs: []
@@ -214,11 +94,22 @@ const foldCoordinator = (state, event) => {
214
94
  case "ai.generation.completed": {
215
95
  const completion = event.payload;
216
96
  if (state.response?.generation?.generationId !== completion.generationId) return state;
217
- return updateResponse(state, (response) => ({
97
+ const { calibration: _, ...uncalibrated } = updateResponse(state, (response) => ({
218
98
  ...response,
219
99
  status: "waiting",
220
- completion
100
+ completion,
101
+ stepCount: response.stepCount + (response.completion === void 0 ? 1 : 0)
221
102
  }));
103
+ const inputTokens = completion.usage?.inputTokens;
104
+ const estimate = completion.inputTokenEstimate;
105
+ return inputTokens !== void 0 && Number.isFinite(inputTokens) && inputTokens >= 0 && estimate !== void 0 ? {
106
+ ...uncalibrated,
107
+ calibration: {
108
+ model: state.response.generation.model,
109
+ inputTokens,
110
+ estimate
111
+ }
112
+ } : uncalibrated;
222
113
  }
223
114
  case "ai.generation.failed": {
224
115
  const failure = event.payload;
@@ -234,18 +125,16 @@ const foldCoordinator = (state, event) => {
234
125
  }
235
126
  case "ai.message.completed": {
236
127
  const messageId = event.payload.messageId;
237
- return state.response?.responseMessageId === messageId ? {
238
- closed: state.closed,
239
- queued: state.queued
240
- } : state;
128
+ if (state.response?.responseMessageId !== messageId) return state;
129
+ const { response: _, ...rest } = state;
130
+ return rest;
241
131
  }
242
132
  case "ai.message.interrupted": {
243
133
  const interruption = event.payload;
244
134
  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;
135
+ if (response?.responseMessageId !== interruption.messageId || interruption.generationId !== response.generation?.generationId && interruption.requestId !== response.activeRequestId) return state;
136
+ const { response: _, ...rest } = state;
137
+ return rest;
249
138
  }
250
139
  case "ai.tool.called": {
251
140
  const call = event.payload;
@@ -253,7 +142,6 @@ const foldCoordinator = (state, event) => {
253
142
  return updateResponse(state, (response) => ({
254
143
  ...response,
255
144
  calls: [...response.calls.filter((candidate) => candidate.call.toolCallId !== call.toolCallId), {
256
- index: event.index,
257
145
  call,
258
146
  terminal: false
259
147
  }]
@@ -277,8 +165,7 @@ const foldCoordinator = (state, event) => {
277
165
  ...response,
278
166
  calls: response.calls.map((candidate) => candidate.approval?.approvalId === approval.approvalId ? {
279
167
  ...candidate,
280
- response: approval,
281
- responseIndex: event.index
168
+ response: approval
282
169
  } : candidate)
283
170
  }));
284
171
  }
@@ -301,34 +188,701 @@ const foldCoordinator = (state, event) => {
301
188
  inputs: [...response.inputs.filter((candidate) => candidate.messageId !== input.messageId || candidate.generationId !== input.generationId || candidate.inputId !== input.inputId), input]
302
189
  }));
303
190
  }
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
191
+ case "ai.input.responded": {
192
+ const input = event.payload;
193
+ const requested = state.response?.inputs.find((candidate) => candidate.messageId === input.messageId && candidate.generationId === input.generationId && candidate.inputId === input.inputId && candidate.name === input.name);
194
+ if (state.response?.responseMessageId !== input.messageId || state.response.generation?.generationId !== input.generationId || requested === void 0) return state;
195
+ return updateResponse(state, (response) => ({
196
+ ...response,
197
+ inputs: response.inputs.filter((candidate) => candidate.inputId !== input.inputId),
198
+ inputResponse: {
199
+ generationId: input.generationId,
200
+ inputId: input.inputId,
201
+ name: requested.name
202
+ }
203
+ }));
204
+ }
205
+ default: return state;
206
+ }
207
+ };
208
+ const aiCoordinatorReducer = (contract) => contract.reducer({
209
+ name: "a2.ai.coordinator.v5",
210
+ initialState: {
211
+ closed: false,
212
+ queued: []
213
+ }
214
+ }).fold((state, event) => event.type === "ai.control.committed" ? applyControlChanges({
215
+ state,
216
+ changes: event.payload.changes.filter((change) => change.path[0] === "coordinator").map((change) => Object.assign({}, change, { path: change.path.slice(1) }))
217
+ }) : state);
218
+ const continuationReady = (state) => {
219
+ const response = state.response;
220
+ 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);
221
+ };
222
+ //#endregion
223
+ //#region src/ai-control-server.ts
224
+ var ControlCancelled = class extends Error {};
225
+ const callReadyForModel = (call) => call.terminal || call.call.providerExecuted === true && !call.call.supportsDeferredResults && call.response !== void 0;
226
+ const approvalBlocksExecution = (call) => call.approval !== void 0 && call.response?.approved !== true;
227
+ const createControlRuntime = (options) => {
228
+ const reducer = options.agent.contract.reducer({
229
+ name: "a2.ai.control.v1",
230
+ initialState: initialControlState()
231
+ }).fold((state, event) => event.type === "ai.control.committed" ? applyControlChanges({
232
+ state,
233
+ changes: event.payload.changes
234
+ }) : state);
235
+ const coordinator = aiCoordinatorReducer(options.agent.contract);
236
+ const handler = async (ctx) => {
237
+ const snapshot = await ctx.session.state(reducer, { through: "latest" });
238
+ const before = structuredClone(snapshot.state);
239
+ const moves = [];
240
+ const state = structuredClone(snapshot.state);
241
+ const facts = [];
242
+ const jobs = [];
243
+ const emit = ({ type, payload, id = `${ctx.event.id}:fact:${facts.length}` }) => {
244
+ const fact = {
245
+ type,
246
+ payload,
247
+ id
248
+ };
249
+ facts.push(fact);
250
+ state.coordinator = foldCoordinator(state.coordinator, {
251
+ ...ctx.event,
252
+ ...fact
253
+ });
254
+ };
255
+ let reason;
256
+ const end = ({ lastSeenIndex, reason: interruptionReason }) => {
257
+ const response = state.coordinator.response;
258
+ if (response === void 0) return;
259
+ const owner = response.generation === void 0 ? { requestId: response.activeRequestId } : { generationId: response.generation.generationId };
260
+ emit({
261
+ type: "ai.message.interrupted",
262
+ payload: {
263
+ messageId: response.responseMessageId,
264
+ ...owner,
265
+ ...interruptionReason === void 0 ? {} : { reason: interruptionReason },
266
+ ...lastSeenIndex === void 0 ? {} : { lastSeenIndex }
267
+ }
268
+ });
269
+ state.active = null;
270
+ };
271
+ const knownInput = (id) => state.inbox.items.some((candidate) => candidate.id === id) || Object.hasOwn(state.reservedIds, id);
272
+ const missingInputReason = (id) => state.reservedIds[id] === "removed" || !knownInput(id) ? "not-found" : "already-active";
273
+ const inputTaken = ({ message, generate = true }) => knownInput(message.id) || generate && message.role === "user" && knownInput(`${message.id}:assistant`);
274
+ const insert = ({ message, generate = true, first = false, afterStepOf }) => {
275
+ const candidate = {
276
+ id: message.id,
277
+ revision: 0,
278
+ message,
279
+ generate: generate && message.role === "user",
280
+ ...afterStepOf === void 0 ? {} : { afterStepOf }
281
+ };
282
+ state.reservedIds = {
283
+ ...state.reservedIds,
284
+ [candidate.id]: true,
285
+ ...candidate.generate ? { [`${candidate.id}:assistant`]: true } : {}
286
+ };
287
+ if (first) state.inbox.items.unshift(candidate);
288
+ else state.inbox.items.push(candidate);
289
+ };
290
+ const moveInput = ({ from, to }) => {
291
+ if (from === to) return;
292
+ const [moved] = state.inbox.items.splice(from, 1);
293
+ state.inbox.items.splice(to, 0, moved);
294
+ const [previous] = before.inbox.items.splice(from, 1);
295
+ before.inbox.items.splice(to, 0, previous);
296
+ moves.push({
297
+ path: ["inbox", "items"],
298
+ move: {
299
+ from,
300
+ to
301
+ }
302
+ });
303
+ };
304
+ const requestModel = ({ request, id }) => {
305
+ const active = state.active;
306
+ const payload = {
307
+ ...request,
308
+ control: {
309
+ turnId: active.turnId,
310
+ version: active.version
311
+ }
312
+ };
313
+ state.coordinator = foldCoordinator(state.coordinator, {
314
+ ...ctx.event,
315
+ type: "ai.generation.requested",
316
+ id,
317
+ payload
318
+ });
319
+ active.model = {
320
+ id,
321
+ version: active.version,
322
+ settled: false
323
+ };
324
+ active.phase = "starting";
325
+ jobs.push({
326
+ type: "ai.generation.requested",
327
+ id,
328
+ payload
329
+ });
330
+ };
331
+ if (ctx.event.type === "ai.session.closed") {
332
+ state.coordinator = {
333
+ closed: true,
334
+ queued: []
335
+ };
336
+ state.active = null;
337
+ state.inbox.items = [];
338
+ } else if (ctx.event.type === "ai.control.requested") {
339
+ const command = ctx.event.payload;
340
+ if (state.coordinator.closed) reason = "closed";
341
+ else switch (command.action) {
342
+ case "send":
343
+ if (inputTaken({
344
+ message: command.message,
345
+ generate: command.generate !== false
346
+ })) reason = "duplicate-input";
347
+ else insert({
348
+ message: command.message,
349
+ generate: command.generate !== false
350
+ });
351
+ break;
352
+ case "edit": {
353
+ const item = state.inbox.items.find((candidate) => candidate.id === command.message.id);
354
+ if (item === void 0) reason = missingInputReason(command.message.id);
355
+ else if (item.revision !== command.expectedRevision) reason = "revision-conflict";
356
+ else if (item.message.role !== command.message.role) reason = "revision-conflict";
357
+ else {
358
+ item.message = command.message;
359
+ item.revision += 1;
360
+ }
361
+ break;
362
+ }
363
+ case "remove": {
364
+ const position = state.inbox.items.findIndex((item) => item.id === command.inputId);
365
+ if (position < 0) reason = missingInputReason(command.inputId);
366
+ else {
367
+ state.inbox.items.splice(position, 1);
368
+ state.reservedIds = {
369
+ ...state.reservedIds,
370
+ [command.inputId]: "removed"
371
+ };
372
+ }
373
+ break;
374
+ }
375
+ case "move": {
376
+ const index = state.inbox.items.findIndex((candidate) => candidate.id === command.inputId);
377
+ if (index < 0) reason = missingInputReason(command.inputId);
378
+ else if (command.beforeId !== null && !state.inbox.items.some((candidate) => candidate.id === command.beforeId)) reason = "not-found";
379
+ else {
380
+ const destination = command.beforeId === null ? state.inbox.items.length : state.inbox.items.findIndex((item) => item.id === command.beforeId);
381
+ moveInput({
382
+ from: index,
383
+ to: destination > index ? destination - 1 : destination
384
+ });
385
+ }
386
+ break;
387
+ }
388
+ case "send-now": {
389
+ const position = state.inbox.items.findIndex((item) => item.id === command.inputId);
390
+ const selected = state.inbox.items[position];
391
+ if (!selected) reason = missingInputReason(command.inputId);
392
+ else if ((state.active?.turnId ?? null) !== command.turnId) reason = "stale-turn";
393
+ else if (selected.message.role !== "user") reason = "not-user-input";
394
+ else if (!selected.generate && knownInput(`${selected.id}:assistant`)) reason = "duplicate-input";
395
+ else {
396
+ if (!selected.generate) {
397
+ state.reservedIds = {
398
+ ...state.reservedIds,
399
+ [`${selected.id}:assistant`]: true
400
+ };
401
+ selected.generate = true;
402
+ }
403
+ if (state.active) selected.afterStepOf = state.active.turnId;
404
+ else delete selected.afterStepOf;
405
+ moveInput({
406
+ from: position,
407
+ to: 0
408
+ });
409
+ }
410
+ break;
411
+ }
412
+ case "stop":
413
+ if (state.active?.turnId !== command.turnId) reason = "stale-turn";
414
+ else end({
415
+ lastSeenIndex: command.lastSeenIndex,
416
+ reason: void 0
417
+ });
418
+ break;
419
+ case "steer":
420
+ if (inputTaken({ message: command.message })) reason = "duplicate-input";
421
+ else insert({
422
+ message: command.message,
423
+ generate: true,
424
+ first: true,
425
+ afterStepOf: command.turnId
426
+ });
427
+ break;
428
+ case "interrupt": {
429
+ const response = state.coordinator.response;
430
+ if (!response || response.responseMessageId !== command.request.messageId || command.request.generationId !== response.generation?.generationId && command.request.requestId !== response.activeRequestId) reason = "stale-turn";
431
+ else end({
432
+ lastSeenIndex: command.request.lastSeenIndex,
433
+ reason: command.request.reason
434
+ });
435
+ break;
436
+ }
437
+ case "pause": {
438
+ state.inbox.paused = true;
439
+ const active = state.active;
440
+ if (command.when === "now" && active !== null && !active.suspended) {
441
+ active.suspended = true;
442
+ active.version += 1;
443
+ const generation = state.coordinator.response?.generation;
444
+ if (generation) emit({
445
+ type: "ai.generation.paused",
446
+ payload: {
447
+ generationId: generation.generationId,
448
+ messageId: generation.responseMessageId
449
+ }
450
+ });
451
+ }
452
+ break;
453
+ }
454
+ case "resume":
455
+ state.inbox.paused = false;
456
+ break;
457
+ case "tool-result":
458
+ 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";
459
+ else emit({
460
+ type: "ai.tool.result",
461
+ payload: command.result
462
+ });
463
+ break;
464
+ case "approval": {
465
+ const response = state.coordinator.response;
466
+ const call = response?.calls.find((candidate) => candidate.approval?.approvalId === command.response.approvalId && candidate.approval.generationId === command.response.generationId && candidate.approval.messageId === command.response.messageId);
467
+ if (response?.failure) reason = "stale-turn";
468
+ else if (!call || call.response || call.terminal) reason = "not-found";
469
+ else emit({
470
+ type: "ai.approval.responded",
471
+ payload: command.response
472
+ });
473
+ break;
474
+ }
475
+ case "request-input":
476
+ case "input": {
477
+ const payload = command.action === "input" ? command.response : command.request;
478
+ const response = state.coordinator.response;
479
+ 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";
480
+ else emit({
481
+ type: command.action === "input" ? "ai.input.responded" : "ai.input.requested",
482
+ payload
483
+ });
484
+ break;
485
+ }
486
+ case "retry": {
487
+ const response = state.coordinator.response;
488
+ if (!state.active || response?.status !== "failed" || response.rootMessageId !== command.request.messageId || response.responseMessageId !== command.request.responseMessageId) reason = "stale-turn";
489
+ else if (!state.inbox.paused && !state.active.suspended) {
490
+ emit({
491
+ type: "ai.retry.requested",
492
+ payload: command.request
493
+ });
494
+ requestModel({
495
+ request: {
496
+ messageId: response.rootMessageId,
497
+ responseMessageId: response.responseMessageId,
498
+ reason: "retry"
499
+ },
500
+ id: `ai.generate:retry:${command.request.retryId}`
501
+ });
502
+ } else reason = "stale-turn";
503
+ break;
504
+ }
505
+ }
506
+ } else {
507
+ const report = ctx.event.payload;
508
+ const active = state.active;
509
+ const call = state.coordinator.response?.calls.find((candidate) => candidate.work?.id === report.workId);
510
+ const work = report.kind === "model" ? active?.model : call?.work;
511
+ if (!(active?.turnId === report.turnId && work?.id === report.workId && work.version === report.version && !work.settled && report.attempt >= (work.attempt ?? 0))) reason = "stale-work";
512
+ else {
513
+ work.attempt = report.attempt;
514
+ const authorized = !active.suspended && active.version === report.version;
515
+ for (const event of report.events) {
516
+ if (work.facts?.includes(event.id)) continue;
517
+ if (authorized || report.kind === "tool" && event.type === "ai.tool.result") {
518
+ emit({
519
+ type: event.type,
520
+ payload: event.payload,
521
+ id: event.id
522
+ });
523
+ work.facts = [...work.facts ?? [], event.id];
524
+ }
525
+ }
526
+ if (report.settled) work.settled = true;
527
+ if (!authorized) reason = "stale-work";
528
+ }
529
+ }
530
+ let active = state.active;
531
+ let response = state.coordinator.response;
532
+ if (active && response === void 0) {
533
+ state.active = null;
534
+ active = null;
535
+ }
536
+ if (active && response) {
537
+ const handoff = state.inbox.items.some((item) => item.afterStepOf === active.turnId);
538
+ const outstanding = active.model !== null && !active.model.settled || response.calls.some((candidate) => candidate.work !== void 0 && !candidate.work.settled);
539
+ if (active.suspended) {
540
+ active.phase = outstanding ? "pausing" : "paused";
541
+ if (!state.inbox.paused && !outstanding && !handoff) active.suspended = false;
542
+ }
543
+ if (!active.suspended && response.failure) active.phase = "failed";
544
+ if (!active.suspended && response.failure === void 0) for (const call of response.calls) {
545
+ if (call.terminal || call.work !== void 0 && (!call.work.settled || call.work.version === active.version) || call.call.providerExecuted === true || call.approval && !call.response) continue;
546
+ if (call.response?.approved === false) {
547
+ emit({
548
+ type: "ai.tool.result",
549
+ payload: {
550
+ ...call.call,
551
+ denied: true,
552
+ phase: "execution"
553
+ }
554
+ });
555
+ continue;
556
+ }
557
+ const id = `${ctx.event.id}:tool:${call.call.toolCallId}`;
558
+ call.work = {
559
+ id,
560
+ version: active.version,
561
+ settled: false
562
+ };
563
+ jobs.push({
564
+ type: "ai.tool.execution.requested",
565
+ id,
566
+ payload: {
567
+ turnId: active.turnId,
568
+ version: active.version,
569
+ call: call.call,
570
+ generation: response.generation
571
+ }
572
+ });
573
+ }
574
+ response = state.coordinator.response;
575
+ const callsDone = response.calls.every(callReadyForModel);
576
+ const modelDone = active.model === null || active.model.settled;
577
+ const workSettled = modelDone && response.calls.every((call) => call.work === void 0 || call.work.settled);
578
+ const externalPending = response.calls.some((call) => call.call.providerExecuted && call.call.supportsDeferredResults && !call.terminal && !approvalBlocksExecution(call));
579
+ const handoffReady = handoff && workSettled && !externalPending && (active.suspended || response.failure !== void 0 || response.calls.every((call) => callReadyForModel(call) || approvalBlocksExecution(call)));
580
+ const readyForModel = !active.suspended && response.failure === void 0 && modelDone && callsDone && response.inputs.length === 0;
581
+ const naturalCompletion = readyForModel && response.completion !== void 0 && response.completion.finishReason !== "tool-calls";
582
+ if (handoffReady && !naturalCompletion) end({
583
+ lastSeenIndex: void 0,
584
+ reason: "steered after step"
585
+ });
586
+ else if (readyForModel && (!handoff || handoffReady)) {
587
+ const resume = active.model !== null && active.model.version !== active.version;
588
+ const input = response.inputResponse;
589
+ if (!handoff && (resume || continuationReady(state.coordinator) || response.completion && input)) {
590
+ const nextReason = resume ? "resume" : input ? "input" : "tool";
591
+ const id = nextReason === "tool" ? `ai.generate:tools:${response.generation.generationId}` : `${ctx.event.id}:generate:${nextReason}`;
592
+ requestModel({
593
+ request: {
594
+ messageId: response.responseMessageId,
595
+ responseMessageId: response.responseMessageId,
596
+ reason: nextReason
597
+ },
598
+ id
599
+ });
600
+ } else if (naturalCompletion) {
601
+ emit({
602
+ type: "ai.message.completed",
603
+ payload: { messageId: response.responseMessageId }
604
+ });
605
+ state.active = null;
606
+ }
607
+ }
608
+ if (!active.suspended && state.active && jobs.every((job) => job.type !== "ai.generation.requested")) state.active.phase = response.failure ? "failed" : !modelDone ? "generating" : !callsDone ? "tools" : "waiting";
609
+ }
610
+ if (state.active === null && !state.inbox.paused && !state.coordinator.closed) while (state.inbox.items.length > 0) {
611
+ const item = state.inbox.items.shift();
612
+ delete item.afterStepOf;
613
+ emit({
614
+ type: "ai.message.created",
615
+ payload: {
616
+ message: item.message,
617
+ generate: item.generate
618
+ },
619
+ id: `ai.admitted:${item.id}`
620
+ });
621
+ if (!item.generate) continue;
622
+ state.active = {
623
+ turnId: item.id,
624
+ input: item,
625
+ phase: "starting",
626
+ version: 0,
627
+ suspended: false,
628
+ model: null
629
+ };
630
+ requestModel({
631
+ request: {
632
+ messageId: item.id,
633
+ reason: "message"
634
+ },
635
+ id: `ai.generate:message:${item.id}`
636
+ });
637
+ break;
638
+ }
639
+ const receipt = {
640
+ commandId: ctx.event.id,
641
+ ...ctx.event.type === "ai.work.reported" ? { work: true } : {},
642
+ outcome: reason === void 0 ? "applied" : "rejected",
643
+ ...reason === void 0 ? {} : { reason }
644
+ };
645
+ return [
646
+ ...facts,
647
+ {
648
+ type: "ai.control.committed",
649
+ payload: controlCommit({
650
+ before,
651
+ after: state,
652
+ moves
653
+ })
654
+ },
655
+ ...jobs,
656
+ {
657
+ type: "ai.control.decided",
658
+ payload: receipt
659
+ }
660
+ ];
661
+ };
662
+ const report = ({ ctx, events, settled, name }) => {
663
+ const payload = ctx.event.payload;
664
+ const { turnId, version } = "turnId" in payload ? payload : payload.control;
665
+ for (const event of events) {
666
+ const schema = options.agent.contract.events[event.type];
667
+ const result = validateSync(schema, event.payload, `AI worker ${event.type}`);
668
+ if (result.issues) throw new TypeError(result.issues.map((issue) => issue.message).join("; "));
669
+ }
670
+ return {
671
+ type: "ai.work.reported",
672
+ id: `${ctx.event.id}:report:${ctx.attempt}:${name}`,
673
+ payload: {
674
+ workId: ctx.event.id,
675
+ attempt: ctx.attempt,
676
+ turnId,
677
+ version,
678
+ kind: ctx.event.type === "ai.generation.requested" ? "model" : "tool",
679
+ settled,
680
+ events: events.filter((event) => event.type !== "ai.message.completed" && event.type !== "ai.message.interrupted").map((event, index) => ({
681
+ type: event.type,
682
+ payload: event.payload,
683
+ id: event.id ?? `${ctx.event.id}:fact:${ctx.attempt}:${name}:${index}`
684
+ }))
685
+ }
686
+ };
687
+ };
688
+ const append = async ({ ctx, name, events }) => {
689
+ const progress = events.filter((event) => event.type === "ai.generation.progress");
690
+ const lifecycle = events.filter((event) => event.type !== "ai.generation.progress");
691
+ if (lifecycle.length === 0) return ctx.session.append(name, ...progress);
692
+ const input = report({
693
+ ctx,
694
+ events: lifecycle,
695
+ settled: false,
696
+ name
697
+ });
698
+ const persisted = await ctx.session.append(name, ...progress, input);
699
+ const index = persisted.at(-1).index;
700
+ const stream = ctx.session.stream({ startAfter: index })[Symbol.asyncIterator]();
701
+ const canonical = [];
702
+ const wanted = new Set(lifecycle.map((event) => event.id));
703
+ let rejectAbort;
704
+ const aborted = new Promise((_, reject) => {
705
+ rejectAbort = reject;
706
+ });
707
+ const onAbort = () => {
708
+ rejectAbort(ctx.signal.reason);
709
+ };
710
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
711
+ try {
712
+ ctx.signal.throwIfAborted();
713
+ for (;;) {
714
+ const item = await Promise.race([stream.next(), aborted]);
715
+ if (item.done) throw new Error("AI control acknowledgement stream ended");
716
+ if (wanted.has(item.value.id)) canonical.push(item.value);
717
+ if (item.value.type === "ai.control.decided" && item.value.payload.commandId === input.id) {
718
+ if (item.value.payload.outcome === "rejected") throw new ControlCancelled("stale AI work");
719
+ return [...persisted.filter((event) => event.type !== "ai.work.reported"), ...canonical];
720
+ }
721
+ }
722
+ } finally {
723
+ ctx.signal.removeEventListener("abort", onAbort);
724
+ await stream.return?.();
725
+ }
726
+ };
727
+ const settled = ({ ctx, events }) => report({
728
+ ctx,
729
+ events: events === void 0 ? [] : Array.isArray(events) ? events : [events],
730
+ settled: true,
731
+ name: "settled"
732
+ });
733
+ const cancelled = ({ error, signal }) => {
734
+ if (signal.reason instanceof A2Error) throw signal.reason;
735
+ return error instanceof ControlCancelled || signal.aborted;
736
+ };
737
+ return {
738
+ handler,
739
+ append,
740
+ settled,
741
+ cancelled,
742
+ reducer,
743
+ coordinator
744
+ };
745
+ };
746
+ //#endregion
747
+ //#region src/ai-sdk-step.ts
748
+ const CONTROLLED_SETTINGS = [
749
+ "_internal",
750
+ "abortSignal",
751
+ "instructions",
752
+ "messages",
753
+ "model",
754
+ "onToolExecutionEnd",
755
+ "onToolExecutionStart",
756
+ "experimental_onToolCallFinish",
757
+ "experimental_onToolCallStart",
758
+ "experimental_sandbox",
759
+ "experimental_toolCallers",
760
+ "experimental_toolApprovalSecret",
761
+ "prompt",
762
+ "prepareStep",
763
+ "stopWhen",
764
+ "system",
765
+ "tools"
766
+ ];
767
+ const safeSettings = (settings) => {
768
+ const result = { ...settings };
769
+ for (const key of CONTROLLED_SETTINGS) Reflect.deleteProperty(result, key);
770
+ if (typeof result.timeout === "object" && result.timeout !== null) {
771
+ const timeout = { ...result.timeout };
772
+ Reflect.deleteProperty(timeout, "toolMs");
773
+ Reflect.deleteProperty(timeout, "tools");
774
+ result.timeout = timeout;
775
+ }
776
+ return result;
777
+ };
778
+ const modelToolSet = (tools) => Object.fromEntries(Object.entries(tools).map(([name, tool]) => {
779
+ if (tool.type === "provider" && tool.isProviderExecuted === true) return [name, tool];
780
+ const definition = { ...tool };
781
+ Reflect.deleteProperty(definition, "execute");
782
+ return [name, definition];
783
+ }));
784
+ const errorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
785
+ /** Run exactly one AI SDK model step while leaving local tool execution to A2. */
786
+ async function generateAISDKStep(input) {
787
+ const settings = safeSettings(input.settings);
788
+ const tools = modelToolSet(input.tools);
789
+ const messages = input.modelMessages ?? await convertToModelMessages(input.messages, { tools });
790
+ const streamOptions = {
791
+ ...settings,
792
+ model: input.model,
793
+ tools,
794
+ messages,
795
+ abortSignal: input.abortSignal,
796
+ ...input.instructions === void 0 ? {} : { instructions: input.instructions },
797
+ stopWhen: stepCountIs(1),
798
+ onError: settings.onError ?? (() => {})
799
+ };
800
+ const result = streamText(streamOptions);
801
+ let completion;
802
+ let fatalError;
803
+ const observed = result.stream.pipeThrough(new TransformStream({ transform(part, controller) {
804
+ if (part.type === "error" && fatalError === void 0) fatalError = { value: part.error };
805
+ else if (part.type === "finish") completion = {
806
+ finishReason: part.finishReason,
807
+ usage: part.totalUsage
808
+ };
809
+ controller.enqueue(part);
810
+ } }));
811
+ return {
812
+ stream: toUIMessageStream({
813
+ stream: observed,
814
+ tools,
815
+ generateMessageId: () => input.responseMessageId,
816
+ onError: errorMessage$1,
817
+ ...input.messageMetadata === void 0 ? {} : { messageMetadata: input.messageMetadata }
818
+ }),
819
+ completion() {
820
+ if (fatalError !== void 0) throw fatalError.value;
821
+ if (completion === void 0) throw new Error("AI SDK step ended without a finish part");
822
+ if (completion.finishReason === "error") throw new Error("AI SDK step finished with an error");
823
+ return completion;
824
+ }
825
+ };
826
+ }
827
+ //#endregion
828
+ //#region src/ai-model-metadata.ts
829
+ const catalogUrl = "https://ai-gateway.vercel.sh/v1/models";
830
+ const catalogLifetimeMs = 36e5;
831
+ const catalogTimeoutMs = 5e3;
832
+ let catalog;
833
+ const gatewayModelId = (model) => {
834
+ if (typeof model === "string") return globalThis.AI_SDK_DEFAULT_PROVIDER === void 0 ? model : void 0;
835
+ return model.provider === "gateway" ? model.modelId : void 0;
836
+ };
837
+ const readCatalog = () => {
838
+ if (catalog && catalog.expiresAt > Date.now()) return catalog.promise;
839
+ const entry = {
840
+ expiresAt: Number.POSITIVE_INFINITY,
841
+ promise: (async () => {
842
+ const controller = new AbortController();
843
+ const timeout = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("model catalog request timed out")), catalogTimeoutMs);
844
+ timeout.unref?.();
845
+ try {
846
+ const response = await fetch(catalogUrl, { signal: controller.signal });
847
+ if (!response.ok) throw new Error(`model catalog request failed: HTTP ${response.status}`);
848
+ const body = await response.json();
849
+ if (typeof body !== "object" || body === null || !("data" in body) || !Array.isArray(body.data)) throw new Error("model catalog response must contain a data array");
850
+ const models = /* @__PURE__ */ new Map();
851
+ for (const row of body.data) {
852
+ 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;
853
+ models.set(row.id, {
854
+ contextWindow: row.context_window,
855
+ maxOutputTokens: row.max_tokens
856
+ });
316
857
  }
317
- }));
318
- }
319
- default: return state;
320
- }
858
+ return models;
859
+ } finally {
860
+ clearTimeout(timeout);
861
+ }
862
+ })()
863
+ };
864
+ catalog = entry;
865
+ entry.promise = entry.promise.then((models) => {
866
+ entry.expiresAt = Date.now() + catalogLifetimeMs;
867
+ return models;
868
+ }, (error) => {
869
+ if (catalog === entry) catalog = void 0;
870
+ throw error;
871
+ });
872
+ return entry.promise;
321
873
  };
322
- const aiCoordinatorReducer = (contract) => contract.reducer({
323
- name: "a2.ai.coordinator.v3",
324
- initialState: {
325
- closed: false,
326
- queued: []
874
+ const readModelLimits = async (options) => {
875
+ options.signal.throwIfAborted();
876
+ let onAbort;
877
+ const aborted = new Promise((_resolve, reject) => {
878
+ onAbort = () => reject(options.signal.reason);
879
+ options.signal.addEventListener("abort", onAbort, { once: true });
880
+ });
881
+ try {
882
+ return (await Promise.race([readCatalog(), aborted])).get(options.modelId) ?? null;
883
+ } finally {
884
+ options.signal.removeEventListener("abort", onAbort);
327
885
  }
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
886
  };
333
887
  //#endregion
334
888
  //#region src/ai-server.ts
@@ -340,9 +894,11 @@ const continuationReady = (state) => {
340
894
  */
341
895
  function validateAgentIngress(context) {
342
896
  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";
897
+ if (event.type !== "ai.control.requested") return true;
898
+ const command = event.payload;
899
+ if (command["action"] === "request-input" || command["action"] === "tool-result") return true;
900
+ if (command["action"] === "send" || command["action"] === "edit" || command["action"] === "steer") return command["message"]?.role !== "user";
901
+ return false;
346
902
  });
347
903
  if (rejected !== void 0) throw new A2Error("INVALID_PAYLOAD", `event '${rejected.type}' is server-only for an AI agent`);
348
904
  }
@@ -396,7 +952,23 @@ const nextOrTimer = async (next, delayMs) => {
396
952
  if (timer !== void 0) clearTimeout(timer);
397
953
  }
398
954
  };
399
- async function generateWithAISDK(context, messageMetadata) {
955
+ async function generateWithAISDK(options) {
956
+ const { agentName, context, messageMetadata } = options;
957
+ const headers = context.generation.headers ?? {};
958
+ const generation = gatewayModelId(context.model) === void 0 ? context.generation : {
959
+ ...context.generation,
960
+ headers: Object.keys(headers).some((key) => key.toLowerCase() === "x-session-affinity") ? headers : {
961
+ ...headers,
962
+ "x-session-affinity": createHash("sha256").update(JSON.stringify([agentName, context.request.sessionId])).digest("hex")
963
+ },
964
+ providerOptions: {
965
+ ...context.generation.providerOptions,
966
+ gateway: {
967
+ caching: "auto",
968
+ ...context.generation.providerOptions?.["gateway"]
969
+ }
970
+ }
971
+ };
400
972
  const step = await generateAISDKStep({
401
973
  model: context.model,
402
974
  tools: context.tools,
@@ -405,9 +977,10 @@ async function generateWithAISDK(context, messageMetadata) {
405
977
  responseMessageId: context.responseMessageId,
406
978
  abortSignal: context.signal,
407
979
  ...context.instructions === void 0 ? {} : { instructions: context.instructions },
408
- settings: context.generation,
980
+ settings: generation,
409
981
  ...messageMetadata === void 0 ? {} : { messageMetadata: ({ part }) => messageMetadata({
410
982
  ...context,
983
+ generation,
411
984
  part
412
985
  }) }
413
986
  });
@@ -491,9 +1064,9 @@ async function* consumeGeneration(options) {
491
1064
  ...completion?.usage === void 0 ? {} : { usage: completion.usage }
492
1065
  };
493
1066
  }
494
- const replay = (definition, history) => {
495
- let state = definition.reducer.initialState;
496
- for (const event of history) state = definition.reducer.fold(state, event);
1067
+ const foldAIEvents = (options) => {
1068
+ let state = options.state;
1069
+ for (const event of options.events) state = options.agent.reducer.fold(state, event);
497
1070
  return state;
498
1071
  };
499
1072
  const summarize = async (options) => {
@@ -525,15 +1098,19 @@ const summarize = async (options) => {
525
1098
  return [name, definition];
526
1099
  }));
527
1100
  const source = await generateWithAISDK({
528
- ...options.context,
529
- generation,
530
- tools,
531
- modelMessages: [...options.context.modelMessages, {
532
- role: "user",
533
- content: instruction
534
- }],
535
- responseMessageId: `${options.context.generationId}:summary`
536
- }, void 0);
1101
+ agentName: options.agentName,
1102
+ context: {
1103
+ ...options.context,
1104
+ generation,
1105
+ tools,
1106
+ modelMessages: [...options.context.modelMessages, {
1107
+ role: "user",
1108
+ content: instruction
1109
+ }],
1110
+ responseMessageId: `${options.context.generationId}:summary`
1111
+ },
1112
+ messageMetadata: void 0
1113
+ });
537
1114
  let summary = "";
538
1115
  let calledTool = false;
539
1116
  let finish;
@@ -558,35 +1135,50 @@ const activeCompaction = (state) => {
558
1135
  const compaction = state.compaction;
559
1136
  return compaction?.status === "completed" && compaction.messages !== void 0 && state.messages.some((message) => message.id === compaction.throughMessageId) ? compaction : null;
560
1137
  };
561
- const contextMessages = (options) => {
562
- const { agent, state, history } = options;
1138
+ const contextMessages = async (options) => {
1139
+ const { agent, session, snapshot } = options;
1140
+ const appended = options.appended ?? [];
1141
+ const state = foldAIEvents({
1142
+ agent,
1143
+ events: appended,
1144
+ state: snapshot.state
1145
+ });
563
1146
  const compaction = activeCompaction(state);
564
- if (compaction === null) return state.messages;
1147
+ const queued = new Set(options.coordinator.queued.map((item) => item.messageId));
1148
+ const visible = (messages) => messages.filter((message) => !queued.has(message.id));
1149
+ if (compaction === null) return visible(state.messages);
565
1150
  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);
1151
+ const throughIndex = compaction.throughIndex;
568
1152
  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;
1153
+ const [prefix, priorCoordinator, tail] = await Promise.all([
1154
+ throughIndex === snapshot.index ? Promise.resolve(snapshot) : session.state(agent.reducer, { through: throughIndex }),
1155
+ session.state(aiCoordinatorReducer(agent.contract), { through: throughIndex }),
1156
+ throughIndex >= snapshot.index ? Promise.resolve([]) : session.history({
1157
+ gte: throughIndex + 1,
1158
+ lte: snapshot.index
1159
+ })
1160
+ ]);
1161
+ for (const item of priorCoordinator.state.queued) retained.add(item.messageId);
1162
+ const events = options.excluded === void 0 ? tail : withoutGenerationLifecycle(tail, options.excluded);
1163
+ const context = foldAIEvents({
1164
+ agent,
1165
+ events: [...events, ...appended],
1166
+ state: {
1167
+ ...prefix.state,
1168
+ messages: [...compaction.messages, ...prefix.state.messages.filter((message) => retained.has(message.id))],
1169
+ activeProjection: null
1170
+ }
1171
+ });
1172
+ const positions = /* @__PURE__ */ new Map();
1173
+ for (const message of [...compaction.messages, ...state.messages]) if (!positions.has(message.id)) positions.set(message.id, positions.size);
1174
+ return visible(context.messages.toSorted((left, right) => (positions.get(left.id) ?? positions.size) - (positions.get(right.id) ?? positions.size)));
579
1175
  }
580
1176
  const boundary = state.messages.findIndex((message) => message.id === compaction.throughMessageId);
581
- return [
1177
+ return visible([
582
1178
  ...compaction.messages,
583
1179
  ...state.messages.slice(0, boundary + 1).filter((message) => retained.has(message.id)),
584
1180
  ...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));
1181
+ ]);
590
1182
  };
591
1183
  const modelContext = async (options) => {
592
1184
  const messages = await convertToModelMessages(options.messages, { tools: options.tools });
@@ -609,22 +1201,8 @@ const estimateInputTokens = async (options) => {
609
1201
  })).byteLength / 4);
610
1202
  };
611
1203
  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);
1204
+ const previous = options.calibration;
1205
+ 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
1206
  };
629
1207
  const toolCalledEvent = (payload) => ({
630
1208
  type: "ai.tool.called",
@@ -793,29 +1371,6 @@ const lifecycleEvents = (options) => {
793
1371
  pending
794
1372
  };
795
1373
  };
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
1374
  function withoutGenerationLifecycle(history, generationIds) {
820
1375
  if (generationIds.size === 0) return history;
821
1376
  const approvalKeys = /* @__PURE__ */ new Set();
@@ -839,7 +1394,7 @@ const validateCompaction = (options) => {
839
1394
  if (compaction !== false && (typeof compaction !== "object" || compaction === null)) throw new TypeError("compaction must resolve to false or a policy object");
840
1395
  if (compaction !== false && "then" in compaction) {
841
1396
  Promise.resolve(compaction).catch(() => {});
842
- throw new TypeError("compaction options must resolve synchronously");
1397
+ throw new TypeError("compaction options must be a policy or a resolver");
843
1398
  }
844
1399
  if (compaction !== false && !("shouldCompact" in compaction)) {
845
1400
  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 +1426,9 @@ function createHandlers(options) {
871
1426
  const tools = options.tools ?? {};
872
1427
  const generation = options.generation ?? {};
873
1428
  const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY;
874
- const coordinator = aiCoordinatorReducer(options.agent.contract);
1429
+ const control = createControlRuntime({ agent: options.agent });
1430
+ const coordinator = control.coordinator;
875
1431
  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
1432
  const resultEvent = (call, suffix, result) => ({
927
1433
  type: "ai.tool.result",
928
1434
  id: `${call.generationId}:tool:${call.toolCallId}:${suffix}`,
@@ -943,22 +1449,35 @@ function createHandlers(options) {
943
1449
  ...call.toolMetadata === void 0 ? {} : { toolMetadata: call.toolMetadata }
944
1450
  }
945
1451
  });
946
- const promptMessages = (sessionId, call, readHistory) => {
947
- const key = promptCacheKey(sessionId, call.generationId);
1452
+ const promptMessages = (input) => {
1453
+ const { ctx, call, generation: owner } = input;
1454
+ const key = promptCacheKey(ctx.event.sessionId, call.generationId);
948
1455
  const cached = promptCache.get(key);
949
1456
  if (cached) return cached;
950
1457
  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);
1458
+ const frontier = owner.promptThroughIndex;
1459
+ if (frontier === void 0) throw new TypeError("AI work requires a prompt checkpoint");
1460
+ const [snapshot, atPrompt, tail] = await Promise.all([
1461
+ ctx.session.state(options.agent.reducer, { through: frontier }),
1462
+ ctx.session.state(coordinator, { through: frontier }),
1463
+ ctx.session.history({
1464
+ gte: frontier + 1,
1465
+ lte: ctx.event.index
1466
+ })
1467
+ ]);
1468
+ 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);
1469
+ const state = foldAIEvents({
1470
+ agent: options.agent,
1471
+ events: appended,
1472
+ state: snapshot.state
1473
+ });
956
1474
  return modelContext({
957
- messages: activeContextMessages({
1475
+ messages: await contextMessages({
958
1476
  agent: options.agent,
959
- state,
960
- history: promptHistory,
961
- coordinator: coordinatorStateAt(promptHistory)
1477
+ session: ctx.session,
1478
+ snapshot,
1479
+ coordinator: atPrompt.state,
1480
+ appended
962
1481
  }),
963
1482
  state,
964
1483
  tools
@@ -974,7 +1493,7 @@ function createHandlers(options) {
974
1493
  const schedulerFailure = consumeSchedulerSendFailure(error);
975
1494
  if (checkAbort(ctx.signal)) return;
976
1495
  if (schedulerFailure === "retryable") throw error;
977
- return resultEvent(call, "execution:error", { error: errorMessage(error) });
1496
+ return resultEvent(call, `${ctx.event.id}:execution:error`, { error: errorMessage(error) });
978
1497
  };
979
1498
  const runToolExecution = async (ctx, call, execute, messages) => {
980
1499
  let output;
@@ -993,7 +1512,7 @@ function createHandlers(options) {
993
1512
  } catch (error) {
994
1513
  return toolExecutionFailure(ctx, call, error);
995
1514
  }
996
- if (iterator === void 0) return resultEvent(call, "execution:0", { output });
1515
+ if (iterator === void 0) return resultEvent(call, `${ctx.event.id}:execution:0`, { output });
997
1516
  let last;
998
1517
  let sequence = 0;
999
1518
  let done = false;
@@ -1005,16 +1524,21 @@ function createHandlers(options) {
1005
1524
  } catch (error) {
1006
1525
  return toolExecutionFailure(ctx, call, error);
1007
1526
  }
1008
- if (checkAbort(ctx.signal)) return;
1009
1527
  if (result.done) {
1528
+ if (ctx.signal.reason instanceof A2Error) throw ctx.signal.reason;
1010
1529
  done = true;
1011
1530
  break;
1012
1531
  }
1532
+ if (checkAbort(ctx.signal)) return;
1013
1533
  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
- }));
1534
+ await control.append({
1535
+ ctx,
1536
+ name: `tool:${call.toolCallId}:preliminary:${sequence}`,
1537
+ events: [resultEvent(call, `${ctx.event.id}:execution:${ctx.attempt}:${sequence}:preliminary`, {
1538
+ output: result.value,
1539
+ preliminary: true
1540
+ })]
1541
+ });
1018
1542
  sequence += 1;
1019
1543
  }
1020
1544
  } finally {
@@ -1022,12 +1546,13 @@ function createHandlers(options) {
1022
1546
  await iterator.return?.();
1023
1547
  } catch {}
1024
1548
  }
1025
- return resultEvent(call, `execution:${sequence}:final`, sequence === 0 ? {} : { output: last });
1549
+ return resultEvent(call, `${ctx.event.id}:execution:${sequence}:final`, sequence === 0 ? {} : { output: last });
1026
1550
  };
1027
- const executeTool = async (ctx, call) => {
1551
+ const executeTool = async (input) => {
1552
+ const { ctx, call } = input;
1028
1553
  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);
1554
+ if (execute === void 0) return resultEvent(call, `${ctx.event.id}:execution:error`, { error: `Tool '${call.toolName}' has no server executor` });
1555
+ const messages = await promptMessages(input);
1031
1556
  if (checkAbort(ctx.signal)) return;
1032
1557
  const scope = {
1033
1558
  contract: options.agent.contract,
@@ -1035,62 +1560,82 @@ function createHandlers(options) {
1035
1560
  };
1036
1561
  return toolScopeStorage.run(scope, () => runToolExecution(ctx, call, execute, messages));
1037
1562
  };
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;
1563
+ const handleToolExecution = async (ctx) => {
1564
+ try {
1565
+ const state = (await ctx.session.state(control.reducer, { through: "latest" })).state;
1566
+ const request = ctx.event.payload;
1567
+ const call = state.coordinator.response?.calls.find((candidate) => candidate.work?.id === ctx.event.id);
1568
+ 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({
1569
+ ctx,
1570
+ events: void 0
1571
+ });
1572
+ return control.settled({
1573
+ ctx,
1574
+ events: await executeTool({
1575
+ ctx,
1576
+ call: request.call,
1577
+ generation: request.generation
1578
+ })
1579
+ });
1580
+ } catch (error) {
1581
+ if (control.cancelled({
1582
+ error,
1583
+ signal: ctx.signal
1584
+ })) return control.settled({
1585
+ ctx,
1586
+ events: void 0
1587
+ });
1588
+ throw error;
1055
1589
  }
1056
- if (!ctx.event.payload.approved) return resultEvent(current.call, "execution:denied", { denied: true });
1057
- return executeTool(ctx, current.call);
1058
1590
  };
1059
1591
  const generationHandler = async (ctx) => {
1060
- const history = await ctx.session.history();
1592
+ if (checkAbort(ctx.signal)) return;
1061
1593
  const requestId = ctx.event.id;
1062
1594
  const request = ctx.event.payload;
1063
- const coordinatorState = (await ctx.session.state(coordinator, { through: "latest" })).state;
1595
+ const snapshot = await ctx.session.state(options.agent.reducer, { through: "latest" });
1596
+ if (snapshot.state.active?.phase === "paused" || snapshot.state.active?.phase === "pausing") return;
1597
+ const coordinatorState = (await ctx.session.state(coordinator, { through: snapshot.index })).state;
1064
1598
  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;
1599
+ const response = coordinatorState.response;
1600
+ const current = response.generation?.requestId === requestId ? response.generation : void 0;
1601
+ if (current !== void 0 && (response.completion !== void 0 || response.failure !== void 0 && response.failure.superseded !== true)) return;
1602
+ if (current !== void 0 && current.attempt >= ctx.attempt) return;
1073
1603
  const attempt = ctx.attempt;
1074
1604
  const generationId = `${requestId}:generation:${attempt}`;
1075
1605
  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;
1606
+ const responseStepCount = response.stepCount;
1607
+ if (request.reason === "tool" && (requestId !== `ai.generate:tools:${response.source?.generation.generationId}` || response.responseMessageId !== request.messageId || response.responseMessageId !== request.responseMessageId)) return;
1608
+ const previous = current;
1609
+ const replaced = previous === void 0 ? [] : [{
1610
+ generation: previous,
1611
+ frontier: response.promptThroughIndex
1612
+ }];
1613
+ if (request.reason === "retry" && response.source?.failed) replaced.push({
1614
+ generation: response.source.generation,
1615
+ frontier: response.source.promptThroughIndex
1616
+ });
1617
+ const replacedGenerationIds = new Set(replaced.map(({ generation: owner }) => owner.generationId));
1618
+ let state = snapshot.state;
1619
+ if (replaced.length > 0) {
1620
+ const through = Math.min(...replaced.map(({ frontier }) => frontier));
1621
+ const baseline = await ctx.session.state(options.agent.reducer, { through });
1622
+ const tail = await ctx.session.history({
1623
+ gte: through + 1,
1624
+ lte: snapshot.index
1625
+ });
1626
+ state = foldAIEvents({
1627
+ agent: options.agent,
1628
+ events: withoutGenerationLifecycle(tail, replacedGenerationIds),
1629
+ state: baseline.state
1630
+ });
1082
1631
  }
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
1632
  const resolverContext = {
1091
1633
  event: ctx.event,
1092
1634
  state,
1093
- history,
1635
+ session: { state: (reducer, readOptions) => ctx.session.state(reducer, {
1636
+ ...readOptions,
1637
+ through: readOptions?.through ?? snapshot.index
1638
+ }) },
1094
1639
  signal: ctx.signal
1095
1640
  };
1096
1641
  let generationStarted = false;
@@ -1101,14 +1646,14 @@ function createHandlers(options) {
1101
1646
  const resolvedInstructions = options.instructions === void 0 ? void 0 : await resolve(options.instructions, resolverContext);
1102
1647
  if (checkAbort(ctx.signal)) return;
1103
1648
  const compaction = typeof configuredCompaction === "function" ? validateCompaction({
1104
- compaction: configuredCompaction(resolverContext),
1649
+ compaction: await resolve(configuredCompaction, resolverContext),
1105
1650
  explicit: true,
1106
1651
  generation: options.generation,
1107
1652
  tools: options.tools
1108
1653
  }) : staticCompaction;
1109
1654
  if (checkAbort(ctx.signal)) return;
1110
1655
  if (request.reason === "tool" && responseStepCount >= maxSteps) {
1111
- const source = sourceCoordinatorState.response?.generation;
1656
+ const source = response.source?.generation;
1112
1657
  if (source === void 0) return;
1113
1658
  return {
1114
1659
  type: "ai.generation.failed",
@@ -1130,7 +1675,7 @@ function createHandlers(options) {
1130
1675
  responseMessageId,
1131
1676
  attempt,
1132
1677
  model: modelName(resolvedModel),
1133
- promptThroughIndex: history.at(-1)?.index ?? ctx.event.index
1678
+ promptThroughIndex: snapshot.index
1134
1679
  };
1135
1680
  const catalogModelId = gatewayModelId(resolvedModel);
1136
1681
  const gatewayOptions = options.generation?.providerOptions?.["gateway"];
@@ -1143,7 +1688,7 @@ function createHandlers(options) {
1143
1688
  payload: { modelId: catalogModelId }
1144
1689
  });
1145
1690
  if (previous) {
1146
- const payload = previous.payload;
1691
+ const payload = previous;
1147
1692
  const superseded = {
1148
1693
  requestId,
1149
1694
  messageId: payload.messageId,
@@ -1163,20 +1708,25 @@ function createHandlers(options) {
1163
1708
  id: generationId,
1164
1709
  payload: started
1165
1710
  });
1166
- const generationHistory = [...history, ...await ctx.session.append("generation-start", ...startEvents)];
1711
+ const generationEvents = await control.append({
1712
+ ctx,
1713
+ name: "generation-start",
1714
+ events: startEvents
1715
+ });
1167
1716
  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({
1717
+ if (checkAbort(ctx.signal)) return;
1718
+ const promptCoordinatorState = coordinatorState;
1719
+ const messages = await contextMessages({
1175
1720
  agent: options.agent,
1176
- state,
1177
- history: promptHistory,
1178
- coordinator: promptCoordinatorState
1721
+ session: ctx.session,
1722
+ snapshot: {
1723
+ state,
1724
+ index: snapshot.index
1725
+ },
1726
+ coordinator: promptCoordinatorState,
1727
+ excluded: replacedGenerationIds
1179
1728
  });
1729
+ let generationMessages = messages;
1180
1730
  let modelMessages = await modelContext({
1181
1731
  messages,
1182
1732
  state,
@@ -1190,7 +1740,7 @@ function createHandlers(options) {
1190
1740
  messages,
1191
1741
  modelMessages,
1192
1742
  state,
1193
- history,
1743
+ session: resolverContext.session,
1194
1744
  signal: ctx.signal,
1195
1745
  model: resolvedModel,
1196
1746
  tools,
@@ -1203,7 +1753,7 @@ function createHandlers(options) {
1203
1753
  const limits = metadata?.status === "resolved" && !usesFallbackModels ? metadata.limits : void 0;
1204
1754
  let inputTokenEstimate;
1205
1755
  let compacted = false;
1206
- const canCompact = sourceCoordinatorState.response?.calls.every((call) => call.terminal) ?? true;
1756
+ const canCompact = response.source?.canCompact ?? true;
1207
1757
  if (policy && canCompact) {
1208
1758
  const compactionContext = {
1209
1759
  ...resolverContext,
@@ -1221,7 +1771,7 @@ function createHandlers(options) {
1221
1771
  const inputTokens = measuredInputTokens({
1222
1772
  estimate: inputTokenEstimate,
1223
1773
  model: modelName(resolvedModel),
1224
- history
1774
+ calibration: coordinatorState.calibration
1225
1775
  });
1226
1776
  const threshold = policy.thresholdTokens ?? (limits === void 0 ? void 0 : Math.floor(Math.min(limits.contextWindow * .75, limits.contextWindow - (generation.maxOutputTokens ?? 0))));
1227
1777
  if (limits !== void 0 && threshold !== void 0 && threshold <= 0) throw new Error(`output allowance exhausts the ${limits.contextWindow}-token context window`);
@@ -1230,19 +1780,24 @@ function createHandlers(options) {
1230
1780
  if (checkAbort(ctx.signal)) return;
1231
1781
  if (shouldCompact) {
1232
1782
  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
- }
1783
+ const throughIndex = snapshot.index;
1784
+ generationEvents.push(...await control.append({
1785
+ ctx,
1786
+ name: "compaction-requested",
1787
+ events: [{
1788
+ type: "ai.compaction.requested",
1789
+ id: `${generationId}:compaction:requested`,
1790
+ payload: {
1791
+ generationId,
1792
+ throughMessageId,
1793
+ throughIndex
1794
+ }
1795
+ }]
1242
1796
  }));
1243
1797
  const result = !("shouldCompact" in policy) ? {
1244
1798
  messages: [],
1245
1799
  ...await summarize({
1800
+ agentName: options.agent.contract.name,
1246
1801
  context: baseContext,
1247
1802
  instructions: policy.instructions
1248
1803
  })
@@ -1255,23 +1810,33 @@ function createHandlers(options) {
1255
1810
  ...result,
1256
1811
  ...promptCoordinatorState.queued.length === 0 ? {} : { retainedMessageIds: promptCoordinatorState.queued.map((item) => item.messageId) }
1257
1812
  };
1258
- generationHistory.push(...await ctx.session.append("compaction-completed", {
1259
- type: "ai.compaction.completed",
1260
- id: `${generationId}:compaction:completed`,
1261
- payload: completed
1813
+ generationEvents.push(...await control.append({
1814
+ ctx,
1815
+ name: "compaction-completed",
1816
+ events: [{
1817
+ type: "ai.compaction.completed",
1818
+ id: `${generationId}:compaction:completed`,
1819
+ payload: completed
1820
+ }]
1262
1821
  }));
1822
+ const queued = new Set(promptCoordinatorState.queued.map((item) => item.messageId));
1823
+ generationMessages = result.messages.filter((message) => !queued.has(message.id));
1263
1824
  compacted = true;
1264
1825
  }
1265
1826
  }
1266
- const currentState = replay(options.agent, generationHistory);
1267
- const currentCoordinatorState = coordinatorStateAt(generationHistory);
1268
- const generationMessages = activeContextMessages({
1827
+ const currentState = foldAIEvents({
1269
1828
  agent: options.agent,
1270
- state: currentState,
1271
- history: generationHistory,
1272
- coordinator: currentCoordinatorState
1829
+ events: generationEvents,
1830
+ state: snapshot.state
1273
1831
  });
1274
- modelMessages = await modelContext({
1832
+ if (request.reason === "retry" || previous !== void 0) generationMessages = await contextMessages({
1833
+ agent: options.agent,
1834
+ session: ctx.session,
1835
+ snapshot,
1836
+ coordinator: promptCoordinatorState,
1837
+ appended: generationEvents
1838
+ });
1839
+ if (policy && "shouldCompact" in policy || generationMessages !== messages || activeCompaction(currentState)?.summary !== activeCompaction(state)?.summary) modelMessages = await modelContext({
1275
1840
  messages: generationMessages,
1276
1841
  state: currentState,
1277
1842
  tools
@@ -1292,7 +1857,7 @@ function createHandlers(options) {
1292
1857
  messages: generationMessages,
1293
1858
  modelMessages,
1294
1859
  state: currentState,
1295
- history,
1860
+ session: resolverContext.session,
1296
1861
  signal: ctx.signal,
1297
1862
  model: resolvedModel,
1298
1863
  tools,
@@ -1301,7 +1866,11 @@ function createHandlers(options) {
1301
1866
  };
1302
1867
  const custom = options.generate !== void 0;
1303
1868
  const updates = consumeGeneration({
1304
- source: options.generate === void 0 ? await generateWithAISDK(generateContext, options.messageMetadata) : { stream: await options.generate(generateContext) },
1869
+ source: options.generate === void 0 ? await generateWithAISDK({
1870
+ agentName: options.agent.contract.name,
1871
+ context: generateContext,
1872
+ messageMetadata: options.messageMetadata
1873
+ }) : { stream: await options.generate(generateContext) },
1305
1874
  ...options.progress === void 0 ? {} : { progress: options.progress }
1306
1875
  });
1307
1876
  let sequence = 0;
@@ -1332,11 +1901,15 @@ function createHandlers(options) {
1332
1901
  custom
1333
1902
  });
1334
1903
  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);
1904
+ await control.append({
1905
+ ctx,
1906
+ name: `generation-progress:${sequence}`,
1907
+ events: [{
1908
+ type: "ai.generation.progress",
1909
+ id: `${generationId}:progress:${sequence}`,
1910
+ payload: progress
1911
+ }, ...lifecycle.events]
1912
+ });
1340
1913
  sequence += 1;
1341
1914
  }
1342
1915
  if (!finish) throw new Error("agent generation finished without output");
@@ -1368,22 +1941,12 @@ function createHandlers(options) {
1368
1941
  }
1369
1942
  ];
1370
1943
  } catch (error) {
1944
+ if (error instanceof ControlCancelled) throw error;
1371
1945
  if (error instanceof A2Error) {
1372
1946
  checkAbort(ctx.signal);
1373
1947
  throw error;
1374
1948
  }
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
- }
1949
+ if (checkAbort(ctx.signal)) return;
1387
1950
  if (!generationStarted) throw error;
1388
1951
  const failed = {
1389
1952
  requestId,
@@ -1399,36 +1962,10 @@ function createHandlers(options) {
1399
1962
  };
1400
1963
  }
1401
1964
  };
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
1965
  const clearPromptCache = (sessionId) => {
1425
1966
  const prefix = `${sessionId}\u001f`;
1426
1967
  for (const key of promptCache.keys()) if (key.startsWith(prefix)) promptCache.delete(key);
1427
1968
  };
1428
- const handleResponseEnded = async (ctx) => {
1429
- clearPromptCache(ctx.event.sessionId);
1430
- return scheduleNext(ctx);
1431
- };
1432
1969
  return {
1433
1970
  "ai.model.metadata.requested": { handler: async (ctx) => {
1434
1971
  const limits = await readModelLimits({
@@ -1445,66 +1982,69 @@ function createHandlers(options) {
1445
1982
  }
1446
1983
  };
1447
1984
  } },
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
1985
+ "ai.control.requested": {
1986
+ lane: "a2.ai.control",
1987
+ handler: control.handler
1455
1988
  },
1456
- "ai.input.responded": {
1457
- lane: "a2.ai.turn",
1458
- handler: handleInputResponse
1989
+ "ai.work.reported": {
1990
+ lane: "a2.ai.control",
1991
+ handler: control.handler
1459
1992
  },
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) => {
1993
+ "ai.message.completed": { handler: async (ctx) => {
1994
+ clearPromptCache(ctx.event.sessionId);
1995
+ } },
1996
+ "ai.message.interrupted": { handler: async (ctx) => {
1469
1997
  clearPromptCache(ctx.event.sessionId);
1470
- return Promise.resolve();
1471
1998
  } },
1999
+ "ai.session.closed": {
2000
+ lane: "a2.ai.control",
2001
+ handler: async (ctx) => {
2002
+ clearPromptCache(ctx.event.sessionId);
2003
+ return control.handler(ctx);
2004
+ }
2005
+ },
1472
2006
  "ai.generation.failed": { handler: (ctx) => {
1473
2007
  promptCache.delete(promptCacheKey(ctx.event.sessionId, ctx.event.payload.generationId));
1474
2008
  return Promise.resolve();
1475
2009
  } },
1476
2010
  "ai.generation.requested": {
1477
- lane: "a2.ai.turn",
2011
+ lane: "a2.ai.model",
1478
2012
  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}`);
2013
+ "ai.control.committed": (event, trigger) => {
2014
+ const active = event.payload.view;
2015
+ return active?.turnId !== trigger.payload.control?.turnId || active?.version !== trigger.payload.control?.version;
1482
2016
  },
1483
2017
  "ai.session.closed": true
1484
2018
  },
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
2019
+ handler: async (ctx) => {
2020
+ try {
2021
+ return control.settled({
2022
+ ctx,
2023
+ events: await generationHandler(ctx)
2024
+ });
2025
+ } catch (error) {
2026
+ if (control.cancelled({
2027
+ error,
2028
+ signal: ctx.signal
2029
+ })) return control.settled({
2030
+ ctx,
2031
+ events: void 0
2032
+ });
2033
+ throw error;
2034
+ }
2035
+ }
1495
2036
  },
1496
- "ai.approval.responded": {
2037
+ "ai.tool.execution.requested": {
1497
2038
  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),
2039
+ "ai.generation.failed": (event, trigger) => event.payload.generationId === trigger.payload.call.generationId,
2040
+ "ai.control.committed": (event, trigger) => {
2041
+ const active = event.payload.view;
2042
+ return active?.turnId !== trigger.payload.turnId || active?.version !== trigger.payload.version;
2043
+ },
1500
2044
  "ai.session.closed": true
1501
2045
  },
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
- } }
2046
+ handler: handleToolExecution
2047
+ }
1508
2048
  };
1509
2049
  }
1510
2050
  /** Assemble an A2 server with the built-in agent handlers and app extensions. */