@assistant-ui/react-ag-ui 0.0.71 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +1 -1
  2. package/dist/agUiExtras.js +1 -2
  3. package/dist/hooks.js +1 -2
  4. package/dist/runtime/{AgUiThreadRuntimeCore.d.ts → AgUiThreadController.d.ts} +6 -12
  5. package/dist/runtime/AgUiThreadController.d.ts.map +1 -0
  6. package/dist/runtime/{AgUiThreadRuntimeCore.js → AgUiThreadController.js} +567 -194
  7. package/dist/runtime/AgUiThreadController.js.map +1 -0
  8. package/dist/runtime/adapter/conversions.d.ts +4 -4
  9. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  10. package/dist/runtime/adapter/conversions.js +120 -35
  11. package/dist/runtime/adapter/conversions.js.map +1 -1
  12. package/dist/runtime/adapter/replay-content.d.ts +9 -0
  13. package/dist/runtime/adapter/replay-content.d.ts.map +1 -0
  14. package/dist/runtime/adapter/replay-content.js +185 -0
  15. package/dist/runtime/adapter/replay-content.js.map +1 -0
  16. package/dist/runtime/adapter/run-aggregator.d.ts +3 -5
  17. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  18. package/dist/runtime/adapter/run-aggregator.js +48 -63
  19. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  20. package/dist/runtime/adapter/subscriber.d.ts +0 -24
  21. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  22. package/dist/runtime/adapter/subscriber.js +2 -24
  23. package/dist/runtime/adapter/subscriber.js.map +1 -1
  24. package/dist/runtime/adapter/tool-approval.d.ts +2 -0
  25. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
  26. package/dist/runtime/adapter/tool-approval.js +23 -6
  27. package/dist/runtime/adapter/tool-approval.js.map +1 -1
  28. package/dist/runtime/agUiThreadState.d.ts +60 -0
  29. package/dist/runtime/agUiThreadState.d.ts.map +1 -0
  30. package/dist/runtime/agUiThreadState.js +94 -0
  31. package/dist/runtime/agUiThreadState.js.map +1 -0
  32. package/dist/runtime/event-parser.d.ts.map +1 -1
  33. package/dist/runtime/event-parser.js +46 -50
  34. package/dist/runtime/event-parser.js.map +1 -1
  35. package/dist/runtime/interrupt-internals.js +1 -2
  36. package/dist/runtime/logger.js +1 -2
  37. package/dist/runtime/mcp-tool-result.d.ts +3 -1
  38. package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
  39. package/dist/runtime/mcp-tool-result.js +7 -5
  40. package/dist/runtime/mcp-tool-result.js.map +1 -1
  41. package/dist/runtime/types.d.ts +11 -35
  42. package/dist/runtime/types.d.ts.map +1 -1
  43. package/dist/runtime/types.js +1 -2
  44. package/dist/runtime/types.js.map +1 -1
  45. package/dist/useAgUiRuntime.js +3 -4
  46. package/dist/useAgUiRuntime.js.map +1 -1
  47. package/package.json +12 -11
  48. package/src/runtime/AgUiThreadController.deferred-results.test.ts +548 -0
  49. package/src/runtime/{AgUiThreadRuntimeCore.interrupt-cancel.test.ts → AgUiThreadController.interrupt-cancel.test.ts} +4 -4
  50. package/src/runtime/AgUiThreadController.replay.test.ts +3932 -0
  51. package/src/runtime/{AgUiThreadRuntimeCore.test.ts → AgUiThreadController.test.ts} +114 -6
  52. package/src/runtime/{AgUiThreadRuntimeCore.ts → AgUiThreadController.ts} +1037 -236
  53. package/src/runtime/adapter/conversions.content.test.ts +149 -5
  54. package/src/runtime/adapter/conversions.tools.test.ts +2 -1
  55. package/src/runtime/adapter/conversions.ts +249 -62
  56. package/src/runtime/adapter/replay-content.test.ts +235 -0
  57. package/src/runtime/adapter/replay-content.ts +306 -0
  58. package/src/runtime/adapter/run-aggregator.ts +79 -114
  59. package/src/runtime/adapter/subscriber.ts +1 -44
  60. package/src/runtime/adapter/tool-approval.ts +41 -3
  61. package/src/runtime/agUiThreadState.test.ts +224 -0
  62. package/src/runtime/agUiThreadState.ts +139 -0
  63. package/src/runtime/event-parser.ts +40 -58
  64. package/src/runtime/mcp-tool-result.ts +16 -2
  65. package/src/runtime/types.ts +16 -28
  66. package/src/useAgUiRuntime.fast-refresh.test.tsx +3 -3
  67. package/src/useAgUiRuntime.thread-switch.test.tsx +4 -4
  68. package/src/useAgUiRuntime.ts +3 -3
  69. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +0 -1
  70. package/dist/runtime/AgUiThreadRuntimeCore.js.map +0 -1
@@ -1,18 +1,57 @@
1
1
  "use client";
2
- import { readMcpAppResourceUri } from "./mcp-tool-result.js";
2
+ import { createAgUiThreadState, reduceAgUiThreadState } from "./agUiThreadState.js";
3
+ import { readMcpAppResourceUri, toolContentText } from "./mcp-tool-result.js";
3
4
  import { AG_UI_METADATA_NAMESPACE } from "./types.js";
4
- import { buildToolApprovalResume, projectAgUiToolApprovals, withSettledToolApprovals, withToolApprovalDecision } from "./adapter/tool-approval.js";
5
+ import { buildToolApprovalResume, preserveToolApprovalDecision, projectAgUiToolApprovals, withSettledToolApprovals, withToolApprovalDecision } from "./adapter/tool-approval.js";
5
6
  import { RunAggregator, isPlainObject, tryParseJSON } from "./adapter/run-aggregator.js";
6
7
  import { fromAgUiMessages, toAgUiMessages, toAgUiTools } from "./adapter/conversions.js";
7
8
  import { createAgUiSubscriber } from "./adapter/subscriber.js";
9
+ import { createReplayContentMerger, groupReplayToolContent, indexReplayToolOwners, preserveToolResult } from "./adapter/replay-content.js";
8
10
  import { fromThreadMessageLike, generateId, isMcpAppUri } from "@assistant-ui/core";
9
- import { appendToolInteraction, createMessageRepositorySession, invokeUserCallback, iterateToolCallParts, mapToolCallPartsDeep } from "@assistant-ui/core/internal";
11
+ import { appendToolInteraction, createHistoryWriteChain, createMessageRepositorySession, invokeUserCallback, iterateToolCallParts, mapToolCallPartsDeep } from "@assistant-ui/core/internal";
10
12
  import jsonpatch from "fast-json-patch";
11
- //#region src/runtime/AgUiThreadRuntimeCore.ts
13
+ // src/runtime/AgUiThreadController.ts
12
14
  const optimisticPrefix = "__optimistic__";
13
15
  const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;
14
16
  const isOptimisticId = (id) => id.startsWith(optimisticPrefix);
15
17
  const isResolvedToolCall = (part) => part.type === "tool-call" && "result" in part && part.result !== void 0;
18
+ const hasResolvedToolCalls = (message) => {
19
+ if (message.role !== "assistant") return false;
20
+ for (const part of iterateToolCallParts(message.content)) if (isResolvedToolCall(part)) return true;
21
+ return false;
22
+ };
23
+ const listedPendingToolCallIds = (assistant) => {
24
+ const ids = assistant.metadata.custom[AG_UI_METADATA_NAMESPACE]?.pendingToolCallIds;
25
+ return ids === void 0 ? void 0 : new Set(ids);
26
+ };
27
+ const withoutPendingToolCallIds = (metadata) => {
28
+ const namespace = metadata.custom[AG_UI_METADATA_NAMESPACE];
29
+ if (!namespace || !("pendingToolCallIds" in namespace)) return metadata;
30
+ const { pendingToolCallIds: _pendingToolCallIds, ...remainingNamespace } = namespace;
31
+ const { [AG_UI_METADATA_NAMESPACE]: _namespace, ...remainingCustom } = metadata.custom;
32
+ return {
33
+ ...metadata,
34
+ custom: {
35
+ ...remainingCustom,
36
+ ...Object.keys(remainingNamespace).length > 0 ? { [AG_UI_METADATA_NAMESPACE]: remainingNamespace } : {}
37
+ }
38
+ };
39
+ };
40
+ const withPendingToolCallIds = (metadata, ids) => ({
41
+ ...metadata,
42
+ custom: {
43
+ ...metadata.custom,
44
+ [AG_UI_METADATA_NAMESPACE]: {
45
+ ...metadata.custom[AG_UI_METADATA_NAMESPACE],
46
+ pendingToolCallIds: [...ids]
47
+ }
48
+ }
49
+ });
50
+ const actionableToolCallIds = (message) => {
51
+ if (message?.role !== "assistant" || message.status?.type !== "requires-action" || message.status.reason !== "tool-calls") return /* @__PURE__ */ new Set();
52
+ const permitted = listedPendingToolCallIds(message);
53
+ return new Set(Array.from(iterateToolCallParts(message.content)).filter((part) => !isResolvedToolCall(part) && (!permitted || permitted.has(part.toolCallId))).map((part) => part.toolCallId));
54
+ };
16
55
  const isActivityPart = (part) => part.type === "data" && part.name.startsWith("agui-activity/");
17
56
  const FALLBACK_USER_STATUS = {
18
57
  type: "complete",
@@ -34,7 +73,7 @@ const mergeAgUiNamespace = (current, incoming) => {
34
73
  };
35
74
  return merged;
36
75
  };
37
- var AgUiThreadRuntimeCore = class {
76
+ var AgUiThreadController = class {
38
77
  agent;
39
78
  logger;
40
79
  showThinking;
@@ -63,24 +102,16 @@ var AgUiThreadRuntimeCore = class {
63
102
  headId: repository.headId
64
103
  };
65
104
  } });
66
- isRunningFlag = false;
105
+ state = createAgUiThreadState();
67
106
  abortController = null;
68
107
  activeRunAgent = null;
69
- stateSnapshot;
70
108
  history;
71
- lastRunConfig;
72
109
  assistantHistoryParents = /* @__PURE__ */ new Map();
73
110
  snapshotHistoryIds = /* @__PURE__ */ new Set();
74
111
  persistedHistoryIds = /* @__PURE__ */ new Set();
75
- historyWrites = /* @__PURE__ */ new Map();
76
- _isLoading = false;
112
+ historyWrites = createHistoryWriteChain();
77
113
  _historyImportPromise;
78
- historyMutationGeneration = 0;
79
114
  _loadPromise;
80
- _loadRequested = false;
81
- pendingResume = null;
82
- pendingA2uiResumeOwner = null;
83
- pendingA2uiAction;
84
115
  constructor(options) {
85
116
  this.agent = options.agent;
86
117
  this.logger = options.logger;
@@ -99,11 +130,14 @@ var AgUiThreadRuntimeCore = class {
99
130
  return () => this.listeners.delete(listener);
100
131
  };
101
132
  getSnapshot = () => this.snapshot ??= {
102
- isLoading: this._isLoading,
103
- isRunning: this.isRunningFlag,
133
+ isLoading: this.state.isLoading,
134
+ isRunning: this.state.isRunning,
104
135
  messageRepository: this.session.export(),
105
- state: this.stateSnapshot
136
+ state: this.state.stateSnapshot
106
137
  };
138
+ dispatch(action) {
139
+ this.state = reduceAgUiThreadState(this.state, action);
140
+ }
107
141
  publishUpdate() {
108
142
  this.snapshot = void 0;
109
143
  for (const listener of this.listeners) listener();
@@ -120,7 +154,7 @@ var AgUiThreadRuntimeCore = class {
120
154
  this.onCancel = options.onCancel;
121
155
  const previousHistory = this.history;
122
156
  this.history = options.history;
123
- if (this._loadRequested && !this._loadPromise && !previousHistory && options.history && this.session.getMessages().length === 0) this.__internal_load();
157
+ if (this.state.loadRequested && !this._loadPromise && !previousHistory && options.history && this.session.getMessages().length === 0) this.__internal_load();
124
158
  }
125
159
  attachRuntime(runtime) {
126
160
  this.runtime = runtime;
@@ -138,16 +172,16 @@ var AgUiThreadRuntimeCore = class {
138
172
  return this.session.export();
139
173
  }
140
174
  getState() {
141
- return this.stateSnapshot;
175
+ return this.state.stateSnapshot;
142
176
  }
143
177
  isRunning() {
144
- return this.isRunningFlag;
178
+ return this.state.isRunning;
145
179
  }
146
180
  get isLoading() {
147
- return this._isLoading;
181
+ return this.state.isLoading;
148
182
  }
149
183
  __internal_load() {
150
- this._loadRequested = true;
184
+ this.dispatch({ type: "loadRequested" });
151
185
  if (this._loadPromise) return this._loadPromise;
152
186
  if (!this.history) return Promise.resolve();
153
187
  const promise = this.history.load().then((repo) => {
@@ -161,7 +195,7 @@ var AgUiThreadRuntimeCore = class {
161
195
  if (repo.state !== void 0) this.loadExternalState(repo.state);
162
196
  return repo;
163
197
  });
164
- this._isLoading = true;
198
+ this.dispatch({ type: "loadStarted" });
165
199
  this._historyImportPromise = promise.then(() => {
166
200
  this._historyImportPromise = void 0;
167
201
  }, () => {
@@ -171,13 +205,13 @@ var AgUiThreadRuntimeCore = class {
171
205
  if (repo?.unstable_resume) {
172
206
  const parentId = repo.headId ?? this.session.headId;
173
207
  const resumeStream = this.history?.resume?.bind(this.history);
174
- await this.startRun(parentId, this.lastRunConfig, void 0, resumeStream);
208
+ await this.startRun(parentId, this.state.lastRunConfig, void 0, resumeStream);
175
209
  }
176
210
  }).catch((error) => {
177
211
  this.logger.error?.("[agui] failed to load history", error);
178
212
  this.reportError(error);
179
213
  }).finally(() => {
180
- this._isLoading = false;
214
+ this.dispatch({ type: "loadFinished" });
181
215
  this.publishUpdate();
182
216
  });
183
217
  this.publishUpdate();
@@ -186,15 +220,15 @@ var AgUiThreadRuntimeCore = class {
186
220
  waitForHistoryImport() {
187
221
  const historyImport = this._historyImportPromise;
188
222
  if (!historyImport) return void 0;
189
- const generation = this.historyMutationGeneration;
190
- return historyImport.then(() => generation === this.historyMutationGeneration && !this.isThreadSwitching?.());
223
+ const generation = this.state.historyMutationGeneration;
224
+ return historyImport.then(() => generation === this.state.historyMutationGeneration && !this.isThreadSwitching?.());
191
225
  }
192
226
  deferUntilHistoryImported(callback) {
193
227
  const historyImport = this._historyImportPromise;
194
228
  if (!historyImport) return false;
195
- const generation = this.historyMutationGeneration;
229
+ const generation = this.state.historyMutationGeneration;
196
230
  historyImport.then(() => {
197
- if (generation !== this.historyMutationGeneration || this.isThreadSwitching?.()) return;
231
+ if (generation !== this.state.historyMutationGeneration || this.isThreadSwitching?.()) return;
198
232
  try {
199
233
  callback();
200
234
  } catch (error) {
@@ -207,9 +241,9 @@ var AgUiThreadRuntimeCore = class {
207
241
  const historyImport = this._historyImportPromise;
208
242
  if (historyImport) {
209
243
  const wasAtTail = message.parentId === this.session.headId;
210
- const generation = this.historyMutationGeneration;
244
+ const generation = this.state.historyMutationGeneration;
211
245
  await historyImport;
212
- if (generation !== this.historyMutationGeneration || this.isThreadSwitching?.()) return;
246
+ if (generation !== this.state.historyMutationGeneration || this.isThreadSwitching?.()) return;
213
247
  if (wasAtTail) message = {
214
248
  ...message,
215
249
  parentId: this.session.headId
@@ -235,10 +269,10 @@ var AgUiThreadRuntimeCore = class {
235
269
  }
236
270
  maybeAutoCancelPendingToolCalls() {
237
271
  if (this.autoCancelPendingToolCalls === false) return;
238
- const pending = this.getPendingToolCalls();
239
- if (!pending) return;
240
- this.cancelUnresolvedToolCalls(pending.messageId);
241
- this.maybeCompleteAfterToolResults(pending.messageId);
272
+ for (const assistant of this.getPendingToolCallMessages()) {
273
+ this.cancelUnresolvedToolCalls(assistant.id);
274
+ this.maybeCompleteAfterToolResults(assistant.id);
275
+ }
242
276
  }
243
277
  appendEntry(message) {
244
278
  const threadMessage = this.toThreadMessage(message);
@@ -260,7 +294,7 @@ var AgUiThreadRuntimeCore = class {
260
294
  await this.startRun(parentId, config.runConfig);
261
295
  }
262
296
  async cancel() {
263
- this.historyMutationGeneration++;
297
+ this.dispatch({ type: "historyMutationAdvanced" });
264
298
  this.abortActiveRun();
265
299
  }
266
300
  abortActiveRun() {
@@ -286,7 +320,7 @@ var AgUiThreadRuntimeCore = class {
286
320
  const historyImport = this.waitForHistoryImport();
287
321
  if (historyImport && !await historyImport) return;
288
322
  this.assertNoPendingInterrupts();
289
- await this.startRun(config.parentId, config.runConfig ?? this.lastRunConfig, void 0, config.stream);
323
+ await this.startRun(config.parentId, config.runConfig ?? this.state.lastRunConfig, void 0, config.stream, true);
290
324
  }
291
325
  async resumeInFlightRun(messages) {
292
326
  const historyImport = this.waitForHistoryImport();
@@ -300,7 +334,7 @@ var AgUiThreadRuntimeCore = class {
300
334
  }
301
335
  const parentId = messages.at(-1)?.id ?? null;
302
336
  try {
303
- await this.startRun(parentId, this.lastRunConfig, void 0, resumeStream);
337
+ await this.startRun(parentId, this.state.lastRunConfig, void 0, resumeStream);
304
338
  } catch {}
305
339
  }
306
340
  assertNoPendingInterrupts() {
@@ -308,9 +342,20 @@ var AgUiThreadRuntimeCore = class {
308
342
  throw new Error("[agui] cannot start a new run while interrupts are pending; resolve them with submitInterruptResponses()");
309
343
  }
310
344
  findRequiresActionAssistant(reason) {
311
- const assistant = this.getMessages().findLast((message) => message.role === "assistant");
312
- if (!assistant || assistant.status?.type !== "requires-action" || assistant.status.reason !== reason) return null;
313
- return assistant;
345
+ return this.getTrailingAssistantTurn().findLast((message) => message.status?.type === "requires-action" && message.status.reason === reason) ?? null;
346
+ }
347
+ getTrailingAssistantTurn() {
348
+ const messages = this.getMessages();
349
+ const turn = [];
350
+ for (let index = messages.length - 1; index >= 0; index--) {
351
+ const message = messages[index];
352
+ if (message?.role !== "assistant") break;
353
+ turn.unshift(message);
354
+ }
355
+ return turn;
356
+ }
357
+ getPendingToolCallMessages() {
358
+ return this.getTrailingAssistantTurn().filter((message) => message.status?.type === "requires-action" && message.status.reason === "tool-calls");
314
359
  }
315
360
  getPendingInterrupts() {
316
361
  const assistant = this.findRequiresActionAssistant("interrupt");
@@ -323,16 +368,18 @@ var AgUiThreadRuntimeCore = class {
323
368
  };
324
369
  }
325
370
  getPendingToolCalls() {
326
- const assistant = this.findRequiresActionAssistant("tool-calls");
327
- if (!assistant) return null;
328
371
  const toolCallIds = [];
329
- for (const part of iterateToolCallParts(assistant.content)) {
330
- if (isResolvedToolCall(part)) continue;
331
- toolCallIds.push(part.toolCallId);
372
+ let messageId;
373
+ for (const assistant of this.getPendingToolCallMessages()) {
374
+ const listed = listedPendingToolCallIds(assistant);
375
+ for (const part of iterateToolCallParts(assistant.content)) {
376
+ if (isResolvedToolCall(part) || listed && !listed.has(part.toolCallId)) continue;
377
+ messageId ??= assistant.id;
378
+ toolCallIds.push(part.toolCallId);
379
+ }
332
380
  }
333
- if (toolCallIds.length === 0) return null;
334
- return {
335
- messageId: assistant.id,
381
+ return messageId === void 0 ? null : {
382
+ messageId,
336
383
  toolCallIds
337
384
  };
338
385
  }
@@ -345,7 +392,7 @@ var AgUiThreadRuntimeCore = class {
345
392
  const openIds = pending.interrupts.map((i) => i.id);
346
393
  const missing = openIds.filter((id) => !responsesById.has(id));
347
394
  if (missing.length > 0) throw new Error(`[agui] submitInterruptResponses: missing responses for open interrupts: ${missing.join(", ")}`);
348
- this.assertInterruptsAnswerable("submitInterruptResponses", pending.interrupts);
395
+ this.assertInterruptsAnswerable("submitInterruptResponses", pending.interrupts, responsesById);
349
396
  await this.resumeWithResponses(pending.messageId, openIds.map((id) => responsesById.get(id)));
350
397
  }
351
398
  /**
@@ -355,8 +402,9 @@ var AgUiThreadRuntimeCore = class {
355
402
  * unretryable.
356
403
  */
357
404
  async resumeWithResponses(messageId, resume) {
405
+ const parentId = this.session.headId ?? messageId;
358
406
  this.clearPendingInterrupts(messageId, resume);
359
- await this.startRun(messageId, this.lastRunConfig, {
407
+ await this.startRun(parentId, this.state.lastRunConfig, {
360
408
  entries: resume,
361
409
  interruptedMessageId: messageId
362
410
  });
@@ -375,15 +423,15 @@ var AgUiThreadRuntimeCore = class {
375
423
  }
376
424
  invokeRuntimeCallback("onError", this.onError, error instanceof Error ? error : new Error(String(error)));
377
425
  }
378
- assertInterruptsAnswerable(method, interrupts) {
426
+ assertInterruptsAnswerable(method, interrupts, responsesById) {
379
427
  const now = Date.now();
380
428
  for (const interrupt of interrupts) {
381
429
  if (!interrupt.expiresAt) continue;
382
430
  const expiry = new Date(interrupt.expiresAt).getTime();
383
431
  if (Number.isNaN(expiry)) throw new Error(`[agui] ${method}: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`);
384
- if (expiry <= now) throw new Error(`[agui] ${method}: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`);
432
+ if (expiry <= now && responsesById?.get(interrupt.id)?.status !== "cancelled") throw new Error(`[agui] ${method}: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`);
385
433
  }
386
- if (this.isRunningFlag) throw new Error(`[agui] ${method}: a run is already in progress`);
434
+ if (this.state.isRunning) throw new Error(`[agui] ${method}: a run is already in progress`);
387
435
  }
388
436
  async respondToToolApproval(options) {
389
437
  const historyImport = this.waitForHistoryImport();
@@ -415,12 +463,13 @@ var AgUiThreadRuntimeCore = class {
415
463
  if (historyImport && !await historyImport) return;
416
464
  const pending = this.getPendingInterrupts();
417
465
  if (!pending) {
418
- const pendingTools = this.getPendingToolCalls();
419
- if (pendingTools) {
466
+ if (this.getPendingToolCalls()) {
420
467
  if (responses?.length) throw new Error("[agui] steerAway: responses are only valid for pending interrupts");
421
- if (this.isRunningFlag) throw new Error("[agui] steerAway: a run is already in progress");
422
- this.cancelUnresolvedToolCalls(pendingTools.messageId);
423
- this.maybeCompleteAfterToolResults(pendingTools.messageId);
468
+ if (this.state.isRunning) throw new Error("[agui] steerAway: a run is already in progress");
469
+ for (const assistant of this.getPendingToolCallMessages()) {
470
+ this.cancelUnresolvedToolCalls(assistant.id);
471
+ this.maybeCompleteAfterToolResults(assistant.id);
472
+ }
424
473
  const normalized = this.toAppendMessage(message);
425
474
  const threadMessageId = this.appendEntry(normalized);
426
475
  await this.startRun(threadMessageId, normalized.runConfig);
@@ -431,7 +480,7 @@ var AgUiThreadRuntimeCore = class {
431
480
  return;
432
481
  }
433
482
  const resume = this.resolveSteerAwayResume(pending.interrupts, responses);
434
- if (this.isRunningFlag) throw new Error("[agui] steerAway: a run is already in progress");
483
+ if (this.state.isRunning) throw new Error("[agui] steerAway: a run is already in progress");
435
484
  const normalized = this.toAppendMessage(message);
436
485
  this.clearPendingInterrupts(pending.messageId, resume);
437
486
  const threadMessageId = this.appendEntry(normalized);
@@ -597,7 +646,7 @@ var AgUiThreadRuntimeCore = class {
597
646
  const message = this.session.tryGetMessage(sessionMessageId)?.message;
598
647
  const history = this.history;
599
648
  if (!message || !history?.update || !this.isPersistableStatus(message.status)) return;
600
- await this.chainHistoryWrite(sessionMessageId, () => history.update({
649
+ await this.historyWrites.chain(sessionMessageId, () => history.update({
601
650
  parentId: item.parentId,
602
651
  message
603
652
  }));
@@ -614,25 +663,42 @@ var AgUiThreadRuntimeCore = class {
614
663
  const userAction = { ...action };
615
664
  delete userAction.type;
616
665
  if (!("timestamp" in userAction)) userAction.timestamp = (/* @__PURE__ */ new Date()).toISOString();
617
- this.pendingA2uiAction = userAction;
666
+ this.dispatch({
667
+ type: "a2uiActionDeferred",
668
+ action: userAction
669
+ });
618
670
  const owner = this.abortController;
619
671
  if (owner) {
620
- this.pendingA2uiResumeOwner = owner;
672
+ this.dispatch({
673
+ type: "a2uiResumeDeferred",
674
+ owner
675
+ });
621
676
  return;
622
677
  }
623
678
  this.startResumeRun(parentId);
624
679
  }
625
- maybeResumeAfterToolResults(messageId) {
680
+ maybeResumeAfterToolResults(messageId, accountedResultIds) {
626
681
  if (!this.maybeCompleteAfterToolResults(messageId)) return;
682
+ if (accountedResultIds) {
683
+ const message = this.session.tryGetMessage(messageId)?.message;
684
+ if (message?.role !== "assistant" || !Array.from(iterateToolCallParts(message.content)).some((part) => isResolvedToolCall(part) && !accountedResultIds.has(part.toolCallId))) return;
685
+ }
686
+ if (this.getPendingInterrupts()) return;
687
+ const turn = this.getTrailingAssistantTurn();
688
+ if (!turn.some((message) => message.id === messageId)) return;
627
689
  const owner = this.abortController;
628
690
  if (owner) {
629
- this.pendingResume = {
691
+ this.dispatch({
692
+ type: "resumeDeferred",
630
693
  owner,
631
694
  messageId
632
- };
695
+ });
633
696
  return;
634
697
  }
635
- this.startResumeRun(messageId);
698
+ if (this.getPendingToolCalls()) return;
699
+ const headId = turn.at(-1)?.id;
700
+ if (headId === void 0) return;
701
+ this.startResumeRun(headId);
636
702
  }
637
703
  maybeCompleteAfterToolResults(messageId) {
638
704
  const message = this.session.tryGetMessage(messageId)?.message;
@@ -640,9 +706,13 @@ var AgUiThreadRuntimeCore = class {
640
706
  const assistant = message;
641
707
  if (assistant.status?.type !== "requires-action" || assistant.status.reason !== "tool-calls") return false;
642
708
  let allResolved = true;
643
- for (const part of iterateToolCallParts(assistant.content)) if (!isResolvedToolCall(part)) {
644
- allResolved = false;
645
- break;
709
+ const listed = listedPendingToolCallIds(assistant);
710
+ for (const part of iterateToolCallParts(assistant.content)) {
711
+ if (listed && !listed.has(part.toolCallId)) continue;
712
+ if (!isResolvedToolCall(part)) {
713
+ allResolved = false;
714
+ break;
715
+ }
646
716
  }
647
717
  if (!allResolved) return false;
648
718
  if (!this.session.updateMessage(messageId, (current) => current.role === "assistant" ? {
@@ -650,14 +720,15 @@ var AgUiThreadRuntimeCore = class {
650
720
  status: {
651
721
  type: "complete",
652
722
  reason: "unknown"
653
- }
723
+ },
724
+ metadata: withoutPendingToolCallIds(current.metadata)
654
725
  } : current)) return false;
655
726
  this.publishUpdate();
656
727
  this.persistAssistantHistory(messageId);
657
728
  return true;
658
729
  }
659
730
  startResumeRun(messageId) {
660
- this.startRun(messageId, this.lastRunConfig).catch((error) => {
731
+ this.startRun(messageId, this.state.lastRunConfig).catch((error) => {
661
732
  this.reportError(error);
662
733
  });
663
734
  }
@@ -674,8 +745,7 @@ var AgUiThreadRuntimeCore = class {
674
745
  }
675
746
  };
676
747
  });
677
- this.pendingA2uiResumeOwner = null;
678
- this.pendingA2uiAction = void 0;
748
+ this.dispatch({ type: "a2uiContinuationsCleared" });
679
749
  this.assistantHistoryParents.clear();
680
750
  if (messages.length === 0) this.session.clear();
681
751
  else {
@@ -683,6 +753,10 @@ var AgUiThreadRuntimeCore = class {
683
753
  let lastAppliedId = null;
684
754
  let hardReplace = false;
685
755
  const seen = /* @__PURE__ */ new Set();
756
+ const retainToolResults = (existing, message) => existing?.role === "assistant" && message.role === "assistant" ? {
757
+ ...message,
758
+ content: this.preserveToolResults(existing.content, message.content)
759
+ } : message;
686
760
  for (const message of messages) {
687
761
  if (seen.has(message.id)) continue;
688
762
  seen.add(message.id);
@@ -691,11 +765,12 @@ var AgUiThreadRuntimeCore = class {
691
765
  hardReplace = true;
692
766
  break;
693
767
  }
694
- this.session.addOrUpdateMessage(expectedParentId, message);
768
+ this.session.addOrUpdateMessage(expectedParentId, retainToolResults(existing?.message, message));
695
769
  expectedParentId = message.id;
696
770
  lastAppliedId = message.id;
697
771
  }
698
772
  if (hardReplace) {
773
+ const previous = new Map(this.getMessageRepository().messages.map(({ message }) => [message.id, message]));
699
774
  this.session.clear();
700
775
  expectedParentId = null;
701
776
  lastAppliedId = null;
@@ -703,39 +778,55 @@ var AgUiThreadRuntimeCore = class {
703
778
  for (const message of messages) {
704
779
  if (seen.has(message.id)) continue;
705
780
  seen.add(message.id);
706
- this.session.addOrUpdateMessage(expectedParentId, message);
781
+ this.session.addOrUpdateMessage(expectedParentId, retainToolResults(previous.get(message.id), message));
707
782
  expectedParentId = message.id;
708
783
  lastAppliedId = message.id;
709
784
  }
710
785
  }
711
786
  this.session.resetHead(lastAppliedId);
787
+ for (const messageId of seen) this.maybeCompleteAfterToolResults(messageId);
712
788
  }
713
789
  this.snapshotHistoryIds.clear();
714
790
  for (const { message } of this.getMessageRepository().messages) this.snapshotHistoryIds.add(message.id);
715
- const resumeTarget = this.pendingResume?.messageId;
716
- if (resumeTarget !== void 0 && !this.session.getMessages().some((message) => message.id === resumeTarget)) this.pendingResume = null;
791
+ const pendingResume = this.state.pendingResume;
792
+ if (pendingResume) {
793
+ const selectedResultOwnerIds = new Set(this.session.getMessages().filter(hasResolvedToolCalls).map((message) => message.id));
794
+ const retained = pendingResume.messageIds.filter((id) => selectedResultOwnerIds.has(id));
795
+ if (retained.length !== pendingResume.messageIds.length) {
796
+ this.dispatch({ type: "resumeCleared" });
797
+ for (const messageId of retained) this.dispatch({
798
+ type: "resumeDeferred",
799
+ owner: pendingResume.owner,
800
+ messageId
801
+ });
802
+ }
803
+ }
717
804
  this.publishUpdate();
718
805
  }
719
806
  loadExternalState(state) {
720
- this.stateSnapshot = state;
807
+ this.dispatch({
808
+ type: "stateSnapshot",
809
+ snapshot: state
810
+ });
721
811
  this.publishUpdate();
722
812
  }
723
813
  setState(next) {
724
- this.stateSnapshot = typeof next === "function" ? next(this.stateSnapshot) : next;
814
+ const snapshot = typeof next === "function" ? next(this.state.stateSnapshot) : next;
815
+ this.dispatch({
816
+ type: "stateSnapshot",
817
+ snapshot
818
+ });
725
819
  this.publishUpdate();
726
820
  }
727
821
  resetState() {
728
- this.stateSnapshot = void 0;
822
+ this.dispatch({ type: "resetState" });
729
823
  this.publishUpdate();
730
824
  }
731
825
  resetThreadState() {
732
- this.historyMutationGeneration++;
826
+ this.dispatch({ type: "historyMutationAdvanced" });
733
827
  const controller = this.abortController;
734
828
  const activeRunAgent = this.activeRunAgent;
735
- this.stateSnapshot = void 0;
736
- this.pendingResume = null;
737
- this.pendingA2uiResumeOwner = null;
738
- this.pendingA2uiAction = void 0;
829
+ this.dispatch({ type: "resetThread" });
739
830
  if (controller) {
740
831
  this.abortController = null;
741
832
  this.activeRunAgent = null;
@@ -749,52 +840,245 @@ var AgUiThreadRuntimeCore = class {
749
840
  }
750
841
  } else this.publishUpdate();
751
842
  }
752
- async startRun(parentId, runConfig, resume, resumeStream) {
843
+ async startRun(parentId, runConfig, resume, resumeStream, replayExistingMessages = false) {
753
844
  if (this.isThreadSwitching?.()) return;
754
845
  if (!this.supersedeActiveRun()) return;
846
+ const generation = this.state.historyMutationGeneration;
755
847
  const normalizedRunConfig = runConfig ?? {};
756
- this.lastRunConfig = normalizedRunConfig;
848
+ this.dispatch({
849
+ type: "runConfigRecorded",
850
+ runConfig: normalizedRunConfig
851
+ });
757
852
  const parent = parentId === null ? void 0 : this.session.tryGetMessage(parentId);
758
853
  const shouldEagerlyInsertAssistant = parentId !== null && parent !== void 0 && parentId !== this.session.headId;
759
854
  const historicalMessages = [...shouldEagerlyInsertAssistant ? this.session.tryGetMessages(parentId) ?? this.session.getMessages() : this.session.getMessages()];
760
855
  const runStartMessageIds = new Set(this.getMessageRepository().messages.map(({ message }) => message.id));
856
+ const selectedMessages = this.session.getMessages();
857
+ const parentIndex = selectedMessages.findIndex((message) => message.id === parentId);
858
+ let replayStart = parentIndex + 1;
859
+ if (selectedMessages[parentIndex]?.role === "assistant") {
860
+ replayStart = parentIndex;
861
+ while (replayStart > 0 && selectedMessages[replayStart - 1]?.role === "assistant") replayStart--;
862
+ }
863
+ const replayMessageIds = [];
864
+ if (replayExistingMessages && !resumeStream && parentIndex >= 0) for (const message of selectedMessages.slice(replayStart)) {
865
+ if (message.role !== "assistant") break;
866
+ replayMessageIds.push(message.id);
867
+ }
868
+ const accountedResultIds = new Set(historicalMessages.flatMap((message) => message.role === "assistant" ? Array.from(iterateToolCallParts(message.content)).filter(isResolvedToolCall).map((part) => part.toolCallId) : []));
869
+ const candidateContexts = /* @__PURE__ */ new Map();
870
+ for (const message of selectedMessages) {
871
+ if (message.role !== "assistant" || !replayMessageIds.includes(message.id)) continue;
872
+ candidateContexts.set(message.id, {
873
+ originalContent: message.content,
874
+ accountedResultIds,
875
+ admittedToolCallIds: actionableToolCallIds(message),
876
+ merge: createReplayContentMerger()
877
+ });
878
+ }
879
+ let replayContexts = /* @__PURE__ */ new Map();
880
+ let lastReplayedIndex = -1;
881
+ let replayEnded = replayMessageIds.length === 0;
882
+ const replayTail = replayMessageIds.at(-1);
761
883
  const joinStrategy = this.joinStrategy;
762
884
  const hasRunUserBoundary = parent?.message.role === "user" && !resume;
763
885
  let pendingError = null;
764
886
  const assistantParentId = parent ? parentId : this.session.headId;
765
887
  let assistantMessageId;
888
+ let snapshotAssistantId;
889
+ let replayInterruptMessageId;
766
890
  let assistantCollided = false;
891
+ const heldMessages = /* @__PURE__ */ new Map();
892
+ let frameOwnerIds = /* @__PURE__ */ new Set();
893
+ let restrictedPendingToolCallIds;
894
+ let settlingRun = false;
767
895
  const ensureAssistant = (allowRecreate = false) => {
768
896
  const cached = assistantMessageId;
769
897
  if (cached !== void 0 && this.session.tryGetMessage(cached)) return cached;
770
898
  if (cached !== void 0 && (assistantCollided || !allowRecreate)) return cached;
771
899
  const repositoryHeadId = this.session.headId;
772
- const parentId = (cached === void 0 || shouldEagerlyInsertAssistant && (repositoryHeadId === null || runStartMessageIds.has(repositoryHeadId))) && assistantParentId && this.session.hasMessage(assistantParentId) ? assistantParentId : repositoryHeadId;
900
+ const parentId = replayContexts.size === 0 && (cached === void 0 || shouldEagerlyInsertAssistant && (repositoryHeadId === null || runStartMessageIds.has(repositoryHeadId))) && (replayMessageIds.length === 0 || repositoryHeadId === null || runStartMessageIds.has(repositoryHeadId)) && assistantParentId && this.session.hasMessage(assistantParentId) ? assistantParentId : repositoryHeadId;
773
901
  const created = this.insertAssistantPlaceholder(parentId, cached);
774
902
  assistantMessageId = created;
775
903
  this.markPendingAssistantHistory(created, parentId);
776
904
  return created;
777
905
  };
778
- if (shouldEagerlyInsertAssistant) ensureAssistant();
906
+ if (shouldEagerlyInsertAssistant && replayEnded) ensureAssistant();
907
+ const settleHeldMessages = (terminalStatus) => {
908
+ const pendingMessages = [];
909
+ const producerPending = restrictedPendingToolCallIds;
910
+ const canWaitForTools = terminalStatus.type === "complete" || terminalStatus.type === "requires-action";
911
+ if (producerPending && canWaitForTools) for (const message of this.getTrailingAssistantTurn()) {
912
+ const replay = replayContexts.get(message.id);
913
+ if (message.id === assistantMessageId || heldMessages.has(message.id) || !replay || message.status?.type === "requires-action" && message.status.reason === "interrupt") continue;
914
+ if (replay.admittedToolCallIds.size > 0 || actionableToolCallIds(message).size > 0 || Array.from(iterateToolCallParts(message.content)).some((part) => producerPending.has(part.toolCallId) && !accountedResultIds.has(part.toolCallId))) heldMessages.set(message.id, /* @__PURE__ */ new Set());
915
+ }
916
+ for (const [messageId, openToolCallIds] of heldMessages) {
917
+ const existing = this.session.tryGetMessage(messageId);
918
+ if (existing) this.markPendingAssistantHistory(messageId, existing.parentId);
919
+ let requiresToolResult = false;
920
+ if (this.session.updateMessage(messageId, (message) => {
921
+ if (message.role !== "assistant") return message;
922
+ const assistant = message;
923
+ const pendingToolCallIds = producerPending ? Array.from(iterateToolCallParts(assistant.content)).filter((part) => producerPending.has(part.toolCallId) && (!isResolvedToolCall(part) || openToolCallIds.has(part.toolCallId) || replayContexts.has(messageId) && !accountedResultIds.has(part.toolCallId))).map((part) => part.toolCallId) : Array.from(openToolCallIds);
924
+ const replay = replayContexts.get(messageId);
925
+ if (replay) replay.admittedToolCallIds = new Set(pendingToolCallIds);
926
+ const status = canWaitForTools ? pendingToolCallIds.length > 0 ? {
927
+ type: "requires-action",
928
+ reason: "tool-calls"
929
+ } : {
930
+ type: "complete",
931
+ reason: "unknown"
932
+ } : terminalStatus;
933
+ requiresToolResult = status.type === "requires-action";
934
+ const metadata = withoutPendingToolCallIds(assistant.metadata);
935
+ return {
936
+ ...assistant,
937
+ status,
938
+ metadata: status.type === "requires-action" && (restrictedPendingToolCallIds || replayContexts.has(messageId)) ? withPendingToolCallIds(metadata, pendingToolCallIds) : metadata
939
+ };
940
+ })) {
941
+ this.publishUpdate();
942
+ this.persistAssistantHistory(messageId);
943
+ if (requiresToolResult) pendingMessages.push(messageId);
944
+ }
945
+ }
946
+ heldMessages.clear();
947
+ return pendingMessages;
948
+ };
949
+ const accountProducerResults = (content) => {
950
+ for (const part of iterateToolCallParts(content)) if (isResolvedToolCall(part)) accountedResultIds.add(part.toolCallId);
951
+ };
952
+ const reconcileReplaySnapshot = (snapshot, previous) => {
953
+ accountProducerResults(snapshot.content);
954
+ const replay = candidateContexts.get(snapshot.id);
955
+ if (!replay) return snapshot;
956
+ replay.admittedToolCallIds = actionableToolCallIds(snapshot);
957
+ if (heldMessages.has(snapshot.id)) heldMessages.set(snapshot.id, new Set(replay.admittedToolCallIds));
958
+ if (!replayContexts.has(snapshot.id)) return snapshot;
959
+ return previous ? {
960
+ ...snapshot,
961
+ content: this.preserveToolResults(previous.content, snapshot.content, replay.originalContent)
962
+ } : snapshot;
963
+ };
779
964
  const applyUpdate = (update) => {
965
+ if (!assistantCollided && replayContexts.size && update.content !== void 0) accountProducerResults(update.content);
966
+ let ownerParts;
967
+ let contentOwnerIds;
780
968
  const hasStreamContent = Array.isArray(update.content) && update.content.some((part) => part.type !== "data");
781
- const resolved = this.updateAssistantMessage(ensureAssistant(hasStreamContent), update);
782
- if (resolved !== assistantMessageId) assistantMessageId = resolved;
969
+ if (!replayEnded && assistantMessageId === void 0) {
970
+ if (!update.status || update.status.type === "running") return;
971
+ replayEnded = true;
972
+ if (replayContexts.size === 0 && snapshotAssistantId !== void 0 && this.session.headId === snapshotAssistantId && !hasStreamContent) {
973
+ if (update.status.type !== "complete") this.updateAssistantMessage(snapshotAssistantId, {
974
+ status: update.status,
975
+ ...update.metadata ? { metadata: update.metadata } : {}
976
+ });
977
+ return;
978
+ }
979
+ }
980
+ const streamMessageId = assistantMessageId;
981
+ let primaryMessageId = streamMessageId;
982
+ const terminalStatus = update.status;
983
+ const replayInterrupt = replayContexts.size > 0 && terminalStatus?.type === "requires-action" && terminalStatus.reason === "interrupt";
984
+ if (streamMessageId !== void 0 && !assistantCollided && replayContexts.size > 0 && (update.content !== void 0 || replayInterrupt)) {
985
+ const owners = this.getTrailingAssistantTurn().filter((message) => replayContexts.has(message.id));
986
+ const grouped = update.content !== void 0 ? groupReplayToolContent(owners, update.content, streamMessageId) : /* @__PURE__ */ new Map([[streamMessageId, [...update.content ?? []]]]);
987
+ if (update.content !== void 0) {
988
+ const currentOwnerIds = new Set(grouped.keys());
989
+ for (const ownerId of frameOwnerIds) grouped.set(ownerId, grouped.get(ownerId) ?? []);
990
+ contentOwnerIds = new Set(grouped.keys());
991
+ frameOwnerIds = currentOwnerIds;
992
+ }
993
+ ownerParts = grouped;
994
+ if (replayInterrupt) {
995
+ const interrupts = (update.metadata?.custom?.[AG_UI_METADATA_NAMESPACE])?.interrupts;
996
+ const approvals = projectAgUiToolApprovals(interrupts);
997
+ for (const owner of owners) if (Array.from(iterateToolCallParts(owner.content)).some((part) => approvals.has(part.toolCallId))) grouped.set(owner.id, grouped.get(owner.id) ?? []);
998
+ const anchor = owners.find((message) => projectAgUiToolApprovals(interrupts, new Set(Array.from(iterateToolCallParts([...message.content, ...grouped.get(message.id) ?? []])).map((part) => part.toolCallId))).size > 0);
999
+ if (anchor) {
1000
+ primaryMessageId = anchor.id;
1001
+ replayInterruptMessageId = anchor.id;
1002
+ grouped.set(anchor.id, grouped.get(anchor.id) ?? []);
1003
+ }
1004
+ }
1005
+ }
1006
+ const pendingMessages = settlingRun && terminalStatus && terminalStatus.type !== "running" ? settleHeldMessages(terminalStatus) : [];
1007
+ const frameMessageId = ensureAssistant(hasStreamContent);
1008
+ const messageId = primaryMessageId ?? frameMessageId;
1009
+ let primaryUpdate = update;
1010
+ if (ownerParts) {
1011
+ const interrupt = terminalStatus?.type === "requires-action" && terminalStatus.reason === "interrupt";
1012
+ const ownerStatus = terminalStatus?.type === "requires-action" ? {
1013
+ type: "complete",
1014
+ reason: "unknown"
1015
+ } : terminalStatus;
1016
+ for (const [ownerId, content] of ownerParts) {
1017
+ const existing = this.session.tryGetMessage(ownerId);
1018
+ if (existing) this.markPendingAssistantHistory(ownerId, existing.parentId);
1019
+ if (ownerId === messageId) continue;
1020
+ this.updateAssistantMessage(ownerId, {
1021
+ ...contentOwnerIds?.has(ownerId) ? { content } : {},
1022
+ ...ownerStatus ? { status: ownerStatus } : {}
1023
+ }, replayContexts.get(ownerId), interrupt ? /* @__PURE__ */ new Set() : restrictedPendingToolCallIds);
1024
+ }
1025
+ const { content: _content, ...controlUpdate } = update;
1026
+ const content = ownerParts.get(messageId) ?? [];
1027
+ primaryUpdate = {
1028
+ ...controlUpdate,
1029
+ ...contentOwnerIds?.has(messageId) ? { content } : {},
1030
+ ...ownerStatus && !interrupt && replayContexts.has(messageId) ? { status: ownerStatus } : {}
1031
+ };
1032
+ }
1033
+ const resolved = this.updateAssistantMessage(messageId, primaryUpdate, replayContexts.get(messageId), restrictedPendingToolCallIds);
1034
+ if (messageId === frameMessageId && resolved !== assistantMessageId) assistantMessageId = resolved;
1035
+ for (const messageId of pendingMessages) this.maybeResumeAfterToolResults(messageId, replayMessageIds.includes(messageId) ? accountedResultIds : void 0);
783
1036
  };
784
- const adoptServerMessageId = (serverId, startNewMessage) => {
1037
+ const adoptServerMessageId = (serverId, startNewMessage, openToolCallIds = []) => {
1038
+ const existing = this.session.tryGetMessage(serverId);
1039
+ const replayIndex = replayMessageIds.indexOf(serverId);
1040
+ const adoptsReplay = !replayEnded && !assistantCollided && replayIndex > lastReplayedIndex && existing?.message.role === "assistant" && this.session.getMessages().some((message) => message.id === serverId);
1041
+ if (adoptsReplay && replayContexts.size === 0) {
1042
+ replayContexts = candidateContexts;
1043
+ const selected = this.session.getMessages();
1044
+ let tailId = serverId;
1045
+ for (const message of selected.slice(selected.findIndex((message) => message.id === serverId) + 1)) {
1046
+ if (message.role !== "assistant") break;
1047
+ tailId = message.id;
1048
+ }
1049
+ this.session.selectHead(tailId);
1050
+ }
785
1051
  if (startNewMessage && assistantMessageId !== void 0) {
786
- applyUpdate({ status: {
1052
+ let hasReplayCallsToHold = false;
1053
+ const replay = replayContexts.get(assistantMessageId);
1054
+ if (replay) {
1055
+ const current = this.session.tryGetMessage(assistantMessageId)?.message;
1056
+ if (current?.role === "assistant") {
1057
+ const retainedPending = Array.from(iterateToolCallParts(current.content)).filter((part) => !isResolvedToolCall(part) || replay.admittedToolCallIds.has(part.toolCallId) && !accountedResultIds.has(part.toolCallId));
1058
+ hasReplayCallsToHold = retainedPending.length > 0;
1059
+ openToolCallIds = [.../* @__PURE__ */ new Set([...openToolCallIds, ...retainedPending.filter((part) => replay.admittedToolCallIds.has(part.toolCallId)).map((part) => part.toolCallId)])];
1060
+ }
1061
+ }
1062
+ if (openToolCallIds.length > 0 || hasReplayCallsToHold) heldMessages.set(assistantMessageId, new Set(openToolCallIds));
1063
+ else applyUpdate({ status: {
787
1064
  type: "complete",
788
1065
  reason: "unknown"
789
1066
  } });
790
1067
  const previousId = assistantMessageId;
791
1068
  assistantMessageId = void 0;
792
- if (this.session.tryGetMessage(previousId)) {
1069
+ if (!adoptsReplay && this.session.tryGetMessage(previousId)) {
793
1070
  const created = this.insertAssistantPlaceholder(previousId);
794
1071
  assistantMessageId = created;
795
1072
  this.markPendingAssistantHistory(created, previousId);
796
1073
  }
797
1074
  }
1075
+ if (adoptsReplay) {
1076
+ assistantMessageId = serverId;
1077
+ lastReplayedIndex = replayIndex;
1078
+ this.markPendingAssistantHistory(serverId, existing.parentId);
1079
+ return;
1080
+ }
1081
+ replayEnded = true;
798
1082
  const placeholder = ensureAssistant(true);
799
1083
  if (placeholder === serverId) return;
800
1084
  const reassigned = this.reassignAssistantId(placeholder, serverId);
@@ -817,14 +1101,60 @@ var AgUiThreadRuntimeCore = class {
817
1101
  onServerMessageId: (serverId) => {
818
1102
  adoptServerMessageId(serverId, false);
819
1103
  },
820
- onTextMessageStart: (serverId) => adoptServerMessageId(serverId, true)
1104
+ onTextMessageStart: (serverId, openToolCallIds) => {
1105
+ const owners = this.getTrailingAssistantTurn().filter((message) => replayContexts.has(message.id));
1106
+ const toolOwners = indexReplayToolOwners(owners);
1107
+ const frameOpenIds = openToolCallIds.filter((id) => {
1108
+ const ownerId = toolOwners.get(id);
1109
+ if (!ownerId || ownerId === assistantMessageId || assistantCollided) return true;
1110
+ const held = heldMessages.get(ownerId) ?? /* @__PURE__ */ new Set();
1111
+ held.add(id);
1112
+ heldMessages.set(ownerId, held);
1113
+ return false;
1114
+ });
1115
+ for (const ownerId of frameOwnerIds) {
1116
+ const replay = replayContexts.get(ownerId);
1117
+ if (replay) replay.merge = createReplayContentMerger();
1118
+ }
1119
+ frameOwnerIds.clear();
1120
+ adoptServerMessageId(serverId, true, frameOpenIds);
1121
+ }
821
1122
  });
822
1123
  let runFinished = false;
823
1124
  const dispatch = (event) => {
824
1125
  if (this.abortController !== abortController) return;
825
- if (event.type === "RUN_FINISHED") runFinished = true;
826
- const nextAssistantMessageId = this.handleEvent(aggregator, event, assistantMessageId, joinStrategy, runStartMessageIds, hasRunUserBoundary);
827
- if (event.type === "MESSAGES_SNAPSHOT") assistantMessageId = nextAssistantMessageId;
1126
+ if (event.type === "RUN_FINISHED") {
1127
+ runFinished = true;
1128
+ if (event.outcome?.type === "interrupt") replayInterruptMessageId = assistantMessageId ?? replayTail;
1129
+ const ids = event.outcome?.type === "success" ? event.outcome.pendingToolCallIds : void 0;
1130
+ restrictedPendingToolCallIds = ids === void 0 ? void 0 : new Set(ids);
1131
+ }
1132
+ if (event.type === "TOOL_CALL_RESULT") {
1133
+ if (!assistantCollided && candidateContexts.size) accountedResultIds.add(event.toolCallId);
1134
+ for (const openToolCallIds of heldMessages.values()) openToolCallIds.delete(event.toolCallId);
1135
+ }
1136
+ settlingRun = event.type === "RUN_FINISHED" || event.type === "RUN_ERROR" || event.type === "RUN_CANCELLED";
1137
+ const beforeSnapshot = event.type === "MESSAGES_SNAPSHOT" ? this.session.getMessages() : [];
1138
+ try {
1139
+ const nextAssistantMessageId = this.handleEvent(aggregator, event, assistantMessageId, joinStrategy, runStartMessageIds, hasRunUserBoundary, candidateContexts.size ? reconcileReplaySnapshot : void 0);
1140
+ if (event.type === "MESSAGES_SNAPSHOT") {
1141
+ for (const message of beforeSnapshot) {
1142
+ const context = replayContexts.get(message.id);
1143
+ const current = this.session.tryGetMessage(message.id)?.message;
1144
+ if (context && current?.content !== message.content) {
1145
+ context.merge = createReplayContentMerger();
1146
+ frameOwnerIds.delete(message.id);
1147
+ }
1148
+ }
1149
+ assistantMessageId = nextAssistantMessageId;
1150
+ if (!replayEnded && assistantMessageId === void 0) {
1151
+ const tail = this.session.getMessages().at(-1);
1152
+ snapshotAssistantId = tail?.role === "assistant" ? tail.id : void 0;
1153
+ }
1154
+ }
1155
+ } finally {
1156
+ settlingRun = false;
1157
+ }
828
1158
  };
829
1159
  this.abortController = abortController;
830
1160
  const runAgentInstance = this.agent;
@@ -898,16 +1228,46 @@ var AgUiThreadRuntimeCore = class {
898
1228
  this.clearDeferredContinuations(abortController);
899
1229
  throw err;
900
1230
  }
901
- if (this.pendingResume?.owner === abortController) {
902
- const { messageId } = this.pendingResume;
903
- this.pendingResume = null;
904
- if (!abortSignal.aborted) this.startResumeRun(messageId);
1231
+ const pending = replayInterruptMessageId ? this.getPendingInterrupts() : null;
1232
+ if (pending && pending.messageId === replayInterruptMessageId && replayContexts.has(pending.messageId) && !abortSignal.aborted && this.abortController === null && generation === this.state.historyMutationGeneration && !this.isThreadSwitching?.()) {
1233
+ const message = this.session.tryGetMessage(pending.messageId)?.message;
1234
+ const responses = message?.role === "assistant" ? buildToolApprovalResume(message.content, pending.interrupts) : null;
1235
+ if (responses) {
1236
+ this.clearDeferredContinuations(abortController);
1237
+ try {
1238
+ await this.submitInterruptResponses(responses);
1239
+ } catch (error) {
1240
+ const err = error instanceof Error ? error : new Error(String(error));
1241
+ if (!this.reportedErrors.has(err)) {
1242
+ this.reportError(err);
1243
+ this.reportedErrors.add(err);
1244
+ }
1245
+ throw err;
1246
+ }
1247
+ return;
1248
+ }
905
1249
  }
906
- if (this.pendingA2uiResumeOwner === abortController) {
907
- this.pendingA2uiResumeOwner = null;
908
- if (!abortSignal.aborted && this.pendingA2uiAction !== void 0) {
1250
+ if (this.state.pendingResume?.owner === abortController) {
1251
+ const { messageIds } = this.state.pendingResume;
1252
+ this.dispatch({ type: "resumeCleared" });
1253
+ if (!abortSignal.aborted && !this.getPendingInterrupts() && !this.getPendingToolCalls()) {
1254
+ const turn = this.getTrailingAssistantTurn();
1255
+ if (turn.some((message) => {
1256
+ if (!messageIds.includes(message.id)) return false;
1257
+ const replay = replayContexts.get(message.id);
1258
+ if (!replay) return hasResolvedToolCalls(message);
1259
+ return Array.from(iterateToolCallParts(message.content)).some((part) => isResolvedToolCall(part) && replay.admittedToolCallIds.has(part.toolCallId) && !accountedResultIds.has(part.toolCallId));
1260
+ })) {
1261
+ const headId = turn.at(-1)?.id;
1262
+ if (headId !== void 0) this.startResumeRun(headId);
1263
+ }
1264
+ }
1265
+ }
1266
+ if (this.state.pendingA2uiResumeOwner === abortController) {
1267
+ this.dispatch({ type: "a2uiResumeCleared" });
1268
+ if (!abortSignal.aborted && this.state.pendingA2uiAction !== void 0) {
909
1269
  if (this.getPendingInterrupts()) {
910
- this.pendingA2uiAction = void 0;
1270
+ this.dispatch({ type: "a2uiActionCleared" });
911
1271
  this.logger.debug("[agui] sendA2uiAction: pending interrupts, dropping action");
912
1272
  return;
913
1273
  }
@@ -915,15 +1275,14 @@ var AgUiThreadRuntimeCore = class {
915
1275
  const parentId = this.session.headId;
916
1276
  if (parentId !== null) this.startResumeRun(parentId);
917
1277
  else {
918
- this.pendingA2uiAction = void 0;
1278
+ this.dispatch({ type: "a2uiActionCleared" });
919
1279
  this.logger.debug("[agui] sendA2uiAction: no messages to resume, dropping action");
920
1280
  }
921
- } else this.pendingA2uiAction = void 0;
1281
+ } else this.dispatch({ type: "a2uiActionCleared" });
922
1282
  }
923
1283
  }
924
1284
  async consumeResumeStream(stream, ctx) {
925
- this.pendingA2uiAction = void 0;
926
- this.pendingA2uiResumeOwner = null;
1285
+ this.dispatch({ type: "a2uiContinuationsCleared" });
927
1286
  const assistantId = ctx.ensureAssistant();
928
1287
  const currentId = () => ctx.getAssistantMessageId() ?? assistantId;
929
1288
  const options = {
@@ -964,8 +1323,8 @@ var AgUiThreadRuntimeCore = class {
964
1323
  } });
965
1324
  }
966
1325
  buildRunInput(runId, runConfig, historyMessages, resume) {
967
- const pendingA2uiAction = this.pendingA2uiAction;
968
- this.pendingA2uiAction = void 0;
1326
+ const pendingA2uiAction = this.state.pendingA2uiAction;
1327
+ this.dispatch({ type: "a2uiActionCleared" });
969
1328
  const threadId = this.agent.threadId || "main";
970
1329
  const messages = toAgUiMessages(this.selectRunMessages(historyMessages ?? this.session.getMessages(), resume));
971
1330
  const context = this.runtime?.thread.getModelContext();
@@ -983,7 +1342,7 @@ var AgUiThreadRuntimeCore = class {
983
1342
  return {
984
1343
  threadId,
985
1344
  runId,
986
- state: this.stateSnapshot ?? null,
1345
+ state: this.state.stateSnapshot ?? null,
987
1346
  messages,
988
1347
  tools: toAgUiTools(context?.tools),
989
1348
  context: [...context?.system ? [{
@@ -1013,14 +1372,11 @@ var AgUiThreadRuntimeCore = class {
1013
1372
  return messages.slice(anchor + 1);
1014
1373
  }
1015
1374
  clearDeferredContinuations(owner) {
1016
- if (this.pendingResume?.owner === owner) this.pendingResume = null;
1017
- if (this.pendingA2uiResumeOwner === owner) {
1018
- this.pendingA2uiResumeOwner = null;
1019
- this.pendingA2uiAction = void 0;
1020
- }
1375
+ if (this.state.pendingResume?.owner === owner) this.dispatch({ type: "resumeCleared" });
1376
+ if (this.state.pendingA2uiResumeOwner === owner) this.dispatch({ type: "a2uiContinuationsCleared" });
1021
1377
  }
1022
1378
  setRunning(running) {
1023
- this.isRunningFlag = running;
1379
+ this.dispatch({ type: running ? "runStarted" : "runFinished" });
1024
1380
  this.publishUpdate();
1025
1381
  }
1026
1382
  finishRun(controller) {
@@ -1037,7 +1393,7 @@ var AgUiThreadRuntimeCore = class {
1037
1393
  status: { type: "running" },
1038
1394
  content: [],
1039
1395
  metadata: {
1040
- unstable_state: this.stateSnapshot ?? null,
1396
+ unstable_state: this.state.stateSnapshot ?? null,
1041
1397
  unstable_annotations: [],
1042
1398
  unstable_data: [],
1043
1399
  steps: [],
@@ -1080,32 +1436,55 @@ var AgUiThreadRuntimeCore = class {
1080
1436
  ids.delete(oldId);
1081
1437
  if (!collidesWithExisting) ids.add(newId);
1082
1438
  }
1083
- const pendingWrite = this.historyWrites.get(oldId);
1084
- if (pendingWrite) {
1085
- this.historyWrites.delete(oldId);
1086
- if (!collidesWithExisting) {
1087
- this.historyWrites.set(newId, pendingWrite);
1088
- const settle = () => {
1089
- if (this.historyWrites.get(newId) === pendingWrite) this.historyWrites.delete(newId);
1090
- };
1091
- pendingWrite.then(() => {
1092
- this.persistedHistoryIds.delete(oldId);
1093
- this.persistedHistoryIds.add(newId);
1094
- settle();
1095
- }, settle);
1096
- }
1097
- }
1439
+ const pendingWrite = this.historyWrites.move(oldId, collidesWithExisting ? void 0 : newId);
1440
+ if (pendingWrite && !collidesWithExisting) pendingWrite.then(() => {
1441
+ this.persistedHistoryIds.delete(oldId);
1442
+ this.persistedHistoryIds.add(newId);
1443
+ }, () => {});
1098
1444
  this.publishUpdate();
1099
1445
  return !collidesWithExisting;
1100
1446
  }
1101
- updateAssistantMessage(messageId, update) {
1447
+ updateAssistantMessage(messageId, update, replay, pendingToolCallIds) {
1102
1448
  let latestStatus;
1103
1449
  if (!this.session.updateMessage(messageId, (message) => {
1104
1450
  if (message.role !== "assistant") return message;
1105
1451
  const assistant = message;
1106
- const metadata = update.metadata ? this.mergeAssistantMetadata(assistant.metadata, update.metadata) : assistant.metadata;
1452
+ const mergedMetadata = update.metadata ? this.mergeAssistantMetadata(assistant.metadata, update.metadata) : assistant.metadata;
1107
1453
  latestStatus = update.status ?? assistant.status;
1108
- const content = update.content !== void 0 ? this.preserveToolResults(assistant.content, update.content) : assistant.content;
1454
+ let content = update.content !== void 0 ? replay ? replay.merge(assistant.content, update.content) : this.preserveToolResults(assistant.content, update.content) : assistant.content;
1455
+ const calls = replay ? Array.from(iterateToolCallParts(content)) : [];
1456
+ const pending = pendingToolCallIds ?? (replay ? /* @__PURE__ */ new Set([...replay.admittedToolCallIds, ...Array.from(iterateToolCallParts(update.content ?? [])).filter((part) => !isResolvedToolCall(part)).map((part) => part.toolCallId)]) : void 0);
1457
+ if (replay && latestStatus?.type !== "running" && pending) replay.admittedToolCallIds = pending;
1458
+ if (replay) {
1459
+ const interrupts = latestStatus?.type === "requires-action" && latestStatus.reason === "interrupt" ? mergedMetadata.custom["agui"]?.interrupts : void 0;
1460
+ const approvals = projectAgUiToolApprovals(interrupts, new Set(calls.map((part) => part.toolCallId)));
1461
+ const originalCalls = new Map(Array.from(iterateToolCallParts(replay.originalContent), (part) => [part.toolCallId, part]));
1462
+ content = mapToolCallPartsDeep(content, (part) => {
1463
+ const approval = approvals.get(part.toolCallId);
1464
+ if (approval) return {
1465
+ ...part,
1466
+ approval: preserveToolApprovalDecision(originalCalls.get(part.toolCallId), {
1467
+ ...part,
1468
+ approval
1469
+ }) ?? approval
1470
+ };
1471
+ if (!part.approval || part.approval.approved !== void 0 || part.approval.resolution) return part;
1472
+ const { approval: _approval, ...withoutApproval } = part;
1473
+ return withoutApproval;
1474
+ }).content;
1475
+ }
1476
+ if (replay && latestStatus?.type === "complete") {
1477
+ const listed = pending ?? listedPendingToolCallIds({
1478
+ ...assistant,
1479
+ metadata: mergedMetadata
1480
+ });
1481
+ if (calls.some((part) => (!isResolvedToolCall(part) || !replay.accountedResultIds.has(part.toolCallId)) && (!listed || listed.has(part.toolCallId)))) latestStatus = {
1482
+ type: "requires-action",
1483
+ reason: "tool-calls"
1484
+ };
1485
+ }
1486
+ const admission = pending && replay ? withPendingToolCallIds(mergedMetadata, pending) : mergedMetadata;
1487
+ const metadata = latestStatus?.type === "requires-action" && latestStatus.reason === "tool-calls" ? admission : replay && latestStatus?.type === "running" ? mergedMetadata : withoutPendingToolCallIds(admission);
1109
1488
  return {
1110
1489
  ...assistant,
1111
1490
  content,
@@ -1120,24 +1499,25 @@ var AgUiThreadRuntimeCore = class {
1120
1499
  resolvedMessageId = stableId;
1121
1500
  } else this.publishUpdate();
1122
1501
  if (this.isPersistableStatus(latestStatus)) this.persistAssistantHistory(resolvedMessageId);
1123
- this.maybeResumeAfterToolResults(resolvedMessageId);
1502
+ this.maybeResumeAfterToolResults(resolvedMessageId, replay?.accountedResultIds);
1124
1503
  return resolvedMessageId;
1125
1504
  }
1126
- preserveToolResults(previous, next) {
1505
+ preserveToolResults(previous, next, decisionSource) {
1127
1506
  const preserved = /* @__PURE__ */ new Map();
1128
1507
  for (const part of iterateToolCallParts(previous)) if (isResolvedToolCall(part)) preserved.set(part.toolCallId, part);
1129
- if (preserved.size === 0) return this.preserveToolInteractions(previous, next);
1508
+ const decisions = new Map(Array.from(iterateToolCallParts(decisionSource ?? [])).filter((part) => part.approval?.approved !== void 0).map((part) => [part.toolCallId, part]));
1509
+ if (preserved.size === 0 && decisions.size === 0) return this.preserveToolInteractions(previous, next);
1130
1510
  const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
1131
1511
  const prior = preserved.get(part.toolCallId);
1132
- if (!prior) return part;
1133
- if (!isResolvedToolCall(part) && isResolvedToolCall(prior)) return {
1134
- ...part,
1135
- result: prior.result,
1136
- ...prior.artifact !== void 0 ? { artifact: prior.artifact } : {},
1137
- ...prior.isError !== void 0 ? { isError: prior.isError } : {},
1138
- ...prior.modelContent !== void 0 ? { modelContent: prior.modelContent } : {}
1139
- };
1140
- return part;
1512
+ const result = prior ? preserveToolResult(prior, part) : part;
1513
+ if (result.approval && decisions.has(part.toolCallId)) {
1514
+ const approval = preserveToolApprovalDecision(decisions.get(part.toolCallId), result);
1515
+ if (approval && approval !== result.approval) return {
1516
+ ...result,
1517
+ approval
1518
+ };
1519
+ }
1520
+ return result;
1141
1521
  });
1142
1522
  return this.preserveToolInteractions(previous, changed ? merged : next);
1143
1523
  }
@@ -1240,18 +1620,23 @@ var AgUiThreadRuntimeCore = class {
1240
1620
  custom: incoming.custom ? mergeAgUiNamespace(current.custom, incoming.custom) : current.custom
1241
1621
  };
1242
1622
  }
1243
- handleEvent(aggregator, event, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary) {
1623
+ handleEvent(aggregator, event, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary, reconcileSnapshot) {
1244
1624
  switch (event.type) {
1245
1625
  case "STATE_SNAPSHOT":
1246
- this.stateSnapshot = event.snapshot;
1626
+ this.dispatch({
1627
+ type: "stateSnapshot",
1628
+ snapshot: event.snapshot
1629
+ });
1247
1630
  this.updateActiveAssistantState(activeAssistantId);
1248
1631
  break;
1249
1632
  case "STATE_DELTA":
1250
1633
  if (event.delta.length === 0) break;
1251
1634
  try {
1252
- const state = this.stateSnapshot ?? {};
1253
- const result = jsonpatch.applyPatch(state, event.delta, true, false);
1254
- this.stateSnapshot = result.newDocument;
1635
+ const result = jsonpatch.applyPatch(this.state.stateSnapshot ?? {}, event.delta, true, false);
1636
+ this.dispatch({
1637
+ type: "stateSnapshot",
1638
+ snapshot: result.newDocument
1639
+ });
1255
1640
  this.updateActiveAssistantState(activeAssistantId);
1256
1641
  } catch (error) {
1257
1642
  this.logger.error?.("[agui] failed to apply state delta", error);
@@ -1261,7 +1646,7 @@ var AgUiThreadRuntimeCore = class {
1261
1646
  const previousHeadId = this.session.headId;
1262
1647
  const activeAssistant = activeAssistantId === void 0 ? void 0 : this.session.tryGetMessage(activeAssistantId)?.message;
1263
1648
  const hasActiveText = activeAssistant?.role === "assistant" && activeAssistant.content.some((part) => part.type === "text");
1264
- this.importMessagesSnapshot(event.messages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary);
1649
+ this.importMessagesSnapshot(event.messages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary, reconcileSnapshot);
1265
1650
  const headId = this.session.headId;
1266
1651
  if (hasActiveText && headId !== previousHeadId) activeAssistantId = (headId === null ? void 0 : this.session.tryGetMessage(headId)?.message)?.role === "assistant" ? headId ?? void 0 : void 0;
1267
1652
  this.updateActiveAssistantState(activeAssistantId);
@@ -1302,7 +1687,7 @@ var AgUiThreadRuntimeCore = class {
1302
1687
  ...assistant,
1303
1688
  metadata: {
1304
1689
  ...assistant.metadata,
1305
- unstable_state: this.stateSnapshot ?? null
1690
+ unstable_state: this.state.stateSnapshot ?? null
1306
1691
  }
1307
1692
  };
1308
1693
  });
@@ -1313,25 +1698,27 @@ var AgUiThreadRuntimeCore = class {
1313
1698
  if (message.role !== "assistant") return message;
1314
1699
  const assistant = message;
1315
1700
  const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
1701
+ const modelText = toolContentText(event.content);
1316
1702
  let matchedToolCall = false;
1317
1703
  const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
1318
1704
  if (part.toolCallId !== event.toolCallId) return part;
1319
1705
  matchedToolCall = true;
1320
1706
  if (part.mcp?.app !== void 0 && isPlainObject(part.result) && Array.isArray(part.result["content"])) return {
1321
1707
  ...part,
1322
- ...part.modelContent === void 0 && event.content ? { modelContent: [{
1708
+ ...part.modelContent === void 0 ? { modelContent: [{
1323
1709
  type: "text",
1324
- text: event.content
1710
+ text: modelText
1325
1711
  }] } : {},
1326
1712
  ...part.isError === void 0 ? typeof event.mcpResult?.isError === "boolean" ? { isError: event.mcpResult.isError } : event.role === "tool" ? { isError: false } : {} : {},
1327
1713
  ...event.messageId ? { unstable_toolMessageId: event.messageId } : {}
1328
1714
  };
1715
+ const { modelContent: _modelContent, ...partWithoutModelContent } = part;
1329
1716
  return {
1330
- ...part,
1331
- result: event.mcpResult ?? tryParseJSON(event.content ?? ""),
1717
+ ...event.mcpResult === void 0 && Array.isArray(event.content) ? partWithoutModelContent : part,
1718
+ result: event.mcpResult ?? (Array.isArray(event.content) ? event.content : tryParseJSON(event.content)),
1332
1719
  ...event.mcpResult !== void 0 ? { modelContent: [{
1333
1720
  type: "text",
1334
- text: event.content
1721
+ text: modelText
1335
1722
  }] } : {},
1336
1723
  ...typeof event.mcpResult?.isError === "boolean" ? { isError: event.mcpResult.isError } : event.role === "tool" ? { isError: false } : {},
1337
1724
  ...part.mcp === void 0 && mcpAppUri !== void 0 ? { mcp: { app: { resourceUri: mcpAppUri } } } : {},
@@ -1386,7 +1773,7 @@ var AgUiThreadRuntimeCore = class {
1386
1773
  this.publishUpdate();
1387
1774
  this.maybeCompleteAfterToolResults(messageId);
1388
1775
  }
1389
- importMessagesSnapshot(rawMessages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary) {
1776
+ importMessagesSnapshot(rawMessages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary, reconcileSnapshot) {
1390
1777
  try {
1391
1778
  const activeAssistantItem = activeAssistantId ? this.session.tryGetMessage(activeAssistantId) : void 0;
1392
1779
  const activeMessage = activeAssistantItem?.message;
@@ -1404,10 +1791,14 @@ var AgUiThreadRuntimeCore = class {
1404
1791
  for (let index = 0; index < converted.length; index++) {
1405
1792
  const convertedMessage = converted[index];
1406
1793
  const existing = this.session.tryGetMessage(convertedMessage.id)?.message;
1407
- if (convertedMessage.role === "assistant" && existing?.role === "assistant") converted[index] = {
1408
- ...convertedMessage,
1409
- content: this.mergeSnapshotAssistantContent(existing.content, convertedMessage.content, snapshotHasReasoning, snapshotHasActivity)
1410
- };
1794
+ if (convertedMessage.role === "assistant") {
1795
+ const previous = existing?.role === "assistant" ? existing : void 0;
1796
+ const snapshot = reconcileSnapshot?.(convertedMessage, previous) ?? convertedMessage;
1797
+ converted[index] = previous ? {
1798
+ ...snapshot,
1799
+ content: this.mergeSnapshotAssistantContent(previous.content, snapshot.content, snapshotHasReasoning, snapshotHasActivity)
1800
+ } : snapshot;
1801
+ }
1411
1802
  }
1412
1803
  if (joinStrategy === "concat-content") {
1413
1804
  const activeIndex = converted.findIndex((message) => message.id === activeAssistantId);
@@ -1501,7 +1892,7 @@ var AgUiThreadRuntimeCore = class {
1501
1892
  const wasPersisted = this.persistedHistoryIds.has(messageId);
1502
1893
  const update = history.update;
1503
1894
  if (update !== void 0 && (wasPersisted || this.snapshotHistoryIds.has(messageId))) {
1504
- const write = this.chainHistoryWrite(messageId, () => update.call(history, {
1895
+ const write = this.historyWrites.chain(messageId, () => update.call(history, {
1505
1896
  parentId,
1506
1897
  message
1507
1898
  }));
@@ -1509,7 +1900,7 @@ var AgUiThreadRuntimeCore = class {
1509
1900
  write.then(() => {
1510
1901
  this.persistedHistoryIds.add(messageId);
1511
1902
  }, (error) => {
1512
- const pending = this.historyWrites.get(messageId);
1903
+ const pending = this.historyWrites.pending(messageId);
1513
1904
  if (pending === void 0 || pending === write) this.assistantHistoryParents.set(messageId, parentId);
1514
1905
  this.logger.error?.("[agui] failed to update history entry", error);
1515
1906
  });
@@ -1523,17 +1914,17 @@ var AgUiThreadRuntimeCore = class {
1523
1914
  if (!write) return;
1524
1915
  this.assistantHistoryParents.delete(messageId);
1525
1916
  write.then(() => {}, (error) => {
1526
- const pending = this.historyWrites.get(messageId);
1917
+ const pending = this.historyWrites.pending(messageId);
1527
1918
  if (pending === void 0 || pending === write) this.assistantHistoryParents.set(messageId, parentId);
1528
1919
  this.logger.error?.("[agui] failed to append history entry", error);
1529
1920
  });
1530
1921
  }
1531
1922
  appendHistoryItem(parentId, message) {
1532
1923
  if (!this.history || this.persistedHistoryIds.has(message.id)) return;
1533
- const pending = this.historyWrites.get(message.id);
1924
+ const pending = this.historyWrites.pending(message.id);
1534
1925
  if (pending) return pending;
1535
1926
  const append = this.history.append.bind(this.history);
1536
- const write = this.chainHistoryWrite(message.id, () => append({
1927
+ const write = this.historyWrites.chain(message.id, () => append({
1537
1928
  parentId,
1538
1929
  message
1539
1930
  }));
@@ -1542,25 +1933,7 @@ var AgUiThreadRuntimeCore = class {
1542
1933
  }, () => {});
1543
1934
  return write;
1544
1935
  }
1545
- chainHistoryWrite(id, write) {
1546
- const pending = this.historyWrites.get(id);
1547
- let next;
1548
- if (pending) next = pending.then(write, write);
1549
- else try {
1550
- next = Promise.resolve(write());
1551
- } catch (error) {
1552
- next = Promise.reject(error);
1553
- }
1554
- this.historyWrites.set(id, next);
1555
- next.then(() => {
1556
- if (this.historyWrites.get(id) === next) this.historyWrites.delete(id);
1557
- }, () => {
1558
- if (this.historyWrites.get(id) === next) this.historyWrites.delete(id);
1559
- });
1560
- return next;
1561
- }
1562
1936
  };
1563
- //#endregion
1564
- export { AgUiThreadRuntimeCore };
1937
+ export { AgUiThreadController };
1565
1938
 
1566
- //# sourceMappingURL=AgUiThreadRuntimeCore.js.map
1939
+ //# sourceMappingURL=AgUiThreadController.js.map