experimental-a2 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/actor-client.d.ts +1 -1
  3. package/dist/{actor-BfQSE0KC.d.ts → actor-ohPC-81x.d.ts} +5 -5
  4. package/dist/{actor-BfQSE0KC.d.ts.map → actor-ohPC-81x.d.ts.map} +1 -1
  5. package/dist/actor-react.d.ts +3 -3
  6. package/dist/actor.d.ts +1 -1
  7. package/dist/actor.js +1 -1
  8. package/dist/{ai-Cai-lCbj.d.ts → ai-CrEf6p_W.d.ts} +73 -14
  9. package/dist/ai-CrEf6p_W.d.ts.map +1 -0
  10. package/dist/ai-DgOBltJ_.js +2451 -0
  11. package/dist/ai-DgOBltJ_.js.map +1 -0
  12. package/dist/ai-server.d.ts +4 -5
  13. package/dist/ai-server.d.ts.map +1 -1
  14. package/dist/ai-server.js +788 -296
  15. package/dist/ai-server.js.map +1 -1
  16. package/dist/ai.d.ts +2 -2
  17. package/dist/ai.js +1 -1999
  18. package/dist/client-BYzHjkwU.js.map +1 -1
  19. package/dist/{client-BAEABRZB.d.ts → client-CzyacQpJ.d.ts} +9 -9
  20. package/dist/client-CzyacQpJ.d.ts.map +1 -0
  21. package/dist/client.d.ts +1 -1
  22. package/dist/idempotent-id-BRJVeylj.js +13 -0
  23. package/dist/idempotent-id-BRJVeylj.js.map +1 -0
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +1 -12
  26. package/dist/otel.d.ts +1 -1
  27. package/dist/react.d.ts +13 -13
  28. package/dist/react.d.ts.map +1 -1
  29. package/dist/react.js.map +1 -1
  30. package/dist/reducer-DEMjEY_O.js.map +1 -1
  31. package/dist/{reducer-BcS9VDKC.d.ts → reducer-otzHjuJj.d.ts} +3 -3
  32. package/dist/{reducer-BcS9VDKC.d.ts.map → reducer-otzHjuJj.d.ts.map} +1 -1
  33. package/dist/scheduler-qstash.d.ts +2 -2
  34. package/dist/scheduler-qstash.js +1 -1
  35. package/dist/scheduler-vercel.d.ts +2 -2
  36. package/dist/scheduler-vercel.js +1 -1
  37. package/dist/{server-CjJSGcF7.js → server-BD5ckxZb.js} +39 -26
  38. package/dist/server-BD5ckxZb.js.map +1 -0
  39. package/dist/{server-Bp5Nd1pF.d.ts → server-DbD7IJVK.d.ts} +5 -5
  40. package/dist/{server-Bp5Nd1pF.d.ts.map → server-DbD7IJVK.d.ts.map} +1 -1
  41. package/dist/server.d.ts +3 -3
  42. package/dist/server.js +1 -1
  43. package/dist/{store-D_yhNdPz.d.ts → store-DyZM6fS5.d.ts} +3 -2
  44. package/dist/{store-D_yhNdPz.d.ts.map → store-DyZM6fS5.d.ts.map} +1 -1
  45. package/dist/store-N8PXxDAS.js.map +1 -1
  46. package/dist/store-memory.d.ts +1 -1
  47. package/dist/store-postgres.d.ts +1 -1
  48. package/dist/store-redis-http.d.ts +1 -1
  49. package/dist/store-redis-http.js +1 -1
  50. package/dist/{store-redis-notify-D2EI6gwX.js → store-redis-notify-BVLUfI3j.js} +14 -13
  51. package/dist/store-redis-notify-BVLUfI3j.js.map +1 -0
  52. package/dist/store-redis.d.ts +1 -1
  53. package/dist/store-redis.js +1 -1
  54. package/dist/store-sqlite.d.ts +1 -1
  55. package/dist/{telemetry-CpeclqB2.d.ts → telemetry-B5jzpy6y.d.ts} +2 -2
  56. package/dist/telemetry-B5jzpy6y.d.ts.map +1 -0
  57. package/docs/concepts/04-state.mdx +9 -4
  58. package/docs/guides/06-ai-agents.mdx +149 -48
  59. package/docs/reference/01-api.mdx +110 -28
  60. package/examples/playground/app/agent/[agentId]/agent-client.tsx +20 -28
  61. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +123 -0
  62. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +37 -43
  63. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +44 -24
  64. package/examples/playground/app/agent/compaction-timeline.test.ts +47 -7
  65. package/examples/playground/app/agent/compaction-timeline.ts +13 -3
  66. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -5
  67. package/examples/playground/app/chat/[chatId]/session.ts +17 -2
  68. package/examples/playground/app/chat/model.test.ts +4 -4
  69. package/examples/playground/app/chat/model.ts +18 -6
  70. package/examples/playground/app/chat/server.ts +6 -3
  71. package/examples/playground/package.json +3 -1
  72. package/package.json +1 -1
  73. package/src/ai-client-state.ts +36 -4
  74. package/src/ai-context-schema.ts +296 -0
  75. package/src/ai-context.ts +680 -0
  76. package/src/ai-control-server.ts +183 -135
  77. package/src/ai-control-state.ts +6 -0
  78. package/src/ai-control.ts +8 -7
  79. package/src/ai-coordinator.ts +92 -38
  80. package/src/ai-id.ts +6 -0
  81. package/src/ai-input-tokens.ts +75 -0
  82. package/src/ai-message-projection.ts +474 -0
  83. package/src/ai-server.ts +247 -336
  84. package/src/ai-stored-state.ts +484 -0
  85. package/src/ai.ts +255 -538
  86. package/src/client.ts +21 -15
  87. package/src/react.ts +25 -19
  88. package/src/reducer.ts +2 -1
  89. package/src/server.ts +73 -56
  90. package/src/store-redis-core.ts +15 -17
  91. package/src/store.ts +1 -0
  92. package/src/telemetry.ts +2 -1
  93. package/dist/ai-Cai-lCbj.d.ts.map +0 -1
  94. package/dist/ai-control-CcD4hh3y.js +0 -119
  95. package/dist/ai-control-CcD4hh3y.js.map +0 -1
  96. package/dist/ai.js.map +0 -1
  97. package/dist/client-BAEABRZB.d.ts.map +0 -1
  98. package/dist/index.js.map +0 -1
  99. package/dist/server-CjJSGcF7.js.map +0 -1
  100. package/dist/store-redis-notify-D2EI6gwX.js.map +0 -1
  101. package/dist/telemetry-CpeclqB2.d.ts.map +0 -1
package/dist/ai-server.js CHANGED
@@ -1,12 +1,29 @@
1
1
  import { n as validateSync } from "./validate-XKT4FSNn.js";
2
2
  import { t as A2Error } from "./errors-DCk6ch5n.js";
3
3
  import { f as consumeSchedulerSendFailure, p as installAmbientToolScopeStorage } from "./internal-Dq2qYxou.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
+ import { i as setServerFetchHooks, t as createServer } from "./server-BD5ckxZb.js";
5
+ import { C as interruptUIMessage, S as progressBatches, _ as projectedMessage, a as events, b as upsertResponse, d as AI_EVENT_ID_PREFIX, f as aiEventId, g as advanceProjection, h as controlCommit, l as aiStateReducer, m as applyControlChanges, p as initialControlState, u as storedAIReducer, v as reduceToolActivity, w as pauseUIMessage, x as flattenProgressBatches, y as upsertMessage } from "./ai-DgOBltJ_.js";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  import { createHash } from "node:crypto";
8
- import { asSchema, convertToModelMessages, stepCountIs, streamText, toUIMessageStream } from "ai";
8
+ import { asSchema, convertToModelMessages, isToolUIPart, stepCountIs, streamText, toUIMessageStream } from "ai";
9
+ import { Buffer } from "node:buffer";
9
10
  //#region src/ai-coordinator.ts
11
+ const completeCall = (candidate) => {
12
+ const { requestId, generationId, messageId, toolCallId, toolName, providerExecuted, supportsDeferredResults } = candidate.call;
13
+ return {
14
+ terminal: true,
15
+ call: {
16
+ requestId,
17
+ generationId,
18
+ messageId,
19
+ toolCallId,
20
+ toolName,
21
+ ...providerExecuted === void 0 ? {} : { providerExecuted },
22
+ ...supportsDeferredResults === void 0 ? {} : { supportsDeferredResults }
23
+ },
24
+ ...candidate.work === void 0 ? {} : { work: candidate.work }
25
+ };
26
+ };
10
27
  const updateResponse = (state, update) => state.response === void 0 ? state : {
11
28
  ...state,
12
29
  response: update(state.response)
@@ -37,7 +54,7 @@ const foldCoordinator = (state, event) => {
37
54
  case "ai.generation.requested": {
38
55
  const request = event.payload;
39
56
  if (request.reason === "message") {
40
- const responseMessageId = request.responseMessageId ?? `${request.messageId}:assistant`;
57
+ const responseMessageId = request.responseMessageId;
41
58
  const requested = state.queued.findIndex((item) => item.messageId === request.messageId);
42
59
  return {
43
60
  ...state,
@@ -48,28 +65,26 @@ const foldCoordinator = (state, event) => {
48
65
  status: "requested",
49
66
  stepCount: 0,
50
67
  activeRequestId: event.id,
68
+ requestReason: request.reason,
69
+ ...request.sourceGenerationId === void 0 ? {} : { sourceGenerationId: request.sourceGenerationId },
51
70
  calls: [],
52
71
  inputs: []
53
72
  }
54
73
  };
55
74
  }
56
75
  if (state.response === void 0 || request.responseMessageId !== state.response.responseMessageId) return state;
57
- if (request.reason === "tool" && (!continuationReady(state) || event.id !== `ai.generate:tools:${state.response.generation?.generationId}`) || request.reason === "retry" && state.response.status !== "failed" || request.reason === "input" && state.response.inputResponse === void 0) return state;
76
+ if (request.reason === "tool" && (!continuationReady(state) || request.sourceGenerationId !== state.response.generation?.generationId) || request.sourceGenerationId !== void 0 && request.sourceGenerationId !== (state.response.generation ?? state.response.source?.generation)?.generationId || request.reason === "retry" && state.response.status !== "failed" || request.reason === "input" && state.response.inputResponse === void 0) return state;
58
77
  return updateResponse(state, (response) => {
59
78
  const generation = response.generation ?? response.source?.generation;
60
- const promptThroughIndex = response.promptThroughIndex ?? response.source?.promptThroughIndex;
61
79
  return {
62
80
  rootMessageId: response.rootMessageId,
63
81
  responseMessageId: response.responseMessageId,
64
82
  status: "requested",
65
83
  stepCount: response.stepCount,
66
84
  activeRequestId: event.id,
67
- ...generation === void 0 || promptThroughIndex === void 0 ? {} : { source: {
68
- generation,
69
- promptThroughIndex,
70
- failed: response.failure?.generationId === generation.generationId,
71
- canCompact: response.calls.every((call) => call.terminal)
72
- } },
85
+ requestReason: request.reason,
86
+ ...request.sourceGenerationId === void 0 ? {} : { sourceGenerationId: request.sourceGenerationId },
87
+ ...generation === void 0 ? {} : { source: { generation } },
73
88
  calls: [],
74
89
  inputs: []
75
90
  };
@@ -84,16 +99,17 @@ const foldCoordinator = (state, event) => {
84
99
  status: "generating",
85
100
  stepCount: response.stepCount,
86
101
  generation,
87
- ...generation.promptThroughIndex === void 0 ? {} : { promptThroughIndex: generation.promptThroughIndex },
88
102
  ...response.source === void 0 ? {} : { source: response.source },
89
103
  activeRequestId: generation.requestId,
104
+ requestReason: response.requestReason,
105
+ ...response.sourceGenerationId === void 0 ? {} : { sourceGenerationId: response.sourceGenerationId },
90
106
  calls: [],
91
107
  inputs: []
92
108
  }));
93
109
  }
94
110
  case "ai.generation.completed": {
95
111
  const completion = event.payload;
96
- if (state.response?.generation?.generationId !== completion.generationId) return state;
112
+ if (state.response?.generation?.requestId !== completion.requestId || state.response.generation.generationId !== completion.generationId) return state;
97
113
  const { calibration: _, ...uncalibrated } = updateResponse(state, (response) => ({
98
114
  ...response,
99
115
  status: "waiting",
@@ -114,8 +130,8 @@ const foldCoordinator = (state, event) => {
114
130
  case "ai.generation.failed": {
115
131
  const failure = event.payload;
116
132
  const response = state.response;
117
- const ownsActiveGeneration = response?.generation?.generationId === failure.generationId;
118
- const ownsActiveStepLimit = failure.stepLimit === true && response?.activeRequestId === `ai.generate:tools:${failure.generationId}` && response.responseMessageId === failure.responseMessageId;
133
+ const ownsActiveGeneration = response?.generation?.generationId === failure.generationId && response.generation.requestId === failure.requestId;
134
+ const ownsActiveStepLimit = failure.stepLimit === true && response?.requestReason === "tool" && response.sourceGenerationId === failure.generationId && response.source?.generation.requestId === failure.requestId && response.responseMessageId === failure.responseMessageId;
119
135
  if (!ownsActiveGeneration && !ownsActiveStepLimit) return state;
120
136
  return updateResponse(state, (current) => ({
121
137
  ...current,
@@ -138,7 +154,7 @@ const foldCoordinator = (state, event) => {
138
154
  }
139
155
  case "ai.tool.called": {
140
156
  const call = event.payload;
141
- if (state.response?.generation?.generationId !== call.generationId) return state;
157
+ if (state.response?.generation?.requestId !== call.requestId || state.response.generation.generationId !== call.generationId) return state;
142
158
  return updateResponse(state, (response) => ({
143
159
  ...response,
144
160
  calls: [...response.calls.filter((candidate) => candidate.call.toolCallId !== call.toolCallId), {
@@ -149,7 +165,7 @@ const foldCoordinator = (state, event) => {
149
165
  }
150
166
  case "ai.approval.requested": {
151
167
  const approval = event.payload;
152
- if (state.response?.generation?.generationId !== approval.generationId) return state;
168
+ if (state.response?.generation?.requestId !== approval.requestId || state.response.generation.generationId !== approval.generationId) return state;
153
169
  return updateResponse(state, (response) => ({
154
170
  ...response,
155
171
  calls: response.calls.map((candidate) => candidate.call.toolCallId === approval.toolCallId ? {
@@ -160,7 +176,7 @@ const foldCoordinator = (state, event) => {
160
176
  }
161
177
  case "ai.approval.responded": {
162
178
  const approval = event.payload;
163
- if (state.response?.responseMessageId !== approval.messageId || state.response.generation?.generationId !== approval.generationId) return state;
179
+ if (state.response?.responseMessageId !== approval.messageId || state.response.generation?.requestId !== approval.requestId || state.response.generation.generationId !== approval.generationId) return state;
164
180
  return updateResponse(state, (response) => ({
165
181
  ...response,
166
182
  calls: response.calls.map((candidate) => candidate.approval?.approvalId === approval.approvalId ? {
@@ -171,18 +187,15 @@ const foldCoordinator = (state, event) => {
171
187
  }
172
188
  case "ai.tool.result": {
173
189
  const result = event.payload;
174
- if (result.preliminary === true || state.response?.generation?.generationId !== result.generationId) return state;
190
+ if (result.preliminary === true || state.response?.generation?.requestId !== result.requestId || state.response.generation.generationId !== result.generationId) return state;
175
191
  return updateResponse(state, (response) => ({
176
192
  ...response,
177
- calls: response.calls.map((candidate) => candidate.call.toolCallId === result.toolCallId ? {
178
- ...candidate,
179
- terminal: true
180
- } : candidate)
193
+ calls: response.calls.map((candidate) => candidate.call.toolCallId === result.toolCallId ? completeCall(candidate) : candidate)
181
194
  }));
182
195
  }
183
196
  case "ai.input.requested": {
184
197
  const input = event.payload;
185
- if (state.response?.responseMessageId !== input.messageId || state.response.generation?.generationId !== input.generationId) return state;
198
+ if (state.response?.responseMessageId !== input.messageId || state.response.generation?.requestId !== input.requestId || state.response.generation.generationId !== input.generationId) return state;
186
199
  return updateResponse(state, (response) => ({
187
200
  ...response,
188
201
  inputs: [...response.inputs.filter((candidate) => candidate.messageId !== input.messageId || candidate.generationId !== input.generationId || candidate.inputId !== input.inputId), input]
@@ -191,7 +204,7 @@ const foldCoordinator = (state, event) => {
191
204
  case "ai.input.responded": {
192
205
  const input = event.payload;
193
206
  const requested = state.response?.inputs.find((candidate) => candidate.messageId === input.messageId && candidate.generationId === input.generationId && candidate.inputId === input.inputId && candidate.name === input.name);
194
- if (state.response?.responseMessageId !== input.messageId || state.response.generation?.generationId !== input.generationId || requested === void 0) return state;
207
+ if (state.response?.responseMessageId !== input.messageId || state.response.generation?.requestId !== input.requestId || state.response.generation.generationId !== input.generationId || requested === void 0) return state;
195
208
  return updateResponse(state, (response) => ({
196
209
  ...response,
197
210
  inputs: response.inputs.filter((candidate) => candidate.inputId !== input.inputId),
@@ -206,7 +219,7 @@ const foldCoordinator = (state, event) => {
206
219
  }
207
220
  };
208
221
  const aiCoordinatorReducer = (contract) => contract.reducer({
209
- name: "a2.ai.coordinator.v5",
222
+ name: "a2.ai.coordinator.v6",
210
223
  initialState: {
211
224
  closed: false,
212
225
  queued: []
@@ -226,12 +239,35 @@ const callReadyForModel = (call) => call.terminal || call.call.providerExecuted
226
239
  const approvalBlocksExecution = (call) => call.approval !== void 0 && call.response?.approved !== true;
227
240
  const createControlRuntime = (options) => {
228
241
  const reducer = options.agent.contract.reducer({
229
- name: "a2.ai.control.v1",
242
+ name: "a2.ai.control.v5",
230
243
  initialState: initialControlState()
231
- }).fold((state, event) => event.type === "ai.control.committed" ? applyControlChanges({
232
- state,
233
- changes: event.payload.changes
234
- }) : state);
244
+ }).fold((state, event) => {
245
+ if (event.type === "ai.control.requested") {
246
+ const command = event.payload;
247
+ if (command.action === "stop") return {
248
+ ...state,
249
+ pendingStops: {
250
+ ...state.pendingStops,
251
+ [event.id]: command.turnId
252
+ }
253
+ };
254
+ }
255
+ if (event.type === "ai.control.decided") {
256
+ const receipt = event.payload;
257
+ if (Object.hasOwn(state.pendingStops, receipt.commandId)) {
258
+ const pendingStops = { ...state.pendingStops };
259
+ delete pendingStops[receipt.commandId];
260
+ return {
261
+ ...state,
262
+ pendingStops
263
+ };
264
+ }
265
+ }
266
+ return event.type === "ai.control.committed" ? applyControlChanges({
267
+ state,
268
+ changes: event.payload.changes
269
+ }) : state;
270
+ });
235
271
  const coordinator = aiCoordinatorReducer(options.agent.contract);
236
272
  const handler = async (ctx) => {
237
273
  const snapshot = await ctx.session.state(reducer, { through: "latest" });
@@ -240,11 +276,11 @@ const createControlRuntime = (options) => {
240
276
  const state = structuredClone(snapshot.state);
241
277
  const facts = [];
242
278
  const jobs = [];
243
- const emit = ({ type, payload, id = `${ctx.event.id}:fact:${facts.length}` }) => {
279
+ const emit = async ({ type, payload, id }) => {
244
280
  const fact = {
245
281
  type,
246
282
  payload,
247
- id
283
+ id: id ?? await aiEventId("a2.ai.fact", ctx.event.id, String(facts.length))
248
284
  };
249
285
  facts.push(fact);
250
286
  state.coordinator = foldCoordinator(state.coordinator, {
@@ -253,11 +289,11 @@ const createControlRuntime = (options) => {
253
289
  });
254
290
  };
255
291
  let reason;
256
- const end = ({ lastSeenIndex, reason: interruptionReason }) => {
292
+ const end = async ({ lastSeenIndex, reason: interruptionReason }) => {
257
293
  const response = state.coordinator.response;
258
294
  if (response === void 0) return;
259
295
  const owner = response.generation === void 0 ? { requestId: response.activeRequestId } : { generationId: response.generation.generationId };
260
- emit({
296
+ await emit({
261
297
  type: "ai.message.interrupted",
262
298
  payload: {
263
299
  messageId: response.responseMessageId,
@@ -268,22 +304,13 @@ const createControlRuntime = (options) => {
268
304
  });
269
305
  state.active = null;
270
306
  };
271
- const knownInput = (id) => state.inbox.items.some((candidate) => candidate.id === id) || Object.hasOwn(state.reservedIds, id);
272
- const missingInputReason = (id) => state.reservedIds[id] === "removed" || !knownInput(id) ? "not-found" : "already-active";
273
- const inputTaken = ({ message, generate = true }) => knownInput(message.id) || generate && message.role === "user" && knownInput(`${message.id}:assistant`);
274
307
  const insert = ({ message, generate = true, first = false, afterStepOf }) => {
275
308
  const candidate = {
276
309
  id: message.id,
277
- revision: 0,
278
310
  message,
279
311
  generate: generate && message.role === "user",
280
312
  ...afterStepOf === void 0 ? {} : { afterStepOf }
281
313
  };
282
- state.reservedIds = {
283
- ...state.reservedIds,
284
- [candidate.id]: true,
285
- ...candidate.generate ? { [`${candidate.id}:assistant`]: true } : {}
286
- };
287
314
  if (first) state.inbox.items.unshift(candidate);
288
315
  else state.inbox.items.push(candidate);
289
316
  };
@@ -340,42 +367,28 @@ const createControlRuntime = (options) => {
340
367
  if (state.coordinator.closed) reason = "closed";
341
368
  else switch (command.action) {
342
369
  case "send":
343
- if (inputTaken({
344
- message: command.message,
345
- generate: command.generate !== false
346
- })) reason = "duplicate-input";
347
- else insert({
370
+ insert({
348
371
  message: command.message,
349
372
  generate: command.generate !== false
350
373
  });
351
374
  break;
352
375
  case "edit": {
353
376
  const item = state.inbox.items.find((candidate) => candidate.id === command.message.id);
354
- if (item === void 0) reason = missingInputReason(command.message.id);
355
- else if (item.revision !== command.expectedRevision) reason = "revision-conflict";
356
- else if (item.message.role !== command.message.role) reason = "revision-conflict";
357
- else {
358
- item.message = command.message;
359
- item.revision += 1;
360
- }
377
+ if (item === void 0) reason = "not-pending";
378
+ else if (item.message.role !== command.message.role) reason = "role-mismatch";
379
+ else item.message = command.message;
361
380
  break;
362
381
  }
363
382
  case "remove": {
364
383
  const position = state.inbox.items.findIndex((item) => item.id === command.inputId);
365
- if (position < 0) reason = missingInputReason(command.inputId);
366
- else {
367
- state.inbox.items.splice(position, 1);
368
- state.reservedIds = {
369
- ...state.reservedIds,
370
- [command.inputId]: "removed"
371
- };
372
- }
384
+ if (position < 0) reason = "not-pending";
385
+ else state.inbox.items.splice(position, 1);
373
386
  break;
374
387
  }
375
388
  case "move": {
376
389
  const index = state.inbox.items.findIndex((candidate) => candidate.id === command.inputId);
377
- if (index < 0) reason = missingInputReason(command.inputId);
378
- else if (command.beforeId !== null && !state.inbox.items.some((candidate) => candidate.id === command.beforeId)) reason = "not-found";
390
+ if (index < 0) reason = "not-pending";
391
+ else if (command.beforeId !== null && !state.inbox.items.some((candidate) => candidate.id === command.beforeId)) reason = "not-pending";
379
392
  else {
380
393
  const destination = command.beforeId === null ? state.inbox.items.length : state.inbox.items.findIndex((item) => item.id === command.beforeId);
381
394
  moveInput({
@@ -388,18 +401,11 @@ const createControlRuntime = (options) => {
388
401
  case "send-now": {
389
402
  const position = state.inbox.items.findIndex((item) => item.id === command.inputId);
390
403
  const selected = state.inbox.items[position];
391
- if (!selected) reason = missingInputReason(command.inputId);
404
+ if (!selected) reason = "not-pending";
392
405
  else if ((state.active?.turnId ?? null) !== command.turnId) reason = "stale-turn";
393
406
  else if (selected.message.role !== "user") reason = "not-user-input";
394
- else if (!selected.generate && knownInput(`${selected.id}:assistant`)) reason = "duplicate-input";
395
407
  else {
396
- if (!selected.generate) {
397
- state.reservedIds = {
398
- ...state.reservedIds,
399
- [`${selected.id}:assistant`]: true
400
- };
401
- selected.generate = true;
402
- }
408
+ selected.generate = true;
403
409
  if (state.active) selected.afterStepOf = state.active.turnId;
404
410
  else delete selected.afterStepOf;
405
411
  moveInput({
@@ -411,14 +417,13 @@ const createControlRuntime = (options) => {
411
417
  }
412
418
  case "stop":
413
419
  if (state.active?.turnId !== command.turnId) reason = "stale-turn";
414
- else end({
420
+ else await end({
415
421
  lastSeenIndex: command.lastSeenIndex,
416
422
  reason: void 0
417
423
  });
418
424
  break;
419
425
  case "steer":
420
- if (inputTaken({ message: command.message })) reason = "duplicate-input";
421
- else insert({
426
+ insert({
422
427
  message: command.message,
423
428
  generate: true,
424
429
  first: true,
@@ -428,7 +433,7 @@ const createControlRuntime = (options) => {
428
433
  case "interrupt": {
429
434
  const response = state.coordinator.response;
430
435
  if (!response || response.responseMessageId !== command.request.messageId || command.request.generationId !== response.generation?.generationId && command.request.requestId !== response.activeRequestId) reason = "stale-turn";
431
- else end({
436
+ else await end({
432
437
  lastSeenIndex: command.request.lastSeenIndex,
433
438
  reason: command.request.reason
434
439
  });
@@ -441,7 +446,7 @@ const createControlRuntime = (options) => {
441
446
  active.suspended = true;
442
447
  active.version += 1;
443
448
  const generation = state.coordinator.response?.generation;
444
- if (generation) emit({
449
+ if (generation) await emit({
445
450
  type: "ai.generation.paused",
446
451
  payload: {
447
452
  generationId: generation.generationId,
@@ -455,18 +460,18 @@ const createControlRuntime = (options) => {
455
460
  state.inbox.paused = false;
456
461
  break;
457
462
  case "tool-result":
458
- if (!state.coordinator.response?.calls.find((candidate) => candidate.call.generationId === command.result.generationId && candidate.call.messageId === command.result.messageId && candidate.call.toolCallId === command.result.toolCallId && candidate.call.providerExecuted === true && !candidate.terminal)) reason = "not-found";
459
- else emit({
463
+ if (!state.coordinator.response?.calls.find((candidate) => candidate.call.requestId === command.result.requestId && 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";
464
+ else await emit({
460
465
  type: "ai.tool.result",
461
466
  payload: command.result
462
467
  });
463
468
  break;
464
469
  case "approval": {
465
470
  const response = state.coordinator.response;
466
- const call = response?.calls.find((candidate) => candidate.approval?.approvalId === command.response.approvalId && candidate.approval.generationId === command.response.generationId && candidate.approval.messageId === command.response.messageId);
471
+ const call = response?.calls.find((candidate) => candidate.approval?.requestId === command.response.requestId && candidate.approval.approvalId === command.response.approvalId && candidate.approval.generationId === command.response.generationId && candidate.approval.messageId === command.response.messageId);
467
472
  if (response?.failure) reason = "stale-turn";
468
473
  else if (!call || call.response || call.terminal) reason = "not-found";
469
- else emit({
474
+ else await emit({
470
475
  type: "ai.approval.responded",
471
476
  payload: command.response
472
477
  });
@@ -476,8 +481,8 @@ const createControlRuntime = (options) => {
476
481
  case "input": {
477
482
  const payload = command.action === "input" ? command.response : command.request;
478
483
  const response = state.coordinator.response;
479
- if (response?.failure !== void 0 || response?.generation?.generationId !== payload.generationId || response.responseMessageId !== payload.messageId || command.action === "input" && !response.inputs.some((input) => input.inputId === payload.inputId && input.name === payload.name)) reason = "not-found";
480
- else emit({
484
+ if (response?.failure !== void 0 || response?.generation?.requestId !== payload.requestId || 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";
485
+ else await emit({
481
486
  type: command.action === "input" ? "ai.input.responded" : "ai.input.requested",
482
487
  payload
483
488
  });
@@ -487,7 +492,7 @@ const createControlRuntime = (options) => {
487
492
  const response = state.coordinator.response;
488
493
  if (!state.active || response?.status !== "failed" || response.rootMessageId !== command.request.messageId || response.responseMessageId !== command.request.responseMessageId) reason = "stale-turn";
489
494
  else if (!state.inbox.paused && !state.active.suspended) {
490
- emit({
495
+ await emit({
491
496
  type: "ai.retry.requested",
492
497
  payload: command.request
493
498
  });
@@ -497,7 +502,7 @@ const createControlRuntime = (options) => {
497
502
  responseMessageId: response.responseMessageId,
498
503
  reason: "retry"
499
504
  },
500
- id: `ai.generate:retry:${command.request.retryId}`
505
+ id: await aiEventId("a2.ai.request.retry", options.agent.contract.name, ctx.event.sessionId, command.request.retryId)
501
506
  });
502
507
  } else reason = "stale-turn";
503
508
  break;
@@ -505,6 +510,10 @@ const createControlRuntime = (options) => {
505
510
  }
506
511
  } else {
507
512
  const report = ctx.event.payload;
513
+ if (report.kind === "tool" && report.settled && state.coordinator.unsettledTools) {
514
+ delete state.coordinator.unsettledTools[report.workId];
515
+ if (Object.keys(state.coordinator.unsettledTools).length === 0) delete state.coordinator.unsettledTools;
516
+ }
508
517
  const active = state.active;
509
518
  const call = state.coordinator.response?.calls.find((candidate) => candidate.work?.id === report.workId);
510
519
  const work = report.kind === "model" ? active?.model : call?.work;
@@ -515,7 +524,7 @@ const createControlRuntime = (options) => {
515
524
  for (const event of report.events) {
516
525
  if (work.facts?.includes(event.id)) continue;
517
526
  if (authorized || report.kind === "tool" && event.type === "ai.tool.result") {
518
- emit({
527
+ await emit({
519
528
  type: event.type,
520
529
  payload: event.payload,
521
530
  id: event.id
@@ -523,7 +532,10 @@ const createControlRuntime = (options) => {
523
532
  work.facts = [...work.facts ?? [], event.id];
524
533
  }
525
534
  }
526
- if (report.settled) work.settled = true;
535
+ if (report.settled) {
536
+ work.settled = true;
537
+ delete work.facts;
538
+ }
527
539
  if (!authorized) reason = "stale-work";
528
540
  }
529
541
  }
@@ -544,7 +556,7 @@ const createControlRuntime = (options) => {
544
556
  if (!active.suspended && response.failure === void 0) for (const call of response.calls) {
545
557
  if (call.terminal || call.work !== void 0 && (!call.work.settled || call.work.version === active.version) || call.call.providerExecuted === true || call.approval && !call.response) continue;
546
558
  if (call.response?.approved === false) {
547
- emit({
559
+ await emit({
548
560
  type: "ai.tool.result",
549
561
  payload: {
550
562
  ...call.call,
@@ -554,12 +566,16 @@ const createControlRuntime = (options) => {
554
566
  });
555
567
  continue;
556
568
  }
557
- const id = `${ctx.event.id}:tool:${call.call.toolCallId}`;
569
+ const id = await aiEventId("a2.ai.tool.execution", ctx.event.id, call.call.generationId, call.call.toolCallId);
558
570
  call.work = {
559
571
  id,
560
572
  version: active.version,
561
573
  settled: false
562
574
  };
575
+ state.coordinator.unsettledTools = {
576
+ ...state.coordinator.unsettledTools,
577
+ [id]: true
578
+ };
563
579
  jobs.push({
564
580
  type: "ai.tool.execution.requested",
565
581
  id,
@@ -579,7 +595,7 @@ const createControlRuntime = (options) => {
579
595
  const handoffReady = handoff && workSettled && !externalPending && (active.suspended || response.failure !== void 0 || response.calls.every((call) => callReadyForModel(call) || approvalBlocksExecution(call)));
580
596
  const readyForModel = !active.suspended && response.failure === void 0 && modelDone && callsDone && response.inputs.length === 0;
581
597
  const naturalCompletion = readyForModel && response.completion !== void 0 && response.completion.finishReason !== "tool-calls";
582
- if (handoffReady && !naturalCompletion) end({
598
+ if (handoffReady && !naturalCompletion) await end({
583
599
  lastSeenIndex: void 0,
584
600
  reason: "steered after step"
585
601
  });
@@ -588,17 +604,19 @@ const createControlRuntime = (options) => {
588
604
  const input = response.inputResponse;
589
605
  if (!handoff && (resume || continuationReady(state.coordinator) || response.completion && input)) {
590
606
  const nextReason = resume ? "resume" : input ? "input" : "tool";
591
- const id = nextReason === "tool" ? `ai.generate:tools:${response.generation.generationId}` : `${ctx.event.id}:generate:${nextReason}`;
607
+ const sourceGenerationId = (response.generation ?? response.source?.generation)?.generationId;
608
+ const id = nextReason === "tool" ? await aiEventId("a2.ai.request.tool", response.generation.generationId) : await aiEventId("a2.ai.request", ctx.event.id, nextReason);
592
609
  requestModel({
593
610
  request: {
594
611
  messageId: response.responseMessageId,
595
612
  responseMessageId: response.responseMessageId,
596
- reason: nextReason
613
+ reason: nextReason,
614
+ ...sourceGenerationId === void 0 ? {} : { sourceGenerationId }
597
615
  },
598
616
  id
599
617
  });
600
618
  } else if (naturalCompletion) {
601
- emit({
619
+ await emit({
602
620
  type: "ai.message.completed",
603
621
  payload: { messageId: response.responseMessageId }
604
622
  });
@@ -610,13 +628,13 @@ const createControlRuntime = (options) => {
610
628
  if (state.active === null && !state.inbox.paused && !state.coordinator.closed) while (state.inbox.items.length > 0) {
611
629
  const item = state.inbox.items.shift();
612
630
  delete item.afterStepOf;
613
- emit({
631
+ await emit({
614
632
  type: "ai.message.created",
615
633
  payload: {
616
634
  message: item.message,
617
635
  generate: item.generate
618
636
  },
619
- id: `ai.admitted:${item.id}`
637
+ id: await aiEventId("a2.ai.admitted", options.agent.contract.name, ctx.event.sessionId, item.id)
620
638
  });
621
639
  if (!item.generate) continue;
622
640
  state.active = {
@@ -627,12 +645,14 @@ const createControlRuntime = (options) => {
627
645
  suspended: false,
628
646
  model: null
629
647
  };
648
+ const requestId = await aiEventId("a2.ai.request.message", options.agent.contract.name, ctx.event.sessionId, item.id);
630
649
  requestModel({
631
650
  request: {
632
651
  messageId: item.id,
652
+ responseMessageId: requestId,
633
653
  reason: "message"
634
654
  },
635
- id: `ai.generate:message:${item.id}`
655
+ id: requestId
636
656
  });
637
657
  break;
638
658
  }
@@ -646,6 +666,7 @@ const createControlRuntime = (options) => {
646
666
  ...facts,
647
667
  {
648
668
  type: "ai.control.committed",
669
+ id: await aiEventId("a2.ai.control.committed", ctx.event.id),
649
670
  payload: controlCommit({
650
671
  before,
651
672
  after: state,
@@ -655,11 +676,12 @@ const createControlRuntime = (options) => {
655
676
  ...jobs,
656
677
  {
657
678
  type: "ai.control.decided",
679
+ id: await aiEventId("a2.ai.control.decided", ctx.event.id),
658
680
  payload: receipt
659
681
  }
660
682
  ];
661
683
  };
662
- const report = ({ ctx, events, settled, name }) => {
684
+ const report = async ({ ctx, events, settled, name }) => {
663
685
  const payload = ctx.event.payload;
664
686
  const { turnId, version } = "turnId" in payload ? payload : payload.control;
665
687
  for (const event of events) {
@@ -669,7 +691,7 @@ const createControlRuntime = (options) => {
669
691
  }
670
692
  return {
671
693
  type: "ai.work.reported",
672
- id: `${ctx.event.id}:report:${ctx.attempt}:${name}`,
694
+ id: await aiEventId("a2.ai.report", ctx.event.id, String(ctx.attempt), name),
673
695
  payload: {
674
696
  workId: ctx.event.id,
675
697
  attempt: ctx.attempt,
@@ -677,11 +699,11 @@ const createControlRuntime = (options) => {
677
699
  version,
678
700
  kind: ctx.event.type === "ai.generation.requested" ? "model" : "tool",
679
701
  settled,
680
- events: events.filter((event) => event.type !== "ai.message.completed" && event.type !== "ai.message.interrupted").map((event, index) => ({
702
+ events: await Promise.all(events.filter((event) => event.type !== "ai.message.completed" && event.type !== "ai.message.interrupted").map(async (event, index) => ({
681
703
  type: event.type,
682
704
  payload: event.payload,
683
- id: event.id ?? `${ctx.event.id}:fact:${ctx.attempt}:${name}:${index}`
684
- }))
705
+ id: event.id ?? await aiEventId("a2.ai.work.fact", ctx.event.id, String(ctx.attempt), name, String(index))
706
+ })))
685
707
  }
686
708
  };
687
709
  };
@@ -689,7 +711,7 @@ const createControlRuntime = (options) => {
689
711
  const progress = events.filter((event) => event.type === "ai.generation.progress");
690
712
  const lifecycle = events.filter((event) => event.type !== "ai.generation.progress");
691
713
  if (lifecycle.length === 0) return ctx.session.append(name, ...progress);
692
- const input = report({
714
+ const input = await report({
693
715
  ctx,
694
716
  events: lifecycle,
695
717
  settled: false,
@@ -744,6 +766,532 @@ const createControlRuntime = (options) => {
744
766
  };
745
767
  };
746
768
  //#endregion
769
+ //#region src/ai-context-schema.ts
770
+ const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
771
+ const index = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
772
+ const id = (value) => typeof value === "string" && value.length > 0;
773
+ const nullableId = (value) => value === null || id(value);
774
+ const matches = (schema, value) => validateSync(schema, value, "AI context checkpoint").issues === void 0;
775
+ const isMessage = (value) => matches(events["ai.message.created"], { message: value });
776
+ const indexes = (value, messages) => record(value) && Object.values(value).every(index) && messages.every((message) => Object.hasOwn(value, message.id));
777
+ const compaction = (value) => value === null || record(value) && Object.keys(value).every((key) => [
778
+ "status",
779
+ "generationId",
780
+ "throughMessageId",
781
+ "throughIndex",
782
+ "summary"
783
+ ].includes(key)) && ["running", "completed"].includes(String(value["status"])) && id(value["generationId"]) && id(value["throughMessageId"]) && index(value["throughIndex"]) && (value["summary"] === void 0 || typeof value["summary"] === "string");
784
+ const progress = (value) => {
785
+ if (!record(value) || !index(value["length"]) || !Array.isArray(value["tail"]) || !Array.isArray(value["blocks"])) return false;
786
+ const pending = [...value["blocks"].toReversed().filter((block) => block !== null), value["tail"]].toReversed();
787
+ const seen = /* @__PURE__ */ new Set();
788
+ let count = 0;
789
+ let last = -1;
790
+ while (pending.length > 0) {
791
+ const block = pending.pop();
792
+ if (typeof block !== "object" || block === null || seen.has(block)) return false;
793
+ seen.add(block);
794
+ if (Array.isArray(block)) for (const batch of block) {
795
+ if (!record(batch) || !index(batch["index"]) || batch["index"] <= last || !Array.isArray(batch["chunks"]) || !batch["chunks"].every((chunk) => record(chunk) && typeof chunk["type"] === "string")) return false;
796
+ count += 1;
797
+ last = batch["index"];
798
+ }
799
+ else if (record(block) && Object.keys(block).length === 2 && Object.hasOwn(block, "left") && Object.hasOwn(block, "right")) pending.push(block["right"], block["left"]);
800
+ else return false;
801
+ }
802
+ return count === value["length"];
803
+ };
804
+ const indexedEvents = (value, types) => Array.isArray(value) && value.every((event) => record(event) && index(event["index"]) && types.includes(event["type"]) && matches(events[event["type"]], event["payload"]));
805
+ const projection = (value, messages) => {
806
+ if (!record(value) || Object.keys(value).some((key) => ![
807
+ "generationId",
808
+ "responseMessageId",
809
+ "promptThroughIndex",
810
+ "completionIndex",
811
+ "baseMessage",
812
+ "baseCompaction",
813
+ "batches",
814
+ "toolEvents",
815
+ "approvalEvents",
816
+ "cursor"
817
+ ].includes(key)) || !id(value["generationId"]) || !id(value["responseMessageId"]) || !index(value["promptThroughIndex"]) || value["completionIndex"] !== void 0 && !index(value["completionIndex"]) || value["baseMessage"] !== void 0 && !isMessage(value["baseMessage"]) || value["baseCompaction"] !== void 0 && !compaction(value["baseCompaction"]) || !progress(value["batches"]) || !indexedEvents(value["toolEvents"], ["ai.tool.called", "ai.tool.result"]) || !indexedEvents(value["approvalEvents"], ["ai.approval.requested", "ai.approval.responded"])) return false;
818
+ const cursor = value["cursor"];
819
+ if (cursor === void 0 || cursor === null) return true;
820
+ if (!record(cursor) || !index(cursor["throughIndex"]) || !record(cursor["stream"])) return false;
821
+ const stream = cursor["stream"];
822
+ const current = messages.find((message) => message.id === value["responseMessageId"]);
823
+ if (!current || !["text", "reasoning"].every((key) => record(stream[key]) && Object.values(stream[key]).every((part) => index(part) && part < current.parts.length)) || !record(stream["tools"])) return false;
824
+ return Object.values(stream["tools"]).every((tool) => record(tool) && typeof tool["text"] === "string" && typeof tool["toolName"] === "string" && typeof tool["dynamic"] === "boolean");
825
+ };
826
+ const authority = (value) => value === null || record(value) && Object.keys(value).every((key) => [
827
+ "requestId",
828
+ "responseMessageId",
829
+ "sourceGenerationId",
830
+ "requestReason",
831
+ "phase"
832
+ ].includes(key)) && [
833
+ "requestId",
834
+ "responseMessageId",
835
+ "sourceGenerationId"
836
+ ].every((key) => nullableId(value[key])) && (value["requestReason"] === null || [
837
+ "message",
838
+ "tool",
839
+ "input",
840
+ "resume",
841
+ "retry"
842
+ ].includes(String(value["requestReason"]))) && (value["phase"] === null || [
843
+ "starting",
844
+ "generating",
845
+ "tools",
846
+ "waiting",
847
+ "failed",
848
+ "pausing",
849
+ "paused"
850
+ ].includes(String(value["phase"])));
851
+ const contextSchema = () => ({ "~standard": {
852
+ version: 1,
853
+ vendor: "a2-context",
854
+ validate(value) {
855
+ const invalid = { issues: [{ message: "invalid AI model context" }] };
856
+ const keys = [
857
+ "messages",
858
+ "compaction",
859
+ "modelMetadata",
860
+ "closed",
861
+ "authority",
862
+ "generation",
863
+ "activeProjection",
864
+ "messageIndexes"
865
+ ];
866
+ try {
867
+ if (!record(value) || keys.some((key) => !Object.hasOwn(value, key)) || Object.keys(value).some((key) => !keys.includes(key))) return invalid;
868
+ const messages = value["messages"];
869
+ const metadata = value["modelMetadata"];
870
+ const generation = value["generation"];
871
+ const current = value["activeProjection"];
872
+ if (!Array.isArray(messages) || !messages.every(isMessage) || !compaction(value["compaction"]) || typeof value["closed"] !== "boolean" || !authority(value["authority"]) || !record(metadata) || !Object.entries(metadata).every(([modelId, entry]) => record(entry) && (entry["status"] === "pending" || entry["status"] === "unavailable" || entry["status"] === "resolved" && matches(events["ai.model.metadata.resolved"], {
873
+ modelId,
874
+ limits: entry["limits"]
875
+ }))) || !indexes(value["messageIndexes"], messages)) return invalid;
876
+ if (generation !== null && (!record(generation) || !matches(events["ai.generation.started"], generation["owner"]) || ![
877
+ "streaming",
878
+ "completed",
879
+ "paused",
880
+ "failed",
881
+ "superseded"
882
+ ].includes(String(generation["phase"])) || generation["responseIndex"] !== void 0 && !index(generation["responseIndex"]) || generation["phase"] === "failed" && typeof generation["error"] !== "string")) return invalid;
883
+ const owner = record(generation) && record(generation["owner"]) ? generation["owner"] : void 0;
884
+ if (current !== null && (!projection(current, messages) || !record(current) || current["generationId"] !== owner?.["generationId"] || current["responseMessageId"] !== owner?.["responseMessageId"])) return invalid;
885
+ if (generation !== null && generation["phase"] !== "superseded" && current === null) return invalid;
886
+ if (value["closed"] && (generation !== null || current !== null)) return invalid;
887
+ return { value };
888
+ } catch {
889
+ return invalid;
890
+ }
891
+ }
892
+ } });
893
+ //#endregion
894
+ //#region src/ai-context.ts
895
+ const replaceResponse = ({ state, message, index }) => {
896
+ let messages = upsertResponse({
897
+ messages: state.messages,
898
+ message,
899
+ inputId: state.generation.owner.messageId
900
+ });
901
+ if (state.generation?.responseIndex !== void 0 && !state.messages.some((item) => item.id === message.id)) {
902
+ messages = messages.filter((item) => item.id !== message.id);
903
+ messages.splice(state.generation.responseIndex, 0, message);
904
+ }
905
+ return {
906
+ ...state,
907
+ messages,
908
+ messageIndexes: Object.hasOwn(state.messageIndexes, message.id) ? state.messageIndexes : {
909
+ ...state.messageIndexes,
910
+ [message.id]: index
911
+ }
912
+ };
913
+ };
914
+ const calledTools = (projection) => /* @__PURE__ */ new Set([...projection.baseMessage?.parts.flatMap((part) => "toolCallId" in part ? [part.toolCallId] : []) ?? [], ...projection.toolEvents.filter((event) => event.type === "ai.tool.called").map((event) => event.payload.toolCallId)]);
915
+ const renderProjection = ({ state, index }) => {
916
+ const projection = state.activeProjection;
917
+ let message = projectedMessage(projection);
918
+ if (projection.baseMessage === void 0 && projection.batches.length === 0 && message.parts.length === 0) return state;
919
+ if (state.generation?.phase === "paused") message = pauseUIMessage({
920
+ message,
921
+ toolCallIds: calledTools(projection)
922
+ });
923
+ if (state.generation?.phase === "failed") message = interruptUIMessage(message, `Generation failed before tool completion: ${state.generation.error}`);
924
+ return replaceResponse({
925
+ state: {
926
+ ...state,
927
+ activeProjection: {
928
+ ...projection,
929
+ cursor: null
930
+ }
931
+ },
932
+ message,
933
+ index
934
+ });
935
+ };
936
+ const generationBase = (state) => {
937
+ const projection = state.activeProjection;
938
+ if (!projection) return state;
939
+ return {
940
+ ...state,
941
+ messages: projection.baseMessage === void 0 ? state.messages.filter((message) => message.id !== projection.responseMessageId) : upsertMessage(state.messages, projection.baseMessage),
942
+ compaction: projection.baseCompaction ?? null
943
+ };
944
+ };
945
+ const contextForAttempt = (state) => ({
946
+ ...state.activeProjection?.completionIndex === void 0 ? generationBase(state) : state,
947
+ generation: state.generation && {
948
+ ...state.generation,
949
+ phase: "superseded"
950
+ },
951
+ activeProjection: null
952
+ });
953
+ const endGeneration = (state) => ({
954
+ ...state,
955
+ authority: null,
956
+ generation: null,
957
+ activeProjection: null
958
+ });
959
+ const ownsGeneration = (state, owner) => state.generation?.owner.generationId === owner.generationId && state.generation.owner.requestId === owner.requestId && state.generation.phase !== "superseded";
960
+ const createContextReducer = ({ agent }) => {
961
+ const reducer = agent.contract.reducer({
962
+ name: "a2.ai.context.v3",
963
+ initialState: {
964
+ messages: [],
965
+ compaction: null,
966
+ modelMetadata: {},
967
+ closed: false,
968
+ authority: null,
969
+ generation: null,
970
+ activeProjection: null,
971
+ messageIndexes: {}
972
+ },
973
+ stateSchema: contextSchema()
974
+ }).fold((state, event) => reduceContext(state, event));
975
+ return storedAIReducer(reducer);
976
+ };
977
+ const reduceContext = (state, event) => {
978
+ if (state.closed) return state;
979
+ switch (event.type) {
980
+ case "ai.control.committed": {
981
+ const { view } = event.payload;
982
+ const { requestId, requestReason, sourceGenerationId, responseMessageId, phase } = view;
983
+ const next = {
984
+ ...state,
985
+ authority: {
986
+ requestId,
987
+ requestReason,
988
+ sourceGenerationId,
989
+ responseMessageId,
990
+ phase
991
+ },
992
+ closed: view.closed
993
+ };
994
+ return view.closed ? endGeneration(next) : next;
995
+ }
996
+ case "ai.session.closed": return endGeneration({
997
+ ...state,
998
+ closed: true,
999
+ compaction: state.compaction?.status === "running" ? state.activeProjection?.baseCompaction ?? null : state.compaction
1000
+ });
1001
+ case "ai.message.created": {
1002
+ const { message } = event.payload;
1003
+ return {
1004
+ ...state,
1005
+ messages: upsertMessage(state.messages, message),
1006
+ activeProjection: state.activeProjection?.responseMessageId === message.id ? {
1007
+ ...state.activeProjection,
1008
+ cursor: null
1009
+ } : state.activeProjection,
1010
+ messageIndexes: {
1011
+ ...state.messageIndexes,
1012
+ [message.id]: event.index
1013
+ }
1014
+ };
1015
+ }
1016
+ case "ai.generation.requested": {
1017
+ const request = event.payload;
1018
+ if (state.authority?.requestId !== event.id || request.responseMessageId !== state.authority.responseMessageId || request.reason !== state.authority.requestReason || (request.sourceGenerationId ?? null) !== state.authority.sourceGenerationId || request.reason !== "retry") return state;
1019
+ return contextForAttempt(state);
1020
+ }
1021
+ case "ai.generation.started": {
1022
+ const owner = event.payload;
1023
+ if (state.authority?.requestId !== owner.requestId || state.authority.responseMessageId !== owner.responseMessageId || state.authority.phase === "paused" || state.authority.phase === "pausing") return state;
1024
+ const previous = state.generation;
1025
+ if (previous?.owner.requestId === owner.requestId && (previous.owner.attempt >= owner.attempt || previous.phase === "failed" || previous.phase === "completed" || state.activeProjection?.completionIndex !== void 0)) return state;
1026
+ const base = previous?.owner.requestId === owner.requestId ? generationBase(state) : state;
1027
+ const baseMessage = base.messages.find((message) => message.id === owner.responseMessageId);
1028
+ return {
1029
+ ...base,
1030
+ generation: {
1031
+ owner,
1032
+ phase: "streaming",
1033
+ ...previous?.owner.responseMessageId === owner.responseMessageId && previous.responseIndex !== void 0 ? { responseIndex: previous.responseIndex } : {}
1034
+ },
1035
+ activeProjection: {
1036
+ generationId: owner.generationId,
1037
+ responseMessageId: owner.responseMessageId,
1038
+ promptThroughIndex: owner.promptThroughIndex ?? event.index - 1,
1039
+ ...baseMessage === void 0 ? {} : { baseMessage },
1040
+ baseCompaction: base.compaction,
1041
+ batches: progressBatches(),
1042
+ toolEvents: [],
1043
+ approvalEvents: []
1044
+ }
1045
+ };
1046
+ }
1047
+ case "ai.generation.progress": {
1048
+ const payload = event.payload;
1049
+ if (!ownsGeneration(state, payload) || state.generation.owner.responseMessageId !== payload.responseMessageId || state.generation.phase !== "streaming" || !state.activeProjection) return state;
1050
+ const projection = state.activeProjection;
1051
+ const progress = advanceProjection({
1052
+ projection,
1053
+ base: state.messages.find((message) => message.id === projection.responseMessageId),
1054
+ chunks: payload.chunks,
1055
+ index: event.index
1056
+ });
1057
+ return replaceResponse({
1058
+ state: {
1059
+ ...state,
1060
+ activeProjection: {
1061
+ ...projection,
1062
+ batches: progress.batches,
1063
+ cursor: progress.cursor
1064
+ }
1065
+ },
1066
+ message: progress.message,
1067
+ index: event.index
1068
+ });
1069
+ }
1070
+ case "ai.generation.completed": {
1071
+ const payload = event.payload;
1072
+ if (!ownsGeneration(state, payload) || state.generation.phase !== "streaming") return state;
1073
+ return {
1074
+ ...state,
1075
+ generation: {
1076
+ ...state.generation,
1077
+ phase: "completed"
1078
+ },
1079
+ activeProjection: {
1080
+ ...state.activeProjection,
1081
+ completionIndex: event.index,
1082
+ cursor: null
1083
+ }
1084
+ };
1085
+ }
1086
+ case "ai.generation.failed": {
1087
+ const payload = event.payload;
1088
+ if (!ownsGeneration(state, payload)) return state;
1089
+ if (state.generation.phase !== "streaming" && !(payload.stepLimit && state.authority?.requestReason === "tool" && state.authority.sourceGenerationId === payload.generationId)) return state;
1090
+ if (payload.superseded) return {
1091
+ ...generationBase(state),
1092
+ generation: {
1093
+ ...state.generation,
1094
+ phase: "superseded"
1095
+ },
1096
+ activeProjection: null
1097
+ };
1098
+ return renderProjection({
1099
+ state: {
1100
+ ...state,
1101
+ compaction: state.activeProjection?.baseCompaction ?? null,
1102
+ generation: {
1103
+ ...state.generation,
1104
+ phase: "failed",
1105
+ error: payload.error
1106
+ }
1107
+ },
1108
+ index: event.index
1109
+ });
1110
+ }
1111
+ case "ai.message.interrupted": {
1112
+ const payload = event.payload;
1113
+ const owner = state.generation?.owner;
1114
+ const ownsRequest = payload.requestId !== void 0 && payload.requestId === state.authority?.requestId && payload.messageId === state.authority.responseMessageId;
1115
+ const ownsOwner = owner?.responseMessageId === payload.messageId && (payload.generationId === owner.generationId || payload.requestId === owner.requestId);
1116
+ if (!ownsRequest && !ownsOwner) return state;
1117
+ if (ownsOwner && state.authority?.requestId !== owner.requestId && !ownsRequest) return state;
1118
+ const projection = state.activeProjection;
1119
+ if (!projection) return endGeneration(state);
1120
+ const cutoff = payload.lastSeenIndex ?? Number.POSITIVE_INFINITY;
1121
+ const retained = {
1122
+ ...projection,
1123
+ batches: progressBatches(flattenProgressBatches(projection.batches).filter((batch) => batch.index <= cutoff)),
1124
+ toolEvents: projection.toolEvents.filter((item) => item.index <= cutoff),
1125
+ approvalEvents: projection.approvalEvents.filter((item) => item.index <= cutoff)
1126
+ };
1127
+ let next = generationBase(state);
1128
+ if (retained.baseMessage !== void 0 || retained.batches.length > 0) next = replaceResponse({
1129
+ state: next,
1130
+ message: interruptUIMessage(projectedMessage(retained)),
1131
+ index: event.index
1132
+ });
1133
+ return endGeneration(next);
1134
+ }
1135
+ case "ai.message.completed": {
1136
+ const { messageId } = event.payload;
1137
+ return state.generation?.owner.responseMessageId === messageId && state.generation.phase !== "failed" && state.generation.phase !== "superseded" ? endGeneration(state) : state;
1138
+ }
1139
+ case "ai.generation.paused": {
1140
+ const { generationId } = event.payload;
1141
+ if (state.generation?.owner.generationId !== generationId || state.generation.phase === "failed" || state.generation.phase === "superseded" || !state.activeProjection) return state;
1142
+ const toolCallIds = calledTools(state.activeProjection);
1143
+ return renderProjection({
1144
+ state: {
1145
+ ...state,
1146
+ generation: {
1147
+ ...state.generation,
1148
+ phase: "paused"
1149
+ },
1150
+ compaction: state.compaction?.status === "running" ? state.activeProjection.baseCompaction ?? null : state.compaction,
1151
+ activeProjection: {
1152
+ ...state.activeProjection,
1153
+ cursor: null,
1154
+ batches: progressBatches(flattenProgressBatches(state.activeProjection.batches).map((batch) => ({
1155
+ index: batch.index,
1156
+ chunks: batch.chunks.filter((chunk) => !("toolCallId" in chunk) || toolCallIds.has(chunk.toolCallId))
1157
+ })))
1158
+ }
1159
+ },
1160
+ index: event.index
1161
+ });
1162
+ }
1163
+ case "ai.tool.called":
1164
+ case "ai.tool.result": {
1165
+ const payload = event.payload;
1166
+ if (!ownsGeneration(state, payload) || !state.activeProjection || state.generation.owner.responseMessageId !== payload.messageId) return state;
1167
+ if (state.generation.phase === "failed") {
1168
+ const result = payload;
1169
+ const running = state.activeProjection.toolEvents.reduce((tools, item) => reduceToolActivity(tools, item), []).some((tool) => tool.toolCallId === payload.toolCallId && tool.status === "running");
1170
+ if (event.type !== "ai.tool.result" || result.preliminary || !running) return state;
1171
+ }
1172
+ const toolEvent = event.type === "ai.tool.called" ? {
1173
+ index: event.index,
1174
+ type: "ai.tool.called",
1175
+ payload
1176
+ } : {
1177
+ index: event.index,
1178
+ type: "ai.tool.result",
1179
+ payload
1180
+ };
1181
+ const next = {
1182
+ ...state,
1183
+ activeProjection: {
1184
+ ...state.activeProjection,
1185
+ toolEvents: [...state.activeProjection.toolEvents, toolEvent]
1186
+ }
1187
+ };
1188
+ if (event.type === "ai.tool.called") return next;
1189
+ next.activeProjection.cursor = null;
1190
+ return renderProjection({
1191
+ state: next,
1192
+ index: event.index
1193
+ });
1194
+ }
1195
+ case "ai.approval.requested":
1196
+ case "ai.approval.responded": {
1197
+ const payload = event.payload;
1198
+ if (!ownsGeneration(state, payload) || !state.activeProjection || state.generation.phase === "failed" || state.generation.owner.responseMessageId !== payload.messageId) return state;
1199
+ const approvalEvent = event.type === "ai.approval.requested" ? {
1200
+ index: event.index,
1201
+ type: "ai.approval.requested",
1202
+ payload
1203
+ } : {
1204
+ index: event.index,
1205
+ type: "ai.approval.responded",
1206
+ payload
1207
+ };
1208
+ return renderProjection({
1209
+ state: {
1210
+ ...state,
1211
+ activeProjection: {
1212
+ ...state.activeProjection,
1213
+ cursor: null,
1214
+ approvalEvents: [...state.activeProjection.approvalEvents, approvalEvent]
1215
+ }
1216
+ },
1217
+ index: event.index
1218
+ });
1219
+ }
1220
+ case "ai.compaction.requested": {
1221
+ const payload = event.payload;
1222
+ if (state.generation?.owner.generationId !== payload.generationId || state.generation.phase !== "streaming") return state;
1223
+ return {
1224
+ ...state,
1225
+ compaction: {
1226
+ status: "running",
1227
+ generationId: payload.generationId,
1228
+ throughMessageId: payload.throughMessageId,
1229
+ throughIndex: payload.throughIndex ?? state.activeProjection.promptThroughIndex
1230
+ }
1231
+ };
1232
+ }
1233
+ case "ai.compaction.completed": {
1234
+ const payload = event.payload;
1235
+ const projection = state.activeProjection;
1236
+ if (state.generation?.owner.generationId !== payload.generationId || state.generation.phase !== "streaming" || state.compaction?.status !== "running" || state.compaction.generationId !== payload.generationId || !projection) return state;
1237
+ const throughIndex = payload.throughIndex ?? projection.promptThroughIndex;
1238
+ const replacements = new Set(payload.messages.map((message) => message.id));
1239
+ const retained = new Set(payload.retainedMessageIds ?? []);
1240
+ const messages = [...payload.messages, ...state.messages.filter((message) => !replacements.has(message.id) && (retained.has(message.id) || (state.messageIndexes[message.id] ?? 0) > throughIndex))];
1241
+ const activeProjection = {
1242
+ ...projection,
1243
+ cursor: null
1244
+ };
1245
+ delete activeProjection.baseMessage;
1246
+ const baseMessage = messages.find((message) => message.id === projection.responseMessageId);
1247
+ if (baseMessage !== void 0) activeProjection.baseMessage = baseMessage;
1248
+ const compaction = {
1249
+ status: "completed",
1250
+ generationId: payload.generationId,
1251
+ throughMessageId: payload.throughMessageId,
1252
+ throughIndex,
1253
+ ...payload.summary === void 0 ? {} : { summary: payload.summary }
1254
+ };
1255
+ activeProjection.baseCompaction = compaction;
1256
+ return {
1257
+ ...state,
1258
+ messages,
1259
+ compaction,
1260
+ generation: {
1261
+ ...state.generation,
1262
+ responseIndex: payload.messages.length
1263
+ },
1264
+ activeProjection,
1265
+ messageIndexes: Object.fromEntries(messages.map((message) => [message.id, replacements.has(message.id) ? throughIndex : state.messageIndexes[message.id]]))
1266
+ };
1267
+ }
1268
+ case "ai.model.metadata.requested": {
1269
+ const { modelId } = event.payload;
1270
+ return Object.hasOwn(state.modelMetadata, modelId) ? state : {
1271
+ ...state,
1272
+ modelMetadata: {
1273
+ ...state.modelMetadata,
1274
+ [modelId]: { status: "pending" }
1275
+ }
1276
+ };
1277
+ }
1278
+ case "ai.model.metadata.resolved": {
1279
+ const { modelId, limits } = event.payload;
1280
+ return {
1281
+ ...state,
1282
+ modelMetadata: {
1283
+ ...state.modelMetadata,
1284
+ [modelId]: limits === null ? { status: "unavailable" } : {
1285
+ status: "resolved",
1286
+ limits
1287
+ }
1288
+ }
1289
+ };
1290
+ }
1291
+ default: return state;
1292
+ }
1293
+ };
1294
+ //#endregion
747
1295
  //#region src/ai-sdk-step.ts
748
1296
  const CONTROLLED_SETTINGS = [
749
1297
  "_internal",
@@ -885,6 +1433,40 @@ const readModelLimits = async (options) => {
885
1433
  }
886
1434
  };
887
1435
  //#endregion
1436
+ //#region src/ai-input-tokens.ts
1437
+ const withoutProviderOptions = (value) => {
1438
+ const result = { ...value };
1439
+ Reflect.deleteProperty(result, "providerOptions");
1440
+ return result;
1441
+ };
1442
+ const estimatedMessage = (message) => ({
1443
+ ...withoutProviderOptions(message),
1444
+ content: typeof message.content === "string" ? message.content : message.content.map((part) => part.type === "tool-result" ? {
1445
+ ...withoutProviderOptions(part),
1446
+ output: {
1447
+ ...withoutProviderOptions(part.output),
1448
+ ...part.output.type === "content" ? { value: part.output.value.map(withoutProviderOptions) } : {}
1449
+ }
1450
+ } : withoutProviderOptions(part))
1451
+ });
1452
+ const estimateInputTokens = async (options) => {
1453
+ const tools = await Promise.all(Object.entries(options.tools).map(async ([name, tool]) => ({
1454
+ name,
1455
+ description: tool.description,
1456
+ inputSchema: await asSchema(tool.inputSchema).jsonSchema
1457
+ })));
1458
+ const instructions = options.instructions;
1459
+ return Math.ceil(Buffer.byteLength(JSON.stringify({
1460
+ messages: options.messages.map(estimatedMessage),
1461
+ instructions: instructions === void 0 || typeof instructions === "string" ? instructions : Array.isArray(instructions) ? instructions.map(estimatedMessage) : estimatedMessage(instructions),
1462
+ tools
1463
+ }), "utf8") / 4);
1464
+ };
1465
+ const measuredInputTokens = (options) => {
1466
+ const previous = options.calibration;
1467
+ return previous !== void 0 && previous.model === options.model && options.estimate >= previous.estimate ? Math.ceil(previous.inputTokens + options.estimate - previous.estimate) : options.estimate;
1468
+ };
1469
+ //#endregion
888
1470
  //#region src/ai-server.ts
889
1471
  /**
890
1472
  * experimental-a2/ai/server — the server-only implementation of an experimental-a2/ai definition.
@@ -1066,7 +1648,7 @@ async function* consumeGeneration(options) {
1066
1648
  }
1067
1649
  const foldAIEvents = (options) => {
1068
1650
  let state = options.state;
1069
- for (const event of options.events) state = options.agent.reducer.fold(state, event);
1651
+ for (const event of options.events) state = options.reducer.fold(state, event);
1070
1652
  return state;
1071
1653
  };
1072
1654
  const summarize = async (options) => {
@@ -1107,7 +1689,7 @@ const summarize = async (options) => {
1107
1689
  role: "user",
1108
1690
  content: instruction
1109
1691
  }],
1110
- responseMessageId: `${options.context.generationId}:summary`
1692
+ responseMessageId: await aiEventId("a2.ai.summary", options.context.generationId)
1111
1693
  },
1112
1694
  messageMetadata: void 0
1113
1695
  });
@@ -1133,52 +1715,11 @@ const summarize = async (options) => {
1133
1715
  };
1134
1716
  const activeCompaction = (state) => {
1135
1717
  const compaction = state.compaction;
1136
- return compaction?.status === "completed" && compaction.messages !== void 0 && state.messages.some((message) => message.id === compaction.throughMessageId) ? compaction : null;
1718
+ return compaction?.status === "completed" ? compaction : null;
1137
1719
  };
1138
- const contextMessages = async (options) => {
1139
- const { agent, session, snapshot } = options;
1140
- const appended = options.appended ?? [];
1141
- const state = foldAIEvents({
1142
- agent,
1143
- events: appended,
1144
- state: snapshot.state
1145
- });
1146
- const compaction = activeCompaction(state);
1147
- const queued = new Set(options.coordinator.queued.map((item) => item.messageId));
1148
- const visible = (messages) => messages.filter((message) => !queued.has(message.id));
1149
- if (compaction === null) return visible(state.messages);
1150
- const retained = new Set(compaction.retainedMessageIds ?? []);
1151
- const throughIndex = compaction.throughIndex;
1152
- if (throughIndex !== void 0) {
1153
- const [prefix, priorCoordinator, tail] = await Promise.all([
1154
- throughIndex === snapshot.index ? Promise.resolve(snapshot) : session.state(agent.reducer, { through: throughIndex }),
1155
- session.state(aiCoordinatorReducer(agent.contract), { through: throughIndex }),
1156
- throughIndex >= snapshot.index ? Promise.resolve([]) : session.history({
1157
- gte: throughIndex + 1,
1158
- lte: snapshot.index
1159
- })
1160
- ]);
1161
- for (const item of priorCoordinator.state.queued) retained.add(item.messageId);
1162
- const events = options.excluded === void 0 ? tail : withoutGenerationLifecycle(tail, options.excluded);
1163
- const context = foldAIEvents({
1164
- agent,
1165
- events: [...events, ...appended],
1166
- state: {
1167
- ...prefix.state,
1168
- messages: [...compaction.messages, ...prefix.state.messages.filter((message) => retained.has(message.id))],
1169
- activeProjection: null
1170
- }
1171
- });
1172
- const positions = /* @__PURE__ */ new Map();
1173
- for (const message of [...compaction.messages, ...state.messages]) if (!positions.has(message.id)) positions.set(message.id, positions.size);
1174
- return visible(context.messages.toSorted((left, right) => (positions.get(left.id) ?? positions.size) - (positions.get(right.id) ?? positions.size)));
1175
- }
1176
- const boundary = state.messages.findIndex((message) => message.id === compaction.throughMessageId);
1177
- return visible([
1178
- ...compaction.messages,
1179
- ...state.messages.slice(0, boundary + 1).filter((message) => retained.has(message.id)),
1180
- ...state.messages.slice(boundary + 1)
1181
- ]);
1720
+ const contextMessages = ({ state, coordinator }) => {
1721
+ const queued = new Set(coordinator.queued.map((item) => item.messageId));
1722
+ return state.messages.filter((message) => !queued.has(message.id));
1182
1723
  };
1183
1724
  const modelContext = async (options) => {
1184
1725
  const messages = await convertToModelMessages(options.messages, { tools: options.tools });
@@ -1188,25 +1729,9 @@ const modelContext = async (options) => {
1188
1729
  content: summary
1189
1730
  }, ...messages];
1190
1731
  };
1191
- const estimateInputTokens = async (options) => {
1192
- const tools = await Promise.all(Object.entries(options.tools).map(async ([name, tool]) => ({
1193
- name,
1194
- description: tool.description,
1195
- inputSchema: await asSchema(tool.inputSchema).jsonSchema
1196
- })));
1197
- return Math.ceil(new TextEncoder().encode(JSON.stringify({
1198
- messages: options.messages,
1199
- instructions: options.instructions,
1200
- tools
1201
- })).byteLength / 4);
1202
- };
1203
- const measuredInputTokens = (options) => {
1204
- const previous = options.calibration;
1205
- return previous !== void 0 && previous.model === options.model && options.estimate >= previous.estimate ? Math.max(options.estimate, Math.ceil(previous.inputTokens + options.estimate - previous.estimate)) : options.estimate;
1206
- };
1207
- const toolCalledEvent = (payload) => ({
1732
+ const toolCalledEvent = async (payload) => ({
1208
1733
  type: "ai.tool.called",
1209
- id: `${payload.generationId}:tool:${payload.toolCallId}:called`,
1734
+ id: await aiEventId("a2.ai.tool.called", payload.generationId, payload.toolCallId),
1210
1735
  payload
1211
1736
  });
1212
1737
  const approvalClassification = (tools, generation, toolName) => {
@@ -1220,7 +1745,7 @@ const approvalClassification = (tools, generation, toolName) => {
1220
1745
  if (typeof tool?.needsApproval === "function") return "unknown";
1221
1746
  return tool?.needsApproval === true ? "approval" : "automatic";
1222
1747
  };
1223
- const lifecycleEvents = (options) => {
1748
+ const lifecycleEvents = async (options) => {
1224
1749
  const result = [];
1225
1750
  const pending = [...options.pending];
1226
1751
  const take = (toolCallId) => {
@@ -1228,12 +1753,12 @@ const lifecycleEvents = (options) => {
1228
1753
  if (index === -1) return void 0;
1229
1754
  return pending.splice(index, 1)[0];
1230
1755
  };
1231
- const flush = (toolCallId) => {
1756
+ const flush = async (toolCallId) => {
1232
1757
  const pendingCall = take(toolCallId);
1233
- if (pendingCall) result.push(toolCalledEvent(pendingCall.call));
1758
+ if (pendingCall) result.push(await toolCalledEvent(pendingCall.call));
1234
1759
  };
1235
1760
  for (const [chunkIndex, chunk] of options.chunks.entries()) {
1236
- const resultId = `${options.generationId}:tool-result:${options.sequence}:${chunkIndex}`;
1761
+ const resultId = () => aiEventId("a2.ai.tool.stream-result", options.generationId, String(options.sequence), String(chunkIndex));
1237
1762
  if (chunk.type === "tool-input-available") {
1238
1763
  const payload = {
1239
1764
  requestId: options.requestId,
@@ -1251,7 +1776,7 @@ const lifecycleEvents = (options) => {
1251
1776
  };
1252
1777
  take(payload.toolCallId);
1253
1778
  const classification = approvalClassification(options.tools, options.generation, chunk.toolName);
1254
- if (classification === "automatic") result.push(toolCalledEvent(payload));
1779
+ if (classification === "automatic") result.push(await toolCalledEvent(payload));
1255
1780
  else pending.push({
1256
1781
  call: payload,
1257
1782
  classification
@@ -1280,13 +1805,13 @@ const lifecycleEvents = (options) => {
1280
1805
  input: chunk.input
1281
1806
  };
1282
1807
  take(chunk.toolCallId);
1283
- result.push(toolCalledEvent(called), {
1808
+ result.push(await toolCalledEvent(called), {
1284
1809
  type: "ai.tool.result",
1285
- id: resultId,
1810
+ id: await resultId(),
1286
1811
  payload
1287
1812
  });
1288
1813
  } else if (chunk.type === "tool-output-available") {
1289
- flush(chunk.toolCallId);
1814
+ await flush(chunk.toolCallId);
1290
1815
  const payload = {
1291
1816
  requestId: options.requestId,
1292
1817
  messageId: options.messageId,
@@ -1302,11 +1827,11 @@ const lifecycleEvents = (options) => {
1302
1827
  };
1303
1828
  result.push({
1304
1829
  type: "ai.tool.result",
1305
- id: resultId,
1830
+ id: await resultId(),
1306
1831
  payload
1307
1832
  });
1308
1833
  } else if (chunk.type === "tool-output-error") {
1309
- flush(chunk.toolCallId);
1834
+ await flush(chunk.toolCallId);
1310
1835
  const payload = {
1311
1836
  requestId: options.requestId,
1312
1837
  messageId: options.messageId,
@@ -1321,11 +1846,11 @@ const lifecycleEvents = (options) => {
1321
1846
  };
1322
1847
  result.push({
1323
1848
  type: "ai.tool.result",
1324
- id: resultId,
1849
+ id: await resultId(),
1325
1850
  payload
1326
1851
  });
1327
1852
  } else if (chunk.type === "tool-output-denied") {
1328
- flush(chunk.toolCallId);
1853
+ await flush(chunk.toolCallId);
1329
1854
  const payload = {
1330
1855
  requestId: options.requestId,
1331
1856
  messageId: options.messageId,
@@ -1336,12 +1861,13 @@ const lifecycleEvents = (options) => {
1336
1861
  };
1337
1862
  result.push({
1338
1863
  type: "ai.tool.result",
1339
- id: resultId,
1864
+ id: await resultId(),
1340
1865
  payload
1341
1866
  });
1342
1867
  } else if (chunk.type === "tool-approval-request") {
1343
- flush(chunk.toolCallId);
1868
+ await flush(chunk.toolCallId);
1344
1869
  const payload = {
1870
+ requestId: options.requestId,
1345
1871
  messageId: options.messageId,
1346
1872
  generationId: options.generationId,
1347
1873
  approvalId: chunk.approvalId,
@@ -1351,13 +1877,14 @@ const lifecycleEvents = (options) => {
1351
1877
  };
1352
1878
  result.push({
1353
1879
  type: "ai.approval.requested",
1354
- id: `${options.generationId}:approval:${chunk.approvalId}`,
1880
+ id: await aiEventId("a2.ai.approval.requested", options.generationId, chunk.approvalId),
1355
1881
  payload
1356
1882
  });
1357
1883
  } else if (chunk.type === "tool-approval-response") result.push({
1358
1884
  type: "ai.approval.responded",
1359
- id: `${options.generationId}:approval:${chunk.approvalId}:automatic-response`,
1885
+ id: await aiEventId("a2.ai.approval.automatic-response", options.generationId, chunk.approvalId),
1360
1886
  payload: {
1887
+ requestId: options.requestId,
1361
1888
  messageId: options.messageId,
1362
1889
  generationId: options.generationId,
1363
1890
  approvalId: chunk.approvalId,
@@ -1371,24 +1898,6 @@ const lifecycleEvents = (options) => {
1371
1898
  pending
1372
1899
  };
1373
1900
  };
1374
- function withoutGenerationLifecycle(history, generationIds) {
1375
- if (generationIds.size === 0) return history;
1376
- const approvalKeys = /* @__PURE__ */ new Set();
1377
- const inputKeys = /* @__PURE__ */ new Set();
1378
- for (const event of history) {
1379
- const payload = event.payload;
1380
- if (typeof payload.generationId !== "string" || !generationIds.has(payload.generationId) || typeof payload.messageId !== "string") continue;
1381
- if (event.type === "ai.approval.requested" && typeof payload.approvalId === "string") approvalKeys.add(`${payload.messageId}\u001f${payload.approvalId}`);
1382
- if (event.type === "ai.input.requested" && typeof payload.inputId === "string") inputKeys.add(`${payload.messageId}\u001f${payload.inputId}`);
1383
- }
1384
- return history.filter((event) => {
1385
- const payload = event.payload;
1386
- if (typeof payload.generationId === "string" && generationIds.has(payload.generationId)) return false;
1387
- if (event.type === "ai.approval.responded" && typeof payload.messageId === "string" && typeof payload.approvalId === "string") return !approvalKeys.has(`${payload.messageId}\u001f${payload.approvalId}`);
1388
- if (event.type === "ai.input.responded" && typeof payload.messageId === "string" && typeof payload.inputId === "string") return !inputKeys.has(`${payload.messageId}\u001f${payload.inputId}`);
1389
- return true;
1390
- });
1391
- }
1392
1901
  const validateCompaction = (options) => {
1393
1902
  let compaction = options.compaction;
1394
1903
  if (compaction !== false && (typeof compaction !== "object" || compaction === null)) throw new TypeError("compaction must resolve to false or a policy object");
@@ -1428,10 +1937,18 @@ function createHandlers(options) {
1428
1937
  const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY;
1429
1938
  const control = createControlRuntime({ agent: options.agent });
1430
1939
  const coordinator = control.coordinator;
1940
+ const contextReducer = createContextReducer({ agent: options.agent });
1941
+ const readContext = async (session, through) => {
1942
+ const snapshot = await session.state(contextReducer, { through });
1943
+ return {
1944
+ state: contextReducer.view(snapshot.state),
1945
+ index: snapshot.index
1946
+ };
1947
+ };
1431
1948
  const promptCache = /* @__PURE__ */ new Map();
1432
- const resultEvent = (call, suffix, result) => ({
1949
+ const resultEvent = async (call, suffix, result) => ({
1433
1950
  type: "ai.tool.result",
1434
- id: `${call.generationId}:tool:${call.toolCallId}:${suffix}`,
1951
+ id: await aiEventId("a2.ai.tool.result", call.generationId, call.toolCallId, suffix),
1435
1952
  payload: {
1436
1953
  requestId: call.requestId,
1437
1954
  messageId: call.messageId,
@@ -1457,28 +1974,11 @@ function createHandlers(options) {
1457
1974
  const computation = (async () => {
1458
1975
  const frontier = owner.promptThroughIndex;
1459
1976
  if (frontier === void 0) throw new TypeError("AI work requires a prompt checkpoint");
1460
- const [snapshot, atPrompt, tail] = await Promise.all([
1461
- ctx.session.state(options.agent.reducer, { through: frontier }),
1462
- ctx.session.state(coordinator, { through: frontier }),
1463
- ctx.session.history({
1464
- gte: frontier + 1,
1465
- lte: ctx.event.index
1466
- })
1467
- ]);
1468
- const appended = tail.filter((event) => (event.type === "ai.generation.started" || event.type === "ai.compaction.requested" || event.type === "ai.compaction.completed") && event.payload.generationId === call.generationId);
1469
- const state = foldAIEvents({
1470
- agent: options.agent,
1471
- events: appended,
1472
- state: snapshot.state
1473
- });
1977
+ const snapshot = await readContext(ctx.session, ctx.event.index);
1978
+ if (snapshot.state.generation?.owner.generationId !== owner.generationId || snapshot.state.generation.owner.requestId !== owner.requestId || snapshot.state.activeProjection?.generationId !== owner.generationId) throw new TypeError("Tool prompt checkpoint does not own its generation");
1979
+ const state = generationBase(snapshot.state);
1474
1980
  return modelContext({
1475
- messages: await contextMessages({
1476
- agent: options.agent,
1477
- session: ctx.session,
1478
- snapshot,
1479
- coordinator: atPrompt.state,
1480
- appended
1481
- }),
1981
+ messages: state.messages.filter((message) => state.messageIndexes[message.id] <= frontier),
1482
1982
  state,
1483
1983
  tools
1484
1984
  });
@@ -1489,7 +1989,7 @@ function createHandlers(options) {
1489
1989
  });
1490
1990
  return computation;
1491
1991
  };
1492
- const toolExecutionFailure = (ctx, call, error) => {
1992
+ const toolExecutionFailure = async (ctx, call, error) => {
1493
1993
  const schedulerFailure = consumeSchedulerSendFailure(error);
1494
1994
  if (checkAbort(ctx.signal)) return;
1495
1995
  if (schedulerFailure === "retryable") throw error;
@@ -1534,7 +2034,7 @@ function createHandlers(options) {
1534
2034
  await control.append({
1535
2035
  ctx,
1536
2036
  name: `tool:${call.toolCallId}:preliminary:${sequence}`,
1537
- events: [resultEvent(call, `${ctx.event.id}:execution:${ctx.attempt}:${sequence}:preliminary`, {
2037
+ events: [await resultEvent(call, `${ctx.event.id}:execution:${ctx.attempt}:${sequence}:preliminary`, {
1538
2038
  output: result.value,
1539
2039
  preliminary: true
1540
2040
  })]
@@ -1565,7 +2065,7 @@ function createHandlers(options) {
1565
2065
  const state = (await ctx.session.state(control.reducer, { through: "latest" })).state;
1566
2066
  const request = ctx.event.payload;
1567
2067
  const call = state.coordinator.response?.calls.find((candidate) => candidate.work?.id === ctx.event.id);
1568
- if (state.active?.turnId !== request.turnId || state.active.suspended || state.active.version !== request.version || state.coordinator.response?.failure !== void 0 || !call || call.terminal || call.work?.settled) return control.settled({
2068
+ if (state.active?.turnId !== request.turnId || Object.values(state.pendingStops).includes(request.turnId) || state.active.suspended || state.active.version !== request.version || state.coordinator.response?.failure !== void 0 || !call || call.terminal || call.work?.settled) return control.settled({
1569
2069
  ctx,
1570
2070
  events: void 0
1571
2071
  });
@@ -1592,46 +2092,29 @@ function createHandlers(options) {
1592
2092
  if (checkAbort(ctx.signal)) return;
1593
2093
  const requestId = ctx.event.id;
1594
2094
  const request = ctx.event.payload;
1595
- const snapshot = await ctx.session.state(options.agent.reducer, { through: "latest" });
1596
- if (snapshot.state.active?.phase === "paused" || snapshot.state.active?.phase === "pausing") return;
1597
- const coordinatorState = (await ctx.session.state(coordinator, { through: snapshot.index })).state;
2095
+ const snapshot = await readContext(ctx.session, "latest");
2096
+ if (snapshot.state.closed || snapshot.state.authority?.requestId !== requestId || snapshot.state.authority?.phase === "paused" || snapshot.state.authority?.phase === "pausing") return;
2097
+ const controlState = (await ctx.session.state(control.reducer, { through: snapshot.index })).state;
2098
+ if (request.control !== void 0 && Object.values(controlState.pendingStops).includes(request.control.turnId)) return;
2099
+ const coordinatorState = controlState.coordinator;
1598
2100
  if (coordinatorState.closed || coordinatorState.response?.activeRequestId !== requestId) return;
1599
2101
  const response = coordinatorState.response;
1600
2102
  const current = response.generation?.requestId === requestId ? response.generation : void 0;
1601
2103
  if (current !== void 0 && (response.completion !== void 0 || response.failure !== void 0 && response.failure.superseded !== true)) return;
1602
2104
  if (current !== void 0 && current.attempt >= ctx.attempt) return;
1603
2105
  const attempt = ctx.attempt;
1604
- const generationId = `${requestId}:generation:${attempt}`;
1605
- const responseMessageId = request.responseMessageId ?? (request.reason === "message" ? `${request.messageId}:assistant` : request.messageId);
2106
+ const generationId = await aiEventId("a2.ai.generation", requestId, String(attempt));
2107
+ const responseMessageId = request.responseMessageId;
1606
2108
  const responseStepCount = response.stepCount;
1607
- if (request.reason === "tool" && (requestId !== `ai.generate:tools:${response.source?.generation.generationId}` || response.responseMessageId !== request.messageId || response.responseMessageId !== request.responseMessageId)) return;
2109
+ if (request.reason === "tool" && (request.sourceGenerationId !== response.source?.generation.generationId || response.responseMessageId !== request.messageId || response.responseMessageId !== request.responseMessageId)) return;
1608
2110
  const previous = current;
1609
- const replaced = previous === void 0 ? [] : [{
1610
- generation: previous,
1611
- frontier: response.promptThroughIndex
1612
- }];
1613
- if (request.reason === "retry" && response.source?.failed) replaced.push({
1614
- generation: response.source.generation,
1615
- frontier: response.source.promptThroughIndex
1616
- });
1617
- const replacedGenerationIds = new Set(replaced.map(({ generation: owner }) => owner.generationId));
1618
- let state = snapshot.state;
1619
- if (replaced.length > 0) {
1620
- const through = Math.min(...replaced.map(({ frontier }) => frontier));
1621
- const baseline = await ctx.session.state(options.agent.reducer, { through });
1622
- const tail = await ctx.session.history({
1623
- gte: through + 1,
1624
- lte: snapshot.index
1625
- });
1626
- state = foldAIEvents({
1627
- agent: options.agent,
1628
- events: withoutGenerationLifecycle(tail, replacedGenerationIds),
1629
- state: baseline.state
1630
- });
1631
- }
2111
+ const state = previous === void 0 ? snapshot.state : contextForAttempt(snapshot.state);
1632
2112
  const resolverContext = {
1633
2113
  event: ctx.event,
1634
- state,
2114
+ messages: contextMessages({
2115
+ state,
2116
+ coordinator: coordinatorState
2117
+ }),
1635
2118
  session: { state: (reducer, readOptions) => ctx.session.state(reducer, {
1636
2119
  ...readOptions,
1637
2120
  through: readOptions?.through ?? snapshot.index
@@ -1657,7 +2140,7 @@ function createHandlers(options) {
1657
2140
  if (source === void 0) return;
1658
2141
  return {
1659
2142
  type: "ai.generation.failed",
1660
- id: `${requestId}:step-limit`,
2143
+ id: await aiEventId("a2.ai.step-limit", requestId),
1661
2144
  payload: {
1662
2145
  requestId: source.requestId,
1663
2146
  messageId: source.messageId,
@@ -1684,7 +2167,7 @@ function createHandlers(options) {
1684
2167
  const startEvents = [];
1685
2168
  if (discoversMetadata && !Object.hasOwn(state.modelMetadata, catalogModelId)) startEvents.push({
1686
2169
  type: "ai.model.metadata.requested",
1687
- id: `ai.model.metadata:${encodeURIComponent(options.agent.contract.name)}:${encodeURIComponent(ctx.event.sessionId)}:${encodeURIComponent(catalogModelId)}`,
2170
+ id: await aiEventId("a2.ai.model.metadata", options.agent.contract.name, ctx.event.sessionId, catalogModelId),
1688
2171
  payload: { modelId: catalogModelId }
1689
2172
  });
1690
2173
  if (previous) {
@@ -1699,7 +2182,7 @@ function createHandlers(options) {
1699
2182
  };
1700
2183
  startEvents.push({
1701
2184
  type: "ai.generation.failed",
1702
- id: `${payload.generationId}:superseded`,
2185
+ id: await aiEventId("a2.ai.superseded", payload.generationId),
1703
2186
  payload: superseded
1704
2187
  });
1705
2188
  }
@@ -1716,15 +2199,9 @@ function createHandlers(options) {
1716
2199
  generationStarted = true;
1717
2200
  if (checkAbort(ctx.signal)) return;
1718
2201
  const promptCoordinatorState = coordinatorState;
1719
- const messages = await contextMessages({
1720
- agent: options.agent,
1721
- session: ctx.session,
1722
- snapshot: {
1723
- state,
1724
- index: snapshot.index
1725
- },
1726
- coordinator: promptCoordinatorState,
1727
- excluded: replacedGenerationIds
2202
+ const messages = contextMessages({
2203
+ state,
2204
+ coordinator: promptCoordinatorState
1728
2205
  });
1729
2206
  let generationMessages = messages;
1730
2207
  let modelMessages = await modelContext({
@@ -1739,7 +2216,6 @@ function createHandlers(options) {
1739
2216
  responseMessageId,
1740
2217
  messages,
1741
2218
  modelMessages,
1742
- state,
1743
2219
  session: resolverContext.session,
1744
2220
  signal: ctx.signal,
1745
2221
  model: resolvedModel,
@@ -1753,8 +2229,9 @@ function createHandlers(options) {
1753
2229
  const limits = metadata?.status === "resolved" && !usesFallbackModels ? metadata.limits : void 0;
1754
2230
  let inputTokenEstimate;
1755
2231
  let compacted = false;
1756
- const canCompact = response.source?.canCompact ?? true;
1757
- if (policy && canCompact) {
2232
+ let compactionEstimate;
2233
+ const hasPendingTools = messages.some((message) => message.parts.some((part) => isToolUIPart(part) && !(part.state === "output-error" || part.state === "output-denied" || part.state === "output-available" && part.preliminary !== true)));
2234
+ if (policy && !hasPendingTools) {
1758
2235
  const compactionContext = {
1759
2236
  ...resolverContext,
1760
2237
  messages,
@@ -1776,8 +2253,18 @@ function createHandlers(options) {
1776
2253
  const threshold = policy.thresholdTokens ?? (limits === void 0 ? void 0 : Math.floor(Math.min(limits.contextWindow * .75, limits.contextWindow - (generation.maxOutputTokens ?? 0))));
1777
2254
  if (limits !== void 0 && threshold !== void 0 && threshold <= 0) throw new Error(`output allowance exhausts the ${limits.contextWindow}-token context window`);
1778
2255
  shouldCompact = threshold !== void 0 && inputTokens >= threshold;
2256
+ if (threshold !== void 0 && shouldCompact) compactionEstimate = {
2257
+ inputTokenEstimate,
2258
+ inputTokens,
2259
+ thresholdTokens: threshold
2260
+ };
1779
2261
  } else shouldCompact = false;
1780
2262
  if (checkAbort(ctx.signal)) return;
2263
+ if (shouldCompact) {
2264
+ const latest = (await ctx.session.state(coordinator, { through: "latest" })).state;
2265
+ shouldCompact = Object.keys(latest.unsettledTools ?? {}).length === 0;
2266
+ }
2267
+ if (checkAbort(ctx.signal)) return;
1781
2268
  if (shouldCompact) {
1782
2269
  const throughMessageId = state.messages.findLast((message) => !promptCoordinatorState.queued.some((queued) => queued.messageId === message.id))?.id ?? request.messageId;
1783
2270
  const throughIndex = snapshot.index;
@@ -1786,11 +2273,12 @@ function createHandlers(options) {
1786
2273
  name: "compaction-requested",
1787
2274
  events: [{
1788
2275
  type: "ai.compaction.requested",
1789
- id: `${generationId}:compaction:requested`,
2276
+ id: await aiEventId("a2.ai.compaction.requested", generationId),
1790
2277
  payload: {
1791
2278
  generationId,
1792
2279
  throughMessageId,
1793
- throughIndex
2280
+ throughIndex,
2281
+ ...compactionEstimate
1794
2282
  }
1795
2283
  }]
1796
2284
  }));
@@ -1815,7 +2303,7 @@ function createHandlers(options) {
1815
2303
  name: "compaction-completed",
1816
2304
  events: [{
1817
2305
  type: "ai.compaction.completed",
1818
- id: `${generationId}:compaction:completed`,
2306
+ id: await aiEventId("a2.ai.compaction.completed", generationId),
1819
2307
  payload: completed
1820
2308
  }]
1821
2309
  }));
@@ -1825,16 +2313,13 @@ function createHandlers(options) {
1825
2313
  }
1826
2314
  }
1827
2315
  const currentState = foldAIEvents({
1828
- agent: options.agent,
2316
+ reducer: contextReducer[aiStateReducer],
1829
2317
  events: generationEvents,
1830
- state: snapshot.state
2318
+ state
1831
2319
  });
1832
- if (request.reason === "retry" || previous !== void 0) generationMessages = await contextMessages({
1833
- agent: options.agent,
1834
- session: ctx.session,
1835
- snapshot,
1836
- coordinator: promptCoordinatorState,
1837
- appended: generationEvents
2320
+ if (request.reason === "retry" || previous !== void 0) generationMessages = contextMessages({
2321
+ state: currentState,
2322
+ coordinator: promptCoordinatorState
1838
2323
  });
1839
2324
  if (policy && "shouldCompact" in policy || generationMessages !== messages || activeCompaction(currentState)?.summary !== activeCompaction(state)?.summary) modelMessages = await modelContext({
1840
2325
  messages: generationMessages,
@@ -1856,7 +2341,6 @@ function createHandlers(options) {
1856
2341
  responseMessageId,
1857
2342
  messages: generationMessages,
1858
2343
  modelMessages,
1859
- state: currentState,
1860
2344
  session: resolverContext.session,
1861
2345
  signal: ctx.signal,
1862
2346
  model: resolvedModel,
@@ -1889,7 +2373,7 @@ function createHandlers(options) {
1889
2373
  sequence,
1890
2374
  chunks: update.chunks
1891
2375
  };
1892
- const lifecycle = lifecycleEvents({
2376
+ const lifecycle = await lifecycleEvents({
1893
2377
  requestId,
1894
2378
  messageId: responseMessageId,
1895
2379
  generationId,
@@ -1906,7 +2390,7 @@ function createHandlers(options) {
1906
2390
  name: `generation-progress:${sequence}`,
1907
2391
  events: [{
1908
2392
  type: "ai.generation.progress",
1909
- id: `${generationId}:progress:${sequence}`,
2393
+ id: await aiEventId("a2.ai.progress", generationId, String(sequence)),
1910
2394
  payload: progress
1911
2395
  }, ...lifecycle.events]
1912
2396
  });
@@ -1924,19 +2408,19 @@ function createHandlers(options) {
1924
2408
  };
1925
2409
  const completionEvent = {
1926
2410
  type: "ai.generation.completed",
1927
- id: `${generationId}:completed`,
2411
+ id: await aiEventId("a2.ai.completed", generationId),
1928
2412
  payload: completed
1929
2413
  };
1930
2414
  const unresolved = options.generate ? pendingToolCalls.filter((pending) => pending.classification !== "automatic") : [];
1931
2415
  if (unresolved.length > 0) throw new Error(`generation ended with unresolved tool authorization for '${unresolved[0].call.toolName}'`);
1932
- const pendingEvents = pendingToolCalls.filter((pending) => pending.classification === "automatic" || options.generate === void 0 && pending.classification === "unknown").map((pending) => toolCalledEvent(pending.call));
2416
+ const pendingEvents = await Promise.all(pendingToolCalls.filter((pending) => pending.classification === "automatic" || options.generate === void 0 && pending.classification === "unknown").map((pending) => toolCalledEvent(pending.call)));
1933
2417
  if (finish.finishReason === "tool-calls") return [...pendingEvents, completionEvent];
1934
2418
  return [
1935
2419
  ...pendingEvents,
1936
2420
  completionEvent,
1937
2421
  {
1938
2422
  type: "ai.message.completed",
1939
- id: `${generationId}:message:completed`,
2423
+ id: await aiEventId("a2.ai.message.completed", generationId),
1940
2424
  payload: { messageId: responseMessageId }
1941
2425
  }
1942
2426
  ];
@@ -1957,7 +2441,7 @@ function createHandlers(options) {
1957
2441
  };
1958
2442
  return {
1959
2443
  type: "ai.generation.failed",
1960
- id: `${generationId}:failed`,
2444
+ id: await aiEventId("a2.ai.failed", generationId),
1961
2445
  payload: failed
1962
2446
  };
1963
2447
  }
@@ -1975,7 +2459,7 @@ function createHandlers(options) {
1975
2459
  ctx.signal.throwIfAborted();
1976
2460
  return {
1977
2461
  type: "ai.model.metadata.resolved",
1978
- id: `${ctx.event.id}:resolved`,
2462
+ id: await aiEventId("a2.ai.model.metadata.resolved", ctx.event.id),
1979
2463
  payload: {
1980
2464
  modelId: ctx.event.payload.modelId,
1981
2465
  limits
@@ -1983,7 +2467,13 @@ function createHandlers(options) {
1983
2467
  };
1984
2468
  } },
1985
2469
  "ai.control.requested": {
1986
- lane: "a2.ai.control",
2470
+ lane: ({ event }) => {
2471
+ const command = event.payload;
2472
+ if (event.id === "") throw new A2Error("INVALID_PAYLOAD", "AI command IDs must be nonempty");
2473
+ if (event.id?.startsWith("a2.ai:")) throw new A2Error("INVALID_PAYLOAD", `AI command IDs cannot use the reserved '${AI_EVENT_ID_PREFIX}' prefix`);
2474
+ if ((command.action === "send" || command.action === "steer") && event.id !== command.message.id) throw new A2Error("INVALID_PAYLOAD", "AI input event ID must equal message.id");
2475
+ return "a2.ai.control";
2476
+ },
1987
2477
  handler: control.handler
1988
2478
  },
1989
2479
  "ai.work.reported": {
@@ -2010,6 +2500,7 @@ function createHandlers(options) {
2010
2500
  "ai.generation.requested": {
2011
2501
  lane: "a2.ai.model",
2012
2502
  abortOn: {
2503
+ "ai.control.requested": (event, trigger) => event.payload.action === "stop" && event.payload.turnId === trigger.payload.control?.turnId,
2013
2504
  "ai.control.committed": (event, trigger) => {
2014
2505
  const active = event.payload.view;
2015
2506
  return active?.turnId !== trigger.payload.control?.turnId || active?.version !== trigger.payload.control?.version;
@@ -2036,6 +2527,7 @@ function createHandlers(options) {
2036
2527
  },
2037
2528
  "ai.tool.execution.requested": {
2038
2529
  abortOn: {
2530
+ "ai.control.requested": (event, trigger) => event.payload.action === "stop" && event.payload.turnId === trigger.payload.turnId,
2039
2531
  "ai.generation.failed": (event, trigger) => event.payload.generationId === trigger.payload.call.generationId,
2040
2532
  "ai.control.committed": (event, trigger) => {
2041
2533
  const active = event.payload.view;