experimental-a2 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +73 -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-CrLYwNEx.js +2431 -0
  9. package/dist/ai-CrLYwNEx.js.map +1 -0
  10. package/dist/{ai-Cai-lCbj.d.ts → ai-HJ9fHfYI.d.ts} +72 -13
  11. package/dist/ai-HJ9fHfYI.d.ts.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 +755 -290
  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 +134 -43
  59. package/docs/reference/01-api.mdx +104 -27
  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 +0 -2
  74. package/src/ai-context-schema.ts +296 -0
  75. package/src/ai-context.ts +680 -0
  76. package/src/ai-control-server.ts +160 -131
  77. package/src/ai-control-state.ts +6 -0
  78. package/src/ai-control.ts +6 -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 +348 -0
  83. package/src/ai-server.ts +232 -334
  84. package/src/ai-stored-state.ts +484 -0
  85. package/src/ai.ts +252 -418
  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 applyControlChanges, S as initialControlState, _ as progressBatches, a as events, b as AI_EVENT_ID_PREFIX, d as advanceProjection, f as projectedMessage, g as flattenProgressBatches, h as upsertResponse, l as aiStateReducer, m as upsertMessage, p as reduceToolActivity, u as storedAIReducer, v as interruptUIMessage, w as controlCommit, x as aiEventId, y as pauseUIMessage } from "./ai-CrLYwNEx.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,7 +239,7 @@ 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.v4",
230
243
  initialState: initialControlState()
231
244
  }).fold((state, event) => event.type === "ai.control.committed" ? applyControlChanges({
232
245
  state,
@@ -240,11 +253,11 @@ const createControlRuntime = (options) => {
240
253
  const state = structuredClone(snapshot.state);
241
254
  const facts = [];
242
255
  const jobs = [];
243
- const emit = ({ type, payload, id = `${ctx.event.id}:fact:${facts.length}` }) => {
256
+ const emit = async ({ type, payload, id }) => {
244
257
  const fact = {
245
258
  type,
246
259
  payload,
247
- id
260
+ id: id ?? await aiEventId("a2.ai.fact", ctx.event.id, String(facts.length))
248
261
  };
249
262
  facts.push(fact);
250
263
  state.coordinator = foldCoordinator(state.coordinator, {
@@ -253,11 +266,11 @@ const createControlRuntime = (options) => {
253
266
  });
254
267
  };
255
268
  let reason;
256
- const end = ({ lastSeenIndex, reason: interruptionReason }) => {
269
+ const end = async ({ lastSeenIndex, reason: interruptionReason }) => {
257
270
  const response = state.coordinator.response;
258
271
  if (response === void 0) return;
259
272
  const owner = response.generation === void 0 ? { requestId: response.activeRequestId } : { generationId: response.generation.generationId };
260
- emit({
273
+ await emit({
261
274
  type: "ai.message.interrupted",
262
275
  payload: {
263
276
  messageId: response.responseMessageId,
@@ -268,22 +281,13 @@ const createControlRuntime = (options) => {
268
281
  });
269
282
  state.active = null;
270
283
  };
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
284
  const insert = ({ message, generate = true, first = false, afterStepOf }) => {
275
285
  const candidate = {
276
286
  id: message.id,
277
- revision: 0,
278
287
  message,
279
288
  generate: generate && message.role === "user",
280
289
  ...afterStepOf === void 0 ? {} : { afterStepOf }
281
290
  };
282
- state.reservedIds = {
283
- ...state.reservedIds,
284
- [candidate.id]: true,
285
- ...candidate.generate ? { [`${candidate.id}:assistant`]: true } : {}
286
- };
287
291
  if (first) state.inbox.items.unshift(candidate);
288
292
  else state.inbox.items.push(candidate);
289
293
  };
@@ -340,42 +344,28 @@ const createControlRuntime = (options) => {
340
344
  if (state.coordinator.closed) reason = "closed";
341
345
  else switch (command.action) {
342
346
  case "send":
343
- if (inputTaken({
344
- message: command.message,
345
- generate: command.generate !== false
346
- })) reason = "duplicate-input";
347
- else insert({
347
+ insert({
348
348
  message: command.message,
349
349
  generate: command.generate !== false
350
350
  });
351
351
  break;
352
352
  case "edit": {
353
353
  const item = state.inbox.items.find((candidate) => candidate.id === command.message.id);
354
- if (item === void 0) reason = missingInputReason(command.message.id);
355
- else if (item.revision !== command.expectedRevision) reason = "revision-conflict";
356
- else if (item.message.role !== command.message.role) reason = "revision-conflict";
357
- else {
358
- item.message = command.message;
359
- item.revision += 1;
360
- }
354
+ if (item === void 0) reason = "not-pending";
355
+ else if (item.message.role !== command.message.role) reason = "role-mismatch";
356
+ else item.message = command.message;
361
357
  break;
362
358
  }
363
359
  case "remove": {
364
360
  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
- }
361
+ if (position < 0) reason = "not-pending";
362
+ else state.inbox.items.splice(position, 1);
373
363
  break;
374
364
  }
375
365
  case "move": {
376
366
  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";
367
+ if (index < 0) reason = "not-pending";
368
+ else if (command.beforeId !== null && !state.inbox.items.some((candidate) => candidate.id === command.beforeId)) reason = "not-pending";
379
369
  else {
380
370
  const destination = command.beforeId === null ? state.inbox.items.length : state.inbox.items.findIndex((item) => item.id === command.beforeId);
381
371
  moveInput({
@@ -388,18 +378,11 @@ const createControlRuntime = (options) => {
388
378
  case "send-now": {
389
379
  const position = state.inbox.items.findIndex((item) => item.id === command.inputId);
390
380
  const selected = state.inbox.items[position];
391
- if (!selected) reason = missingInputReason(command.inputId);
381
+ if (!selected) reason = "not-pending";
392
382
  else if ((state.active?.turnId ?? null) !== command.turnId) reason = "stale-turn";
393
383
  else if (selected.message.role !== "user") reason = "not-user-input";
394
- else if (!selected.generate && knownInput(`${selected.id}:assistant`)) reason = "duplicate-input";
395
384
  else {
396
- if (!selected.generate) {
397
- state.reservedIds = {
398
- ...state.reservedIds,
399
- [`${selected.id}:assistant`]: true
400
- };
401
- selected.generate = true;
402
- }
385
+ selected.generate = true;
403
386
  if (state.active) selected.afterStepOf = state.active.turnId;
404
387
  else delete selected.afterStepOf;
405
388
  moveInput({
@@ -411,14 +394,13 @@ const createControlRuntime = (options) => {
411
394
  }
412
395
  case "stop":
413
396
  if (state.active?.turnId !== command.turnId) reason = "stale-turn";
414
- else end({
397
+ else await end({
415
398
  lastSeenIndex: command.lastSeenIndex,
416
399
  reason: void 0
417
400
  });
418
401
  break;
419
402
  case "steer":
420
- if (inputTaken({ message: command.message })) reason = "duplicate-input";
421
- else insert({
403
+ insert({
422
404
  message: command.message,
423
405
  generate: true,
424
406
  first: true,
@@ -428,7 +410,7 @@ const createControlRuntime = (options) => {
428
410
  case "interrupt": {
429
411
  const response = state.coordinator.response;
430
412
  if (!response || response.responseMessageId !== command.request.messageId || command.request.generationId !== response.generation?.generationId && command.request.requestId !== response.activeRequestId) reason = "stale-turn";
431
- else end({
413
+ else await end({
432
414
  lastSeenIndex: command.request.lastSeenIndex,
433
415
  reason: command.request.reason
434
416
  });
@@ -441,7 +423,7 @@ const createControlRuntime = (options) => {
441
423
  active.suspended = true;
442
424
  active.version += 1;
443
425
  const generation = state.coordinator.response?.generation;
444
- if (generation) emit({
426
+ if (generation) await emit({
445
427
  type: "ai.generation.paused",
446
428
  payload: {
447
429
  generationId: generation.generationId,
@@ -455,18 +437,18 @@ const createControlRuntime = (options) => {
455
437
  state.inbox.paused = false;
456
438
  break;
457
439
  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({
440
+ 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";
441
+ else await emit({
460
442
  type: "ai.tool.result",
461
443
  payload: command.result
462
444
  });
463
445
  break;
464
446
  case "approval": {
465
447
  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);
448
+ 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
449
  if (response?.failure) reason = "stale-turn";
468
450
  else if (!call || call.response || call.terminal) reason = "not-found";
469
- else emit({
451
+ else await emit({
470
452
  type: "ai.approval.responded",
471
453
  payload: command.response
472
454
  });
@@ -476,8 +458,8 @@ const createControlRuntime = (options) => {
476
458
  case "input": {
477
459
  const payload = command.action === "input" ? command.response : command.request;
478
460
  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({
461
+ 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";
462
+ else await emit({
481
463
  type: command.action === "input" ? "ai.input.responded" : "ai.input.requested",
482
464
  payload
483
465
  });
@@ -487,7 +469,7 @@ const createControlRuntime = (options) => {
487
469
  const response = state.coordinator.response;
488
470
  if (!state.active || response?.status !== "failed" || response.rootMessageId !== command.request.messageId || response.responseMessageId !== command.request.responseMessageId) reason = "stale-turn";
489
471
  else if (!state.inbox.paused && !state.active.suspended) {
490
- emit({
472
+ await emit({
491
473
  type: "ai.retry.requested",
492
474
  payload: command.request
493
475
  });
@@ -497,7 +479,7 @@ const createControlRuntime = (options) => {
497
479
  responseMessageId: response.responseMessageId,
498
480
  reason: "retry"
499
481
  },
500
- id: `ai.generate:retry:${command.request.retryId}`
482
+ id: await aiEventId("a2.ai.request.retry", options.agent.contract.name, ctx.event.sessionId, command.request.retryId)
501
483
  });
502
484
  } else reason = "stale-turn";
503
485
  break;
@@ -505,6 +487,10 @@ const createControlRuntime = (options) => {
505
487
  }
506
488
  } else {
507
489
  const report = ctx.event.payload;
490
+ if (report.kind === "tool" && report.settled && state.coordinator.unsettledTools) {
491
+ delete state.coordinator.unsettledTools[report.workId];
492
+ if (Object.keys(state.coordinator.unsettledTools).length === 0) delete state.coordinator.unsettledTools;
493
+ }
508
494
  const active = state.active;
509
495
  const call = state.coordinator.response?.calls.find((candidate) => candidate.work?.id === report.workId);
510
496
  const work = report.kind === "model" ? active?.model : call?.work;
@@ -515,7 +501,7 @@ const createControlRuntime = (options) => {
515
501
  for (const event of report.events) {
516
502
  if (work.facts?.includes(event.id)) continue;
517
503
  if (authorized || report.kind === "tool" && event.type === "ai.tool.result") {
518
- emit({
504
+ await emit({
519
505
  type: event.type,
520
506
  payload: event.payload,
521
507
  id: event.id
@@ -523,7 +509,10 @@ const createControlRuntime = (options) => {
523
509
  work.facts = [...work.facts ?? [], event.id];
524
510
  }
525
511
  }
526
- if (report.settled) work.settled = true;
512
+ if (report.settled) {
513
+ work.settled = true;
514
+ delete work.facts;
515
+ }
527
516
  if (!authorized) reason = "stale-work";
528
517
  }
529
518
  }
@@ -544,7 +533,7 @@ const createControlRuntime = (options) => {
544
533
  if (!active.suspended && response.failure === void 0) for (const call of response.calls) {
545
534
  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
535
  if (call.response?.approved === false) {
547
- emit({
536
+ await emit({
548
537
  type: "ai.tool.result",
549
538
  payload: {
550
539
  ...call.call,
@@ -554,12 +543,16 @@ const createControlRuntime = (options) => {
554
543
  });
555
544
  continue;
556
545
  }
557
- const id = `${ctx.event.id}:tool:${call.call.toolCallId}`;
546
+ const id = await aiEventId("a2.ai.tool.execution", ctx.event.id, call.call.generationId, call.call.toolCallId);
558
547
  call.work = {
559
548
  id,
560
549
  version: active.version,
561
550
  settled: false
562
551
  };
552
+ state.coordinator.unsettledTools = {
553
+ ...state.coordinator.unsettledTools,
554
+ [id]: true
555
+ };
563
556
  jobs.push({
564
557
  type: "ai.tool.execution.requested",
565
558
  id,
@@ -579,7 +572,7 @@ const createControlRuntime = (options) => {
579
572
  const handoffReady = handoff && workSettled && !externalPending && (active.suspended || response.failure !== void 0 || response.calls.every((call) => callReadyForModel(call) || approvalBlocksExecution(call)));
580
573
  const readyForModel = !active.suspended && response.failure === void 0 && modelDone && callsDone && response.inputs.length === 0;
581
574
  const naturalCompletion = readyForModel && response.completion !== void 0 && response.completion.finishReason !== "tool-calls";
582
- if (handoffReady && !naturalCompletion) end({
575
+ if (handoffReady && !naturalCompletion) await end({
583
576
  lastSeenIndex: void 0,
584
577
  reason: "steered after step"
585
578
  });
@@ -588,17 +581,19 @@ const createControlRuntime = (options) => {
588
581
  const input = response.inputResponse;
589
582
  if (!handoff && (resume || continuationReady(state.coordinator) || response.completion && input)) {
590
583
  const nextReason = resume ? "resume" : input ? "input" : "tool";
591
- const id = nextReason === "tool" ? `ai.generate:tools:${response.generation.generationId}` : `${ctx.event.id}:generate:${nextReason}`;
584
+ const sourceGenerationId = (response.generation ?? response.source?.generation)?.generationId;
585
+ const id = nextReason === "tool" ? await aiEventId("a2.ai.request.tool", response.generation.generationId) : await aiEventId("a2.ai.request", ctx.event.id, nextReason);
592
586
  requestModel({
593
587
  request: {
594
588
  messageId: response.responseMessageId,
595
589
  responseMessageId: response.responseMessageId,
596
- reason: nextReason
590
+ reason: nextReason,
591
+ ...sourceGenerationId === void 0 ? {} : { sourceGenerationId }
597
592
  },
598
593
  id
599
594
  });
600
595
  } else if (naturalCompletion) {
601
- emit({
596
+ await emit({
602
597
  type: "ai.message.completed",
603
598
  payload: { messageId: response.responseMessageId }
604
599
  });
@@ -610,13 +605,13 @@ const createControlRuntime = (options) => {
610
605
  if (state.active === null && !state.inbox.paused && !state.coordinator.closed) while (state.inbox.items.length > 0) {
611
606
  const item = state.inbox.items.shift();
612
607
  delete item.afterStepOf;
613
- emit({
608
+ await emit({
614
609
  type: "ai.message.created",
615
610
  payload: {
616
611
  message: item.message,
617
612
  generate: item.generate
618
613
  },
619
- id: `ai.admitted:${item.id}`
614
+ id: await aiEventId("a2.ai.admitted", options.agent.contract.name, ctx.event.sessionId, item.id)
620
615
  });
621
616
  if (!item.generate) continue;
622
617
  state.active = {
@@ -627,12 +622,14 @@ const createControlRuntime = (options) => {
627
622
  suspended: false,
628
623
  model: null
629
624
  };
625
+ const requestId = await aiEventId("a2.ai.request.message", options.agent.contract.name, ctx.event.sessionId, item.id);
630
626
  requestModel({
631
627
  request: {
632
628
  messageId: item.id,
629
+ responseMessageId: requestId,
633
630
  reason: "message"
634
631
  },
635
- id: `ai.generate:message:${item.id}`
632
+ id: requestId
636
633
  });
637
634
  break;
638
635
  }
@@ -646,6 +643,7 @@ const createControlRuntime = (options) => {
646
643
  ...facts,
647
644
  {
648
645
  type: "ai.control.committed",
646
+ id: await aiEventId("a2.ai.control.committed", ctx.event.id),
649
647
  payload: controlCommit({
650
648
  before,
651
649
  after: state,
@@ -655,11 +653,12 @@ const createControlRuntime = (options) => {
655
653
  ...jobs,
656
654
  {
657
655
  type: "ai.control.decided",
656
+ id: await aiEventId("a2.ai.control.decided", ctx.event.id),
658
657
  payload: receipt
659
658
  }
660
659
  ];
661
660
  };
662
- const report = ({ ctx, events, settled, name }) => {
661
+ const report = async ({ ctx, events, settled, name }) => {
663
662
  const payload = ctx.event.payload;
664
663
  const { turnId, version } = "turnId" in payload ? payload : payload.control;
665
664
  for (const event of events) {
@@ -669,7 +668,7 @@ const createControlRuntime = (options) => {
669
668
  }
670
669
  return {
671
670
  type: "ai.work.reported",
672
- id: `${ctx.event.id}:report:${ctx.attempt}:${name}`,
671
+ id: await aiEventId("a2.ai.report", ctx.event.id, String(ctx.attempt), name),
673
672
  payload: {
674
673
  workId: ctx.event.id,
675
674
  attempt: ctx.attempt,
@@ -677,11 +676,11 @@ const createControlRuntime = (options) => {
677
676
  version,
678
677
  kind: ctx.event.type === "ai.generation.requested" ? "model" : "tool",
679
678
  settled,
680
- events: events.filter((event) => event.type !== "ai.message.completed" && event.type !== "ai.message.interrupted").map((event, index) => ({
679
+ events: await Promise.all(events.filter((event) => event.type !== "ai.message.completed" && event.type !== "ai.message.interrupted").map(async (event, index) => ({
681
680
  type: event.type,
682
681
  payload: event.payload,
683
- id: event.id ?? `${ctx.event.id}:fact:${ctx.attempt}:${name}:${index}`
684
- }))
682
+ id: event.id ?? await aiEventId("a2.ai.work.fact", ctx.event.id, String(ctx.attempt), name, String(index))
683
+ })))
685
684
  }
686
685
  };
687
686
  };
@@ -689,7 +688,7 @@ const createControlRuntime = (options) => {
689
688
  const progress = events.filter((event) => event.type === "ai.generation.progress");
690
689
  const lifecycle = events.filter((event) => event.type !== "ai.generation.progress");
691
690
  if (lifecycle.length === 0) return ctx.session.append(name, ...progress);
692
- const input = report({
691
+ const input = await report({
693
692
  ctx,
694
693
  events: lifecycle,
695
694
  settled: false,
@@ -744,6 +743,532 @@ const createControlRuntime = (options) => {
744
743
  };
745
744
  };
746
745
  //#endregion
746
+ //#region src/ai-context-schema.ts
747
+ const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
748
+ const index = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
749
+ const id = (value) => typeof value === "string" && value.length > 0;
750
+ const nullableId = (value) => value === null || id(value);
751
+ const matches = (schema, value) => validateSync(schema, value, "AI context checkpoint").issues === void 0;
752
+ const isMessage = (value) => matches(events["ai.message.created"], { message: value });
753
+ const indexes = (value, messages) => record(value) && Object.values(value).every(index) && messages.every((message) => Object.hasOwn(value, message.id));
754
+ const compaction = (value) => value === null || record(value) && Object.keys(value).every((key) => [
755
+ "status",
756
+ "generationId",
757
+ "throughMessageId",
758
+ "throughIndex",
759
+ "summary"
760
+ ].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");
761
+ const progress = (value) => {
762
+ if (!record(value) || !index(value["length"]) || !Array.isArray(value["tail"]) || !Array.isArray(value["blocks"])) return false;
763
+ const pending = [...value["blocks"].toReversed().filter((block) => block !== null), value["tail"]].toReversed();
764
+ const seen = /* @__PURE__ */ new Set();
765
+ let count = 0;
766
+ let last = -1;
767
+ while (pending.length > 0) {
768
+ const block = pending.pop();
769
+ if (typeof block !== "object" || block === null || seen.has(block)) return false;
770
+ seen.add(block);
771
+ if (Array.isArray(block)) for (const batch of block) {
772
+ 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;
773
+ count += 1;
774
+ last = batch["index"];
775
+ }
776
+ else if (record(block) && Object.keys(block).length === 2 && Object.hasOwn(block, "left") && Object.hasOwn(block, "right")) pending.push(block["right"], block["left"]);
777
+ else return false;
778
+ }
779
+ return count === value["length"];
780
+ };
781
+ 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"]));
782
+ const projection = (value, messages) => {
783
+ if (!record(value) || Object.keys(value).some((key) => ![
784
+ "generationId",
785
+ "responseMessageId",
786
+ "promptThroughIndex",
787
+ "completionIndex",
788
+ "baseMessage",
789
+ "baseCompaction",
790
+ "batches",
791
+ "toolEvents",
792
+ "approvalEvents",
793
+ "cursor"
794
+ ].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;
795
+ const cursor = value["cursor"];
796
+ if (cursor === void 0 || cursor === null) return true;
797
+ if (!record(cursor) || !index(cursor["throughIndex"]) || !record(cursor["stream"])) return false;
798
+ const stream = cursor["stream"];
799
+ const current = messages.find((message) => message.id === value["responseMessageId"]);
800
+ 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;
801
+ return Object.values(stream["tools"]).every((tool) => record(tool) && typeof tool["text"] === "string" && typeof tool["toolName"] === "string" && typeof tool["dynamic"] === "boolean");
802
+ };
803
+ const authority = (value) => value === null || record(value) && Object.keys(value).every((key) => [
804
+ "requestId",
805
+ "responseMessageId",
806
+ "sourceGenerationId",
807
+ "requestReason",
808
+ "phase"
809
+ ].includes(key)) && [
810
+ "requestId",
811
+ "responseMessageId",
812
+ "sourceGenerationId"
813
+ ].every((key) => nullableId(value[key])) && (value["requestReason"] === null || [
814
+ "message",
815
+ "tool",
816
+ "input",
817
+ "resume",
818
+ "retry"
819
+ ].includes(String(value["requestReason"]))) && (value["phase"] === null || [
820
+ "starting",
821
+ "generating",
822
+ "tools",
823
+ "waiting",
824
+ "failed",
825
+ "pausing",
826
+ "paused"
827
+ ].includes(String(value["phase"])));
828
+ const contextSchema = () => ({ "~standard": {
829
+ version: 1,
830
+ vendor: "a2-context",
831
+ validate(value) {
832
+ const invalid = { issues: [{ message: "invalid AI model context" }] };
833
+ const keys = [
834
+ "messages",
835
+ "compaction",
836
+ "modelMetadata",
837
+ "closed",
838
+ "authority",
839
+ "generation",
840
+ "activeProjection",
841
+ "messageIndexes"
842
+ ];
843
+ try {
844
+ if (!record(value) || keys.some((key) => !Object.hasOwn(value, key)) || Object.keys(value).some((key) => !keys.includes(key))) return invalid;
845
+ const messages = value["messages"];
846
+ const metadata = value["modelMetadata"];
847
+ const generation = value["generation"];
848
+ const current = value["activeProjection"];
849
+ 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"], {
850
+ modelId,
851
+ limits: entry["limits"]
852
+ }))) || !indexes(value["messageIndexes"], messages)) return invalid;
853
+ if (generation !== null && (!record(generation) || !matches(events["ai.generation.started"], generation["owner"]) || ![
854
+ "streaming",
855
+ "completed",
856
+ "paused",
857
+ "failed",
858
+ "superseded"
859
+ ].includes(String(generation["phase"])) || generation["responseIndex"] !== void 0 && !index(generation["responseIndex"]) || generation["phase"] === "failed" && typeof generation["error"] !== "string")) return invalid;
860
+ const owner = record(generation) && record(generation["owner"]) ? generation["owner"] : void 0;
861
+ if (current !== null && (!projection(current, messages) || !record(current) || current["generationId"] !== owner?.["generationId"] || current["responseMessageId"] !== owner?.["responseMessageId"])) return invalid;
862
+ if (generation !== null && generation["phase"] !== "superseded" && current === null) return invalid;
863
+ if (value["closed"] && (generation !== null || current !== null)) return invalid;
864
+ return { value };
865
+ } catch {
866
+ return invalid;
867
+ }
868
+ }
869
+ } });
870
+ //#endregion
871
+ //#region src/ai-context.ts
872
+ const replaceResponse = ({ state, message, index }) => {
873
+ let messages = upsertResponse({
874
+ messages: state.messages,
875
+ message,
876
+ inputId: state.generation.owner.messageId
877
+ });
878
+ if (state.generation?.responseIndex !== void 0 && !state.messages.some((item) => item.id === message.id)) {
879
+ messages = messages.filter((item) => item.id !== message.id);
880
+ messages.splice(state.generation.responseIndex, 0, message);
881
+ }
882
+ return {
883
+ ...state,
884
+ messages,
885
+ messageIndexes: Object.hasOwn(state.messageIndexes, message.id) ? state.messageIndexes : {
886
+ ...state.messageIndexes,
887
+ [message.id]: index
888
+ }
889
+ };
890
+ };
891
+ 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)]);
892
+ const renderProjection = ({ state, index }) => {
893
+ const projection = state.activeProjection;
894
+ let message = projectedMessage(projection);
895
+ if (projection.baseMessage === void 0 && projection.batches.length === 0 && message.parts.length === 0) return state;
896
+ if (state.generation?.phase === "paused") message = pauseUIMessage({
897
+ message,
898
+ toolCallIds: calledTools(projection)
899
+ });
900
+ if (state.generation?.phase === "failed") message = interruptUIMessage(message, `Generation failed before tool completion: ${state.generation.error}`);
901
+ return replaceResponse({
902
+ state: {
903
+ ...state,
904
+ activeProjection: {
905
+ ...projection,
906
+ cursor: null
907
+ }
908
+ },
909
+ message,
910
+ index
911
+ });
912
+ };
913
+ const generationBase = (state) => {
914
+ const projection = state.activeProjection;
915
+ if (!projection) return state;
916
+ return {
917
+ ...state,
918
+ messages: projection.baseMessage === void 0 ? state.messages.filter((message) => message.id !== projection.responseMessageId) : upsertMessage(state.messages, projection.baseMessage),
919
+ compaction: projection.baseCompaction ?? null
920
+ };
921
+ };
922
+ const contextForAttempt = (state) => ({
923
+ ...state.activeProjection?.completionIndex === void 0 ? generationBase(state) : state,
924
+ generation: state.generation && {
925
+ ...state.generation,
926
+ phase: "superseded"
927
+ },
928
+ activeProjection: null
929
+ });
930
+ const endGeneration = (state) => ({
931
+ ...state,
932
+ authority: null,
933
+ generation: null,
934
+ activeProjection: null
935
+ });
936
+ const ownsGeneration = (state, owner) => state.generation?.owner.generationId === owner.generationId && state.generation.owner.requestId === owner.requestId && state.generation.phase !== "superseded";
937
+ const createContextReducer = ({ agent }) => {
938
+ const reducer = agent.contract.reducer({
939
+ name: "a2.ai.context.v3",
940
+ initialState: {
941
+ messages: [],
942
+ compaction: null,
943
+ modelMetadata: {},
944
+ closed: false,
945
+ authority: null,
946
+ generation: null,
947
+ activeProjection: null,
948
+ messageIndexes: {}
949
+ },
950
+ stateSchema: contextSchema()
951
+ }).fold((state, event) => reduceContext(state, event));
952
+ return storedAIReducer(reducer);
953
+ };
954
+ const reduceContext = (state, event) => {
955
+ if (state.closed) return state;
956
+ switch (event.type) {
957
+ case "ai.control.committed": {
958
+ const { view } = event.payload;
959
+ const { requestId, requestReason, sourceGenerationId, responseMessageId, phase } = view;
960
+ const next = {
961
+ ...state,
962
+ authority: {
963
+ requestId,
964
+ requestReason,
965
+ sourceGenerationId,
966
+ responseMessageId,
967
+ phase
968
+ },
969
+ closed: view.closed
970
+ };
971
+ return view.closed ? endGeneration(next) : next;
972
+ }
973
+ case "ai.session.closed": return endGeneration({
974
+ ...state,
975
+ closed: true,
976
+ compaction: state.compaction?.status === "running" ? state.activeProjection?.baseCompaction ?? null : state.compaction
977
+ });
978
+ case "ai.message.created": {
979
+ const { message } = event.payload;
980
+ return {
981
+ ...state,
982
+ messages: upsertMessage(state.messages, message),
983
+ activeProjection: state.activeProjection?.responseMessageId === message.id ? {
984
+ ...state.activeProjection,
985
+ cursor: null
986
+ } : state.activeProjection,
987
+ messageIndexes: {
988
+ ...state.messageIndexes,
989
+ [message.id]: event.index
990
+ }
991
+ };
992
+ }
993
+ case "ai.generation.requested": {
994
+ const request = event.payload;
995
+ 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;
996
+ return contextForAttempt(state);
997
+ }
998
+ case "ai.generation.started": {
999
+ const owner = event.payload;
1000
+ if (state.authority?.requestId !== owner.requestId || state.authority.responseMessageId !== owner.responseMessageId || state.authority.phase === "paused" || state.authority.phase === "pausing") return state;
1001
+ const previous = state.generation;
1002
+ if (previous?.owner.requestId === owner.requestId && (previous.owner.attempt >= owner.attempt || previous.phase === "failed" || previous.phase === "completed" || state.activeProjection?.completionIndex !== void 0)) return state;
1003
+ const base = previous?.owner.requestId === owner.requestId ? generationBase(state) : state;
1004
+ const baseMessage = base.messages.find((message) => message.id === owner.responseMessageId);
1005
+ return {
1006
+ ...base,
1007
+ generation: {
1008
+ owner,
1009
+ phase: "streaming",
1010
+ ...previous?.owner.responseMessageId === owner.responseMessageId && previous.responseIndex !== void 0 ? { responseIndex: previous.responseIndex } : {}
1011
+ },
1012
+ activeProjection: {
1013
+ generationId: owner.generationId,
1014
+ responseMessageId: owner.responseMessageId,
1015
+ promptThroughIndex: owner.promptThroughIndex ?? event.index - 1,
1016
+ ...baseMessage === void 0 ? {} : { baseMessage },
1017
+ baseCompaction: base.compaction,
1018
+ batches: progressBatches(),
1019
+ toolEvents: [],
1020
+ approvalEvents: []
1021
+ }
1022
+ };
1023
+ }
1024
+ case "ai.generation.progress": {
1025
+ const payload = event.payload;
1026
+ if (!ownsGeneration(state, payload) || state.generation.owner.responseMessageId !== payload.responseMessageId || state.generation.phase !== "streaming" || !state.activeProjection) return state;
1027
+ const projection = state.activeProjection;
1028
+ const progress = advanceProjection({
1029
+ projection,
1030
+ base: state.messages.find((message) => message.id === projection.responseMessageId),
1031
+ chunks: payload.chunks,
1032
+ index: event.index
1033
+ });
1034
+ return replaceResponse({
1035
+ state: {
1036
+ ...state,
1037
+ activeProjection: {
1038
+ ...projection,
1039
+ batches: progress.batches,
1040
+ cursor: progress.cursor
1041
+ }
1042
+ },
1043
+ message: progress.message,
1044
+ index: event.index
1045
+ });
1046
+ }
1047
+ case "ai.generation.completed": {
1048
+ const payload = event.payload;
1049
+ if (!ownsGeneration(state, payload) || state.generation.phase !== "streaming") return state;
1050
+ return {
1051
+ ...state,
1052
+ generation: {
1053
+ ...state.generation,
1054
+ phase: "completed"
1055
+ },
1056
+ activeProjection: {
1057
+ ...state.activeProjection,
1058
+ completionIndex: event.index,
1059
+ cursor: null
1060
+ }
1061
+ };
1062
+ }
1063
+ case "ai.generation.failed": {
1064
+ const payload = event.payload;
1065
+ if (!ownsGeneration(state, payload)) return state;
1066
+ if (state.generation.phase !== "streaming" && !(payload.stepLimit && state.authority?.requestReason === "tool" && state.authority.sourceGenerationId === payload.generationId)) return state;
1067
+ if (payload.superseded) return {
1068
+ ...generationBase(state),
1069
+ generation: {
1070
+ ...state.generation,
1071
+ phase: "superseded"
1072
+ },
1073
+ activeProjection: null
1074
+ };
1075
+ return renderProjection({
1076
+ state: {
1077
+ ...state,
1078
+ compaction: state.activeProjection?.baseCompaction ?? null,
1079
+ generation: {
1080
+ ...state.generation,
1081
+ phase: "failed",
1082
+ error: payload.error
1083
+ }
1084
+ },
1085
+ index: event.index
1086
+ });
1087
+ }
1088
+ case "ai.message.interrupted": {
1089
+ const payload = event.payload;
1090
+ const owner = state.generation?.owner;
1091
+ const ownsRequest = payload.requestId !== void 0 && payload.requestId === state.authority?.requestId && payload.messageId === state.authority.responseMessageId;
1092
+ const ownsOwner = owner?.responseMessageId === payload.messageId && (payload.generationId === owner.generationId || payload.requestId === owner.requestId);
1093
+ if (!ownsRequest && !ownsOwner) return state;
1094
+ if (ownsOwner && state.authority?.requestId !== owner.requestId && !ownsRequest) return state;
1095
+ const projection = state.activeProjection;
1096
+ if (!projection) return endGeneration(state);
1097
+ const cutoff = payload.lastSeenIndex ?? Number.POSITIVE_INFINITY;
1098
+ const retained = {
1099
+ ...projection,
1100
+ batches: progressBatches(flattenProgressBatches(projection.batches).filter((batch) => batch.index <= cutoff)),
1101
+ toolEvents: projection.toolEvents.filter((item) => item.index <= cutoff),
1102
+ approvalEvents: projection.approvalEvents.filter((item) => item.index <= cutoff)
1103
+ };
1104
+ let next = generationBase(state);
1105
+ if (retained.baseMessage !== void 0 || retained.batches.length > 0) next = replaceResponse({
1106
+ state: next,
1107
+ message: interruptUIMessage(projectedMessage(retained)),
1108
+ index: event.index
1109
+ });
1110
+ return endGeneration(next);
1111
+ }
1112
+ case "ai.message.completed": {
1113
+ const { messageId } = event.payload;
1114
+ return state.generation?.owner.responseMessageId === messageId && state.generation.phase !== "failed" && state.generation.phase !== "superseded" ? endGeneration(state) : state;
1115
+ }
1116
+ case "ai.generation.paused": {
1117
+ const { generationId } = event.payload;
1118
+ if (state.generation?.owner.generationId !== generationId || state.generation.phase === "failed" || state.generation.phase === "superseded" || !state.activeProjection) return state;
1119
+ const toolCallIds = calledTools(state.activeProjection);
1120
+ return renderProjection({
1121
+ state: {
1122
+ ...state,
1123
+ generation: {
1124
+ ...state.generation,
1125
+ phase: "paused"
1126
+ },
1127
+ compaction: state.compaction?.status === "running" ? state.activeProjection.baseCompaction ?? null : state.compaction,
1128
+ activeProjection: {
1129
+ ...state.activeProjection,
1130
+ cursor: null,
1131
+ batches: progressBatches(flattenProgressBatches(state.activeProjection.batches).map((batch) => ({
1132
+ index: batch.index,
1133
+ chunks: batch.chunks.filter((chunk) => !("toolCallId" in chunk) || toolCallIds.has(chunk.toolCallId))
1134
+ })))
1135
+ }
1136
+ },
1137
+ index: event.index
1138
+ });
1139
+ }
1140
+ case "ai.tool.called":
1141
+ case "ai.tool.result": {
1142
+ const payload = event.payload;
1143
+ if (!ownsGeneration(state, payload) || !state.activeProjection || state.generation.owner.responseMessageId !== payload.messageId) return state;
1144
+ if (state.generation.phase === "failed") {
1145
+ const result = payload;
1146
+ const running = state.activeProjection.toolEvents.reduce((tools, item) => reduceToolActivity(tools, item), []).some((tool) => tool.toolCallId === payload.toolCallId && tool.status === "running");
1147
+ if (event.type !== "ai.tool.result" || result.preliminary || !running) return state;
1148
+ }
1149
+ const toolEvent = event.type === "ai.tool.called" ? {
1150
+ index: event.index,
1151
+ type: "ai.tool.called",
1152
+ payload
1153
+ } : {
1154
+ index: event.index,
1155
+ type: "ai.tool.result",
1156
+ payload
1157
+ };
1158
+ const next = {
1159
+ ...state,
1160
+ activeProjection: {
1161
+ ...state.activeProjection,
1162
+ toolEvents: [...state.activeProjection.toolEvents, toolEvent]
1163
+ }
1164
+ };
1165
+ if (event.type === "ai.tool.called") return next;
1166
+ next.activeProjection.cursor = null;
1167
+ return renderProjection({
1168
+ state: next,
1169
+ index: event.index
1170
+ });
1171
+ }
1172
+ case "ai.approval.requested":
1173
+ case "ai.approval.responded": {
1174
+ const payload = event.payload;
1175
+ if (!ownsGeneration(state, payload) || !state.activeProjection || state.generation.phase === "failed" || state.generation.owner.responseMessageId !== payload.messageId) return state;
1176
+ const approvalEvent = event.type === "ai.approval.requested" ? {
1177
+ index: event.index,
1178
+ type: "ai.approval.requested",
1179
+ payload
1180
+ } : {
1181
+ index: event.index,
1182
+ type: "ai.approval.responded",
1183
+ payload
1184
+ };
1185
+ return renderProjection({
1186
+ state: {
1187
+ ...state,
1188
+ activeProjection: {
1189
+ ...state.activeProjection,
1190
+ cursor: null,
1191
+ approvalEvents: [...state.activeProjection.approvalEvents, approvalEvent]
1192
+ }
1193
+ },
1194
+ index: event.index
1195
+ });
1196
+ }
1197
+ case "ai.compaction.requested": {
1198
+ const payload = event.payload;
1199
+ if (state.generation?.owner.generationId !== payload.generationId || state.generation.phase !== "streaming") return state;
1200
+ return {
1201
+ ...state,
1202
+ compaction: {
1203
+ status: "running",
1204
+ generationId: payload.generationId,
1205
+ throughMessageId: payload.throughMessageId,
1206
+ throughIndex: payload.throughIndex ?? state.activeProjection.promptThroughIndex
1207
+ }
1208
+ };
1209
+ }
1210
+ case "ai.compaction.completed": {
1211
+ const payload = event.payload;
1212
+ const projection = state.activeProjection;
1213
+ if (state.generation?.owner.generationId !== payload.generationId || state.generation.phase !== "streaming" || state.compaction?.status !== "running" || state.compaction.generationId !== payload.generationId || !projection) return state;
1214
+ const throughIndex = payload.throughIndex ?? projection.promptThroughIndex;
1215
+ const replacements = new Set(payload.messages.map((message) => message.id));
1216
+ const retained = new Set(payload.retainedMessageIds ?? []);
1217
+ const messages = [...payload.messages, ...state.messages.filter((message) => !replacements.has(message.id) && (retained.has(message.id) || (state.messageIndexes[message.id] ?? 0) > throughIndex))];
1218
+ const activeProjection = {
1219
+ ...projection,
1220
+ cursor: null
1221
+ };
1222
+ delete activeProjection.baseMessage;
1223
+ const baseMessage = messages.find((message) => message.id === projection.responseMessageId);
1224
+ if (baseMessage !== void 0) activeProjection.baseMessage = baseMessage;
1225
+ const compaction = {
1226
+ status: "completed",
1227
+ generationId: payload.generationId,
1228
+ throughMessageId: payload.throughMessageId,
1229
+ throughIndex,
1230
+ ...payload.summary === void 0 ? {} : { summary: payload.summary }
1231
+ };
1232
+ activeProjection.baseCompaction = compaction;
1233
+ return {
1234
+ ...state,
1235
+ messages,
1236
+ compaction,
1237
+ generation: {
1238
+ ...state.generation,
1239
+ responseIndex: payload.messages.length
1240
+ },
1241
+ activeProjection,
1242
+ messageIndexes: Object.fromEntries(messages.map((message) => [message.id, replacements.has(message.id) ? throughIndex : state.messageIndexes[message.id]]))
1243
+ };
1244
+ }
1245
+ case "ai.model.metadata.requested": {
1246
+ const { modelId } = event.payload;
1247
+ return Object.hasOwn(state.modelMetadata, modelId) ? state : {
1248
+ ...state,
1249
+ modelMetadata: {
1250
+ ...state.modelMetadata,
1251
+ [modelId]: { status: "pending" }
1252
+ }
1253
+ };
1254
+ }
1255
+ case "ai.model.metadata.resolved": {
1256
+ const { modelId, limits } = event.payload;
1257
+ return {
1258
+ ...state,
1259
+ modelMetadata: {
1260
+ ...state.modelMetadata,
1261
+ [modelId]: limits === null ? { status: "unavailable" } : {
1262
+ status: "resolved",
1263
+ limits
1264
+ }
1265
+ }
1266
+ };
1267
+ }
1268
+ default: return state;
1269
+ }
1270
+ };
1271
+ //#endregion
747
1272
  //#region src/ai-sdk-step.ts
748
1273
  const CONTROLLED_SETTINGS = [
749
1274
  "_internal",
@@ -885,6 +1410,40 @@ const readModelLimits = async (options) => {
885
1410
  }
886
1411
  };
887
1412
  //#endregion
1413
+ //#region src/ai-input-tokens.ts
1414
+ const withoutProviderOptions = (value) => {
1415
+ const result = { ...value };
1416
+ Reflect.deleteProperty(result, "providerOptions");
1417
+ return result;
1418
+ };
1419
+ const estimatedMessage = (message) => ({
1420
+ ...withoutProviderOptions(message),
1421
+ content: typeof message.content === "string" ? message.content : message.content.map((part) => part.type === "tool-result" ? {
1422
+ ...withoutProviderOptions(part),
1423
+ output: {
1424
+ ...withoutProviderOptions(part.output),
1425
+ ...part.output.type === "content" ? { value: part.output.value.map(withoutProviderOptions) } : {}
1426
+ }
1427
+ } : withoutProviderOptions(part))
1428
+ });
1429
+ const estimateInputTokens = async (options) => {
1430
+ const tools = await Promise.all(Object.entries(options.tools).map(async ([name, tool]) => ({
1431
+ name,
1432
+ description: tool.description,
1433
+ inputSchema: await asSchema(tool.inputSchema).jsonSchema
1434
+ })));
1435
+ const instructions = options.instructions;
1436
+ return Math.ceil(Buffer.byteLength(JSON.stringify({
1437
+ messages: options.messages.map(estimatedMessage),
1438
+ instructions: instructions === void 0 || typeof instructions === "string" ? instructions : Array.isArray(instructions) ? instructions.map(estimatedMessage) : estimatedMessage(instructions),
1439
+ tools
1440
+ }), "utf8") / 4);
1441
+ };
1442
+ const measuredInputTokens = (options) => {
1443
+ const previous = options.calibration;
1444
+ return previous !== void 0 && previous.model === options.model && options.estimate >= previous.estimate ? Math.ceil(previous.inputTokens + options.estimate - previous.estimate) : options.estimate;
1445
+ };
1446
+ //#endregion
888
1447
  //#region src/ai-server.ts
889
1448
  /**
890
1449
  * experimental-a2/ai/server — the server-only implementation of an experimental-a2/ai definition.
@@ -1066,7 +1625,7 @@ async function* consumeGeneration(options) {
1066
1625
  }
1067
1626
  const foldAIEvents = (options) => {
1068
1627
  let state = options.state;
1069
- for (const event of options.events) state = options.agent.reducer.fold(state, event);
1628
+ for (const event of options.events) state = options.reducer.fold(state, event);
1070
1629
  return state;
1071
1630
  };
1072
1631
  const summarize = async (options) => {
@@ -1107,7 +1666,7 @@ const summarize = async (options) => {
1107
1666
  role: "user",
1108
1667
  content: instruction
1109
1668
  }],
1110
- responseMessageId: `${options.context.generationId}:summary`
1669
+ responseMessageId: await aiEventId("a2.ai.summary", options.context.generationId)
1111
1670
  },
1112
1671
  messageMetadata: void 0
1113
1672
  });
@@ -1133,52 +1692,11 @@ const summarize = async (options) => {
1133
1692
  };
1134
1693
  const activeCompaction = (state) => {
1135
1694
  const compaction = state.compaction;
1136
- return compaction?.status === "completed" && compaction.messages !== void 0 && state.messages.some((message) => message.id === compaction.throughMessageId) ? compaction : null;
1695
+ return compaction?.status === "completed" ? compaction : null;
1137
1696
  };
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
- ]);
1697
+ const contextMessages = ({ state, coordinator }) => {
1698
+ const queued = new Set(coordinator.queued.map((item) => item.messageId));
1699
+ return state.messages.filter((message) => !queued.has(message.id));
1182
1700
  };
1183
1701
  const modelContext = async (options) => {
1184
1702
  const messages = await convertToModelMessages(options.messages, { tools: options.tools });
@@ -1188,25 +1706,9 @@ const modelContext = async (options) => {
1188
1706
  content: summary
1189
1707
  }, ...messages];
1190
1708
  };
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) => ({
1709
+ const toolCalledEvent = async (payload) => ({
1208
1710
  type: "ai.tool.called",
1209
- id: `${payload.generationId}:tool:${payload.toolCallId}:called`,
1711
+ id: await aiEventId("a2.ai.tool.called", payload.generationId, payload.toolCallId),
1210
1712
  payload
1211
1713
  });
1212
1714
  const approvalClassification = (tools, generation, toolName) => {
@@ -1220,7 +1722,7 @@ const approvalClassification = (tools, generation, toolName) => {
1220
1722
  if (typeof tool?.needsApproval === "function") return "unknown";
1221
1723
  return tool?.needsApproval === true ? "approval" : "automatic";
1222
1724
  };
1223
- const lifecycleEvents = (options) => {
1725
+ const lifecycleEvents = async (options) => {
1224
1726
  const result = [];
1225
1727
  const pending = [...options.pending];
1226
1728
  const take = (toolCallId) => {
@@ -1228,12 +1730,12 @@ const lifecycleEvents = (options) => {
1228
1730
  if (index === -1) return void 0;
1229
1731
  return pending.splice(index, 1)[0];
1230
1732
  };
1231
- const flush = (toolCallId) => {
1733
+ const flush = async (toolCallId) => {
1232
1734
  const pendingCall = take(toolCallId);
1233
- if (pendingCall) result.push(toolCalledEvent(pendingCall.call));
1735
+ if (pendingCall) result.push(await toolCalledEvent(pendingCall.call));
1234
1736
  };
1235
1737
  for (const [chunkIndex, chunk] of options.chunks.entries()) {
1236
- const resultId = `${options.generationId}:tool-result:${options.sequence}:${chunkIndex}`;
1738
+ const resultId = () => aiEventId("a2.ai.tool.stream-result", options.generationId, String(options.sequence), String(chunkIndex));
1237
1739
  if (chunk.type === "tool-input-available") {
1238
1740
  const payload = {
1239
1741
  requestId: options.requestId,
@@ -1251,7 +1753,7 @@ const lifecycleEvents = (options) => {
1251
1753
  };
1252
1754
  take(payload.toolCallId);
1253
1755
  const classification = approvalClassification(options.tools, options.generation, chunk.toolName);
1254
- if (classification === "automatic") result.push(toolCalledEvent(payload));
1756
+ if (classification === "automatic") result.push(await toolCalledEvent(payload));
1255
1757
  else pending.push({
1256
1758
  call: payload,
1257
1759
  classification
@@ -1280,13 +1782,13 @@ const lifecycleEvents = (options) => {
1280
1782
  input: chunk.input
1281
1783
  };
1282
1784
  take(chunk.toolCallId);
1283
- result.push(toolCalledEvent(called), {
1785
+ result.push(await toolCalledEvent(called), {
1284
1786
  type: "ai.tool.result",
1285
- id: resultId,
1787
+ id: await resultId(),
1286
1788
  payload
1287
1789
  });
1288
1790
  } else if (chunk.type === "tool-output-available") {
1289
- flush(chunk.toolCallId);
1791
+ await flush(chunk.toolCallId);
1290
1792
  const payload = {
1291
1793
  requestId: options.requestId,
1292
1794
  messageId: options.messageId,
@@ -1302,11 +1804,11 @@ const lifecycleEvents = (options) => {
1302
1804
  };
1303
1805
  result.push({
1304
1806
  type: "ai.tool.result",
1305
- id: resultId,
1807
+ id: await resultId(),
1306
1808
  payload
1307
1809
  });
1308
1810
  } else if (chunk.type === "tool-output-error") {
1309
- flush(chunk.toolCallId);
1811
+ await flush(chunk.toolCallId);
1310
1812
  const payload = {
1311
1813
  requestId: options.requestId,
1312
1814
  messageId: options.messageId,
@@ -1321,11 +1823,11 @@ const lifecycleEvents = (options) => {
1321
1823
  };
1322
1824
  result.push({
1323
1825
  type: "ai.tool.result",
1324
- id: resultId,
1826
+ id: await resultId(),
1325
1827
  payload
1326
1828
  });
1327
1829
  } else if (chunk.type === "tool-output-denied") {
1328
- flush(chunk.toolCallId);
1830
+ await flush(chunk.toolCallId);
1329
1831
  const payload = {
1330
1832
  requestId: options.requestId,
1331
1833
  messageId: options.messageId,
@@ -1336,12 +1838,13 @@ const lifecycleEvents = (options) => {
1336
1838
  };
1337
1839
  result.push({
1338
1840
  type: "ai.tool.result",
1339
- id: resultId,
1841
+ id: await resultId(),
1340
1842
  payload
1341
1843
  });
1342
1844
  } else if (chunk.type === "tool-approval-request") {
1343
- flush(chunk.toolCallId);
1845
+ await flush(chunk.toolCallId);
1344
1846
  const payload = {
1847
+ requestId: options.requestId,
1345
1848
  messageId: options.messageId,
1346
1849
  generationId: options.generationId,
1347
1850
  approvalId: chunk.approvalId,
@@ -1351,13 +1854,14 @@ const lifecycleEvents = (options) => {
1351
1854
  };
1352
1855
  result.push({
1353
1856
  type: "ai.approval.requested",
1354
- id: `${options.generationId}:approval:${chunk.approvalId}`,
1857
+ id: await aiEventId("a2.ai.approval.requested", options.generationId, chunk.approvalId),
1355
1858
  payload
1356
1859
  });
1357
1860
  } else if (chunk.type === "tool-approval-response") result.push({
1358
1861
  type: "ai.approval.responded",
1359
- id: `${options.generationId}:approval:${chunk.approvalId}:automatic-response`,
1862
+ id: await aiEventId("a2.ai.approval.automatic-response", options.generationId, chunk.approvalId),
1360
1863
  payload: {
1864
+ requestId: options.requestId,
1361
1865
  messageId: options.messageId,
1362
1866
  generationId: options.generationId,
1363
1867
  approvalId: chunk.approvalId,
@@ -1371,24 +1875,6 @@ const lifecycleEvents = (options) => {
1371
1875
  pending
1372
1876
  };
1373
1877
  };
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
1878
  const validateCompaction = (options) => {
1393
1879
  let compaction = options.compaction;
1394
1880
  if (compaction !== false && (typeof compaction !== "object" || compaction === null)) throw new TypeError("compaction must resolve to false or a policy object");
@@ -1428,10 +1914,18 @@ function createHandlers(options) {
1428
1914
  const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY;
1429
1915
  const control = createControlRuntime({ agent: options.agent });
1430
1916
  const coordinator = control.coordinator;
1917
+ const contextReducer = createContextReducer({ agent: options.agent });
1918
+ const readContext = async (session, through) => {
1919
+ const snapshot = await session.state(contextReducer, { through });
1920
+ return {
1921
+ state: contextReducer.view(snapshot.state),
1922
+ index: snapshot.index
1923
+ };
1924
+ };
1431
1925
  const promptCache = /* @__PURE__ */ new Map();
1432
- const resultEvent = (call, suffix, result) => ({
1926
+ const resultEvent = async (call, suffix, result) => ({
1433
1927
  type: "ai.tool.result",
1434
- id: `${call.generationId}:tool:${call.toolCallId}:${suffix}`,
1928
+ id: await aiEventId("a2.ai.tool.result", call.generationId, call.toolCallId, suffix),
1435
1929
  payload: {
1436
1930
  requestId: call.requestId,
1437
1931
  messageId: call.messageId,
@@ -1457,28 +1951,11 @@ function createHandlers(options) {
1457
1951
  const computation = (async () => {
1458
1952
  const frontier = owner.promptThroughIndex;
1459
1953
  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
- });
1954
+ const snapshot = await readContext(ctx.session, ctx.event.index);
1955
+ 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");
1956
+ const state = generationBase(snapshot.state);
1474
1957
  return modelContext({
1475
- messages: await contextMessages({
1476
- agent: options.agent,
1477
- session: ctx.session,
1478
- snapshot,
1479
- coordinator: atPrompt.state,
1480
- appended
1481
- }),
1958
+ messages: state.messages.filter((message) => state.messageIndexes[message.id] <= frontier),
1482
1959
  state,
1483
1960
  tools
1484
1961
  });
@@ -1489,7 +1966,7 @@ function createHandlers(options) {
1489
1966
  });
1490
1967
  return computation;
1491
1968
  };
1492
- const toolExecutionFailure = (ctx, call, error) => {
1969
+ const toolExecutionFailure = async (ctx, call, error) => {
1493
1970
  const schedulerFailure = consumeSchedulerSendFailure(error);
1494
1971
  if (checkAbort(ctx.signal)) return;
1495
1972
  if (schedulerFailure === "retryable") throw error;
@@ -1534,7 +2011,7 @@ function createHandlers(options) {
1534
2011
  await control.append({
1535
2012
  ctx,
1536
2013
  name: `tool:${call.toolCallId}:preliminary:${sequence}`,
1537
- events: [resultEvent(call, `${ctx.event.id}:execution:${ctx.attempt}:${sequence}:preliminary`, {
2014
+ events: [await resultEvent(call, `${ctx.event.id}:execution:${ctx.attempt}:${sequence}:preliminary`, {
1538
2015
  output: result.value,
1539
2016
  preliminary: true
1540
2017
  })]
@@ -1592,8 +2069,8 @@ function createHandlers(options) {
1592
2069
  if (checkAbort(ctx.signal)) return;
1593
2070
  const requestId = ctx.event.id;
1594
2071
  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;
2072
+ const snapshot = await readContext(ctx.session, "latest");
2073
+ if (snapshot.state.closed || snapshot.state.authority?.requestId !== requestId || snapshot.state.authority?.phase === "paused" || snapshot.state.authority?.phase === "pausing") return;
1597
2074
  const coordinatorState = (await ctx.session.state(coordinator, { through: snapshot.index })).state;
1598
2075
  if (coordinatorState.closed || coordinatorState.response?.activeRequestId !== requestId) return;
1599
2076
  const response = coordinatorState.response;
@@ -1601,37 +2078,18 @@ function createHandlers(options) {
1601
2078
  if (current !== void 0 && (response.completion !== void 0 || response.failure !== void 0 && response.failure.superseded !== true)) return;
1602
2079
  if (current !== void 0 && current.attempt >= ctx.attempt) return;
1603
2080
  const attempt = ctx.attempt;
1604
- const generationId = `${requestId}:generation:${attempt}`;
1605
- const responseMessageId = request.responseMessageId ?? (request.reason === "message" ? `${request.messageId}:assistant` : request.messageId);
2081
+ const generationId = await aiEventId("a2.ai.generation", requestId, String(attempt));
2082
+ const responseMessageId = request.responseMessageId;
1606
2083
  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;
2084
+ if (request.reason === "tool" && (request.sourceGenerationId !== response.source?.generation.generationId || response.responseMessageId !== request.messageId || response.responseMessageId !== request.responseMessageId)) return;
1608
2085
  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
- }
2086
+ const state = previous === void 0 ? snapshot.state : contextForAttempt(snapshot.state);
1632
2087
  const resolverContext = {
1633
2088
  event: ctx.event,
1634
- state,
2089
+ messages: contextMessages({
2090
+ state,
2091
+ coordinator: coordinatorState
2092
+ }),
1635
2093
  session: { state: (reducer, readOptions) => ctx.session.state(reducer, {
1636
2094
  ...readOptions,
1637
2095
  through: readOptions?.through ?? snapshot.index
@@ -1657,7 +2115,7 @@ function createHandlers(options) {
1657
2115
  if (source === void 0) return;
1658
2116
  return {
1659
2117
  type: "ai.generation.failed",
1660
- id: `${requestId}:step-limit`,
2118
+ id: await aiEventId("a2.ai.step-limit", requestId),
1661
2119
  payload: {
1662
2120
  requestId: source.requestId,
1663
2121
  messageId: source.messageId,
@@ -1684,7 +2142,7 @@ function createHandlers(options) {
1684
2142
  const startEvents = [];
1685
2143
  if (discoversMetadata && !Object.hasOwn(state.modelMetadata, catalogModelId)) startEvents.push({
1686
2144
  type: "ai.model.metadata.requested",
1687
- id: `ai.model.metadata:${encodeURIComponent(options.agent.contract.name)}:${encodeURIComponent(ctx.event.sessionId)}:${encodeURIComponent(catalogModelId)}`,
2145
+ id: await aiEventId("a2.ai.model.metadata", options.agent.contract.name, ctx.event.sessionId, catalogModelId),
1688
2146
  payload: { modelId: catalogModelId }
1689
2147
  });
1690
2148
  if (previous) {
@@ -1699,7 +2157,7 @@ function createHandlers(options) {
1699
2157
  };
1700
2158
  startEvents.push({
1701
2159
  type: "ai.generation.failed",
1702
- id: `${payload.generationId}:superseded`,
2160
+ id: await aiEventId("a2.ai.superseded", payload.generationId),
1703
2161
  payload: superseded
1704
2162
  });
1705
2163
  }
@@ -1716,15 +2174,9 @@ function createHandlers(options) {
1716
2174
  generationStarted = true;
1717
2175
  if (checkAbort(ctx.signal)) return;
1718
2176
  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
2177
+ const messages = contextMessages({
2178
+ state,
2179
+ coordinator: promptCoordinatorState
1728
2180
  });
1729
2181
  let generationMessages = messages;
1730
2182
  let modelMessages = await modelContext({
@@ -1739,7 +2191,6 @@ function createHandlers(options) {
1739
2191
  responseMessageId,
1740
2192
  messages,
1741
2193
  modelMessages,
1742
- state,
1743
2194
  session: resolverContext.session,
1744
2195
  signal: ctx.signal,
1745
2196
  model: resolvedModel,
@@ -1753,8 +2204,9 @@ function createHandlers(options) {
1753
2204
  const limits = metadata?.status === "resolved" && !usesFallbackModels ? metadata.limits : void 0;
1754
2205
  let inputTokenEstimate;
1755
2206
  let compacted = false;
1756
- const canCompact = response.source?.canCompact ?? true;
1757
- if (policy && canCompact) {
2207
+ let compactionEstimate;
2208
+ 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)));
2209
+ if (policy && !hasPendingTools) {
1758
2210
  const compactionContext = {
1759
2211
  ...resolverContext,
1760
2212
  messages,
@@ -1776,8 +2228,18 @@ function createHandlers(options) {
1776
2228
  const threshold = policy.thresholdTokens ?? (limits === void 0 ? void 0 : Math.floor(Math.min(limits.contextWindow * .75, limits.contextWindow - (generation.maxOutputTokens ?? 0))));
1777
2229
  if (limits !== void 0 && threshold !== void 0 && threshold <= 0) throw new Error(`output allowance exhausts the ${limits.contextWindow}-token context window`);
1778
2230
  shouldCompact = threshold !== void 0 && inputTokens >= threshold;
2231
+ if (threshold !== void 0 && shouldCompact) compactionEstimate = {
2232
+ inputTokenEstimate,
2233
+ inputTokens,
2234
+ thresholdTokens: threshold
2235
+ };
1779
2236
  } else shouldCompact = false;
1780
2237
  if (checkAbort(ctx.signal)) return;
2238
+ if (shouldCompact) {
2239
+ const latest = (await ctx.session.state(coordinator, { through: "latest" })).state;
2240
+ shouldCompact = Object.keys(latest.unsettledTools ?? {}).length === 0;
2241
+ }
2242
+ if (checkAbort(ctx.signal)) return;
1781
2243
  if (shouldCompact) {
1782
2244
  const throughMessageId = state.messages.findLast((message) => !promptCoordinatorState.queued.some((queued) => queued.messageId === message.id))?.id ?? request.messageId;
1783
2245
  const throughIndex = snapshot.index;
@@ -1786,11 +2248,12 @@ function createHandlers(options) {
1786
2248
  name: "compaction-requested",
1787
2249
  events: [{
1788
2250
  type: "ai.compaction.requested",
1789
- id: `${generationId}:compaction:requested`,
2251
+ id: await aiEventId("a2.ai.compaction.requested", generationId),
1790
2252
  payload: {
1791
2253
  generationId,
1792
2254
  throughMessageId,
1793
- throughIndex
2255
+ throughIndex,
2256
+ ...compactionEstimate
1794
2257
  }
1795
2258
  }]
1796
2259
  }));
@@ -1815,7 +2278,7 @@ function createHandlers(options) {
1815
2278
  name: "compaction-completed",
1816
2279
  events: [{
1817
2280
  type: "ai.compaction.completed",
1818
- id: `${generationId}:compaction:completed`,
2281
+ id: await aiEventId("a2.ai.compaction.completed", generationId),
1819
2282
  payload: completed
1820
2283
  }]
1821
2284
  }));
@@ -1825,16 +2288,13 @@ function createHandlers(options) {
1825
2288
  }
1826
2289
  }
1827
2290
  const currentState = foldAIEvents({
1828
- agent: options.agent,
2291
+ reducer: contextReducer[aiStateReducer],
1829
2292
  events: generationEvents,
1830
- state: snapshot.state
2293
+ state
1831
2294
  });
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
2295
+ if (request.reason === "retry" || previous !== void 0) generationMessages = contextMessages({
2296
+ state: currentState,
2297
+ coordinator: promptCoordinatorState
1838
2298
  });
1839
2299
  if (policy && "shouldCompact" in policy || generationMessages !== messages || activeCompaction(currentState)?.summary !== activeCompaction(state)?.summary) modelMessages = await modelContext({
1840
2300
  messages: generationMessages,
@@ -1856,7 +2316,6 @@ function createHandlers(options) {
1856
2316
  responseMessageId,
1857
2317
  messages: generationMessages,
1858
2318
  modelMessages,
1859
- state: currentState,
1860
2319
  session: resolverContext.session,
1861
2320
  signal: ctx.signal,
1862
2321
  model: resolvedModel,
@@ -1889,7 +2348,7 @@ function createHandlers(options) {
1889
2348
  sequence,
1890
2349
  chunks: update.chunks
1891
2350
  };
1892
- const lifecycle = lifecycleEvents({
2351
+ const lifecycle = await lifecycleEvents({
1893
2352
  requestId,
1894
2353
  messageId: responseMessageId,
1895
2354
  generationId,
@@ -1906,7 +2365,7 @@ function createHandlers(options) {
1906
2365
  name: `generation-progress:${sequence}`,
1907
2366
  events: [{
1908
2367
  type: "ai.generation.progress",
1909
- id: `${generationId}:progress:${sequence}`,
2368
+ id: await aiEventId("a2.ai.progress", generationId, String(sequence)),
1910
2369
  payload: progress
1911
2370
  }, ...lifecycle.events]
1912
2371
  });
@@ -1924,19 +2383,19 @@ function createHandlers(options) {
1924
2383
  };
1925
2384
  const completionEvent = {
1926
2385
  type: "ai.generation.completed",
1927
- id: `${generationId}:completed`,
2386
+ id: await aiEventId("a2.ai.completed", generationId),
1928
2387
  payload: completed
1929
2388
  };
1930
2389
  const unresolved = options.generate ? pendingToolCalls.filter((pending) => pending.classification !== "automatic") : [];
1931
2390
  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));
2391
+ const pendingEvents = await Promise.all(pendingToolCalls.filter((pending) => pending.classification === "automatic" || options.generate === void 0 && pending.classification === "unknown").map((pending) => toolCalledEvent(pending.call)));
1933
2392
  if (finish.finishReason === "tool-calls") return [...pendingEvents, completionEvent];
1934
2393
  return [
1935
2394
  ...pendingEvents,
1936
2395
  completionEvent,
1937
2396
  {
1938
2397
  type: "ai.message.completed",
1939
- id: `${generationId}:message:completed`,
2398
+ id: await aiEventId("a2.ai.message.completed", generationId),
1940
2399
  payload: { messageId: responseMessageId }
1941
2400
  }
1942
2401
  ];
@@ -1957,7 +2416,7 @@ function createHandlers(options) {
1957
2416
  };
1958
2417
  return {
1959
2418
  type: "ai.generation.failed",
1960
- id: `${generationId}:failed`,
2419
+ id: await aiEventId("a2.ai.failed", generationId),
1961
2420
  payload: failed
1962
2421
  };
1963
2422
  }
@@ -1975,7 +2434,7 @@ function createHandlers(options) {
1975
2434
  ctx.signal.throwIfAborted();
1976
2435
  return {
1977
2436
  type: "ai.model.metadata.resolved",
1978
- id: `${ctx.event.id}:resolved`,
2437
+ id: await aiEventId("a2.ai.model.metadata.resolved", ctx.event.id),
1979
2438
  payload: {
1980
2439
  modelId: ctx.event.payload.modelId,
1981
2440
  limits
@@ -1983,7 +2442,13 @@ function createHandlers(options) {
1983
2442
  };
1984
2443
  } },
1985
2444
  "ai.control.requested": {
1986
- lane: "a2.ai.control",
2445
+ lane: ({ event }) => {
2446
+ const command = event.payload;
2447
+ if (event.id === "") throw new A2Error("INVALID_PAYLOAD", "AI command IDs must be nonempty");
2448
+ if (event.id?.startsWith("a2.ai:")) throw new A2Error("INVALID_PAYLOAD", `AI command IDs cannot use the reserved '${AI_EVENT_ID_PREFIX}' prefix`);
2449
+ 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");
2450
+ return "a2.ai.control";
2451
+ },
1987
2452
  handler: control.handler
1988
2453
  },
1989
2454
  "ai.work.reported": {