@assistant-ui/react-ag-ui 0.0.70 → 0.1.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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/runtime/{AgUiThreadRuntimeCore.d.ts → AgUiThreadController.d.ts} +8 -12
  3. package/dist/runtime/AgUiThreadController.d.ts.map +1 -0
  4. package/dist/runtime/{AgUiThreadRuntimeCore.js → AgUiThreadController.js} +302 -162
  5. package/dist/runtime/AgUiThreadController.js.map +1 -0
  6. package/dist/runtime/adapter/conversions.d.ts +4 -4
  7. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  8. package/dist/runtime/adapter/conversions.js +111 -33
  9. package/dist/runtime/adapter/conversions.js.map +1 -1
  10. package/dist/runtime/adapter/run-aggregator.d.ts +6 -5
  11. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  12. package/dist/runtime/adapter/run-aggregator.js +50 -62
  13. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  14. package/dist/runtime/adapter/subscriber.d.ts +0 -24
  15. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  16. package/dist/runtime/adapter/subscriber.js +1 -22
  17. package/dist/runtime/adapter/subscriber.js.map +1 -1
  18. package/dist/runtime/agUiThreadState.d.ts +60 -0
  19. package/dist/runtime/agUiThreadState.d.ts.map +1 -0
  20. package/dist/runtime/agUiThreadState.js +95 -0
  21. package/dist/runtime/agUiThreadState.js.map +1 -0
  22. package/dist/runtime/event-parser.d.ts.map +1 -1
  23. package/dist/runtime/event-parser.js +45 -48
  24. package/dist/runtime/event-parser.js.map +1 -1
  25. package/dist/runtime/mcp-tool-result.d.ts +3 -1
  26. package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
  27. package/dist/runtime/mcp-tool-result.js +6 -3
  28. package/dist/runtime/mcp-tool-result.js.map +1 -1
  29. package/dist/runtime/types.d.ts +13 -35
  30. package/dist/runtime/types.d.ts.map +1 -1
  31. package/dist/runtime/types.js.map +1 -1
  32. package/dist/useAgUiRuntime.d.ts.map +1 -1
  33. package/dist/useAgUiRuntime.js +4 -2
  34. package/dist/useAgUiRuntime.js.map +1 -1
  35. package/package.json +13 -12
  36. package/src/runtime/{AgUiThreadRuntimeCore.interrupt-cancel.test.ts → AgUiThreadController.interrupt-cancel.test.ts} +4 -4
  37. package/src/runtime/{AgUiThreadRuntimeCore.test.ts → AgUiThreadController.test.ts} +35 -6
  38. package/src/runtime/{AgUiThreadRuntimeCore.ts → AgUiThreadController.ts} +471 -169
  39. package/src/runtime/adapter/conversions.content.test.ts +37 -5
  40. package/src/runtime/adapter/conversions.tools.test.ts +2 -1
  41. package/src/runtime/adapter/conversions.ts +235 -58
  42. package/src/runtime/adapter/run-aggregator.ts +84 -114
  43. package/src/runtime/adapter/subscriber.ts +1 -44
  44. package/src/runtime/agUiThreadState.test.ts +205 -0
  45. package/src/runtime/agUiThreadState.ts +130 -0
  46. package/src/runtime/event-parser.ts +40 -58
  47. package/src/runtime/mcp-tool-result.ts +16 -2
  48. package/src/runtime/types.ts +18 -28
  49. package/src/useAgUiRuntime.external-store.test.tsx +39 -0
  50. package/src/useAgUiRuntime.fast-refresh.test.tsx +3 -3
  51. package/src/useAgUiRuntime.thread-switch.test.tsx +4 -4
  52. package/src/useAgUiRuntime.ts +5 -3
  53. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +0 -1
  54. package/dist/runtime/AgUiThreadRuntimeCore.js.map +0 -1
@@ -1,18 +1,36 @@
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
5
  import { buildToolApprovalResume, 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";
8
9
  import { fromThreadMessageLike, generateId, isMcpAppUri } from "@assistant-ui/core";
9
- import { appendToolInteraction, createMessageRepositorySession, invokeUserCallback, iterateToolCallParts, mapToolCallPartsDeep } from "@assistant-ui/core/internal";
10
+ import { appendToolInteraction, createHistoryWriteChain, createMessageRepositorySession, invokeUserCallback, iterateToolCallParts, mapToolCallPartsDeep } from "@assistant-ui/core/internal";
10
11
  import jsonpatch from "fast-json-patch";
11
- //#region src/runtime/AgUiThreadRuntimeCore.ts
12
+ //#region src/runtime/AgUiThreadController.ts
12
13
  const optimisticPrefix = "__optimistic__";
13
14
  const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;
14
15
  const isOptimisticId = (id) => id.startsWith(optimisticPrefix);
15
16
  const isResolvedToolCall = (part) => part.type === "tool-call" && "result" in part && part.result !== void 0;
17
+ const listedPendingToolCallIds = (assistant) => {
18
+ const ids = assistant.metadata.custom[AG_UI_METADATA_NAMESPACE]?.pendingToolCallIds;
19
+ return ids?.length ? new Set(ids) : void 0;
20
+ };
21
+ const withoutPendingToolCallIds = (metadata) => {
22
+ const namespace = metadata.custom[AG_UI_METADATA_NAMESPACE];
23
+ if (!namespace || !("pendingToolCallIds" in namespace)) return metadata;
24
+ const { pendingToolCallIds: _pendingToolCallIds, ...remainingNamespace } = namespace;
25
+ const { [AG_UI_METADATA_NAMESPACE]: _namespace, ...remainingCustom } = metadata.custom;
26
+ return {
27
+ ...metadata,
28
+ custom: {
29
+ ...remainingCustom,
30
+ ...Object.keys(remainingNamespace).length > 0 ? { [AG_UI_METADATA_NAMESPACE]: remainingNamespace } : {}
31
+ }
32
+ };
33
+ };
16
34
  const isActivityPart = (part) => part.type === "data" && part.name.startsWith("agui-activity/");
17
35
  const FALLBACK_USER_STATUS = {
18
36
  type: "complete",
@@ -34,10 +52,11 @@ const mergeAgUiNamespace = (current, incoming) => {
34
52
  };
35
53
  return merged;
36
54
  };
37
- var AgUiThreadRuntimeCore = class {
55
+ var AgUiThreadController = class {
38
56
  agent;
39
57
  logger;
40
58
  showThinking;
59
+ joinStrategy;
41
60
  getRunContext;
42
61
  resumeTranscript;
43
62
  autoCancelPendingToolCalls;
@@ -62,28 +81,21 @@ var AgUiThreadRuntimeCore = class {
62
81
  headId: repository.headId
63
82
  };
64
83
  } });
65
- isRunningFlag = false;
84
+ state = createAgUiThreadState();
66
85
  abortController = null;
67
86
  activeRunAgent = null;
68
- stateSnapshot;
69
87
  history;
70
- lastRunConfig;
71
88
  assistantHistoryParents = /* @__PURE__ */ new Map();
72
89
  snapshotHistoryIds = /* @__PURE__ */ new Set();
73
90
  persistedHistoryIds = /* @__PURE__ */ new Set();
74
- historyWrites = /* @__PURE__ */ new Map();
75
- _isLoading = false;
91
+ historyWrites = createHistoryWriteChain();
76
92
  _historyImportPromise;
77
- historyMutationGeneration = 0;
78
93
  _loadPromise;
79
- _loadRequested = false;
80
- pendingResume = null;
81
- pendingA2uiResumeOwner = null;
82
- pendingA2uiAction;
83
94
  constructor(options) {
84
95
  this.agent = options.agent;
85
96
  this.logger = options.logger;
86
97
  this.showThinking = options.showThinking;
98
+ this.joinStrategy = options.joinStrategy;
87
99
  this.getRunContext = options.getRunContext;
88
100
  this.resumeTranscript = options.resumeTranscript ?? "full";
89
101
  this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
@@ -97,11 +109,14 @@ var AgUiThreadRuntimeCore = class {
97
109
  return () => this.listeners.delete(listener);
98
110
  };
99
111
  getSnapshot = () => this.snapshot ??= {
100
- isLoading: this._isLoading,
101
- isRunning: this.isRunningFlag,
112
+ isLoading: this.state.isLoading,
113
+ isRunning: this.state.isRunning,
102
114
  messageRepository: this.session.export(),
103
- state: this.stateSnapshot
115
+ state: this.state.stateSnapshot
104
116
  };
117
+ dispatch(action) {
118
+ this.state = reduceAgUiThreadState(this.state, action);
119
+ }
105
120
  publishUpdate() {
106
121
  this.snapshot = void 0;
107
122
  for (const listener of this.listeners) listener();
@@ -110,6 +125,7 @@ var AgUiThreadRuntimeCore = class {
110
125
  this.agent = options.agent;
111
126
  this.logger = options.logger;
112
127
  this.showThinking = options.showThinking;
128
+ this.joinStrategy = options.joinStrategy;
113
129
  this.getRunContext = options.getRunContext;
114
130
  this.resumeTranscript = options.resumeTranscript ?? "full";
115
131
  this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
@@ -117,7 +133,7 @@ var AgUiThreadRuntimeCore = class {
117
133
  this.onCancel = options.onCancel;
118
134
  const previousHistory = this.history;
119
135
  this.history = options.history;
120
- if (this._loadRequested && !this._loadPromise && !previousHistory && options.history && this.session.getMessages().length === 0) this.__internal_load();
136
+ if (this.state.loadRequested && !this._loadPromise && !previousHistory && options.history && this.session.getMessages().length === 0) this.__internal_load();
121
137
  }
122
138
  attachRuntime(runtime) {
123
139
  this.runtime = runtime;
@@ -135,16 +151,16 @@ var AgUiThreadRuntimeCore = class {
135
151
  return this.session.export();
136
152
  }
137
153
  getState() {
138
- return this.stateSnapshot;
154
+ return this.state.stateSnapshot;
139
155
  }
140
156
  isRunning() {
141
- return this.isRunningFlag;
157
+ return this.state.isRunning;
142
158
  }
143
159
  get isLoading() {
144
- return this._isLoading;
160
+ return this.state.isLoading;
145
161
  }
146
162
  __internal_load() {
147
- this._loadRequested = true;
163
+ this.dispatch({ type: "loadRequested" });
148
164
  if (this._loadPromise) return this._loadPromise;
149
165
  if (!this.history) return Promise.resolve();
150
166
  const promise = this.history.load().then((repo) => {
@@ -158,7 +174,7 @@ var AgUiThreadRuntimeCore = class {
158
174
  if (repo.state !== void 0) this.loadExternalState(repo.state);
159
175
  return repo;
160
176
  });
161
- this._isLoading = true;
177
+ this.dispatch({ type: "loadStarted" });
162
178
  this._historyImportPromise = promise.then(() => {
163
179
  this._historyImportPromise = void 0;
164
180
  }, () => {
@@ -168,13 +184,13 @@ var AgUiThreadRuntimeCore = class {
168
184
  if (repo?.unstable_resume) {
169
185
  const parentId = repo.headId ?? this.session.headId;
170
186
  const resumeStream = this.history?.resume?.bind(this.history);
171
- await this.startRun(parentId, this.lastRunConfig, void 0, resumeStream);
187
+ await this.startRun(parentId, this.state.lastRunConfig, void 0, resumeStream);
172
188
  }
173
189
  }).catch((error) => {
174
190
  this.logger.error?.("[agui] failed to load history", error);
175
191
  this.reportError(error);
176
192
  }).finally(() => {
177
- this._isLoading = false;
193
+ this.dispatch({ type: "loadFinished" });
178
194
  this.publishUpdate();
179
195
  });
180
196
  this.publishUpdate();
@@ -183,15 +199,15 @@ var AgUiThreadRuntimeCore = class {
183
199
  waitForHistoryImport() {
184
200
  const historyImport = this._historyImportPromise;
185
201
  if (!historyImport) return void 0;
186
- const generation = this.historyMutationGeneration;
187
- return historyImport.then(() => generation === this.historyMutationGeneration && !this.isThreadSwitching?.());
202
+ const generation = this.state.historyMutationGeneration;
203
+ return historyImport.then(() => generation === this.state.historyMutationGeneration && !this.isThreadSwitching?.());
188
204
  }
189
205
  deferUntilHistoryImported(callback) {
190
206
  const historyImport = this._historyImportPromise;
191
207
  if (!historyImport) return false;
192
- const generation = this.historyMutationGeneration;
208
+ const generation = this.state.historyMutationGeneration;
193
209
  historyImport.then(() => {
194
- if (generation !== this.historyMutationGeneration || this.isThreadSwitching?.()) return;
210
+ if (generation !== this.state.historyMutationGeneration || this.isThreadSwitching?.()) return;
195
211
  try {
196
212
  callback();
197
213
  } catch (error) {
@@ -204,9 +220,9 @@ var AgUiThreadRuntimeCore = class {
204
220
  const historyImport = this._historyImportPromise;
205
221
  if (historyImport) {
206
222
  const wasAtTail = message.parentId === this.session.headId;
207
- const generation = this.historyMutationGeneration;
223
+ const generation = this.state.historyMutationGeneration;
208
224
  await historyImport;
209
- if (generation !== this.historyMutationGeneration || this.isThreadSwitching?.()) return;
225
+ if (generation !== this.state.historyMutationGeneration || this.isThreadSwitching?.()) return;
210
226
  if (wasAtTail) message = {
211
227
  ...message,
212
228
  parentId: this.session.headId
@@ -232,10 +248,10 @@ var AgUiThreadRuntimeCore = class {
232
248
  }
233
249
  maybeAutoCancelPendingToolCalls() {
234
250
  if (this.autoCancelPendingToolCalls === false) return;
235
- const pending = this.getPendingToolCalls();
236
- if (!pending) return;
237
- this.cancelUnresolvedToolCalls(pending.messageId);
238
- this.maybeCompleteAfterToolResults(pending.messageId);
251
+ for (const assistant of this.getPendingToolCallMessages()) {
252
+ this.cancelUnresolvedToolCalls(assistant.id);
253
+ this.maybeCompleteAfterToolResults(assistant.id);
254
+ }
239
255
  }
240
256
  appendEntry(message) {
241
257
  const threadMessage = this.toThreadMessage(message);
@@ -257,7 +273,7 @@ var AgUiThreadRuntimeCore = class {
257
273
  await this.startRun(parentId, config.runConfig);
258
274
  }
259
275
  async cancel() {
260
- this.historyMutationGeneration++;
276
+ this.dispatch({ type: "historyMutationAdvanced" });
261
277
  this.abortActiveRun();
262
278
  }
263
279
  abortActiveRun() {
@@ -283,7 +299,7 @@ var AgUiThreadRuntimeCore = class {
283
299
  const historyImport = this.waitForHistoryImport();
284
300
  if (historyImport && !await historyImport) return;
285
301
  this.assertNoPendingInterrupts();
286
- await this.startRun(config.parentId, config.runConfig ?? this.lastRunConfig, void 0, config.stream);
302
+ await this.startRun(config.parentId, config.runConfig ?? this.state.lastRunConfig, void 0, config.stream);
287
303
  }
288
304
  async resumeInFlightRun(messages) {
289
305
  const historyImport = this.waitForHistoryImport();
@@ -297,7 +313,7 @@ var AgUiThreadRuntimeCore = class {
297
313
  }
298
314
  const parentId = messages.at(-1)?.id ?? null;
299
315
  try {
300
- await this.startRun(parentId, this.lastRunConfig, void 0, resumeStream);
316
+ await this.startRun(parentId, this.state.lastRunConfig, void 0, resumeStream);
301
317
  } catch {}
302
318
  }
303
319
  assertNoPendingInterrupts() {
@@ -309,6 +325,19 @@ var AgUiThreadRuntimeCore = class {
309
325
  if (!assistant || assistant.status?.type !== "requires-action" || assistant.status.reason !== reason) return null;
310
326
  return assistant;
311
327
  }
328
+ getTrailingAssistantTurn() {
329
+ const messages = this.getMessages();
330
+ const turn = [];
331
+ for (let index = messages.length - 1; index >= 0; index--) {
332
+ const message = messages[index];
333
+ if (message?.role !== "assistant") break;
334
+ turn.unshift(message);
335
+ }
336
+ return turn;
337
+ }
338
+ getPendingToolCallMessages() {
339
+ return this.getTrailingAssistantTurn().filter((message) => message.status?.type === "requires-action" && message.status.reason === "tool-calls");
340
+ }
312
341
  getPendingInterrupts() {
313
342
  const assistant = this.findRequiresActionAssistant("interrupt");
314
343
  if (!assistant) return null;
@@ -320,16 +349,18 @@ var AgUiThreadRuntimeCore = class {
320
349
  };
321
350
  }
322
351
  getPendingToolCalls() {
323
- const assistant = this.findRequiresActionAssistant("tool-calls");
324
- if (!assistant) return null;
325
352
  const toolCallIds = [];
326
- for (const part of iterateToolCallParts(assistant.content)) {
327
- if (isResolvedToolCall(part)) continue;
328
- toolCallIds.push(part.toolCallId);
353
+ let messageId;
354
+ for (const assistant of this.getPendingToolCallMessages()) {
355
+ const listed = listedPendingToolCallIds(assistant);
356
+ for (const part of iterateToolCallParts(assistant.content)) {
357
+ if (isResolvedToolCall(part) || listed && !listed.has(part.toolCallId)) continue;
358
+ messageId ??= assistant.id;
359
+ toolCallIds.push(part.toolCallId);
360
+ }
329
361
  }
330
- if (toolCallIds.length === 0) return null;
331
- return {
332
- messageId: assistant.id,
362
+ return messageId === void 0 ? null : {
363
+ messageId,
333
364
  toolCallIds
334
365
  };
335
366
  }
@@ -342,7 +373,7 @@ var AgUiThreadRuntimeCore = class {
342
373
  const openIds = pending.interrupts.map((i) => i.id);
343
374
  const missing = openIds.filter((id) => !responsesById.has(id));
344
375
  if (missing.length > 0) throw new Error(`[agui] submitInterruptResponses: missing responses for open interrupts: ${missing.join(", ")}`);
345
- this.assertInterruptsAnswerable("submitInterruptResponses", pending.interrupts);
376
+ this.assertInterruptsAnswerable("submitInterruptResponses", pending.interrupts, responsesById);
346
377
  await this.resumeWithResponses(pending.messageId, openIds.map((id) => responsesById.get(id)));
347
378
  }
348
379
  /**
@@ -353,7 +384,7 @@ var AgUiThreadRuntimeCore = class {
353
384
  */
354
385
  async resumeWithResponses(messageId, resume) {
355
386
  this.clearPendingInterrupts(messageId, resume);
356
- await this.startRun(messageId, this.lastRunConfig, {
387
+ await this.startRun(messageId, this.state.lastRunConfig, {
357
388
  entries: resume,
358
389
  interruptedMessageId: messageId
359
390
  });
@@ -372,15 +403,15 @@ var AgUiThreadRuntimeCore = class {
372
403
  }
373
404
  invokeRuntimeCallback("onError", this.onError, error instanceof Error ? error : new Error(String(error)));
374
405
  }
375
- assertInterruptsAnswerable(method, interrupts) {
406
+ assertInterruptsAnswerable(method, interrupts, responsesById) {
376
407
  const now = Date.now();
377
408
  for (const interrupt of interrupts) {
378
409
  if (!interrupt.expiresAt) continue;
379
410
  const expiry = new Date(interrupt.expiresAt).getTime();
380
411
  if (Number.isNaN(expiry)) throw new Error(`[agui] ${method}: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`);
381
- if (expiry <= now) throw new Error(`[agui] ${method}: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`);
412
+ if (expiry <= now && responsesById?.get(interrupt.id)?.status !== "cancelled") throw new Error(`[agui] ${method}: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`);
382
413
  }
383
- if (this.isRunningFlag) throw new Error(`[agui] ${method}: a run is already in progress`);
414
+ if (this.state.isRunning) throw new Error(`[agui] ${method}: a run is already in progress`);
384
415
  }
385
416
  async respondToToolApproval(options) {
386
417
  const historyImport = this.waitForHistoryImport();
@@ -412,12 +443,13 @@ var AgUiThreadRuntimeCore = class {
412
443
  if (historyImport && !await historyImport) return;
413
444
  const pending = this.getPendingInterrupts();
414
445
  if (!pending) {
415
- const pendingTools = this.getPendingToolCalls();
416
- if (pendingTools) {
446
+ if (this.getPendingToolCalls()) {
417
447
  if (responses?.length) throw new Error("[agui] steerAway: responses are only valid for pending interrupts");
418
- if (this.isRunningFlag) throw new Error("[agui] steerAway: a run is already in progress");
419
- this.cancelUnresolvedToolCalls(pendingTools.messageId);
420
- this.maybeCompleteAfterToolResults(pendingTools.messageId);
448
+ if (this.state.isRunning) throw new Error("[agui] steerAway: a run is already in progress");
449
+ for (const assistant of this.getPendingToolCallMessages()) {
450
+ this.cancelUnresolvedToolCalls(assistant.id);
451
+ this.maybeCompleteAfterToolResults(assistant.id);
452
+ }
421
453
  const normalized = this.toAppendMessage(message);
422
454
  const threadMessageId = this.appendEntry(normalized);
423
455
  await this.startRun(threadMessageId, normalized.runConfig);
@@ -428,7 +460,7 @@ var AgUiThreadRuntimeCore = class {
428
460
  return;
429
461
  }
430
462
  const resume = this.resolveSteerAwayResume(pending.interrupts, responses);
431
- if (this.isRunningFlag) throw new Error("[agui] steerAway: a run is already in progress");
463
+ if (this.state.isRunning) throw new Error("[agui] steerAway: a run is already in progress");
432
464
  const normalized = this.toAppendMessage(message);
433
465
  this.clearPendingInterrupts(pending.messageId, resume);
434
466
  const threadMessageId = this.appendEntry(normalized);
@@ -594,7 +626,7 @@ var AgUiThreadRuntimeCore = class {
594
626
  const message = this.session.tryGetMessage(sessionMessageId)?.message;
595
627
  const history = this.history;
596
628
  if (!message || !history?.update || !this.isPersistableStatus(message.status)) return;
597
- await this.chainHistoryWrite(sessionMessageId, () => history.update({
629
+ await this.historyWrites.chain(sessionMessageId, () => history.update({
598
630
  parentId: item.parentId,
599
631
  message
600
632
  }));
@@ -611,25 +643,38 @@ var AgUiThreadRuntimeCore = class {
611
643
  const userAction = { ...action };
612
644
  delete userAction.type;
613
645
  if (!("timestamp" in userAction)) userAction.timestamp = (/* @__PURE__ */ new Date()).toISOString();
614
- this.pendingA2uiAction = userAction;
646
+ this.dispatch({
647
+ type: "a2uiActionDeferred",
648
+ action: userAction
649
+ });
615
650
  const owner = this.abortController;
616
651
  if (owner) {
617
- this.pendingA2uiResumeOwner = owner;
652
+ this.dispatch({
653
+ type: "a2uiResumeDeferred",
654
+ owner
655
+ });
618
656
  return;
619
657
  }
620
658
  this.startResumeRun(parentId);
621
659
  }
622
660
  maybeResumeAfterToolResults(messageId) {
623
661
  if (!this.maybeCompleteAfterToolResults(messageId)) return;
662
+ if (this.getPendingInterrupts()) return;
663
+ const turn = this.getTrailingAssistantTurn();
664
+ if (!turn.some((message) => message.id === messageId)) return;
665
+ if (this.getPendingToolCalls()) return;
666
+ const headId = turn.at(-1)?.id;
667
+ if (headId === void 0) return;
624
668
  const owner = this.abortController;
625
669
  if (owner) {
626
- this.pendingResume = {
670
+ this.dispatch({
671
+ type: "resumeDeferred",
627
672
  owner,
628
- messageId
629
- };
673
+ messageId: headId
674
+ });
630
675
  return;
631
676
  }
632
- this.startResumeRun(messageId);
677
+ this.startResumeRun(headId);
633
678
  }
634
679
  maybeCompleteAfterToolResults(messageId) {
635
680
  const message = this.session.tryGetMessage(messageId)?.message;
@@ -637,9 +682,13 @@ var AgUiThreadRuntimeCore = class {
637
682
  const assistant = message;
638
683
  if (assistant.status?.type !== "requires-action" || assistant.status.reason !== "tool-calls") return false;
639
684
  let allResolved = true;
640
- for (const part of iterateToolCallParts(assistant.content)) if (!isResolvedToolCall(part)) {
641
- allResolved = false;
642
- break;
685
+ const listed = listedPendingToolCallIds(assistant);
686
+ for (const part of iterateToolCallParts(assistant.content)) {
687
+ if (listed && !listed.has(part.toolCallId)) continue;
688
+ if (!isResolvedToolCall(part)) {
689
+ allResolved = false;
690
+ break;
691
+ }
643
692
  }
644
693
  if (!allResolved) return false;
645
694
  if (!this.session.updateMessage(messageId, (current) => current.role === "assistant" ? {
@@ -647,14 +696,15 @@ var AgUiThreadRuntimeCore = class {
647
696
  status: {
648
697
  type: "complete",
649
698
  reason: "unknown"
650
- }
699
+ },
700
+ metadata: withoutPendingToolCallIds(current.metadata)
651
701
  } : current)) return false;
652
702
  this.publishUpdate();
653
703
  this.persistAssistantHistory(messageId);
654
704
  return true;
655
705
  }
656
706
  startResumeRun(messageId) {
657
- this.startRun(messageId, this.lastRunConfig).catch((error) => {
707
+ this.startRun(messageId, this.state.lastRunConfig).catch((error) => {
658
708
  this.reportError(error);
659
709
  });
660
710
  }
@@ -671,8 +721,7 @@ var AgUiThreadRuntimeCore = class {
671
721
  }
672
722
  };
673
723
  });
674
- this.pendingA2uiResumeOwner = null;
675
- this.pendingA2uiAction = void 0;
724
+ this.dispatch({ type: "a2uiContinuationsCleared" });
676
725
  this.assistantHistoryParents.clear();
677
726
  if (messages.length === 0) this.session.clear();
678
727
  else {
@@ -709,30 +758,34 @@ var AgUiThreadRuntimeCore = class {
709
758
  }
710
759
  this.snapshotHistoryIds.clear();
711
760
  for (const { message } of this.getMessageRepository().messages) this.snapshotHistoryIds.add(message.id);
712
- const resumeTarget = this.pendingResume?.messageId;
713
- if (resumeTarget !== void 0 && !this.session.getMessages().some((message) => message.id === resumeTarget)) this.pendingResume = null;
761
+ const resumeTarget = this.state.pendingResume?.messageId;
762
+ if (resumeTarget !== void 0 && !this.session.getMessages().some((message) => message.id === resumeTarget)) this.dispatch({ type: "resumeCleared" });
714
763
  this.publishUpdate();
715
764
  }
716
765
  loadExternalState(state) {
717
- this.stateSnapshot = state;
766
+ this.dispatch({
767
+ type: "stateSnapshot",
768
+ snapshot: state
769
+ });
718
770
  this.publishUpdate();
719
771
  }
720
772
  setState(next) {
721
- this.stateSnapshot = typeof next === "function" ? next(this.stateSnapshot) : next;
773
+ const snapshot = typeof next === "function" ? next(this.state.stateSnapshot) : next;
774
+ this.dispatch({
775
+ type: "stateSnapshot",
776
+ snapshot
777
+ });
722
778
  this.publishUpdate();
723
779
  }
724
780
  resetState() {
725
- this.stateSnapshot = void 0;
781
+ this.dispatch({ type: "resetState" });
726
782
  this.publishUpdate();
727
783
  }
728
784
  resetThreadState() {
729
- this.historyMutationGeneration++;
785
+ this.dispatch({ type: "historyMutationAdvanced" });
730
786
  const controller = this.abortController;
731
787
  const activeRunAgent = this.activeRunAgent;
732
- this.stateSnapshot = void 0;
733
- this.pendingResume = null;
734
- this.pendingA2uiResumeOwner = null;
735
- this.pendingA2uiAction = void 0;
788
+ this.dispatch({ type: "resetThread" });
736
789
  if (controller) {
737
790
  this.abortController = null;
738
791
  this.activeRunAgent = null;
@@ -750,15 +803,23 @@ var AgUiThreadRuntimeCore = class {
750
803
  if (this.isThreadSwitching?.()) return;
751
804
  if (!this.supersedeActiveRun()) return;
752
805
  const normalizedRunConfig = runConfig ?? {};
753
- this.lastRunConfig = normalizedRunConfig;
806
+ this.dispatch({
807
+ type: "runConfigRecorded",
808
+ runConfig: normalizedRunConfig
809
+ });
754
810
  const parent = parentId === null ? void 0 : this.session.tryGetMessage(parentId);
755
811
  const shouldEagerlyInsertAssistant = parentId !== null && parent !== void 0 && parentId !== this.session.headId;
756
812
  const historicalMessages = [...shouldEagerlyInsertAssistant ? this.session.tryGetMessages(parentId) ?? this.session.getMessages() : this.session.getMessages()];
757
813
  const runStartMessageIds = new Set(this.getMessageRepository().messages.map(({ message }) => message.id));
814
+ const joinStrategy = this.joinStrategy;
815
+ const hasRunUserBoundary = parent?.message.role === "user" && !resume;
758
816
  let pendingError = null;
759
817
  const assistantParentId = parent ? parentId : this.session.headId;
760
818
  let assistantMessageId;
761
819
  let assistantCollided = false;
820
+ const heldMessages = /* @__PURE__ */ new Map();
821
+ let restrictedPendingToolCallIds;
822
+ let settlingRun = false;
762
823
  const ensureAssistant = (allowRecreate = false) => {
763
824
  const cached = assistantMessageId;
764
825
  if (cached !== void 0 && this.session.tryGetMessage(cached)) return cached;
@@ -771,14 +832,58 @@ var AgUiThreadRuntimeCore = class {
771
832
  return created;
772
833
  };
773
834
  if (shouldEagerlyInsertAssistant) ensureAssistant();
835
+ const settleHeldMessages = (terminalStatus) => {
836
+ const pendingMessages = [];
837
+ for (const [messageId, openToolCallIds] of heldMessages) {
838
+ let requiresToolResult = false;
839
+ if (this.session.updateMessage(messageId, (message) => {
840
+ if (message.role !== "assistant") return message;
841
+ const assistant = message;
842
+ const pendingToolCallIds = Array.from(openToolCallIds).filter((id) => !restrictedPendingToolCallIds || restrictedPendingToolCallIds.has(id));
843
+ const status = terminalStatus.type === "complete" || terminalStatus.type === "requires-action" ? pendingToolCallIds.length > 0 ? {
844
+ type: "requires-action",
845
+ reason: "tool-calls"
846
+ } : {
847
+ type: "complete",
848
+ reason: "unknown"
849
+ } : terminalStatus;
850
+ requiresToolResult = status.type === "requires-action";
851
+ const metadata = withoutPendingToolCallIds(assistant.metadata);
852
+ return {
853
+ ...assistant,
854
+ status,
855
+ metadata: status.type === "requires-action" && restrictedPendingToolCallIds ? {
856
+ ...metadata,
857
+ custom: {
858
+ ...metadata.custom,
859
+ ["agui"]: {
860
+ ...metadata.custom["agui"],
861
+ pendingToolCallIds
862
+ }
863
+ }
864
+ } : metadata
865
+ };
866
+ })) {
867
+ this.publishUpdate();
868
+ this.persistAssistantHistory(messageId);
869
+ if (requiresToolResult) pendingMessages.push(messageId);
870
+ }
871
+ }
872
+ heldMessages.clear();
873
+ return pendingMessages;
874
+ };
774
875
  const applyUpdate = (update) => {
876
+ const terminalStatus = update.status;
877
+ const pendingMessages = settlingRun && terminalStatus && terminalStatus.type !== "running" ? settleHeldMessages(terminalStatus) : [];
775
878
  const hasStreamContent = Array.isArray(update.content) && update.content.some((part) => part.type !== "data");
776
879
  const resolved = this.updateAssistantMessage(ensureAssistant(hasStreamContent), update);
777
880
  if (resolved !== assistantMessageId) assistantMessageId = resolved;
881
+ for (const messageId of pendingMessages) this.maybeResumeAfterToolResults(messageId);
778
882
  };
779
- const adoptServerMessageId = (serverId, startNewMessage) => {
883
+ const adoptServerMessageId = (serverId, startNewMessage, openToolCallIds = []) => {
780
884
  if (startNewMessage && assistantMessageId !== void 0) {
781
- applyUpdate({ status: {
885
+ if (openToolCallIds.length > 0) heldMessages.set(assistantMessageId, new Set(openToolCallIds));
886
+ else applyUpdate({ status: {
782
887
  type: "complete",
783
888
  reason: "unknown"
784
889
  } });
@@ -806,19 +911,30 @@ var AgUiThreadRuntimeCore = class {
806
911
  const abortSignal = abortController.signal;
807
912
  const aggregator = new RunAggregator({
808
913
  showThinking: this.showThinking,
914
+ joinStrategy,
809
915
  logger: this.logger,
810
916
  emit: applyUpdate,
811
917
  onServerMessageId: (serverId) => {
812
918
  adoptServerMessageId(serverId, false);
813
919
  },
814
- onTextMessageStart: (serverId) => adoptServerMessageId(serverId, true)
920
+ onTextMessageStart: (serverId, openToolCallIds) => adoptServerMessageId(serverId, true, openToolCallIds)
815
921
  });
816
922
  let runFinished = false;
817
923
  const dispatch = (event) => {
818
924
  if (this.abortController !== abortController) return;
819
- if (event.type === "RUN_FINISHED") runFinished = true;
820
- const nextAssistantMessageId = this.handleEvent(aggregator, event, assistantMessageId);
821
- if (event.type === "MESSAGES_SNAPSHOT") assistantMessageId = nextAssistantMessageId;
925
+ if (event.type === "RUN_FINISHED") {
926
+ runFinished = true;
927
+ const ids = event.outcome?.type === "success" ? event.outcome.pendingToolCallIds : void 0;
928
+ restrictedPendingToolCallIds = ids?.length ? new Set(ids) : void 0;
929
+ }
930
+ if (event.type === "TOOL_CALL_RESULT") for (const openToolCallIds of heldMessages.values()) openToolCallIds.delete(event.toolCallId);
931
+ settlingRun = event.type === "RUN_FINISHED" || event.type === "RUN_ERROR" || event.type === "RUN_CANCELLED";
932
+ try {
933
+ const nextAssistantMessageId = this.handleEvent(aggregator, event, assistantMessageId, joinStrategy, runStartMessageIds, hasRunUserBoundary);
934
+ if (event.type === "MESSAGES_SNAPSHOT") assistantMessageId = nextAssistantMessageId;
935
+ } finally {
936
+ settlingRun = false;
937
+ }
822
938
  };
823
939
  this.abortController = abortController;
824
940
  const runAgentInstance = this.agent;
@@ -892,16 +1008,16 @@ var AgUiThreadRuntimeCore = class {
892
1008
  this.clearDeferredContinuations(abortController);
893
1009
  throw err;
894
1010
  }
895
- if (this.pendingResume?.owner === abortController) {
896
- const { messageId } = this.pendingResume;
897
- this.pendingResume = null;
1011
+ if (this.state.pendingResume?.owner === abortController) {
1012
+ const { messageId } = this.state.pendingResume;
1013
+ this.dispatch({ type: "resumeCleared" });
898
1014
  if (!abortSignal.aborted) this.startResumeRun(messageId);
899
1015
  }
900
- if (this.pendingA2uiResumeOwner === abortController) {
901
- this.pendingA2uiResumeOwner = null;
902
- if (!abortSignal.aborted && this.pendingA2uiAction !== void 0) {
1016
+ if (this.state.pendingA2uiResumeOwner === abortController) {
1017
+ this.dispatch({ type: "a2uiResumeCleared" });
1018
+ if (!abortSignal.aborted && this.state.pendingA2uiAction !== void 0) {
903
1019
  if (this.getPendingInterrupts()) {
904
- this.pendingA2uiAction = void 0;
1020
+ this.dispatch({ type: "a2uiActionCleared" });
905
1021
  this.logger.debug("[agui] sendA2uiAction: pending interrupts, dropping action");
906
1022
  return;
907
1023
  }
@@ -909,15 +1025,14 @@ var AgUiThreadRuntimeCore = class {
909
1025
  const parentId = this.session.headId;
910
1026
  if (parentId !== null) this.startResumeRun(parentId);
911
1027
  else {
912
- this.pendingA2uiAction = void 0;
1028
+ this.dispatch({ type: "a2uiActionCleared" });
913
1029
  this.logger.debug("[agui] sendA2uiAction: no messages to resume, dropping action");
914
1030
  }
915
- } else this.pendingA2uiAction = void 0;
1031
+ } else this.dispatch({ type: "a2uiActionCleared" });
916
1032
  }
917
1033
  }
918
1034
  async consumeResumeStream(stream, ctx) {
919
- this.pendingA2uiAction = void 0;
920
- this.pendingA2uiResumeOwner = null;
1035
+ this.dispatch({ type: "a2uiContinuationsCleared" });
921
1036
  const assistantId = ctx.ensureAssistant();
922
1037
  const currentId = () => ctx.getAssistantMessageId() ?? assistantId;
923
1038
  const options = {
@@ -958,8 +1073,8 @@ var AgUiThreadRuntimeCore = class {
958
1073
  } });
959
1074
  }
960
1075
  buildRunInput(runId, runConfig, historyMessages, resume) {
961
- const pendingA2uiAction = this.pendingA2uiAction;
962
- this.pendingA2uiAction = void 0;
1076
+ const pendingA2uiAction = this.state.pendingA2uiAction;
1077
+ this.dispatch({ type: "a2uiActionCleared" });
963
1078
  const threadId = this.agent.threadId || "main";
964
1079
  const messages = toAgUiMessages(this.selectRunMessages(historyMessages ?? this.session.getMessages(), resume));
965
1080
  const context = this.runtime?.thread.getModelContext();
@@ -977,7 +1092,7 @@ var AgUiThreadRuntimeCore = class {
977
1092
  return {
978
1093
  threadId,
979
1094
  runId,
980
- state: this.stateSnapshot ?? null,
1095
+ state: this.state.stateSnapshot ?? null,
981
1096
  messages,
982
1097
  tools: toAgUiTools(context?.tools),
983
1098
  context: [...context?.system ? [{
@@ -1007,14 +1122,11 @@ var AgUiThreadRuntimeCore = class {
1007
1122
  return messages.slice(anchor + 1);
1008
1123
  }
1009
1124
  clearDeferredContinuations(owner) {
1010
- if (this.pendingResume?.owner === owner) this.pendingResume = null;
1011
- if (this.pendingA2uiResumeOwner === owner) {
1012
- this.pendingA2uiResumeOwner = null;
1013
- this.pendingA2uiAction = void 0;
1014
- }
1125
+ if (this.state.pendingResume?.owner === owner) this.dispatch({ type: "resumeCleared" });
1126
+ if (this.state.pendingA2uiResumeOwner === owner) this.dispatch({ type: "a2uiContinuationsCleared" });
1015
1127
  }
1016
1128
  setRunning(running) {
1017
- this.isRunningFlag = running;
1129
+ this.dispatch({ type: running ? "runStarted" : "runFinished" });
1018
1130
  this.publishUpdate();
1019
1131
  }
1020
1132
  finishRun(controller) {
@@ -1031,7 +1143,7 @@ var AgUiThreadRuntimeCore = class {
1031
1143
  status: { type: "running" },
1032
1144
  content: [],
1033
1145
  metadata: {
1034
- unstable_state: this.stateSnapshot ?? null,
1146
+ unstable_state: this.state.stateSnapshot ?? null,
1035
1147
  unstable_annotations: [],
1036
1148
  unstable_data: [],
1037
1149
  steps: [],
@@ -1074,21 +1186,11 @@ var AgUiThreadRuntimeCore = class {
1074
1186
  ids.delete(oldId);
1075
1187
  if (!collidesWithExisting) ids.add(newId);
1076
1188
  }
1077
- const pendingWrite = this.historyWrites.get(oldId);
1078
- if (pendingWrite) {
1079
- this.historyWrites.delete(oldId);
1080
- if (!collidesWithExisting) {
1081
- this.historyWrites.set(newId, pendingWrite);
1082
- const settle = () => {
1083
- if (this.historyWrites.get(newId) === pendingWrite) this.historyWrites.delete(newId);
1084
- };
1085
- pendingWrite.then(() => {
1086
- this.persistedHistoryIds.delete(oldId);
1087
- this.persistedHistoryIds.add(newId);
1088
- settle();
1089
- }, settle);
1090
- }
1091
- }
1189
+ const pendingWrite = this.historyWrites.move(oldId, collidesWithExisting ? void 0 : newId);
1190
+ if (pendingWrite && !collidesWithExisting) pendingWrite.then(() => {
1191
+ this.persistedHistoryIds.delete(oldId);
1192
+ this.persistedHistoryIds.add(newId);
1193
+ }, () => {});
1092
1194
  this.publishUpdate();
1093
1195
  return !collidesWithExisting;
1094
1196
  }
@@ -1097,8 +1199,9 @@ var AgUiThreadRuntimeCore = class {
1097
1199
  if (!this.session.updateMessage(messageId, (message) => {
1098
1200
  if (message.role !== "assistant") return message;
1099
1201
  const assistant = message;
1100
- const metadata = update.metadata ? this.mergeAssistantMetadata(assistant.metadata, update.metadata) : assistant.metadata;
1202
+ const mergedMetadata = update.metadata ? this.mergeAssistantMetadata(assistant.metadata, update.metadata) : assistant.metadata;
1101
1203
  latestStatus = update.status ?? assistant.status;
1204
+ const metadata = latestStatus?.type === "requires-action" && latestStatus.reason === "tool-calls" ? mergedMetadata : withoutPendingToolCallIds(mergedMetadata);
1102
1205
  const content = update.content !== void 0 ? this.preserveToolResults(assistant.content, update.content) : assistant.content;
1103
1206
  return {
1104
1207
  ...assistant,
@@ -1234,18 +1337,23 @@ var AgUiThreadRuntimeCore = class {
1234
1337
  custom: incoming.custom ? mergeAgUiNamespace(current.custom, incoming.custom) : current.custom
1235
1338
  };
1236
1339
  }
1237
- handleEvent(aggregator, event, activeAssistantId) {
1340
+ handleEvent(aggregator, event, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary) {
1238
1341
  switch (event.type) {
1239
1342
  case "STATE_SNAPSHOT":
1240
- this.stateSnapshot = event.snapshot;
1343
+ this.dispatch({
1344
+ type: "stateSnapshot",
1345
+ snapshot: event.snapshot
1346
+ });
1241
1347
  this.updateActiveAssistantState(activeAssistantId);
1242
1348
  break;
1243
1349
  case "STATE_DELTA":
1244
1350
  if (event.delta.length === 0) break;
1245
1351
  try {
1246
- const state = this.stateSnapshot ?? {};
1247
- const result = jsonpatch.applyPatch(state, event.delta, true, false);
1248
- this.stateSnapshot = result.newDocument;
1352
+ const result = jsonpatch.applyPatch(this.state.stateSnapshot ?? {}, event.delta, true, false);
1353
+ this.dispatch({
1354
+ type: "stateSnapshot",
1355
+ snapshot: result.newDocument
1356
+ });
1249
1357
  this.updateActiveAssistantState(activeAssistantId);
1250
1358
  } catch (error) {
1251
1359
  this.logger.error?.("[agui] failed to apply state delta", error);
@@ -1255,7 +1363,7 @@ var AgUiThreadRuntimeCore = class {
1255
1363
  const previousHeadId = this.session.headId;
1256
1364
  const activeAssistant = activeAssistantId === void 0 ? void 0 : this.session.tryGetMessage(activeAssistantId)?.message;
1257
1365
  const hasActiveText = activeAssistant?.role === "assistant" && activeAssistant.content.some((part) => part.type === "text");
1258
- this.importMessagesSnapshot(event.messages, activeAssistantId);
1366
+ this.importMessagesSnapshot(event.messages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary);
1259
1367
  const headId = this.session.headId;
1260
1368
  if (hasActiveText && headId !== previousHeadId) activeAssistantId = (headId === null ? void 0 : this.session.tryGetMessage(headId)?.message)?.role === "assistant" ? headId ?? void 0 : void 0;
1261
1369
  this.updateActiveAssistantState(activeAssistantId);
@@ -1296,7 +1404,7 @@ var AgUiThreadRuntimeCore = class {
1296
1404
  ...assistant,
1297
1405
  metadata: {
1298
1406
  ...assistant.metadata,
1299
- unstable_state: this.stateSnapshot ?? null
1407
+ unstable_state: this.state.stateSnapshot ?? null
1300
1408
  }
1301
1409
  };
1302
1410
  });
@@ -1307,25 +1415,27 @@ var AgUiThreadRuntimeCore = class {
1307
1415
  if (message.role !== "assistant") return message;
1308
1416
  const assistant = message;
1309
1417
  const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
1418
+ const modelText = toolContentText(event.content);
1310
1419
  let matchedToolCall = false;
1311
1420
  const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
1312
1421
  if (part.toolCallId !== event.toolCallId) return part;
1313
1422
  matchedToolCall = true;
1314
1423
  if (part.mcp?.app !== void 0 && isPlainObject(part.result) && Array.isArray(part.result["content"])) return {
1315
1424
  ...part,
1316
- ...part.modelContent === void 0 && event.content ? { modelContent: [{
1425
+ ...part.modelContent === void 0 ? { modelContent: [{
1317
1426
  type: "text",
1318
- text: event.content
1427
+ text: modelText
1319
1428
  }] } : {},
1320
1429
  ...part.isError === void 0 ? typeof event.mcpResult?.isError === "boolean" ? { isError: event.mcpResult.isError } : event.role === "tool" ? { isError: false } : {} : {},
1321
1430
  ...event.messageId ? { unstable_toolMessageId: event.messageId } : {}
1322
1431
  };
1432
+ const { modelContent: _modelContent, ...partWithoutModelContent } = part;
1323
1433
  return {
1324
- ...part,
1325
- result: event.mcpResult ?? tryParseJSON(event.content ?? ""),
1434
+ ...event.mcpResult === void 0 && Array.isArray(event.content) ? partWithoutModelContent : part,
1435
+ result: event.mcpResult ?? (Array.isArray(event.content) ? event.content : tryParseJSON(event.content)),
1326
1436
  ...event.mcpResult !== void 0 ? { modelContent: [{
1327
1437
  type: "text",
1328
- text: event.content
1438
+ text: modelText
1329
1439
  }] } : {},
1330
1440
  ...typeof event.mcpResult?.isError === "boolean" ? { isError: event.mcpResult.isError } : event.role === "tool" ? { isError: false } : {},
1331
1441
  ...part.mcp === void 0 && mcpAppUri !== void 0 ? { mcp: { app: { resourceUri: mcpAppUri } } } : {},
@@ -1380,7 +1490,7 @@ var AgUiThreadRuntimeCore = class {
1380
1490
  this.publishUpdate();
1381
1491
  this.maybeCompleteAfterToolResults(messageId);
1382
1492
  }
1383
- importMessagesSnapshot(rawMessages, activeAssistantId) {
1493
+ importMessagesSnapshot(rawMessages, activeAssistantId, joinStrategy, runStartMessageIds, hasRunUserBoundary) {
1384
1494
  try {
1385
1495
  const activeAssistantItem = activeAssistantId ? this.session.tryGetMessage(activeAssistantId) : void 0;
1386
1496
  const activeMessage = activeAssistantItem?.message;
@@ -1403,6 +1513,53 @@ var AgUiThreadRuntimeCore = class {
1403
1513
  content: this.mergeSnapshotAssistantContent(existing.content, convertedMessage.content, snapshotHasReasoning, snapshotHasActivity)
1404
1514
  };
1405
1515
  }
1516
+ if (joinStrategy === "concat-content") {
1517
+ const activeIndex = converted.findIndex((message) => message.id === activeAssistantId);
1518
+ const lastKnownIndex = converted.findLastIndex((message) => runStartMessageIds.has(message.id));
1519
+ const lastUserIndex = hasRunUserBoundary ? converted.findLastIndex((message) => message.role === "user") : -1;
1520
+ const startIndex = activeIndex >= 0 ? activeIndex : lastKnownIndex >= 0 || lastUserIndex >= 0 ? Math.max(lastKnownIndex, lastUserIndex) + 1 : -1;
1521
+ if (startIndex >= 0 && converted[startIndex]?.role === "assistant") {
1522
+ let endIndex = startIndex + 1;
1523
+ while (converted[endIndex]?.role === "assistant") endIndex++;
1524
+ const runMessages = converted.slice(startIndex, endIndex);
1525
+ const includesStreamedPrefix = activeIndex < 0 && activeAssistant?.content.some((part) => part.type !== "data");
1526
+ if (includesStreamedPrefix && activeAssistant) runMessages.unshift(activeAssistant);
1527
+ if (runMessages.length > 1) {
1528
+ const [first, ...rest] = runMessages;
1529
+ let content = runMessages.flatMap((message) => message.content);
1530
+ if (includesStreamedPrefix) {
1531
+ content = [...first.content];
1532
+ let cursor = 0;
1533
+ const keyOf = (part) => part.type === "tool-call" ? `tool:${part.toolCallId}` : part.type === "text" || part.type === "reasoning" ? `${part.type}:${part.text}` : void 0;
1534
+ for (const message of rest) for (const part of message.content) {
1535
+ const key = keyOf(part);
1536
+ const match = key === void 0 ? -1 : content.findIndex((previous, index) => index >= cursor && keyOf(previous) === key);
1537
+ if (match < 0) {
1538
+ content.push(part);
1539
+ cursor = content.length;
1540
+ } else {
1541
+ if (part.type === "tool-call") content[match] = this.preserveToolResults([content[match]], [part])[0];
1542
+ cursor = match + 1;
1543
+ }
1544
+ }
1545
+ }
1546
+ converted.splice(startIndex, endIndex - startIndex, {
1547
+ ...first,
1548
+ content,
1549
+ status: rest.at(-1).status,
1550
+ metadata: rest.reduce((metadata, message) => ({
1551
+ ...metadata,
1552
+ ...message.metadata,
1553
+ unstable_state: message.metadata.unstable_state,
1554
+ unstable_annotations: [...metadata.unstable_annotations, ...message.metadata.unstable_annotations],
1555
+ unstable_data: [...metadata.unstable_data, ...message.metadata.unstable_data],
1556
+ steps: [...metadata.steps, ...message.metadata.steps],
1557
+ custom: mergeAgUiNamespace(metadata.custom, message.metadata.custom)
1558
+ }), first.metadata)
1559
+ });
1560
+ }
1561
+ }
1562
+ }
1406
1563
  const snapshotContainsActiveAssistant = converted.some((message) => message.id === activeAssistant?.id);
1407
1564
  const preservesActiveAssistant = activeAssistant !== void 0 && !snapshotContainsActiveAssistant && (activeAssistant.metadata.isOptimistic !== true || converted.at(-1)?.role !== "assistant" || activeAssistantParentId !== void 0 && converted.at(-1)?.id === activeAssistantParentId);
1408
1565
  if (preservesActiveAssistant) converted.push(activeAssistant);
@@ -1448,7 +1605,7 @@ var AgUiThreadRuntimeCore = class {
1448
1605
  const wasPersisted = this.persistedHistoryIds.has(messageId);
1449
1606
  const update = history.update;
1450
1607
  if (update !== void 0 && (wasPersisted || this.snapshotHistoryIds.has(messageId))) {
1451
- const write = this.chainHistoryWrite(messageId, () => update.call(history, {
1608
+ const write = this.historyWrites.chain(messageId, () => update.call(history, {
1452
1609
  parentId,
1453
1610
  message
1454
1611
  }));
@@ -1456,7 +1613,7 @@ var AgUiThreadRuntimeCore = class {
1456
1613
  write.then(() => {
1457
1614
  this.persistedHistoryIds.add(messageId);
1458
1615
  }, (error) => {
1459
- const pending = this.historyWrites.get(messageId);
1616
+ const pending = this.historyWrites.pending(messageId);
1460
1617
  if (pending === void 0 || pending === write) this.assistantHistoryParents.set(messageId, parentId);
1461
1618
  this.logger.error?.("[agui] failed to update history entry", error);
1462
1619
  });
@@ -1470,17 +1627,17 @@ var AgUiThreadRuntimeCore = class {
1470
1627
  if (!write) return;
1471
1628
  this.assistantHistoryParents.delete(messageId);
1472
1629
  write.then(() => {}, (error) => {
1473
- const pending = this.historyWrites.get(messageId);
1630
+ const pending = this.historyWrites.pending(messageId);
1474
1631
  if (pending === void 0 || pending === write) this.assistantHistoryParents.set(messageId, parentId);
1475
1632
  this.logger.error?.("[agui] failed to append history entry", error);
1476
1633
  });
1477
1634
  }
1478
1635
  appendHistoryItem(parentId, message) {
1479
1636
  if (!this.history || this.persistedHistoryIds.has(message.id)) return;
1480
- const pending = this.historyWrites.get(message.id);
1637
+ const pending = this.historyWrites.pending(message.id);
1481
1638
  if (pending) return pending;
1482
1639
  const append = this.history.append.bind(this.history);
1483
- const write = this.chainHistoryWrite(message.id, () => append({
1640
+ const write = this.historyWrites.chain(message.id, () => append({
1484
1641
  parentId,
1485
1642
  message
1486
1643
  }));
@@ -1489,25 +1646,8 @@ var AgUiThreadRuntimeCore = class {
1489
1646
  }, () => {});
1490
1647
  return write;
1491
1648
  }
1492
- chainHistoryWrite(id, write) {
1493
- const pending = this.historyWrites.get(id);
1494
- let next;
1495
- if (pending) next = pending.then(write, write);
1496
- else try {
1497
- next = Promise.resolve(write());
1498
- } catch (error) {
1499
- next = Promise.reject(error);
1500
- }
1501
- this.historyWrites.set(id, next);
1502
- next.then(() => {
1503
- if (this.historyWrites.get(id) === next) this.historyWrites.delete(id);
1504
- }, () => {
1505
- if (this.historyWrites.get(id) === next) this.historyWrites.delete(id);
1506
- });
1507
- return next;
1508
- }
1509
1649
  };
1510
1650
  //#endregion
1511
- export { AgUiThreadRuntimeCore };
1651
+ export { AgUiThreadController };
1512
1652
 
1513
- //# sourceMappingURL=AgUiThreadRuntimeCore.js.map
1653
+ //# sourceMappingURL=AgUiThreadController.js.map