experimental-a2 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/{actor-DJi3RsNu.d.ts → actor-BfQSE0KC.d.ts} +4 -4
  3. package/dist/{actor-DJi3RsNu.d.ts.map → actor-BfQSE0KC.d.ts.map} +1 -1
  4. package/dist/actor-client.d.ts +1 -1
  5. package/dist/actor-client.js +1 -1
  6. package/dist/actor-react.d.ts +3 -3
  7. package/dist/actor-react.js +2 -2
  8. package/dist/{actor-shared-DI7J5upy.js → actor-shared-B5tJfzt-.js} +2 -2
  9. package/dist/{actor-shared-DI7J5upy.js.map → actor-shared-B5tJfzt-.js.map} +1 -1
  10. package/dist/actor.d.ts +1 -1
  11. package/dist/actor.js +3 -3
  12. package/dist/ai-Cai-lCbj.d.ts +580 -0
  13. package/dist/ai-Cai-lCbj.d.ts.map +1 -0
  14. package/dist/ai-control-CcD4hh3y.js +119 -0
  15. package/dist/ai-control-CcD4hh3y.js.map +1 -0
  16. package/dist/ai-server.d.ts +6 -6
  17. package/dist/ai-server.d.ts.map +1 -1
  18. package/dist/ai-server.js +1014 -501
  19. package/dist/ai-server.js.map +1 -1
  20. package/dist/ai.d.ts +2 -365
  21. package/dist/ai.js +801 -80
  22. package/dist/ai.js.map +1 -1
  23. package/dist/{client-P_NNNRM-.d.ts → client-BAEABRZB.d.ts} +2 -2
  24. package/dist/{client-P_NNNRM-.d.ts.map → client-BAEABRZB.d.ts.map} +1 -1
  25. package/dist/{client-Bf6uSEAk.js → client-BYzHjkwU.js} +21 -6
  26. package/dist/client-BYzHjkwU.js.map +1 -0
  27. package/dist/client.d.ts +1 -1
  28. package/dist/client.js +1 -1
  29. package/dist/{contract-48bUMgcL.js → contract-CKRg_E4q.js} +3 -26
  30. package/dist/contract-CKRg_E4q.js.map +1 -0
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +1 -1
  33. package/dist/react.d.ts +2 -2
  34. package/dist/react.js +1 -1
  35. package/dist/{reducer-DJKWm3cp.d.ts → reducer-BcS9VDKC.d.ts} +4 -1
  36. package/dist/{reducer-DJKWm3cp.d.ts.map → reducer-BcS9VDKC.d.ts.map} +1 -1
  37. package/dist/reducer-DEMjEY_O.js +29 -0
  38. package/dist/reducer-DEMjEY_O.js.map +1 -0
  39. package/dist/scheduler-qstash.d.ts +2 -2
  40. package/dist/scheduler-qstash.js +1 -1
  41. package/dist/scheduler-vercel.d.ts +2 -2
  42. package/dist/scheduler-vercel.js +1 -1
  43. package/dist/{server-DjZZa1wr.d.ts → server-Bp5Nd1pF.d.ts} +3 -3
  44. package/dist/{server-DjZZa1wr.d.ts.map → server-Bp5Nd1pF.d.ts.map} +1 -1
  45. package/dist/{server-BeNADlCI.js → server-CjJSGcF7.js} +4 -3
  46. package/dist/server-CjJSGcF7.js.map +1 -0
  47. package/dist/server.d.ts +3 -3
  48. package/dist/server.js +1 -1
  49. package/dist/{store-DtDOWLSn.d.ts → store-D_yhNdPz.d.ts} +7 -2
  50. package/dist/{store-DtDOWLSn.d.ts.map → store-D_yhNdPz.d.ts.map} +1 -1
  51. package/dist/store-N8PXxDAS.js.map +1 -1
  52. package/dist/store-memory.d.ts +1 -1
  53. package/dist/store-postgres.d.ts +1 -1
  54. package/dist/store-postgres.js +19 -0
  55. package/dist/store-postgres.js.map +1 -1
  56. package/dist/store-redis-http.d.ts +1 -1
  57. package/dist/store-redis-http.js +1 -1
  58. package/dist/{store-redis-notify-BUCyXOn0.js → store-redis-notify-D2EI6gwX.js} +27 -2
  59. package/dist/store-redis-notify-D2EI6gwX.js.map +1 -0
  60. package/dist/store-redis.d.ts +1 -1
  61. package/dist/store-redis.js +1 -1
  62. package/dist/store-sqlite.d.ts +1 -1
  63. package/docs/guides/06-ai-agents.mdx +265 -62
  64. package/docs/reference/01-api.mdx +122 -27
  65. package/examples/playground/app/agent/[agentId]/agent-client.tsx +118 -21
  66. package/examples/playground/app/agent/[agentId]/agent-queue.tsx +289 -0
  67. package/examples/playground/app/agent/compaction-settings.test.ts +22 -6
  68. package/examples/playground/app/agent/model.ts +11 -2
  69. package/examples/playground/app/agent/server.ts +8 -2
  70. package/examples/playground/app/chat/[chatId]/chat-client.tsx +3 -13
  71. package/examples/playground/app/chat/model.ts +2 -2
  72. package/examples/playground/app/chat/server.ts +24 -17
  73. package/examples/playground/app/globals.css +179 -0
  74. package/examples/playground/package.json +1 -1
  75. package/package.json +1 -1
  76. package/src/ai-client-state.ts +185 -0
  77. package/src/ai-control-server.ts +829 -0
  78. package/src/ai-control-state.ts +152 -0
  79. package/src/ai-control.ts +139 -0
  80. package/src/ai-coordinator.ts +99 -32
  81. package/src/ai-progress-batches.ts +68 -0
  82. package/src/ai-projector.ts +76 -15
  83. package/src/ai-sdk-step.ts +0 -1
  84. package/src/ai-server.ts +381 -608
  85. package/src/ai.ts +553 -108
  86. package/src/client.ts +31 -9
  87. package/src/licenses/Apache-2.0.txt +55 -0
  88. package/src/parse-partial-json.ts +441 -0
  89. package/src/reducer.ts +6 -0
  90. package/src/server.ts +8 -4
  91. package/src/store-postgres.ts +27 -0
  92. package/src/store-redis-core.ts +53 -1
  93. package/src/store-redis-notify.ts +1 -0
  94. package/src/store.ts +6 -0
  95. package/dist/ai.d.ts.map +0 -1
  96. package/dist/client-Bf6uSEAk.js.map +0 -1
  97. package/dist/contract-48bUMgcL.js.map +0 -1
  98. package/dist/server-BeNADlCI.js.map +0 -1
  99. package/dist/store-redis-notify-BUCyXOn0.js.map +0 -1
package/dist/ai.js CHANGED
@@ -1,13 +1,407 @@
1
1
  import { n as validateSync, t as assertSyncSchema } from "./validate-XKT4FSNn.js";
2
- import { t as contract } from "./contract-48bUMgcL.js";
2
+ import { t as clientStateProjection } from "./reducer-DEMjEY_O.js";
3
+ import { t as contract } from "./contract-CKRg_E4q.js";
3
4
  import { l as ambientToolScopeStorage } from "./internal-Dq2qYxou.js";
5
+ import { n as applyControlChanges, t as initialControlState } from "./ai-control-CcD4hh3y.js";
6
+ //#region src/ai-client-state.ts
7
+ const isQueueCommand = (command) => command.action === "send" || command.action === "edit" || command.action === "move" || command.action === "remove" || command.action === "send-now" || command.action === "steer" || command.action === "pause" || command.action === "resume";
8
+ const moveItem = ({ items, inputId, beforeId }) => {
9
+ const from = items.findIndex((item) => item.id === inputId);
10
+ if (from < 0 || inputId === beforeId || beforeId !== null && !items.some((item) => item.id === beforeId)) return items;
11
+ const next = [...items];
12
+ const [item] = next.splice(from, 1);
13
+ const to = beforeId === null ? next.length : next.findIndex((entry) => entry.id === beforeId);
14
+ next.splice(to, 0, item);
15
+ return next;
16
+ };
17
+ function projectAIClientState(state) {
18
+ const pending = state.pendingQueueCommands ?? [];
19
+ if (state.status === "closed") return state;
20
+ if (pending.length === 0 && !state.inbox.items.some((item) => item.afterStepOf !== void 0)) return state;
21
+ let messages = state.messages;
22
+ let { items, paused } = state.inbox;
23
+ let turnId = state.active?.turnId ?? null;
24
+ let starting = false;
25
+ let suspended = state.active?.phase === "paused" || state.active?.phase === "pausing";
26
+ let messageIds;
27
+ const show = (message) => {
28
+ messageIds ??= new Set(messages.map((item) => item.id));
29
+ if (messageIds.has(message.id)) return;
30
+ if (messages === state.messages) messages = state.messages.slice();
31
+ messages.push(message);
32
+ messageIds.add(message.id);
33
+ };
34
+ const admit = () => {
35
+ if (turnId !== null || paused || items.length === 0) return;
36
+ let count = 0;
37
+ for (const item of items) {
38
+ count += 1;
39
+ show(item.message);
40
+ if (item.generate) {
41
+ turnId = item.id;
42
+ starting = true;
43
+ suspended = false;
44
+ break;
45
+ }
46
+ }
47
+ if (count > 0) items = items.slice(count);
48
+ };
49
+ for (const { command } of pending) {
50
+ switch (command.action) {
51
+ case "send":
52
+ case "steer": {
53
+ if (items.some((item) => item.id === command.message.id) || turnId === command.message.id) break;
54
+ const item = {
55
+ ...command.action === "steer" ? { afterStepOf: command.turnId } : {},
56
+ id: command.message.id,
57
+ revision: 0,
58
+ message: command.message,
59
+ generate: command.message.role === "user" && (command.action === "steer" || command.generate !== false)
60
+ };
61
+ items = command.action === "steer" ? [item, ...items] : [...items, item];
62
+ break;
63
+ }
64
+ case "edit":
65
+ items = items.map((item) => item.id === command.message.id && item.revision === command.expectedRevision && item.message.role === command.message.role ? {
66
+ ...item,
67
+ message: command.message
68
+ } : item);
69
+ break;
70
+ case "move":
71
+ items = moveItem({
72
+ items,
73
+ inputId: command.inputId,
74
+ beforeId: command.beforeId
75
+ });
76
+ break;
77
+ case "remove":
78
+ items = items.filter((item) => item.id !== command.inputId);
79
+ break;
80
+ case "send-now": {
81
+ if (turnId !== command.turnId) break;
82
+ const selected = items.find((item) => item.id === command.inputId);
83
+ if (!selected || selected.message.role !== "user") break;
84
+ items = items.map((item) => item.id === selected.id ? {
85
+ ...item,
86
+ generate: true,
87
+ ...turnId === null ? {} : { afterStepOf: turnId }
88
+ } : item);
89
+ items = moveItem({
90
+ items,
91
+ inputId: command.inputId,
92
+ beforeId: items[0]?.id ?? null
93
+ });
94
+ break;
95
+ }
96
+ case "pause":
97
+ paused = true;
98
+ if (command.when === "now") suspended = true;
99
+ break;
100
+ case "resume":
101
+ paused = false;
102
+ suspended = false;
103
+ }
104
+ admit();
105
+ }
106
+ items = items.filter((item) => {
107
+ if (item.afterStepOf === void 0) return true;
108
+ show(item.message);
109
+ return false;
110
+ });
111
+ return {
112
+ ...state,
113
+ messages,
114
+ inbox: {
115
+ items,
116
+ paused
117
+ },
118
+ starting: starting && !suspended
119
+ };
120
+ }
121
+ //#endregion
122
+ //#region src/parse-partial-json.ts
123
+ /*!
124
+ * Adapted from AI SDK 7.0.58, packages/ai/src/util/fix-json.ts.
125
+ * Source: https://github.com/vercel/ai, packages/ai/src/util/fix-json.ts.
126
+ * Modified to expose a synchronous partial parser with strict TypeScript types.
127
+ * Also preserves positive exponents in unfinished containers.
128
+ * Copyright 2023 Vercel, Inc.
129
+ * @license Apache-2.0. See src/licenses/Apache-2.0.txt.
130
+ */
131
+ function parsePartialJSON(text) {
132
+ if (text === void 0 || text.trim() === "") return void 0;
133
+ try {
134
+ return JSON.parse(text);
135
+ } catch {
136
+ try {
137
+ return JSON.parse(repairPartialJSON(text));
138
+ } catch {
139
+ return;
140
+ }
141
+ }
142
+ }
143
+ const isHexDigit = (char) => char >= "0" && char <= "9" || char >= "A" && char <= "F" || char >= "a" && char <= "f";
144
+ function repairPartialJSON(input) {
145
+ const stack = ["ROOT"];
146
+ let lastValidIndex = -1;
147
+ let literalStart = null;
148
+ let unicodeEscapeDigits = 0;
149
+ function processValueStart(char, i, swapState) {
150
+ switch (char) {
151
+ case "\"":
152
+ lastValidIndex = i;
153
+ stack.pop();
154
+ stack.push(swapState);
155
+ stack.push("INSIDE_STRING");
156
+ break;
157
+ case "f":
158
+ case "t":
159
+ case "n":
160
+ lastValidIndex = i;
161
+ literalStart = i;
162
+ stack.pop();
163
+ stack.push(swapState);
164
+ stack.push("INSIDE_LITERAL");
165
+ break;
166
+ case "-":
167
+ stack.pop();
168
+ stack.push(swapState);
169
+ stack.push("INSIDE_NUMBER");
170
+ break;
171
+ case "0":
172
+ case "1":
173
+ case "2":
174
+ case "3":
175
+ case "4":
176
+ case "5":
177
+ case "6":
178
+ case "7":
179
+ case "8":
180
+ case "9":
181
+ lastValidIndex = i;
182
+ stack.pop();
183
+ stack.push(swapState);
184
+ stack.push("INSIDE_NUMBER");
185
+ break;
186
+ case "{":
187
+ lastValidIndex = i;
188
+ stack.pop();
189
+ stack.push(swapState);
190
+ stack.push("INSIDE_OBJECT_START");
191
+ break;
192
+ case "[":
193
+ lastValidIndex = i;
194
+ stack.pop();
195
+ stack.push(swapState);
196
+ stack.push("INSIDE_ARRAY_START");
197
+ }
198
+ }
199
+ function processAfterObjectValue(char, i) {
200
+ switch (char) {
201
+ case ",":
202
+ stack.pop();
203
+ stack.push("INSIDE_OBJECT_AFTER_COMMA");
204
+ break;
205
+ case "}":
206
+ lastValidIndex = i;
207
+ stack.pop();
208
+ }
209
+ }
210
+ function processAfterArrayValue(char, i) {
211
+ switch (char) {
212
+ case ",":
213
+ stack.pop();
214
+ stack.push("INSIDE_ARRAY_AFTER_COMMA");
215
+ break;
216
+ case "]":
217
+ lastValidIndex = i;
218
+ stack.pop();
219
+ }
220
+ }
221
+ for (let i = 0; i < input.length; i++) {
222
+ const char = input[i];
223
+ switch (stack[stack.length - 1]) {
224
+ case "ROOT":
225
+ processValueStart(char, i, "FINISH");
226
+ break;
227
+ case "INSIDE_OBJECT_START":
228
+ switch (char) {
229
+ case "\"":
230
+ stack.pop();
231
+ stack.push("INSIDE_OBJECT_KEY");
232
+ break;
233
+ case "}":
234
+ lastValidIndex = i;
235
+ stack.pop();
236
+ }
237
+ break;
238
+ case "INSIDE_OBJECT_AFTER_COMMA":
239
+ switch (char) {
240
+ case "\"":
241
+ stack.pop();
242
+ stack.push("INSIDE_OBJECT_KEY");
243
+ }
244
+ break;
245
+ case "INSIDE_OBJECT_KEY":
246
+ switch (char) {
247
+ case "\"":
248
+ stack.pop();
249
+ stack.push("INSIDE_OBJECT_AFTER_KEY");
250
+ }
251
+ break;
252
+ case "INSIDE_OBJECT_AFTER_KEY":
253
+ switch (char) {
254
+ case ":":
255
+ stack.pop();
256
+ stack.push("INSIDE_OBJECT_BEFORE_VALUE");
257
+ }
258
+ break;
259
+ case "INSIDE_OBJECT_BEFORE_VALUE":
260
+ processValueStart(char, i, "INSIDE_OBJECT_AFTER_VALUE");
261
+ break;
262
+ case "INSIDE_OBJECT_AFTER_VALUE":
263
+ processAfterObjectValue(char, i);
264
+ break;
265
+ case "INSIDE_STRING":
266
+ switch (char) {
267
+ case "\"":
268
+ stack.pop();
269
+ lastValidIndex = i;
270
+ break;
271
+ case "\\":
272
+ stack.push("INSIDE_STRING_ESCAPE");
273
+ break;
274
+ default: lastValidIndex = i;
275
+ }
276
+ break;
277
+ case "INSIDE_ARRAY_START":
278
+ switch (char) {
279
+ case "]":
280
+ lastValidIndex = i;
281
+ stack.pop();
282
+ break;
283
+ default:
284
+ lastValidIndex = i;
285
+ processValueStart(char, i, "INSIDE_ARRAY_AFTER_VALUE");
286
+ }
287
+ break;
288
+ case "INSIDE_ARRAY_AFTER_VALUE":
289
+ switch (char) {
290
+ case ",":
291
+ stack.pop();
292
+ stack.push("INSIDE_ARRAY_AFTER_COMMA");
293
+ break;
294
+ case "]":
295
+ lastValidIndex = i;
296
+ stack.pop();
297
+ break;
298
+ default: lastValidIndex = i;
299
+ }
300
+ break;
301
+ case "INSIDE_ARRAY_AFTER_COMMA":
302
+ processValueStart(char, i, "INSIDE_ARRAY_AFTER_VALUE");
303
+ break;
304
+ case "INSIDE_STRING_ESCAPE":
305
+ stack.pop();
306
+ if (char === "u") {
307
+ unicodeEscapeDigits = 0;
308
+ stack.push("INSIDE_STRING_UNICODE_ESCAPE");
309
+ } else lastValidIndex = i;
310
+ break;
311
+ case "INSIDE_STRING_UNICODE_ESCAPE":
312
+ if (isHexDigit(char)) {
313
+ unicodeEscapeDigits++;
314
+ if (unicodeEscapeDigits === 4) {
315
+ stack.pop();
316
+ lastValidIndex = i;
317
+ }
318
+ }
319
+ break;
320
+ case "INSIDE_NUMBER":
321
+ switch (char) {
322
+ case "0":
323
+ case "1":
324
+ case "2":
325
+ case "3":
326
+ case "4":
327
+ case "5":
328
+ case "6":
329
+ case "7":
330
+ case "8":
331
+ case "9":
332
+ lastValidIndex = i;
333
+ break;
334
+ case "e":
335
+ case "E":
336
+ case "-":
337
+ case "+":
338
+ case ".": break;
339
+ case ",":
340
+ stack.pop();
341
+ if (stack[stack.length - 1] === "INSIDE_ARRAY_AFTER_VALUE") processAfterArrayValue(char, i);
342
+ if (stack[stack.length - 1] === "INSIDE_OBJECT_AFTER_VALUE") processAfterObjectValue(char, i);
343
+ break;
344
+ case "}":
345
+ stack.pop();
346
+ if (stack[stack.length - 1] === "INSIDE_OBJECT_AFTER_VALUE") processAfterObjectValue(char, i);
347
+ break;
348
+ case "]":
349
+ stack.pop();
350
+ if (stack[stack.length - 1] === "INSIDE_ARRAY_AFTER_VALUE") processAfterArrayValue(char, i);
351
+ break;
352
+ default: stack.pop();
353
+ }
354
+ break;
355
+ case "INSIDE_LITERAL": {
356
+ const partialLiteral = input.substring(literalStart, i + 1);
357
+ if (!"false".startsWith(partialLiteral) && !"true".startsWith(partialLiteral) && !"null".startsWith(partialLiteral)) {
358
+ stack.pop();
359
+ if (stack[stack.length - 1] === "INSIDE_OBJECT_AFTER_VALUE") processAfterObjectValue(char, i);
360
+ else if (stack[stack.length - 1] === "INSIDE_ARRAY_AFTER_VALUE") processAfterArrayValue(char, i);
361
+ } else lastValidIndex = i;
362
+ break;
363
+ }
364
+ }
365
+ }
366
+ let result = input.slice(0, lastValidIndex + 1);
367
+ for (let i = stack.length - 1; i >= 0; i--) switch (stack[i]) {
368
+ case "INSIDE_STRING":
369
+ result += "\"";
370
+ break;
371
+ case "INSIDE_OBJECT_KEY":
372
+ case "INSIDE_OBJECT_AFTER_KEY":
373
+ case "INSIDE_OBJECT_AFTER_COMMA":
374
+ case "INSIDE_OBJECT_START":
375
+ case "INSIDE_OBJECT_BEFORE_VALUE":
376
+ case "INSIDE_OBJECT_AFTER_VALUE":
377
+ result += "}";
378
+ break;
379
+ case "INSIDE_ARRAY_START":
380
+ case "INSIDE_ARRAY_AFTER_COMMA":
381
+ case "INSIDE_ARRAY_AFTER_VALUE":
382
+ result += "]";
383
+ break;
384
+ case "INSIDE_LITERAL": {
385
+ const partialLiteral = input.substring(literalStart, input.length);
386
+ if ("true".startsWith(partialLiteral)) result += "true".slice(partialLiteral.length);
387
+ else if ("false".startsWith(partialLiteral)) result += "false".slice(partialLiteral.length);
388
+ else if ("null".startsWith(partialLiteral)) result += "null".slice(partialLiteral.length);
389
+ }
390
+ }
391
+ return result;
392
+ }
393
+ //#endregion
4
394
  //#region src/ai-projector.ts
5
395
  const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6
- const cloneMessage = (messageId, base) => structuredClone(base ?? {
396
+ const cloneMessage = (messageId, base) => base === void 0 ? {
7
397
  id: messageId,
8
398
  role: "assistant",
9
399
  parts: []
10
- });
400
+ } : {
401
+ ...base,
402
+ id: messageId,
403
+ parts: base.parts.map((part) => ({ ...part }))
404
+ };
11
405
  const currentStepParts = (parts) => {
12
406
  let start = parts.length - 1;
13
407
  while (start >= 0 && parts[start]?.["type"] !== "step-start") start -= 1;
@@ -60,21 +454,16 @@ const mergeMetadata = (current, update) => {
60
454
  ...update
61
455
  } : update;
62
456
  };
63
- const parseCompleteJSON = (value) => {
64
- try {
65
- return JSON.parse(value);
66
- } catch {
67
- return;
68
- }
69
- };
70
- /** Internal synchronous counterpart of the AI SDK UI message stream fold. */
71
457
  function projectUIMessage(options) {
458
+ return projectUIMessageStream(options).message;
459
+ }
460
+ function projectUIMessageStream(options) {
72
461
  const message = cloneMessage(options.messageId, options.base);
73
462
  const record = message;
74
463
  const parts = message.parts;
75
- const activeText = /* @__PURE__ */ new Map();
76
- const activeReasoning = /* @__PURE__ */ new Map();
77
- const partialTools = /* @__PURE__ */ new Map();
464
+ const activeText = new Map(Object.entries(options.cursor?.text ?? {}).map(([id, index]) => [id, parts[index]]));
465
+ const activeReasoning = new Map(Object.entries(options.cursor?.reasoning ?? {}).map(([id, index]) => [id, parts[index]]));
466
+ const partialTools = new Map(Object.entries(options.cursor?.tools ?? {}).map(([id, partial]) => [id, { ...partial }]));
78
467
  for (const chunk of options.chunks) switch (chunk.type) {
79
468
  case "text-start": {
80
469
  const part = {
@@ -213,7 +602,7 @@ function projectUIMessage(options) {
213
602
  toolName: partial.toolName,
214
603
  dynamic: partial.dynamic,
215
604
  state: "input-streaming",
216
- input: parseCompleteJSON(partial.text),
605
+ input: parsePartialJSON(partial.text),
217
606
  providerExecuted: partial.providerExecuted,
218
607
  providerMetadata: partial.providerMetadata,
219
608
  toolMetadata: partial.toolMetadata,
@@ -333,7 +722,6 @@ function projectUIMessage(options) {
333
722
  activeReasoning.clear();
334
723
  break;
335
724
  case "start":
336
- if (chunk.messageId !== void 0) record["id"] = chunk.messageId;
337
725
  if (chunk.messageMetadata !== void 0 && chunk.messageMetadata !== null) record["metadata"] = mergeMetadata(record["metadata"], chunk.messageMetadata);
338
726
  break;
339
727
  case "finish":
@@ -355,10 +743,28 @@ function projectUIMessage(options) {
355
743
  });
356
744
  }
357
745
  }
358
- return message;
746
+ const indexes = new Map(parts.map((part, index) => [part, index]));
747
+ return {
748
+ message,
749
+ cursor: {
750
+ text: Object.fromEntries([...activeText].map(([id, part]) => [id, indexes.get(part)])),
751
+ reasoning: Object.fromEntries([...activeReasoning].map(([id, part]) => [id, indexes.get(part)])),
752
+ tools: Object.fromEntries(partialTools)
753
+ }
754
+ };
755
+ }
756
+ function pauseUIMessage(options) {
757
+ const next = structuredClone(options.message);
758
+ next.parts = next.parts.filter((part) => !("toolCallId" in part && !options.toolCallIds.has(part.toolCallId)));
759
+ for (const part of next.parts) if ((part.type === "text" || part.type === "reasoning") && part.state === "streaming") part.state = "done";
760
+ return next;
359
761
  }
360
762
  function interruptUIMessage(message, toolError = "Tool execution was interrupted.") {
361
763
  const next = structuredClone(message);
764
+ next.parts = next.parts.filter((value) => {
765
+ const part = value;
766
+ return !isToolPart(part) || part["state"] !== "input-streaming";
767
+ });
362
768
  for (const value of next.parts) {
363
769
  if (value["type"] === "text" || value["type"] === "reasoning") {
364
770
  if (value["state"] === "streaming") value["state"] = "done";
@@ -380,7 +786,7 @@ function interruptUIMessage(message, toolError = "Tool execution was interrupted
380
786
  value["errorText"] = toolError;
381
787
  continue;
382
788
  }
383
- if (state === "input-streaming" || state === "input-available" || state === "approval-requested" || state === "approval-responded") {
789
+ if (state === "input-available" || state === "approval-requested" || state === "approval-responded") {
384
790
  if (state === "approval-requested") delete value["approval"];
385
791
  value["state"] = "output-error";
386
792
  value["errorText"] = toolError;
@@ -389,6 +795,63 @@ function interruptUIMessage(message, toolError = "Tool execution was interrupted
389
795
  return next;
390
796
  }
391
797
  //#endregion
798
+ //#region src/ai-progress-batches.ts
799
+ const PAGE_SIZE = 64;
800
+ const appendProgressBatch = ({ batches, batch }) => {
801
+ if (batches.tail.length < PAGE_SIZE) return {
802
+ ...batches,
803
+ length: batches.length + 1,
804
+ tail: [...batches.tail, batch]
805
+ };
806
+ const blocks = [...batches.blocks];
807
+ let block = batches.tail;
808
+ let level = 0;
809
+ for (;;) {
810
+ const left = blocks[level];
811
+ if (left == null) break;
812
+ block = {
813
+ left,
814
+ right: block
815
+ };
816
+ blocks[level] = null;
817
+ level += 1;
818
+ }
819
+ blocks[level] = block;
820
+ return {
821
+ length: batches.length + 1,
822
+ tail: [batch],
823
+ blocks
824
+ };
825
+ };
826
+ const progressBatches = (items = []) => {
827
+ let batches = {
828
+ length: 0,
829
+ tail: [],
830
+ blocks: []
831
+ };
832
+ for (const batch of items) batches = appendProgressBatch({
833
+ batches,
834
+ batch
835
+ });
836
+ return batches;
837
+ };
838
+ const flattenProgressBatches = (batches) => {
839
+ const items = [];
840
+ const visit = (block) => {
841
+ if (Array.isArray(block)) items.push(...block);
842
+ else {
843
+ visit(block.left);
844
+ visit(block.right);
845
+ }
846
+ };
847
+ for (let level = batches.blocks.length - 1; level >= 0; level -= 1) {
848
+ const block = batches.blocks[level];
849
+ if (block) visit(block);
850
+ }
851
+ items.push(...batches.tail);
852
+ return items;
853
+ };
854
+ //#endregion
392
855
  //#region src/ai.ts
393
856
  const issue = (message) => ({ issues: [{ message }] });
394
857
  const schema = (label, parse) => ({ "~standard": {
@@ -467,13 +930,79 @@ const compactionPayloadSchema = (messageSchema) => schema("ai.compaction.complet
467
930
  function createEvents(options) {
468
931
  const messageSchema = options?.messageSchema;
469
932
  if (messageSchema) assertSyncSchema(messageSchema, "the AI message schema");
470
- return {
933
+ const definition = {
934
+ "ai.control.requested": schema("ai.control.requested", (value) => {
935
+ if (!isRecord(value) || !isJSONCompatible(value)) return issue("invalid AI command");
936
+ const action = value["action"];
937
+ const forwarded = {
938
+ approval: ["ai.approval.responded", "response"],
939
+ "tool-result": ["ai.tool.result", "result"],
940
+ input: ["ai.input.responded", "response"],
941
+ "request-input": ["ai.input.requested", "request"],
942
+ retry: ["ai.retry.requested", "request"],
943
+ interrupt: ["ai.message.interrupted", "request"]
944
+ };
945
+ if (typeof action === "string" && Object.hasOwn(forwarded, action)) {
946
+ const [type, key] = forwarded[action];
947
+ const payload = validateSync(definition[type], value[key], "AI command");
948
+ if (payload.issues) return { issues: payload.issues };
949
+ return { value: {
950
+ action,
951
+ [key]: payload.value
952
+ } };
953
+ }
954
+ if (action === "send" || action === "edit" || action === "steer") {
955
+ const parsed = validateMessage(value["message"], messageSchema);
956
+ if (parsed.issues) return parsed;
957
+ if (action === "edit" && (!Number.isSafeInteger(value["expectedRevision"]) || Number(value["expectedRevision"]) < 0) || action === "steer" && (!stringField(value, "turnId") || !optionalIndexField(value, "lastSeenIndex")) || action === "send" && value["generate"] !== void 0 && typeof value["generate"] !== "boolean") return issue("invalid AI command");
958
+ return { value: {
959
+ ...value,
960
+ message: parsed.value
961
+ } };
962
+ }
963
+ if (action === "remove" && stringField(value, "inputId") || action === "send-now" && stringField(value, "inputId") && (value["turnId"] === null || stringField(value, "turnId")) && optionalIndexField(value, "lastSeenIndex") || action === "move" && stringField(value, "inputId") && (value["beforeId"] === null || stringField(value, "beforeId")) || action === "stop" && stringField(value, "turnId") && optionalIndexField(value, "lastSeenIndex") || action === "pause" && (value["when"] === "now" || value["when"] === "after-turn") || action === "resume") return { value };
964
+ return issue("invalid AI command");
965
+ }),
966
+ "ai.control.committed": jsonObject("ai.control.committed", (value) => Array.isArray(value["changes"]) && isRecord(value["view"])),
967
+ "ai.control.decided": jsonObject("ai.control.decided", (value) => stringField(value, "commandId") && (value["outcome"] === "applied" || value["outcome"] === "rejected")),
968
+ "ai.work.reported": schema("ai.work.reported", (value) => {
969
+ if (!isRecord(value) || !isJSONCompatible(value) || !stringField(value, "workId") || !stringField(value, "turnId") || typeof value["version"] !== "number" || !optionalIndexField(value, "version") || typeof value["attempt"] !== "number" || !Number.isSafeInteger(value["attempt"]) || value["attempt"] < 1 || typeof value["settled"] !== "boolean" || value["kind"] !== "model" && value["kind"] !== "tool" || !Array.isArray(value["events"])) return issue("invalid AI work report");
970
+ const allowed = /* @__PURE__ */ new Set([
971
+ "ai.generation.started",
972
+ "ai.generation.completed",
973
+ "ai.generation.failed",
974
+ "ai.tool.called",
975
+ "ai.tool.result",
976
+ "ai.approval.requested",
977
+ "ai.approval.responded",
978
+ "ai.compaction.requested",
979
+ "ai.compaction.completed",
980
+ "ai.model.metadata.requested"
981
+ ]);
982
+ const events = [];
983
+ for (const event of value["events"]) {
984
+ if (!isRecord(event) || !stringField(event, "id") || typeof event["type"] !== "string" || !allowed.has(event["type"]) || value["kind"] === "tool" && event["type"] !== "ai.tool.result") return issue("invalid AI work fact");
985
+ const parsed = validateSync(definition[event["type"]], event["payload"], "AI work fact");
986
+ if (parsed.issues) return { issues: parsed.issues };
987
+ events.push({
988
+ id: event["id"],
989
+ type: event["type"],
990
+ payload: parsed.value
991
+ });
992
+ }
993
+ return { value: {
994
+ ...value,
995
+ events
996
+ } };
997
+ }),
998
+ "ai.tool.execution.requested": jsonObject("ai.tool.execution.requested", (value) => stringField(value, "turnId") && optionalIndexField(value, "version") && isRecord(value["call"]) && isRecord(value["generation"])),
999
+ "ai.generation.paused": jsonObject("ai.generation.paused", (value) => stringField(value, "generationId") && stringField(value, "messageId")),
471
1000
  "ai.session.created": jsonObject("ai.session.created", () => true),
472
1001
  "ai.session.closed": jsonObject("ai.session.closed", (value) => optionalStringField(value, "reason")),
473
1002
  "ai.message.created": messagePayloadSchema(messageSchema),
474
1003
  "ai.message.completed": jsonObject("ai.message.completed", (value) => stringField(value, "messageId")),
475
1004
  "ai.message.interrupted": jsonObject("ai.message.interrupted", (value) => stringField(value, "messageId") && (stringField(value, "generationId") && value["requestId"] === void 0 || stringField(value, "requestId") && value["generationId"] === void 0) && optionalStringField(value, "reason") && (value["lastSeenIndex"] === void 0 || typeof value["lastSeenIndex"] === "number" && Number.isInteger(value["lastSeenIndex"]) && value["lastSeenIndex"] >= 0)),
476
- "ai.generation.requested": jsonObject("ai.generation.requested", (value) => stringField(value, "messageId") && optionalStringField(value, "responseMessageId") && (value["reason"] === "message" || value["reason"] === "tool" || value["reason"] === "input" || value["reason"] === "retry")),
1005
+ "ai.generation.requested": jsonObject("ai.generation.requested", (value) => stringField(value, "messageId") && optionalStringField(value, "responseMessageId") && (value["reason"] === "message" || value["reason"] === "tool" || value["reason"] === "input" || value["reason"] === "retry" || value["reason"] === "resume")),
477
1006
  "ai.retry.requested": jsonObject("ai.retry.requested", (value) => stringField(value, "messageId") && stringField(value, "responseMessageId") && stringField(value, "retryId")),
478
1007
  "ai.generation.started": jsonObject("ai.generation.started", (value) => stringField(value, "requestId") && stringField(value, "messageId") && stringField(value, "generationId") && stringField(value, "responseMessageId") && typeof value["attempt"] === "number" && Number.isInteger(value["attempt"]) && value["attempt"] > 0 && stringField(value, "model") && optionalIndexField(value, "promptThroughIndex")),
479
1008
  "ai.generation.progress": progressPayloadSchema,
@@ -490,9 +1019,15 @@ function createEvents(options) {
490
1019
  "ai.model.metadata.requested": jsonObject("ai.model.metadata.requested", (value) => stringField(value, "modelId")),
491
1020
  "ai.model.metadata.resolved": jsonObject("ai.model.metadata.resolved", (value) => stringField(value, "modelId") && (value["limits"] === null || isRecord(value["limits"]) && typeof value["limits"]["contextWindow"] === "number" && Number.isSafeInteger(value["limits"]["contextWindow"]) && value["limits"]["contextWindow"] > 0 && typeof value["limits"]["maxOutputTokens"] === "number" && Number.isSafeInteger(value["limits"]["maxOutputTokens"]) && value["limits"]["maxOutputTokens"] > 0))
492
1021
  };
1022
+ return definition;
493
1023
  }
494
1024
  const events = createEvents();
495
1025
  const initialState = () => ({
1026
+ inbox: initialControlState().inbox,
1027
+ pendingQueueCommands: [],
1028
+ active: null,
1029
+ starting: false,
1030
+ receipt: null,
496
1031
  session: { created: false },
497
1032
  messages: [],
498
1033
  status: "idle",
@@ -516,6 +1051,17 @@ const upsertMessage = (messages, message) => {
516
1051
  next[index] = message;
517
1052
  return next;
518
1053
  };
1054
+ const upsertResponse = (options) => {
1055
+ const { messages, message, inputId } = options;
1056
+ if (messages.some((item) => item.id === message.id)) return upsertMessage(messages, message);
1057
+ const inputIndex = messages.findIndex((item) => item.id === inputId);
1058
+ if (inputIndex === -1) return [...messages, message];
1059
+ return [
1060
+ ...messages.slice(0, inputIndex + 1),
1061
+ message,
1062
+ ...messages.slice(inputIndex + 1)
1063
+ ];
1064
+ };
519
1065
  const answerApproval = (messages, response) => messages.map((message) => {
520
1066
  if (message.id !== response.messageId) return message;
521
1067
  let changed = false;
@@ -601,8 +1147,8 @@ const isStaleGeneration = (state, messageId, generationId) => {
601
1147
  const current = state.responseGenerationIds[messageId];
602
1148
  return current !== void 0 && current !== generationId;
603
1149
  };
604
- const projectedMessage = (projection) => {
605
- const projectionEvents = [...projection.batches];
1150
+ const projectedMessageStream = (projection) => {
1151
+ const projectionEvents = flattenProgressBatches(projection.batches);
606
1152
  for (const event of projection.toolEvents) if (event.type === "ai.tool.result") projectionEvents.push({
607
1153
  index: event.index,
608
1154
  chunks: [toolResultChunk(event.payload)]
@@ -611,12 +1157,20 @@ const projectedMessage = (projection) => {
611
1157
  index: event.index,
612
1158
  chunks: [event.type === "ai.approval.requested" ? approvalRequestedChunk(event.payload) : approvalRespondedChunk(event.payload)]
613
1159
  });
614
- return projectUIMessage({
1160
+ return projectUIMessageStream({
615
1161
  messageId: projection.responseMessageId,
616
1162
  ...projection.baseMessage === void 0 ? {} : { base: projection.baseMessage },
617
1163
  chunks: projectionEvents.toSorted((left, right) => left.index - right.index).flatMap((event) => event.chunks)
618
1164
  });
619
1165
  };
1166
+ const projectedMessage = (projection) => projectedMessageStream(projection).message;
1167
+ const projectedResponse = (options) => {
1168
+ const message = projectedMessage(options.projection);
1169
+ return options.state.pausedGenerationIds?.[options.projection.generationId] ? pauseUIMessage({
1170
+ message,
1171
+ toolCallIds: new Set(options.state.tools.map((tool) => tool.toolCallId))
1172
+ }) : message;
1173
+ };
620
1174
  const reduceToolActivity = (tools, event) => {
621
1175
  if (event.type === "ai.tool.called") {
622
1176
  const payload = event.payload;
@@ -687,7 +1241,7 @@ const interruptGenerationProjection = (state, generationId, responseMessageId, c
687
1241
  if (projection === null) return null;
688
1242
  const retained = {
689
1243
  ...projection,
690
- batches: projection.batches.filter((batch) => batch.index <= cutoff),
1244
+ batches: progressBatches(flattenProgressBatches(projection.batches).filter((batch) => batch.index <= cutoff)),
691
1245
  toolEvents: projection.toolEvents.filter((toolEvent) => toolEvent.index <= cutoff),
692
1246
  approvalEvents: projection.approvalEvents.filter((approvalEvent) => approvalEvent.index <= cutoff)
693
1247
  };
@@ -695,7 +1249,7 @@ const interruptGenerationProjection = (state, generationId, responseMessageId, c
695
1249
  const restored = restoreGenerationBase(state, generationId, responseMessageId, "interrupted");
696
1250
  return {
697
1251
  ...restored,
698
- messages: shouldProject ? upsertMessage(restored.messages, interruptUIMessage(projectedMessage(retained))) : restored.messages,
1252
+ messages: shouldProject ? upsertMessage(state.messages, interruptUIMessage(projectedMessage(retained))) : restored.messages,
699
1253
  tools: stopGenerationTools(projectedTools(retained), generationId, responseMessageId, "Tool execution was interrupted."),
700
1254
  usage: projection.completionIndex === void 0 || projection.completionIndex <= cutoff ? restored.usage : restored.usage.filter((entry) => entry.generationId !== generationId)
701
1255
  };
@@ -714,6 +1268,76 @@ const terminalGenerationReason = (state, generationId) => {
714
1268
  function reduceAIState(state, event) {
715
1269
  if (state.status === "closed") return state;
716
1270
  switch (event.type) {
1271
+ case "ai.control.requested": {
1272
+ const command = event.payload;
1273
+ if (!event.id || !isQueueCommand(command)) return state;
1274
+ const pending = state.pendingQueueCommands ?? [];
1275
+ if (pending.some((item) => item.commandId === event.id)) return state;
1276
+ if ((command.action === "send" || command.action === "steer") && (state.messages.some((message) => message.id === command.message.id) || state.inbox.items.some((item) => item.id === command.message.id))) return state;
1277
+ return {
1278
+ ...state,
1279
+ pendingQueueCommands: [...pending, {
1280
+ commandId: event.id,
1281
+ command
1282
+ }]
1283
+ };
1284
+ }
1285
+ case "ai.control.committed": {
1286
+ const commit = event.payload;
1287
+ const visible = applyControlChanges({
1288
+ state: {
1289
+ inbox: state.inbox,
1290
+ active: state.active
1291
+ },
1292
+ changes: commit.changes.filter((change) => change.path[0] === "inbox" || change.path[0] === "active")
1293
+ });
1294
+ const { phase } = commit.view;
1295
+ const status = commit.view.closed ? "closed" : phase === "paused" || phase === "pausing" || commit.view.paused && phase === null ? "paused" : phase === "failed" ? "failed" : phase === "starting" || phase === "generating" ? "generating" : phase ? "waiting" : "idle";
1296
+ return {
1297
+ ...state,
1298
+ ...visible,
1299
+ status,
1300
+ activeRequestId: commit.view.requestId,
1301
+ activeResponseMessageId: commit.view.responseMessageId
1302
+ };
1303
+ }
1304
+ case "ai.control.decided": {
1305
+ const receipt = event.payload;
1306
+ if (receipt.work) return state;
1307
+ const pending = state.pendingQueueCommands ?? [];
1308
+ const settled = pending.find((item) => item.commandId === receipt.commandId);
1309
+ return {
1310
+ ...state,
1311
+ receipt,
1312
+ ...settled === void 0 ? {} : { pendingQueueCommands: pending.filter((item) => item.commandId !== receipt.commandId) }
1313
+ };
1314
+ }
1315
+ case "ai.generation.paused": {
1316
+ const payload = event.payload;
1317
+ const toolCallIds = new Set(state.tools.filter((tool) => tool.messageId === payload.messageId).map((tool) => tool.toolCallId));
1318
+ const projection = state.activeProjection;
1319
+ return {
1320
+ ...state,
1321
+ activeGeneration: null,
1322
+ activeProjection: projection?.generationId === payload.generationId ? {
1323
+ ...projection,
1324
+ cursor: null,
1325
+ batches: progressBatches(flattenProgressBatches(projection.batches).map((batch) => ({
1326
+ index: batch.index,
1327
+ chunks: batch.chunks.filter((chunk) => !("toolCallId" in chunk) || toolCallIds.has(chunk.toolCallId))
1328
+ })))
1329
+ } : projection,
1330
+ compaction: state.compaction?.status === "running" ? projection?.baseCompaction ?? null : state.compaction,
1331
+ messages: state.messages.map((message) => message.id === payload.messageId ? pauseUIMessage({
1332
+ message,
1333
+ toolCallIds
1334
+ }) : message),
1335
+ pausedGenerationIds: {
1336
+ ...state.pausedGenerationIds,
1337
+ [payload.generationId]: true
1338
+ }
1339
+ };
1340
+ }
717
1341
  case "ai.model.metadata.requested": {
718
1342
  const { modelId } = event.payload;
719
1343
  if (Object.hasOwn(state.modelMetadata, modelId)) return state;
@@ -750,6 +1374,12 @@ function reduceAIState(state, event) {
750
1374
  }
751
1375
  case "ai.session.closed": return {
752
1376
  ...state,
1377
+ active: null,
1378
+ inbox: {
1379
+ paused: true,
1380
+ items: []
1381
+ },
1382
+ pendingQueueCommands: [],
753
1383
  status: "closed",
754
1384
  compaction: state.compaction?.status === "running" ? state.activeProjection?.baseCompaction ?? null : state.compaction,
755
1385
  activeGeneration: null,
@@ -761,7 +1391,11 @@ function reduceAIState(state, event) {
761
1391
  const { message } = event.payload;
762
1392
  return {
763
1393
  ...state,
764
- messages: upsertMessage(state.messages, message)
1394
+ messages: upsertMessage(state.messages, message),
1395
+ activeProjection: state.activeProjection?.responseMessageId === message.id ? {
1396
+ ...state.activeProjection,
1397
+ cursor: null
1398
+ } : state.activeProjection
765
1399
  };
766
1400
  }
767
1401
  case "ai.message.completed": {
@@ -792,7 +1426,7 @@ function reduceAIState(state, event) {
792
1426
  const ownerRequestId = ownerGenerationId === void 0 ? void 0 : generationRequestId(ownerGenerationId);
793
1427
  const requestId = event.id;
794
1428
  if (requestId !== void 0 && state.terminalRequestIds?.[requestId] === true) return state;
795
- if (!(payload.reason === "message" || (payload.reason === "tool" ? ownerGenerationId !== void 0 && requestId === `ai.generate:tools:${ownerGenerationId}` && state.activeRequestId === ownerRequestId && state.status !== "failed" : payload.reason === "retry" ? state.status === "failed" : ownerGenerationId !== void 0 && requestId?.startsWith(`ai.generate:input:${encodeURIComponent(responseMessageId)}:${encodeURIComponent(ownerGenerationId)}:`) === true && state.activeRequestId === ownerRequestId && state.status !== "failed"))) return state;
1429
+ if (!(payload.control !== void 0 && state.activeRequestId === event.id || payload.reason === "message" || (payload.reason === "tool" ? ownerGenerationId !== void 0 && requestId === `ai.generate:tools:${ownerGenerationId}` && state.activeRequestId === ownerRequestId && state.status !== "failed" : payload.reason === "retry" ? state.status === "failed" : ownerGenerationId !== void 0 && requestId?.startsWith(`ai.generate:input:${encodeURIComponent(responseMessageId)}:${encodeURIComponent(ownerGenerationId)}:`) === true && state.activeRequestId === ownerRequestId && state.status !== "failed"))) return state;
796
1430
  return {
797
1431
  ...state,
798
1432
  messages: payload.reason === "retry" && payload.responseMessageId !== void 0 ? state.messages.filter((message) => message.id !== payload.responseMessageId) : state.messages,
@@ -828,9 +1462,10 @@ function reduceAIState(state, event) {
828
1462
  activeProjection: {
829
1463
  generationId: payload.generationId,
830
1464
  responseMessageId: payload.responseMessageId,
1465
+ promptThroughIndex: payload.promptThroughIndex ?? event.index - 1,
831
1466
  ...baseMessage === void 0 ? {} : { baseMessage },
832
1467
  baseCompaction: baseState.compaction,
833
- batches: [],
1468
+ batches: progressBatches(),
834
1469
  baseTools: baseState.tools,
835
1470
  toolEvents: [],
836
1471
  approvalEvents: []
@@ -840,19 +1475,43 @@ function reduceAIState(state, event) {
840
1475
  }
841
1476
  case "ai.generation.progress": {
842
1477
  const payload = event.payload;
1478
+ if (state.pausedGenerationIds?.[payload.generationId]) return state;
843
1479
  if (terminalGenerationReason(state, payload.generationId) !== void 0) return state;
844
1480
  const activeProjection = state.activeProjection;
845
1481
  if (state.activeGeneration?.generationId !== payload.generationId || activeProjection?.generationId !== payload.generationId) return state;
1482
+ const ordered = event.index > (activeProjection.batches.tail.at(-1)?.index ?? -1);
846
1483
  const projection = {
847
1484
  ...activeProjection,
848
- batches: [...activeProjection.batches.filter((batch) => batch.index !== event.index), {
1485
+ batches: ordered ? appendProgressBatch({
1486
+ batches: activeProjection.batches,
1487
+ batch: {
1488
+ index: event.index,
1489
+ chunks: payload.chunks
1490
+ }
1491
+ }) : progressBatches([...flattenProgressBatches(activeProjection.batches).filter((batch) => batch.index !== event.index), {
849
1492
  index: event.index,
850
1493
  chunks: payload.chunks
851
- }].toSorted((left, right) => left.index - right.index)
1494
+ }].toSorted((left, right) => left.index - right.index))
1495
+ };
1496
+ const base = state.messages.find((message) => message.id === projection.responseMessageId);
1497
+ const incremental = base !== void 0 && activeProjection.cursor != null && event.index > activeProjection.cursor.throughIndex;
1498
+ const projected = incremental ? projectUIMessageStream({
1499
+ messageId: projection.responseMessageId,
1500
+ base,
1501
+ cursor: activeProjection.cursor.stream,
1502
+ chunks: payload.chunks
1503
+ }) : projectedMessageStream(projection);
1504
+ projection.cursor = {
1505
+ throughIndex: incremental ? event.index : Math.max(event.index, projection.batches.tail.at(-1)?.index ?? 0, projection.toolEvents.reduce((index, entry) => Math.max(index, entry.index), 0), projection.approvalEvents.reduce((index, entry) => Math.max(index, entry.index), 0)),
1506
+ stream: projected.cursor
852
1507
  };
853
1508
  return {
854
1509
  ...state,
855
- messages: upsertMessage(state.messages, projectedMessage(projection)),
1510
+ messages: upsertResponse({
1511
+ messages: state.messages,
1512
+ message: projected.message,
1513
+ inputId: state.activeGeneration.messageId
1514
+ }),
856
1515
  activeProjection: projection,
857
1516
  status: "generating"
858
1517
  };
@@ -1051,13 +1710,17 @@ function reduceAIState(state, event) {
1051
1710
  }
1052
1711
  const activeProjection = {
1053
1712
  ...projection,
1713
+ cursor: null,
1054
1714
  toolEvents: [...projection.toolEvents, {
1055
1715
  index: event.index,
1056
1716
  type: "ai.tool.result",
1057
1717
  payload
1058
1718
  }]
1059
1719
  };
1060
- const projected = projectedMessage(activeProjection);
1720
+ const projected = projectedResponse({
1721
+ state,
1722
+ projection: activeProjection
1723
+ });
1061
1724
  const tools = projectedTools(activeProjection);
1062
1725
  const failureError = state.error ?? "Generation failed.";
1063
1726
  const next = {
@@ -1078,6 +1741,7 @@ function reduceAIState(state, event) {
1078
1741
  if (state.tools.some((activity) => activity.generationId === payload.generationId && activity.toolCallId === payload.toolCallId && activity.status === "failed" && state.activeGeneration?.generationId !== payload.generationId)) return state;
1079
1742
  const activeProjection = projection?.generationId === payload.generationId ? {
1080
1743
  ...projection,
1744
+ cursor: null,
1081
1745
  approvalEvents: [...projection.approvalEvents, {
1082
1746
  index: event.index,
1083
1747
  type: "ai.approval.requested",
@@ -1086,7 +1750,10 @@ function reduceAIState(state, event) {
1086
1750
  } : projection;
1087
1751
  const next = {
1088
1752
  ...state,
1089
- messages: activeProjection === null || activeProjection === projection ? projectMessageChunks(state.messages, payload.messageId, [approvalRequestedChunk(payload)]) : upsertMessage(state.messages, projectedMessage(activeProjection)),
1753
+ messages: activeProjection === null || activeProjection === projection ? projectMessageChunks(state.messages, payload.messageId, [approvalRequestedChunk(payload)]) : upsertMessage(state.messages, projectedResponse({
1754
+ state,
1755
+ projection: activeProjection
1756
+ })),
1090
1757
  activeProjection,
1091
1758
  pendingApprovals: payload.isAutomatic === true ? state.pendingApprovals : [...state.pendingApprovals.filter((approval) => approval.approvalId !== payload.approvalId || approval.messageId !== payload.messageId), payload]
1092
1759
  };
@@ -1104,6 +1771,7 @@ function reduceAIState(state, event) {
1104
1771
  if (state.status === "failed" && pendingRequest === void 0 && activeRequest === void 0) return state;
1105
1772
  const activeProjection = projection !== null && activeRequest !== void 0 ? {
1106
1773
  ...projection,
1774
+ cursor: null,
1107
1775
  approvalEvents: [...projection.approvalEvents, {
1108
1776
  index: event.index,
1109
1777
  type: "ai.approval.responded",
@@ -1112,8 +1780,14 @@ function reduceAIState(state, event) {
1112
1780
  } : projection;
1113
1781
  const next = {
1114
1782
  ...state,
1115
- messages: activeProjection === null || activeProjection === projection ? answerApproval(state.messages, payload) : upsertMessage(state.messages, projectedMessage(activeProjection)),
1116
- activeProjection,
1783
+ messages: activeProjection === null || activeProjection === projection ? answerApproval(state.messages, payload) : upsertMessage(state.messages, projectedResponse({
1784
+ state,
1785
+ projection: activeProjection
1786
+ })),
1787
+ activeProjection: activeProjection === null ? null : {
1788
+ ...activeProjection,
1789
+ cursor: null
1790
+ },
1117
1791
  pendingApprovals: state.pendingApprovals.filter((approval) => approval.approvalId !== payload.approvalId || approval.messageId !== payload.messageId || approval.generationId !== payload.generationId)
1118
1792
  };
1119
1793
  return {
@@ -1163,7 +1837,8 @@ function reduceAIState(state, event) {
1163
1837
  ...state,
1164
1838
  compaction: {
1165
1839
  status: "completed",
1166
- ...payload
1840
+ ...payload,
1841
+ throughIndex: payload.throughIndex ?? state.activeProjection.promptThroughIndex
1167
1842
  }
1168
1843
  };
1169
1844
  }
@@ -1178,62 +1853,105 @@ function deriveUIMessages(history) {
1178
1853
  }
1179
1854
  /** Build the standard AI state projection for any contract containing the protocol. */
1180
1855
  function createReducer(options) {
1181
- return options.contract.reducer({
1182
- name: options.name ?? "a2.ai.state.v10",
1183
- initialState: initialState()
1184
- }).fold((state, event) => reduceAIState(state, event));
1856
+ return {
1857
+ ...options.contract.reducer({
1858
+ name: options.name ?? "a2.ai.state.v17",
1859
+ initialState: initialState()
1860
+ }).fold((state, event) => reduceAIState(state, event)),
1861
+ [clientStateProjection]: projectAIClientState
1862
+ };
1185
1863
  }
1864
+ const commandInput = (payload) => [{
1865
+ type: "ai.control.requested",
1866
+ payload
1867
+ }];
1186
1868
  const messageInput = (message, generate) => [{
1187
- type: "ai.message.created",
1869
+ type: "ai.control.requested",
1188
1870
  id: `ai.message:${message.id}`,
1189
1871
  payload: {
1872
+ action: "send",
1190
1873
  message,
1191
1874
  ...generate === false ? { generate: false } : {}
1192
1875
  }
1193
1876
  }];
1194
1877
  /** Pure append inputs for the built-in AI protocol. */
1195
1878
  const inputs = {
1879
+ toolResult: (result) => commandInput({
1880
+ action: "tool-result",
1881
+ result
1882
+ }),
1883
+ queue: {
1884
+ remove: (options) => commandInput({
1885
+ action: "remove",
1886
+ ...options
1887
+ }),
1888
+ sendNow: (options) => commandInput({
1889
+ action: "send-now",
1890
+ ...options
1891
+ }),
1892
+ edit: (options) => commandInput({
1893
+ action: "edit",
1894
+ ...options
1895
+ }),
1896
+ move: (options) => commandInput({
1897
+ action: "move",
1898
+ ...options
1899
+ })
1900
+ },
1901
+ stop: (options) => commandInput({
1902
+ action: "stop",
1903
+ ...options
1904
+ }),
1905
+ steer: (options) => commandInput({
1906
+ action: "steer",
1907
+ ...options
1908
+ }),
1909
+ pause: (options) => commandInput({
1910
+ action: "pause",
1911
+ ...options
1912
+ }),
1913
+ resume: () => commandInput({ action: "resume" }),
1196
1914
  message(message, options) {
1197
1915
  return messageInput(message, options?.generate);
1198
1916
  },
1199
1917
  seed(message) {
1200
1918
  return messageInput(message, false);
1201
1919
  },
1202
- approval(response) {
1203
- return [{
1204
- type: "ai.approval.responded",
1205
- id: `ai.approval:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.approvalId)}:response`,
1206
- payload: response
1207
- }];
1208
- },
1209
- input(response) {
1210
- return [{
1211
- type: "ai.input.responded",
1212
- id: `ai.input:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.inputId)}:response`,
1213
- payload: response
1214
- }];
1215
- },
1216
- requestInput(request) {
1217
- return [{
1218
- type: "ai.input.requested",
1219
- id: `ai.input:${encodeURIComponent(request.messageId)}:${encodeURIComponent(request.generationId)}:${encodeURIComponent(request.inputId)}:request`,
1220
- payload: request
1221
- }];
1222
- },
1223
- retry(retryOptions) {
1224
- return [{
1225
- type: "ai.retry.requested",
1226
- id: `ai.retry:${retryOptions.retryId}`,
1227
- payload: retryOptions
1228
- }];
1229
- },
1230
- interrupt(interruption) {
1231
- return [{
1232
- type: "ai.message.interrupted",
1233
- id: `ai.interrupt:${interruption.generationId ?? interruption.requestId}`,
1234
- payload: interruption
1235
- }];
1236
- }
1920
+ approval: (response) => [{
1921
+ ...commandInput({
1922
+ action: "approval",
1923
+ response
1924
+ })[0],
1925
+ id: `ai.approval:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.approvalId)}:response`
1926
+ }],
1927
+ input: (response) => [{
1928
+ ...commandInput({
1929
+ action: "input",
1930
+ response
1931
+ })[0],
1932
+ id: `ai.input:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.inputId)}:response`
1933
+ }],
1934
+ requestInput: (request) => [{
1935
+ ...commandInput({
1936
+ action: "request-input",
1937
+ request
1938
+ })[0],
1939
+ id: `ai.input:${encodeURIComponent(request.messageId)}:${encodeURIComponent(request.generationId)}:${encodeURIComponent(request.inputId)}:request`
1940
+ }],
1941
+ retry: (request) => [{
1942
+ ...commandInput({
1943
+ action: "retry",
1944
+ request
1945
+ })[0],
1946
+ id: `ai.retry:${request.retryId}`
1947
+ }],
1948
+ interrupt: (request) => [{
1949
+ ...commandInput({
1950
+ action: "interrupt",
1951
+ request
1952
+ })[0],
1953
+ id: `ai.interrupt:${request.generationId ?? request.requestId}`
1954
+ }]
1237
1955
  };
1238
1956
  /**
1239
1957
  * Read the durable handler context ambient to the current AI tool
@@ -1267,10 +1985,13 @@ function agent(options) {
1267
1985
  });
1268
1986
  return {
1269
1987
  contract: agentContract,
1270
- reducer: agentContract.reducer({
1271
- name: options.reducerName ?? "a2.ai.state.v10",
1272
- initialState: initialState()
1273
- }).fold((state, event) => reduceAIState(state, event))
1988
+ reducer: {
1989
+ ...agentContract.reducer({
1990
+ name: options.reducerName ?? "a2.ai.state.v17",
1991
+ initialState: initialState()
1992
+ }).fold((state, event) => reduceAIState(state, event)),
1993
+ [clientStateProjection]: projectAIClientState
1994
+ }
1274
1995
  };
1275
1996
  }
1276
1997
  //#endregion