experimental-a2 0.12.0 → 0.14.0

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