@assistant-ui/react-ag-ui 0.0.54 → 0.0.57

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 (47) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +2 -2
  3. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +25 -15
  4. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  5. package/dist/runtime/AgUiThreadRuntimeCore.js +257 -210
  6. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  7. package/dist/runtime/adapter/conversions.d.ts +13 -3
  8. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  9. package/dist/runtime/adapter/conversions.js +77 -39
  10. package/dist/runtime/adapter/conversions.js.map +1 -1
  11. package/dist/runtime/adapter/run-aggregator.d.ts +23 -2
  12. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  13. package/dist/runtime/adapter/run-aggregator.js +155 -41
  14. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  15. package/dist/runtime/adapter/subscriber.d.ts +3 -0
  16. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  17. package/dist/runtime/adapter/subscriber.js +9 -0
  18. package/dist/runtime/adapter/subscriber.js.map +1 -1
  19. package/dist/runtime/adapter/tool-approval.d.ts +56 -0
  20. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -0
  21. package/dist/runtime/adapter/tool-approval.js +244 -0
  22. package/dist/runtime/adapter/tool-approval.js.map +1 -0
  23. package/dist/runtime/event-parser.d.ts.map +1 -1
  24. package/dist/runtime/event-parser.js +2 -1
  25. package/dist/runtime/event-parser.js.map +1 -1
  26. package/dist/runtime/interrupt-internals.d.ts +8 -0
  27. package/dist/runtime/interrupt-internals.d.ts.map +1 -0
  28. package/dist/runtime/interrupt-internals.js +25 -0
  29. package/dist/runtime/interrupt-internals.js.map +1 -0
  30. package/dist/useAgUiRuntime.js +14 -9
  31. package/dist/useAgUiRuntime.js.map +1 -1
  32. package/package.json +9 -9
  33. package/src/index.ts +8 -2
  34. package/src/runtime/AgUiThreadRuntimeCore.test.ts +105 -0
  35. package/src/runtime/AgUiThreadRuntimeCore.ts +436 -272
  36. package/src/runtime/adapter/conversions.content.test.ts +61 -3
  37. package/src/runtime/adapter/conversions.ts +123 -57
  38. package/src/runtime/adapter/run-aggregator.approval.test.ts +103 -0
  39. package/src/runtime/adapter/run-aggregator.ts +234 -47
  40. package/src/runtime/adapter/subscriber.ts +14 -0
  41. package/src/runtime/adapter/tool-approval.test.ts +681 -0
  42. package/src/runtime/adapter/tool-approval.ts +366 -0
  43. package/src/runtime/event-parser.ts +10 -1
  44. package/src/runtime/interrupt-internals.ts +35 -0
  45. package/src/useAgUiRuntime.approval.test.tsx +885 -0
  46. package/src/useAgUiRuntime.history.test.tsx +76 -0
  47. package/src/useAgUiRuntime.ts +15 -9
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
  import { readMcpAppResourceUri } from "./mcp-tool-result.js";
3
+ import { buildToolApprovalResume, projectAgUiToolApprovals, withSettledToolApprovals, withToolApprovalDecision } from "./adapter/tool-approval.js";
3
4
  import { AG_UI_METADATA_NAMESPACE, RunAggregator, isPlainObject, tryParseJSON } from "./adapter/run-aggregator.js";
4
5
  import { fromAgUiMessages, toAgUiMessages, toAgUiTools } from "./adapter/conversions.js";
5
6
  import { createAgUiSubscriber } from "./adapter/subscriber.js";
6
7
  import { fromThreadMessageLike, generateId, isMcpAppUri } from "@assistant-ui/core";
7
- import { MessageRepository } from "@assistant-ui/core/internal";
8
+ import { createMessageRepositorySession, invokeUserCallback } from "@assistant-ui/core/internal";
8
9
  import jsonpatch from "fast-json-patch";
9
10
  //#region src/runtime/AgUiThreadRuntimeCore.ts
10
11
  const optimisticPrefix = "__optimistic__";
@@ -16,19 +17,8 @@ const FALLBACK_USER_STATUS = {
16
17
  type: "complete",
17
18
  reason: "unknown"
18
19
  };
19
- const reportCallbackError = (name, error) => {
20
- console.error(`[react-ag-ui] ${name} callback threw an error`, error);
21
- };
22
20
  const invokeRuntimeCallback = (name, callback, ...args) => {
23
- if (!callback) return;
24
- try {
25
- const result = callback(...args);
26
- if (result !== null && (typeof result === "object" || typeof result === "function") && "then" in result && typeof result.then === "function") Promise.resolve(result).catch((error) => {
27
- reportCallbackError(name, error);
28
- });
29
- } catch (error) {
30
- reportCallbackError(name, error);
31
- }
21
+ invokeUserCallback("react-ag-ui", name, callback, ...args);
32
22
  };
33
23
  const mergeAgUiNamespace = (current, incoming) => {
34
24
  const merged = {
@@ -51,9 +41,22 @@ var AgUiThreadRuntimeCore = class {
51
41
  onError;
52
42
  onCancel;
53
43
  notifyUpdate;
44
+ reportedErrors = /* @__PURE__ */ new WeakSet();
54
45
  runtime;
55
- repository = new MessageRepository();
56
- exportedRepository;
46
+ session = createMessageRepositorySession({ decorateExport: (exported, repository) => {
47
+ let parentId = null;
48
+ for (const message of repository.getMessages()) {
49
+ if (message.metadata.isOptimistic) exported.messages.push({
50
+ parentId,
51
+ message
52
+ });
53
+ parentId = message.id;
54
+ }
55
+ return {
56
+ ...exported,
57
+ headId: repository.headId
58
+ };
59
+ } });
57
60
  isRunningFlag = false;
58
61
  abortController = null;
59
62
  activeRunAgent = null;
@@ -62,9 +65,12 @@ var AgUiThreadRuntimeCore = class {
62
65
  history;
63
66
  lastRunConfig;
64
67
  assistantHistoryParents = /* @__PURE__ */ new Map();
65
- recordedHistoryIds = /* @__PURE__ */ new Set();
68
+ snapshotHistoryIds = /* @__PURE__ */ new Set();
69
+ persistedHistoryIds = /* @__PURE__ */ new Set();
70
+ historyWrites = /* @__PURE__ */ new Map();
66
71
  _isLoading = false;
67
72
  _loadPromise;
73
+ _loadRequested = false;
68
74
  pendingResumeMessageId = null;
69
75
  pendingA2uiResume = false;
70
76
  pendingA2uiAction;
@@ -86,84 +92,25 @@ var AgUiThreadRuntimeCore = class {
86
92
  this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
87
93
  this.onError = options.onError;
88
94
  this.onCancel = options.onCancel;
95
+ const previousHistory = this.history;
89
96
  this.history = options.history;
90
97
  this.installResumeShim();
98
+ if (this._loadRequested && !this._loadPromise && !previousHistory && options.history && this.session.getMessages().length === 0) this.__internal_load();
91
99
  }
92
100
  attachRuntime(runtime) {
93
101
  this.runtime = runtime;
94
102
  }
95
103
  detachRuntime() {
96
104
  this.runtime = void 0;
105
+ this.cancel().catch((error) => {
106
+ this.logger.error("[agui] failed to cancel run during teardown", error);
107
+ });
97
108
  }
98
109
  getMessages() {
99
- return this.repository.getMessages();
110
+ return this.session.getMessages();
100
111
  }
101
112
  getMessageRepository() {
102
- if (this.exportedRepository) return this.exportedRepository;
103
- const exportedRepository = this.repository.export();
104
- let parentId = null;
105
- for (const message of this.repository.getMessages()) {
106
- if (message.metadata.isOptimistic) exportedRepository.messages.push({
107
- parentId,
108
- message
109
- });
110
- parentId = message.id;
111
- }
112
- this.exportedRepository = {
113
- ...exportedRepository,
114
- headId: this.repository.headId
115
- };
116
- return this.exportedRepository;
117
- }
118
- tryGetMessage(messageId) {
119
- try {
120
- return this.repository.getMessage(messageId);
121
- } catch {
122
- return;
123
- }
124
- }
125
- tryGetMessages(messageId) {
126
- try {
127
- return this.repository.getMessages(messageId);
128
- } catch {
129
- return;
130
- }
131
- }
132
- hasMessage(messageId) {
133
- return this.tryGetMessage(messageId) !== void 0;
134
- }
135
- addOrUpdateMessage(parentId, message) {
136
- this.repository.addOrUpdateMessage(parentId, message);
137
- this.exportedRepository = void 0;
138
- }
139
- deleteMessage(messageId, replacementId) {
140
- this.repository.deleteMessage(messageId, replacementId);
141
- this.exportedRepository = void 0;
142
- }
143
- tryDeleteMessage(messageId) {
144
- if (!this.hasMessage(messageId)) return false;
145
- this.deleteMessage(messageId);
146
- return true;
147
- }
148
- switchToBranch(messageId) {
149
- this.repository.switchToBranch(messageId);
150
- this.exportedRepository = void 0;
151
- }
152
- resetRepositoryHead(messageId) {
153
- this.repository.resetHead(messageId);
154
- this.exportedRepository = void 0;
155
- }
156
- clearRepository() {
157
- this.repository.clear();
158
- this.exportedRepository = void 0;
159
- }
160
- updateMessage(messageId, updater) {
161
- const item = this.tryGetMessage(messageId);
162
- if (!item) return false;
163
- const message = updater(item.message);
164
- if (message === item.message) return false;
165
- this.addOrUpdateMessage(item.parentId, message);
166
- return true;
113
+ return this.session.export();
167
114
  }
168
115
  getState() {
169
116
  return this.stateSnapshot;
@@ -175,15 +122,22 @@ var AgUiThreadRuntimeCore = class {
175
122
  return this._isLoading;
176
123
  }
177
124
  __internal_load() {
125
+ this._loadRequested = true;
178
126
  if (this._loadPromise) return this._loadPromise;
179
- const promise = this.history?.load() ?? Promise.resolve(null);
127
+ if (!this.history) return Promise.resolve();
128
+ const promise = this.history.load();
180
129
  this._isLoading = true;
181
130
  this._loadPromise = promise.then(async (repo) => {
182
131
  if (!repo) return;
183
- this.applyExternalMessageRepository(repo);
132
+ this.session.applyExternalMessageRepository(repo);
133
+ this.assistantHistoryParents.clear();
134
+ this.snapshotHistoryIds.clear();
135
+ this.persistedHistoryIds.clear();
136
+ for (const { message } of repo.messages) this.persistedHistoryIds.add(message.id);
137
+ this.notifyUpdate();
184
138
  if (repo.state !== void 0) this.loadExternalState(repo.state);
185
139
  if (repo.unstable_resume) {
186
- const parentId = repo.headId ?? this.repository.headId;
140
+ const parentId = repo.headId ?? this.session.headId;
187
141
  const resumeStream = this.history?.resume?.bind(this.history);
188
142
  await this.startRun(parentId, this.lastRunConfig, void 0, resumeStream);
189
143
  }
@@ -215,11 +169,11 @@ var AgUiThreadRuntimeCore = class {
215
169
  this.maybeCompleteAfterToolResults(pending.messageId);
216
170
  }
217
171
  appendEntry(message) {
218
- if (message.sourceId) this.tryDeleteMessage(message.sourceId);
172
+ if (message.sourceId) this.session.tryDeleteMessage(message.sourceId);
219
173
  const threadMessage = this.toThreadMessage(message);
220
- const parentId = message.parentId === null ? null : message.parentId && this.hasMessage(message.parentId) ? message.parentId : this.repository.headId;
221
- this.addOrUpdateMessage(parentId, threadMessage);
222
- this.switchToBranch(threadMessage.id);
174
+ const parentId = message.parentId === null ? null : message.parentId && this.session.hasMessage(message.parentId) ? message.parentId : this.session.headId;
175
+ this.session.addOrUpdateMessage(parentId, threadMessage);
176
+ this.session.switchToBranch(threadMessage.id);
223
177
  this.notifyUpdate();
224
178
  this.recordHistoryEntry(parentId, threadMessage);
225
179
  return threadMessage.id;
@@ -298,17 +252,65 @@ var AgUiThreadRuntimeCore = class {
298
252
  const openIds = pending.interrupts.map((i) => i.id);
299
253
  const missing = openIds.filter((id) => !responsesById.has(id));
300
254
  if (missing.length > 0) throw new Error(`[agui] submitInterruptResponses: missing responses for open interrupts: ${missing.join(", ")}`);
255
+ this.assertInterruptsAnswerable("submitInterruptResponses", pending.interrupts);
256
+ await this.resumeWithResponses(pending.messageId, openIds.map((id) => responsesById.get(id)));
257
+ }
258
+ /**
259
+ * Resumes with an already validated resume array. Answerability is checked
260
+ * once per submission: re-checking here would let a clock crossing reject a
261
+ * decision this runtime has already recorded, stranding the gate decided and
262
+ * unretryable.
263
+ */
264
+ async resumeWithResponses(messageId, resume) {
265
+ this.clearPendingInterrupts(messageId, resume);
266
+ await this.startRun(messageId, this.lastRunConfig, resume);
267
+ }
268
+ /**
269
+ * The core seams that reach `respondToToolApproval` discard the promise they
270
+ * receive, so a rejected decision would otherwise surface only as an
271
+ * unhandled rejection. A failure raised by the resumed run itself was already
272
+ * reported by `startRun` before it rethrew, so reporting it here again would
273
+ * give the consumer two notifications for one failure.
274
+ */
275
+ reportError(error) {
276
+ if (this.reportedErrors.has(error)) {
277
+ this.reportedErrors.delete(error);
278
+ return;
279
+ }
280
+ invokeRuntimeCallback("onError", this.onError, error instanceof Error ? error : new Error(String(error)));
281
+ }
282
+ assertInterruptsAnswerable(method, interrupts) {
301
283
  const now = Date.now();
302
- for (const interrupt of pending.interrupts) {
284
+ for (const interrupt of interrupts) {
303
285
  if (!interrupt.expiresAt) continue;
304
286
  const expiry = new Date(interrupt.expiresAt).getTime();
305
- if (Number.isNaN(expiry)) throw new Error(`[agui] submitInterruptResponses: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`);
306
- if (expiry <= now) throw new Error(`[agui] submitInterruptResponses: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`);
287
+ if (Number.isNaN(expiry)) throw new Error(`[agui] ${method}: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`);
288
+ if (expiry <= now) throw new Error(`[agui] ${method}: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`);
307
289
  }
308
- const resume = openIds.map((id) => responsesById.get(id));
309
- if (this.isRunningFlag) throw new Error("[agui] submitInterruptResponses: a run is already in progress");
310
- this.clearPendingInterrupts(pending.messageId);
311
- await this.startRun(pending.messageId, this.lastRunConfig, resume);
290
+ if (this.isRunningFlag) throw new Error(`[agui] ${method}: a run is already in progress`);
291
+ }
292
+ async respondToToolApproval(options) {
293
+ const pending = this.getPendingInterrupts();
294
+ if (!pending) throw new Error("[agui] respondToToolApproval: no pending interrupts on this thread");
295
+ const gatedMessage = this.session.tryGetMessage(pending.messageId)?.message;
296
+ if (![...projectAgUiToolApprovals(pending.interrupts, new Set((gatedMessage?.content ?? []).filter((part) => part.type === "tool-call").map((part) => part.toolCallId))).values()].some((approval) => approval.id === options.approvalId)) throw new Error(`[agui] respondToToolApproval: no pending tool-call interrupt for approval id "${options.approvalId}"`);
297
+ this.assertInterruptsAnswerable("respondToToolApproval", pending.interrupts);
298
+ if (!this.session.updateMessage(pending.messageId, (message) => {
299
+ if (message.role !== "assistant") return message;
300
+ const assistant = message;
301
+ const content = withToolApprovalDecision(assistant.content, options);
302
+ if (content === assistant.content) return assistant;
303
+ return {
304
+ ...assistant,
305
+ content
306
+ };
307
+ })) throw new Error(`[agui] respondToToolApproval: approval "${options.approvalId}" is already decided`);
308
+ this.notifyUpdate();
309
+ const assistant = this.session.tryGetMessage(pending.messageId)?.message;
310
+ if (!assistant) return;
311
+ const resume = buildToolApprovalResume(assistant.content, pending.interrupts);
312
+ if (!resume) return;
313
+ await this.resumeWithResponses(pending.messageId, resume);
312
314
  }
313
315
  async steerAway(message, responses) {
314
316
  const pending = this.getPendingInterrupts();
@@ -331,7 +333,7 @@ var AgUiThreadRuntimeCore = class {
331
333
  const resume = this.resolveSteerAwayResume(pending.interrupts, responses);
332
334
  if (this.isRunningFlag) throw new Error("[agui] steerAway: a run is already in progress");
333
335
  const normalized = this.toAppendMessage(message);
334
- this.clearPendingInterrupts(pending.messageId);
336
+ this.clearPendingInterrupts(pending.messageId, resume);
335
337
  const threadMessageId = this.appendEntry(normalized);
336
338
  await this.startRun(threadMessageId, normalized.runConfig, resume);
337
339
  }
@@ -357,7 +359,7 @@ var AgUiThreadRuntimeCore = class {
357
359
  toAppendMessage(message) {
358
360
  if (typeof message === "string") return {
359
361
  createdAt: /* @__PURE__ */ new Date(),
360
- parentId: this.repository.headId,
362
+ parentId: this.session.headId,
361
363
  sourceId: null,
362
364
  runConfig: {},
363
365
  role: "user",
@@ -370,7 +372,7 @@ var AgUiThreadRuntimeCore = class {
370
372
  };
371
373
  return {
372
374
  createdAt: message.createdAt ?? /* @__PURE__ */ new Date(),
373
- parentId: message.parentId ?? this.repository.headId,
375
+ parentId: message.parentId ?? this.session.headId,
374
376
  sourceId: message.sourceId ?? null,
375
377
  role: message.role ?? "user",
376
378
  content: message.content,
@@ -380,8 +382,8 @@ var AgUiThreadRuntimeCore = class {
380
382
  startRun: message.startRun
381
383
  };
382
384
  }
383
- clearPendingInterrupts(messageId) {
384
- if (this.updateMessage(messageId, (message) => {
385
+ clearPendingInterrupts(messageId, resume) {
386
+ if (this.session.updateMessage(messageId, (message) => {
385
387
  if (message.role !== "assistant") return message;
386
388
  const assistant = message;
387
389
  if (assistant.status?.type !== "requires-action" || assistant.status.reason !== "interrupt") return assistant;
@@ -391,6 +393,7 @@ var AgUiThreadRuntimeCore = class {
391
393
  else delete newCustom["agui"];
392
394
  return {
393
395
  ...assistant,
396
+ content: withSettledToolApprovals(assistant.content, resume),
394
397
  status: {
395
398
  type: "complete",
396
399
  reason: "unknown"
@@ -417,7 +420,7 @@ var AgUiThreadRuntimeCore = class {
417
420
  return fallbackMessageId;
418
421
  }
419
422
  cancelUnresolvedToolCalls(messageId) {
420
- if (this.updateMessage(messageId, (message) => {
423
+ if (this.session.updateMessage(messageId, (message) => {
421
424
  if (message.role !== "assistant") return message;
422
425
  const assistant = message;
423
426
  const content = assistant.content.map((part) => {
@@ -435,7 +438,7 @@ var AgUiThreadRuntimeCore = class {
435
438
  })) this.notifyUpdate();
436
439
  }
437
440
  addToolResult(options) {
438
- if (!this.updateMessage(options.messageId, (message) => {
441
+ if (!this.session.updateMessage(options.messageId, (message) => {
439
442
  if (message.role !== "assistant") return message;
440
443
  const assistant = message;
441
444
  let matchedToolCall = false;
@@ -461,7 +464,7 @@ var AgUiThreadRuntimeCore = class {
461
464
  sendA2uiAction(action) {
462
465
  this.assertNoPendingInterrupts();
463
466
  this.maybeAutoCancelPendingToolCalls();
464
- const parentId = this.repository.headId;
467
+ const parentId = this.session.headId;
465
468
  if (parentId === null) {
466
469
  this.logger.debug("[agui] sendA2uiAction: no messages to resume, dropping action");
467
470
  return;
@@ -485,12 +488,12 @@ var AgUiThreadRuntimeCore = class {
485
488
  this.startResumeRun(messageId);
486
489
  }
487
490
  maybeCompleteAfterToolResults(messageId) {
488
- const message = this.tryGetMessage(messageId)?.message;
491
+ const message = this.session.tryGetMessage(messageId)?.message;
489
492
  if (!message || message.role !== "assistant") return false;
490
493
  const assistant = message;
491
494
  if (assistant.status?.type !== "requires-action" || assistant.status.reason !== "tool-calls") return false;
492
495
  if (!assistant.content.every((part) => part.type !== "tool-call" || isResolvedToolCall(part))) return false;
493
- if (!this.updateMessage(messageId, (current) => current.role === "assistant" ? {
496
+ if (!this.session.updateMessage(messageId, (current) => current.role === "assistant" ? {
494
497
  ...current,
495
498
  status: {
496
499
  type: "complete",
@@ -510,7 +513,7 @@ var AgUiThreadRuntimeCore = class {
510
513
  this.pendingA2uiResume = false;
511
514
  this.pendingA2uiAction = void 0;
512
515
  this.assistantHistoryParents.clear();
513
- if (messages.length === 0) this.clearRepository();
516
+ if (messages.length === 0) this.session.clear();
514
517
  else {
515
518
  let expectedParentId = null;
516
519
  let lastAppliedId = null;
@@ -519,82 +522,32 @@ var AgUiThreadRuntimeCore = class {
519
522
  for (const message of messages) {
520
523
  if (seen.has(message.id)) continue;
521
524
  seen.add(message.id);
522
- const existing = this.tryGetMessage(message.id);
525
+ const existing = this.session.tryGetMessage(message.id);
523
526
  if (existing && existing.parentId !== expectedParentId) {
524
527
  hardReplace = true;
525
528
  break;
526
529
  }
527
- this.addOrUpdateMessage(expectedParentId, message);
530
+ this.session.addOrUpdateMessage(expectedParentId, message);
528
531
  expectedParentId = message.id;
529
532
  lastAppliedId = message.id;
530
533
  }
531
534
  if (hardReplace) {
532
- this.clearRepository();
535
+ this.session.clear();
533
536
  expectedParentId = null;
534
537
  lastAppliedId = null;
535
538
  seen.clear();
536
539
  for (const message of messages) {
537
540
  if (seen.has(message.id)) continue;
538
541
  seen.add(message.id);
539
- this.addOrUpdateMessage(expectedParentId, message);
542
+ this.session.addOrUpdateMessage(expectedParentId, message);
540
543
  expectedParentId = message.id;
541
544
  lastAppliedId = message.id;
542
545
  }
543
546
  }
544
- this.resetRepositoryHead(lastAppliedId);
547
+ this.session.resetHead(lastAppliedId);
545
548
  }
546
- this.recordedHistoryIds.clear();
547
- for (const { message } of this.getMessageRepository().messages) this.recordedHistoryIds.add(message.id);
548
- this.notifyUpdate();
549
- }
550
- applyExternalMessageRepository(loaded) {
551
- const headId = loaded.headId ?? loaded.messages.at(-1)?.message.id ?? null;
552
- const ids = /* @__PURE__ */ new Set();
553
- let degenerate = false;
554
- for (const { message } of loaded.messages) {
555
- if (ids.has(message.id)) {
556
- degenerate = true;
557
- break;
558
- }
559
- ids.add(message.id);
560
- }
561
- if (headId !== null && !ids.has(headId)) degenerate = true;
562
- if (!degenerate) {
563
- this.clearRepository();
564
- let pending = [...loaded.messages];
565
- const importedIds = /* @__PURE__ */ new Set();
566
- while (pending.length > 0) {
567
- const unresolved = [];
568
- let progressed = false;
569
- for (const item of pending) {
570
- if (item.parentId !== null && !importedIds.has(item.parentId)) {
571
- unresolved.push(item);
572
- continue;
573
- }
574
- this.addOrUpdateMessage(item.parentId, item.message);
575
- importedIds.add(item.message.id);
576
- progressed = true;
577
- }
578
- if (!progressed) {
579
- degenerate = true;
580
- break;
581
- }
582
- pending = unresolved;
583
- }
584
- }
585
- if (degenerate) {
586
- this.clearRepository();
587
- let previousId = null;
588
- for (const { message } of loaded.messages) {
589
- const existing = this.tryGetMessage(message.id);
590
- this.addOrUpdateMessage(existing ? existing.parentId : previousId, message);
591
- previousId = message.id;
592
- }
593
- this.resetRepositoryHead(previousId);
594
- } else this.resetRepositoryHead(headId);
595
- this.assistantHistoryParents.clear();
596
- this.recordedHistoryIds.clear();
597
- for (const { message } of loaded.messages) this.recordedHistoryIds.add(message.id);
549
+ this.snapshotHistoryIds.clear();
550
+ for (const { message } of this.getMessageRepository().messages) this.snapshotHistoryIds.add(message.id);
598
551
  this.notifyUpdate();
599
552
  }
600
553
  loadExternalState(state) {
@@ -612,18 +565,20 @@ var AgUiThreadRuntimeCore = class {
612
565
  async startRun(parentId, runConfig, resume, resumeStream) {
613
566
  const normalizedRunConfig = runConfig ?? {};
614
567
  this.lastRunConfig = normalizedRunConfig;
615
- const parent = parentId === null ? void 0 : this.tryGetMessage(parentId);
616
- const shouldEagerlyInsertAssistant = parentId !== null && parent !== void 0 && parentId !== this.repository.headId;
617
- const historicalMessages = [...shouldEagerlyInsertAssistant ? this.tryGetMessages(parentId) ?? this.repository.getMessages() : this.repository.getMessages()];
568
+ const parent = parentId === null ? void 0 : this.session.tryGetMessage(parentId);
569
+ const shouldEagerlyInsertAssistant = parentId !== null && parent !== void 0 && parentId !== this.session.headId;
570
+ const historicalMessages = [...shouldEagerlyInsertAssistant ? this.session.tryGetMessages(parentId) ?? this.session.getMessages() : this.session.getMessages()];
571
+ const runStartMessageIds = new Set(this.getMessageRepository().messages.map(({ message }) => message.id));
618
572
  this.pendingError = null;
619
- const assistantParentId = parent ? parentId : this.repository.headId;
573
+ const assistantParentId = parent ? parentId : this.session.headId;
620
574
  let assistantMessageId;
621
575
  let assistantCollided = false;
622
576
  const ensureAssistant = (allowRecreate = false) => {
623
577
  const cached = assistantMessageId;
624
- if (cached !== void 0 && this.tryGetMessage(cached)) return cached;
578
+ if (cached !== void 0 && this.session.tryGetMessage(cached)) return cached;
625
579
  if (cached !== void 0 && (assistantCollided || !allowRecreate)) return cached;
626
- const parentId = cached === void 0 && assistantParentId && this.hasMessage(assistantParentId) ? assistantParentId : this.repository.headId;
580
+ const repositoryHeadId = this.session.headId;
581
+ const parentId = (cached === void 0 || shouldEagerlyInsertAssistant && (repositoryHeadId === null || runStartMessageIds.has(repositoryHeadId))) && assistantParentId && this.session.hasMessage(assistantParentId) ? assistantParentId : repositoryHeadId;
627
582
  const created = this.insertAssistantPlaceholder(parentId, cached);
628
583
  assistantMessageId = created;
629
584
  this.markPendingAssistantHistory(created, parentId);
@@ -631,20 +586,44 @@ var AgUiThreadRuntimeCore = class {
631
586
  };
632
587
  if (shouldEagerlyInsertAssistant) ensureAssistant();
633
588
  const applyUpdate = (update) => {
634
- const hasContent = Array.isArray(update.content) && update.content.length > 0;
635
- const resolved = this.updateAssistantMessage(ensureAssistant(hasContent), update);
589
+ const hasStreamContent = Array.isArray(update.content) && update.content.some((part) => part.type !== "data");
590
+ const resolved = this.updateAssistantMessage(ensureAssistant(hasStreamContent), update);
636
591
  if (resolved !== assistantMessageId) assistantMessageId = resolved;
637
592
  };
593
+ const adoptServerMessageId = (serverId, startNewMessage) => {
594
+ if (startNewMessage && assistantMessageId !== void 0) {
595
+ applyUpdate({ status: {
596
+ type: "complete",
597
+ reason: "unknown"
598
+ } });
599
+ const previousId = assistantMessageId;
600
+ assistantMessageId = void 0;
601
+ if (this.session.tryGetMessage(previousId)) {
602
+ const created = this.insertAssistantPlaceholder(previousId);
603
+ assistantMessageId = created;
604
+ this.markPendingAssistantHistory(created, previousId);
605
+ }
606
+ }
607
+ const placeholder = ensureAssistant(true);
608
+ if (placeholder === serverId) return;
609
+ const reassigned = this.reassignAssistantId(placeholder, serverId);
610
+ const adoptsVisibleCollision = !reassigned && !runStartMessageIds.has(serverId) && this.session.headId === serverId;
611
+ if (reassigned || adoptsVisibleCollision) {
612
+ assistantMessageId = serverId;
613
+ if (adoptsVisibleCollision) {
614
+ const parentId = this.session.tryGetMessage(serverId)?.parentId ?? null;
615
+ this.markPendingAssistantHistory(serverId, parentId);
616
+ }
617
+ } else assistantCollided = true;
618
+ };
638
619
  const aggregator = new RunAggregator({
639
620
  showThinking: this.showThinking,
640
621
  logger: this.logger,
641
622
  emit: applyUpdate,
642
623
  onServerMessageId: (serverId) => {
643
- const placeholder = ensureAssistant(true);
644
- if (placeholder === serverId) return;
645
- if (this.reassignAssistantId(placeholder, serverId)) assistantMessageId = serverId;
646
- else assistantCollided = true;
647
- }
624
+ adoptServerMessageId(serverId, false);
625
+ },
626
+ onTextMessageStart: (serverId) => adoptServerMessageId(serverId, true)
648
627
  });
649
628
  const dispatch = (event) => this.handleEvent(aggregator, event, assistantMessageId);
650
629
  const abortController = new AbortController();
@@ -714,6 +693,7 @@ var AgUiThreadRuntimeCore = class {
714
693
  }
715
694
  if (this.pendingError) {
716
695
  const err = this.pendingError;
696
+ this.reportedErrors.add(err);
717
697
  this.pendingError = null;
718
698
  this.pendingResumeMessageId = null;
719
699
  this.pendingA2uiResume = false;
@@ -735,7 +715,7 @@ var AgUiThreadRuntimeCore = class {
735
715
  return;
736
716
  }
737
717
  this.maybeAutoCancelPendingToolCalls();
738
- const parentId = this.repository.headId;
718
+ const parentId = this.session.headId;
739
719
  if (parentId !== null) this.startResumeRun(parentId);
740
720
  else {
741
721
  this.pendingA2uiAction = void 0;
@@ -758,7 +738,7 @@ var AgUiThreadRuntimeCore = class {
758
738
  unstable_threadId: ctx.threadId,
759
739
  unstable_parentId: ctx.parentId,
760
740
  unstable_getMessage: () => {
761
- const message = this.tryGetMessage(currentId())?.message;
741
+ const message = this.session.tryGetMessage(currentId())?.message;
762
742
  if (!message) throw new Error("[agui] resume stream requested the assistant message before it existed");
763
743
  return message;
764
744
  }
@@ -781,7 +761,7 @@ var AgUiThreadRuntimeCore = class {
781
761
  return;
782
762
  }
783
763
  if (ctx.abortSignal.aborted) return;
784
- const current = this.tryGetMessage(currentId())?.message;
764
+ const current = this.session.tryGetMessage(currentId())?.message;
785
765
  if (!current || current.status?.type === "running") ctx.applyUpdate({ status: {
786
766
  type: "complete",
787
767
  reason: "unknown"
@@ -789,7 +769,7 @@ var AgUiThreadRuntimeCore = class {
789
769
  }
790
770
  buildRunInput(runId, runConfig, historyMessages, resume) {
791
771
  const threadId = this.agent.threadId || "main";
792
- const messages = toAgUiMessages(historyMessages ?? this.repository.getMessages());
772
+ const messages = toAgUiMessages(historyMessages ?? this.session.getMessages());
793
773
  const context = this.runtime?.thread.getModelContext();
794
774
  const input = {
795
775
  threadId,
@@ -854,47 +834,62 @@ var AgUiThreadRuntimeCore = class {
854
834
  custom: {}
855
835
  }
856
836
  };
857
- this.addOrUpdateMessage(parentId ?? this.repository.headId, assistant);
858
- this.switchToBranch(id);
837
+ this.session.addOrUpdateMessage(parentId ?? this.session.headId, assistant);
838
+ this.session.switchToBranch(id);
859
839
  this.notifyUpdate();
860
840
  return id;
861
841
  }
862
842
  reassignAssistantId(oldId, newId) {
863
843
  if (oldId === newId) return true;
864
- const oldItem = this.tryGetMessage(oldId);
844
+ const oldItem = this.session.tryGetMessage(oldId);
865
845
  if (!oldItem) return false;
866
- const collidesWithExisting = this.hasMessage(newId);
846
+ const collidesWithExisting = this.session.hasMessage(newId);
867
847
  if (collidesWithExisting) {
868
848
  this.logger.debug?.("[agui] reassignAssistantId: server id already present in messages or repository, dropping placeholder", {
869
849
  oldId,
870
850
  newId
871
851
  });
872
- this.deleteMessage(oldId, oldItem.parentId ?? null);
852
+ this.session.deleteMessage(oldId, oldItem.parentId ?? null);
873
853
  } else {
874
854
  const { isOptimistic: _, ...metadata } = oldItem.message.metadata;
875
- this.addOrUpdateMessage(oldItem.parentId, {
855
+ this.session.addOrUpdateMessage(oldItem.parentId, {
876
856
  ...oldItem.message,
877
857
  id: newId,
878
858
  metadata
879
859
  });
880
- this.switchToBranch(newId);
881
- this.tryDeleteMessage(oldId);
860
+ this.session.switchToBranch(newId);
861
+ this.session.tryDeleteMessage(oldId);
882
862
  }
883
863
  const pendingParent = this.assistantHistoryParents.get(oldId);
884
864
  if (pendingParent !== void 0) {
885
865
  this.assistantHistoryParents.delete(oldId);
886
866
  if (!collidesWithExisting && !this.assistantHistoryParents.has(newId)) this.assistantHistoryParents.set(newId, pendingParent);
887
867
  }
888
- if (this.recordedHistoryIds.has(oldId)) {
889
- this.recordedHistoryIds.delete(oldId);
890
- if (!collidesWithExisting) this.recordedHistoryIds.add(newId);
868
+ for (const ids of [this.snapshotHistoryIds, this.persistedHistoryIds]) if (ids.has(oldId)) {
869
+ ids.delete(oldId);
870
+ if (!collidesWithExisting) ids.add(newId);
871
+ }
872
+ const pendingWrite = this.historyWrites.get(oldId);
873
+ if (pendingWrite) {
874
+ this.historyWrites.delete(oldId);
875
+ if (!collidesWithExisting) {
876
+ this.historyWrites.set(newId, pendingWrite);
877
+ const settle = () => {
878
+ if (this.historyWrites.get(newId) === pendingWrite) this.historyWrites.delete(newId);
879
+ };
880
+ pendingWrite.then(() => {
881
+ this.persistedHistoryIds.delete(oldId);
882
+ this.persistedHistoryIds.add(newId);
883
+ settle();
884
+ }, settle);
885
+ }
891
886
  }
892
887
  this.notifyUpdate();
893
888
  return !collidesWithExisting;
894
889
  }
895
890
  updateAssistantMessage(messageId, update) {
896
891
  let latestStatus;
897
- if (!this.updateMessage(messageId, (message) => {
892
+ if (!this.session.updateMessage(messageId, (message) => {
898
893
  if (message.role !== "assistant") return message;
899
894
  const assistant = message;
900
895
  const metadata = update.metadata ? this.mergeAssistantMetadata(assistant.metadata, update.metadata) : assistant.metadata;
@@ -996,7 +991,7 @@ var AgUiThreadRuntimeCore = class {
996
991
  }
997
992
  }
998
993
  applyCrossRunToolResult(messageId, event) {
999
- if (!this.updateMessage(messageId, (message) => {
994
+ if (!this.session.updateMessage(messageId, (message) => {
1000
995
  if (message.role !== "assistant") return message;
1001
996
  const assistant = message;
1002
997
  const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
@@ -1041,7 +1036,7 @@ var AgUiThreadRuntimeCore = class {
1041
1036
  const serverHash = event.content["serverHash"];
1042
1037
  const appServerId = typeof serverId === "string" && serverId.length > 0 ? serverId : typeof serverHash === "string" && serverHash.length > 0 ? serverHash : void 0;
1043
1038
  const result = event.content["result"];
1044
- if (!this.updateMessage(messageId, (message) => {
1039
+ if (!this.session.updateMessage(messageId, (message) => {
1045
1040
  if (message.role !== "assistant") return message;
1046
1041
  const assistant = message;
1047
1042
  let matchedToolCall = false;
@@ -1075,7 +1070,7 @@ var AgUiThreadRuntimeCore = class {
1075
1070
  }
1076
1071
  importMessagesSnapshot(rawMessages, activeAssistantId) {
1077
1072
  try {
1078
- const activeMessage = activeAssistantId ? this.tryGetMessage(activeAssistantId)?.message : void 0;
1073
+ const activeMessage = activeAssistantId ? this.session.tryGetMessage(activeAssistantId)?.message : void 0;
1079
1074
  const activeAssistant = activeMessage?.role === "assistant" ? activeMessage : void 0;
1080
1075
  const normalized = fromAgUiMessages(rawMessages, { showThinking: this.showThinking });
1081
1076
  const converted = [];
@@ -1084,14 +1079,16 @@ var AgUiThreadRuntimeCore = class {
1084
1079
  } catch (error) {
1085
1080
  this.logger.error?.("[agui] failed to import message from snapshot", error);
1086
1081
  }
1087
- const snapshotHeadId = converted.at(-1)?.id ?? null;
1088
1082
  const snapshotContainsActiveAssistant = converted.some((message) => message.id === activeAssistant?.id);
1089
1083
  const preservesActiveAssistant = activeAssistant !== void 0 && !snapshotContainsActiveAssistant && (activeAssistant.metadata.isOptimistic !== true || converted.at(-1)?.role !== "assistant");
1090
1084
  if (preservesActiveAssistant) converted.push(activeAssistant);
1091
1085
  this.applyExternalMessages(converted);
1092
- if (preservesActiveAssistant) {
1093
- this.recordedHistoryIds.delete(activeAssistant.id);
1094
- this.markPendingAssistantHistory(activeAssistant.id, snapshotHeadId);
1086
+ if (activeAssistant !== void 0) {
1087
+ const activeItem = this.session.tryGetMessage(activeAssistant.id);
1088
+ if (activeItem) {
1089
+ if (preservesActiveAssistant) this.snapshotHistoryIds.delete(activeAssistant.id);
1090
+ this.markPendingAssistantHistory(activeAssistant.id, activeItem.parentId);
1091
+ }
1095
1092
  }
1096
1093
  } catch (error) {
1097
1094
  this.logger.error?.("[agui] failed to import messages snapshot", error);
@@ -1108,32 +1105,82 @@ var AgUiThreadRuntimeCore = class {
1108
1105
  return status?.type === "requires-action" && status.reason === "interrupt";
1109
1106
  }
1110
1107
  recordHistoryEntry(parentId, message) {
1111
- this.appendHistoryItem(parentId, message);
1108
+ this.appendHistoryItem(parentId, message)?.catch((error) => {
1109
+ this.logger.error?.("[agui] failed to append history entry", error);
1110
+ });
1112
1111
  }
1113
1112
  markPendingAssistantHistory(messageId, parentId) {
1114
1113
  if (!this.history) return;
1115
1114
  this.assistantHistoryParents.set(messageId, parentId);
1116
1115
  }
1117
1116
  persistAssistantHistory(messageId) {
1118
- if (!this.history) return;
1117
+ const history = this.history;
1118
+ if (!history) return;
1119
1119
  const parentId = this.assistantHistoryParents.get(messageId);
1120
1120
  if (parentId === void 0) return;
1121
- const message = this.tryGetMessage(messageId)?.message;
1121
+ const message = this.session.tryGetMessage(messageId)?.message;
1122
1122
  if (!message || message.role !== "assistant") return;
1123
1123
  if (!this.isPersistableStatus(message.status)) return;
1124
+ const wasPersisted = this.persistedHistoryIds.has(messageId);
1125
+ const update = history.update;
1126
+ if (update !== void 0 && (wasPersisted || this.snapshotHistoryIds.has(messageId))) {
1127
+ const write = this.chainHistoryWrite(messageId, () => update.call(history, {
1128
+ parentId,
1129
+ message
1130
+ }));
1131
+ this.assistantHistoryParents.delete(messageId);
1132
+ write.then(() => {
1133
+ this.persistedHistoryIds.add(messageId);
1134
+ }, (error) => {
1135
+ const pending = this.historyWrites.get(messageId);
1136
+ if (pending === void 0 || pending === write) this.assistantHistoryParents.set(messageId, parentId);
1137
+ this.logger.error?.("[agui] failed to update history entry", error);
1138
+ });
1139
+ return;
1140
+ }
1141
+ if (wasPersisted) {
1142
+ this.assistantHistoryParents.delete(messageId);
1143
+ return;
1144
+ }
1145
+ const write = this.appendHistoryItem(parentId, message);
1146
+ if (!write) return;
1124
1147
  this.assistantHistoryParents.delete(messageId);
1125
- this.appendHistoryItem(parentId, message);
1148
+ write.then(() => {}, (error) => {
1149
+ const pending = this.historyWrites.get(messageId);
1150
+ if (pending === void 0 || pending === write) this.assistantHistoryParents.set(messageId, parentId);
1151
+ this.logger.error?.("[agui] failed to append history entry", error);
1152
+ });
1126
1153
  }
1127
1154
  appendHistoryItem(parentId, message) {
1128
- if (!this.history || this.recordedHistoryIds.has(message.id)) return;
1129
- this.recordedHistoryIds.add(message.id);
1130
- this.history.append({
1155
+ if (!this.history || this.persistedHistoryIds.has(message.id)) return;
1156
+ const pending = this.historyWrites.get(message.id);
1157
+ if (pending) return pending;
1158
+ const append = this.history.append.bind(this.history);
1159
+ const write = this.chainHistoryWrite(message.id, () => append({
1131
1160
  parentId,
1132
1161
  message
1133
- }).catch((error) => {
1134
- this.recordedHistoryIds.delete(message.id);
1135
- this.logger.error?.("[agui] failed to append history entry", error);
1162
+ }));
1163
+ write.then(() => {
1164
+ this.persistedHistoryIds.add(message.id);
1165
+ }, () => {});
1166
+ return write;
1167
+ }
1168
+ chainHistoryWrite(id, write) {
1169
+ const pending = this.historyWrites.get(id);
1170
+ let next;
1171
+ if (pending) next = pending.then(write, write);
1172
+ else try {
1173
+ next = Promise.resolve(write());
1174
+ } catch (error) {
1175
+ next = Promise.reject(error);
1176
+ }
1177
+ this.historyWrites.set(id, next);
1178
+ next.then(() => {
1179
+ if (this.historyWrites.get(id) === next) this.historyWrites.delete(id);
1180
+ }, () => {
1181
+ if (this.historyWrites.get(id) === next) this.historyWrites.delete(id);
1136
1182
  });
1183
+ return next;
1137
1184
  }
1138
1185
  };
1139
1186
  //#endregion