@assistant-ui/react-ag-ui 0.0.53 → 0.0.56

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 (49) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +2 -2
  3. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +23 -2
  4. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  5. package/dist/runtime/AgUiThreadRuntimeCore.js +183 -39
  6. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  7. package/dist/runtime/adapter/conversions.d.ts +19 -3
  8. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  9. package/dist/runtime/adapter/conversions.js +197 -36
  10. package/dist/runtime/adapter/conversions.js.map +1 -1
  11. package/dist/runtime/adapter/run-aggregator.d.ts +15 -2
  12. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  13. package/dist/runtime/adapter/run-aggregator.js +62 -10
  14. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  15. package/dist/runtime/adapter/subscriber.d.ts +6 -0
  16. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  17. package/dist/runtime/adapter/subscriber.js +10 -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 +15 -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/runtime/types.d.ts +5 -0
  31. package/dist/runtime/types.d.ts.map +1 -1
  32. package/dist/useAgUiRuntime.js +4 -1
  33. package/dist/useAgUiRuntime.js.map +1 -1
  34. package/package.json +7 -7
  35. package/src/index.ts +8 -2
  36. package/src/runtime/AgUiThreadRuntimeCore.ts +335 -42
  37. package/src/runtime/adapter/conversions.content.test.ts +278 -0
  38. package/src/runtime/adapter/conversions.ts +298 -50
  39. package/src/runtime/adapter/run-aggregator.approval.test.ts +103 -0
  40. package/src/runtime/adapter/run-aggregator.ts +96 -5
  41. package/src/runtime/adapter/subscriber.ts +17 -0
  42. package/src/runtime/adapter/tool-approval.test.ts +681 -0
  43. package/src/runtime/adapter/tool-approval.ts +366 -0
  44. package/src/runtime/event-parser.ts +23 -1
  45. package/src/runtime/interrupt-internals.ts +35 -0
  46. package/src/runtime/types.ts +6 -0
  47. package/src/useAgUiRuntime.approval.test.tsx +885 -0
  48. package/src/useAgUiRuntime.queue.test.tsx +68 -1
  49. package/src/useAgUiRuntime.ts +5 -1
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AgUiInterrupt, AgUiInterruptReason, AgUiResumeEntry, AgUiRunFinishedOutcome, UseAgUiRuntimeAdapters, UseAgUiRuntimeOptions, UseAgUiThreadListAdapter } from "./runtime/types.js";
2
2
  import { useAgUiInterrupts, useAgUiSendA2uiAction, useAgUiSetState, useAgUiState, useAgUiSteerAway, useAgUiSubmitInterruptResponses } from "./hooks.js";
3
3
  import { AgUiAssistantRuntime, useAgUiRuntime } from "./useAgUiRuntime.js";
4
- import { FromAgUiMessagesOptions, fromAgUiMessages } from "./runtime/adapter/conversions.js";
5
- export { type AgUiAssistantRuntime, type AgUiInterrupt, type AgUiInterruptReason, type AgUiResumeEntry, type AgUiRunFinishedOutcome, type FromAgUiMessagesOptions, type UseAgUiRuntimeAdapters, type UseAgUiRuntimeOptions, type UseAgUiThreadListAdapter, fromAgUiMessages, useAgUiInterrupts, useAgUiRuntime, useAgUiSendA2uiAction, useAgUiSetState, useAgUiState, useAgUiSteerAway, useAgUiSubmitInterruptResponses };
4
+ import { AgUiMessage, FromAgUiMessagesOptions, fromAgUiMessages, toAgUiMessages } from "./runtime/adapter/conversions.js";
5
+ export { type AgUiAssistantRuntime, type AgUiInterrupt, type AgUiInterruptReason, type AgUiMessage, type AgUiResumeEntry, type AgUiRunFinishedOutcome, type FromAgUiMessagesOptions, type UseAgUiRuntimeAdapters, type UseAgUiRuntimeOptions, type UseAgUiThreadListAdapter, fromAgUiMessages, toAgUiMessages, useAgUiInterrupts, useAgUiRuntime, useAgUiSendA2uiAction, useAgUiSetState, useAgUiState, useAgUiSteerAway, useAgUiSubmitInterruptResponses };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { useAgUiInterrupts, useAgUiSendA2uiAction, useAgUiSetState, useAgUiState, useAgUiSteerAway, useAgUiSubmitInterruptResponses } from "./hooks.js";
2
- import { fromAgUiMessages } from "./runtime/adapter/conversions.js";
2
+ import { fromAgUiMessages, toAgUiMessages } from "./runtime/adapter/conversions.js";
3
3
  import { useAgUiRuntime } from "./useAgUiRuntime.js";
4
- export { fromAgUiMessages, useAgUiInterrupts, useAgUiRuntime, useAgUiSendA2uiAction, useAgUiSetState, useAgUiState, useAgUiSteerAway, useAgUiSubmitInterruptResponses };
4
+ export { fromAgUiMessages, toAgUiMessages, useAgUiInterrupts, useAgUiRuntime, useAgUiSendA2uiAction, useAgUiSetState, useAgUiState, useAgUiSteerAway, useAgUiSubmitInterruptResponses };
@@ -1,6 +1,6 @@
1
1
  import { Logger } from "./logger.js";
2
2
  import { AgUiInterrupt, AgUiResumeEntry } from "./types.js";
3
- import { AddToolResultOptions, AppendMessage, AssistantRuntime, ChatModelRunOptions, ChatModelRunResult, CreateAppendMessage, ExportedMessageRepository, ThreadHistoryAdapter, ThreadMessage } from "@assistant-ui/core";
3
+ import { AddToolResultOptions, AppendMessage, AssistantRuntime, ChatModelRunOptions, ChatModelRunResult, CreateAppendMessage, ExportedMessageRepository, RespondToToolApprovalOptions, ThreadHistoryAdapter, ThreadMessage } from "@assistant-ui/core";
4
4
  import { AbstractAgent } from "@ag-ui/client";
5
5
  import { ReadonlyJSONValue } from "assistant-stream/utils";
6
6
  //#region src/runtime/AgUiThreadRuntimeCore.d.ts
@@ -30,6 +30,7 @@ declare class AgUiThreadRuntimeCore {
30
30
  private onError;
31
31
  private onCancel;
32
32
  private readonly notifyUpdate;
33
+ private readonly reportedErrors;
33
34
  private runtime;
34
35
  private readonly repository;
35
36
  private exportedRepository;
@@ -41,7 +42,9 @@ declare class AgUiThreadRuntimeCore {
41
42
  private history;
42
43
  private lastRunConfig;
43
44
  private readonly assistantHistoryParents;
44
- private readonly recordedHistoryIds;
45
+ private readonly snapshotHistoryIds;
46
+ private readonly persistedHistoryIds;
47
+ private readonly historyWrites;
45
48
  private _isLoading;
46
49
  private _loadPromise;
47
50
  private pendingResumeMessageId;
@@ -88,6 +91,23 @@ declare class AgUiThreadRuntimeCore {
88
91
  toolCallIds: string[];
89
92
  } | null;
90
93
  submitInterruptResponses(responses: readonly AgUiResumeEntry[]): Promise<void>;
94
+ /**
95
+ * Resumes with an already validated resume array. Answerability is checked
96
+ * once per submission: re-checking here would let a clock crossing reject a
97
+ * decision this runtime has already recorded, stranding the gate decided and
98
+ * unretryable.
99
+ */
100
+ private resumeWithResponses;
101
+ /**
102
+ * The core seams that reach `respondToToolApproval` discard the promise they
103
+ * receive, so a rejected decision would otherwise surface only as an
104
+ * unhandled rejection. A failure raised by the resumed run itself was already
105
+ * reported by `startRun` before it rethrew, so reporting it here again would
106
+ * give the consumer two notifications for one failure.
107
+ */
108
+ reportError(error: unknown): void;
109
+ private assertInterruptsAnswerable;
110
+ respondToToolApproval(options: RespondToToolApprovalOptions): Promise<void>;
91
111
  steerAway(message: CreateAppendMessage, responses?: readonly AgUiResumeEntry[]): Promise<void>;
92
112
  private resolveSteerAwayResume;
93
113
  private collectInterruptResponses;
@@ -127,6 +147,7 @@ declare class AgUiThreadRuntimeCore {
127
147
  private markPendingAssistantHistory;
128
148
  private persistAssistantHistory;
129
149
  private appendHistoryItem;
150
+ private chainHistoryWrite;
130
151
  }
131
152
  //#endregion
132
153
  export { AgUiThreadRuntimeCore };
@@ -1 +1 @@
1
- {"version":3,"file":"AgUiThreadRuntimeCore.d.ts","names":[],"sources":["../../src/runtime/AgUiThreadRuntimeCore.ts"],"mappings":";;;;;;KAoEK,YAAY,YAAY;KACxB,gBACH,SAAS,wBACN,eAAe;KACf;EACH;EACA;EACA,WAAW;EACX,SAAS;;KAGN;EACH,OAAO;EACP,QAAQ;EACR;EACA;EACA,WAAW,OAAO;EAClB;EACA,UAAU;EACV;;cAmCW;UACH;UACA;UACA;UACA;UACA;UACA;mBACS;UAET;mBACS;UACT;UACA;UACA;UAGA;UACA;UACA;UACA;UACA;mBACS;mBACA;UACT;UACA;UACA;UACA;UACA;EAEI,YAAA,SAAS;EAYrB,cAAc,SAAS,KAAK;EAW5B,cAAc,SAAS;EAIvB;EAIA,wBAAwB;EAIxB,wBAAwB;UAmBhB;UAQA;UAUA;UAIA;UAQA;UAQA;UAMA;UAKA;UAKA;UAKA;EAYR,YAAY;EAIZ;MAII;EAIJ,mBAAmB;EA6Cb,OAAO,SAAS,gBAAgB;UAW9B;UAQA;EAiBF,KAAK,SAAS,gBAAgB;EAI9B,OACJ,yBACA;IAAU,YAAY;MACrB;EAMG,UAAU;EAcV,OAAO,QAAQ,kBAAkB;EAUjC,kBAAkB,mBAAmB,kBAAkB;UAwBrD;UAOA;EAgBR;IACE;IACA,qBAAqB;;EAavB;IACE;IACA;;EAcI,yBACJ,oBAAoB,oBACnB;EAoDG,UACJ,SAAS,qBACT,qBAAqB,oBACpB;UAyCK;UAkBA;UAiCA;UA0BA;EA+BR,yBAAyB;UAkBjB;EAiBR,cAAc,SAAS;EAyBvB,eAAe,QAAQ;UA4Bf;UAYA;UA6BA;EAUR,sBAAsB,mBAAmB;UAkDjC;EAgER,kBAAkB,OAAO;EAKzB,SACE,MACI,sBACE,MAAM,kCAAkC;EAOhD;UAKc;UAuMA;UA2DN;UAkCA;UAqBA;UAKA;UAQA;UAyBA;UA2CA;UAiDA;UA4BA;UA6BA;UAgEA;UA2EA;UA8DA;UAgDA;UAQA;UAIA;UAKA;UAIA;UAQA;UAWA"}
1
+ {"version":3,"file":"AgUiThreadRuntimeCore.d.ts","names":[],"sources":["../../src/runtime/AgUiThreadRuntimeCore.ts"],"mappings":";;;;;;KA2EK,YAAY,YAAY;KACxB,gBACH,SAAS,wBACN,eAAe;KACf;EACH;EACA;EACA,WAAW;EACX,SAAS;;KAGN;EACH,OAAO;EACP,QAAQ;EACR;EACA;EACA,WAAW,OAAO;EAClB;EACA,UAAU;EACV;;cAuDW;UACH;UACA;UACA;UACA;UACA;UACA;mBACS;mBACA;UAET;mBACS;UACT;UACA;UACA;UAGA;UACA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;UACT;UACA;UACA;UACA;UACA;EAEI,YAAA,SAAS;EAYrB,cAAc,SAAS,KAAK;EAW5B,cAAc,SAAS;EAIvB;EAOA,wBAAwB;EAIxB,wBAAwB;UAmBhB;UAQA;UAUA;UAIA;UAQA;UAQA;UAMA;UAKA;UAKA;UAKA;EAYR,YAAY;EAIZ;MAII;EAIJ,mBAAmB;EA6Cb,OAAO,SAAS,gBAAgB;UAW9B;UAQA;EAiBF,KAAK,SAAS,gBAAgB;EAI9B,OACJ,yBACA;IAAU,YAAY;MACrB;EAMG,UAAU;EAcV,OAAO,QAAQ,kBAAkB;EAUjC,kBAAkB,mBAAmB,kBAAkB;UAwBrD;UAOA;EAgBR;IACE;IACA,qBAAqB;;EAavB;IACE;IACA;;EAcI,yBACJ,oBAAoB,oBACnB;;;;;;;UAuCW;;;;;;;;EAed,YAAY;UAYJ;EAwBF,sBACJ,SAAS,+BACR;EAoEG,UACJ,SAAS,qBACT,qBAAqB,oBACpB;UAyCK;UAkBA;UAiCA;UA0BA;EAmCR,yBAAyB;UAkBjB;EAiBR,cAAc,SAAS;EAyBvB,eAAe,QAAQ;UA4Bf;UAYA;UA6BA;EAUR,sBAAsB,mBAAmB;UAkDjC;EAiER,kBAAkB,OAAO;EAKzB,SACE,MACI,sBACE,MAAM,kCAAkC;EAOhD;UAKc;UAoOA;UA2DN;UAkCA;UAqBA;UAKA;UAQA;UAyBA;UAkEA;UAiDA;UA4BA;UA6BA;UAgEA;UA2EA;UA8DA;UAuDA;UAQA;UAIA;UAKA;UAMA;UAQA;UAsDA;UAqBA"}
@@ -1,5 +1,6 @@
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";
@@ -30,6 +31,19 @@ const invokeRuntimeCallback = (name, callback, ...args) => {
30
31
  reportCallbackError(name, error);
31
32
  }
32
33
  };
34
+ const mergeAgUiNamespace = (current, incoming) => {
35
+ const merged = {
36
+ ...current,
37
+ ...incoming
38
+ };
39
+ const currentNs = current?.[AG_UI_METADATA_NAMESPACE];
40
+ const incomingNs = incoming[AG_UI_METADATA_NAMESPACE];
41
+ if (typeof currentNs === "object" && currentNs !== null && typeof incomingNs === "object" && incomingNs !== null) merged[AG_UI_METADATA_NAMESPACE] = {
42
+ ...currentNs,
43
+ ...incomingNs
44
+ };
45
+ return merged;
46
+ };
33
47
  var AgUiThreadRuntimeCore = class {
34
48
  agent;
35
49
  logger;
@@ -38,6 +52,7 @@ var AgUiThreadRuntimeCore = class {
38
52
  onError;
39
53
  onCancel;
40
54
  notifyUpdate;
55
+ reportedErrors = /* @__PURE__ */ new WeakSet();
41
56
  runtime;
42
57
  repository = new MessageRepository();
43
58
  exportedRepository;
@@ -49,7 +64,9 @@ var AgUiThreadRuntimeCore = class {
49
64
  history;
50
65
  lastRunConfig;
51
66
  assistantHistoryParents = /* @__PURE__ */ new Map();
52
- recordedHistoryIds = /* @__PURE__ */ new Set();
67
+ snapshotHistoryIds = /* @__PURE__ */ new Set();
68
+ persistedHistoryIds = /* @__PURE__ */ new Set();
69
+ historyWrites = /* @__PURE__ */ new Map();
53
70
  _isLoading = false;
54
71
  _loadPromise;
55
72
  pendingResumeMessageId = null;
@@ -81,6 +98,9 @@ var AgUiThreadRuntimeCore = class {
81
98
  }
82
99
  detachRuntime() {
83
100
  this.runtime = void 0;
101
+ this.cancel().catch((error) => {
102
+ this.logger.error("[agui] failed to cancel run during teardown", error);
103
+ });
84
104
  }
85
105
  getMessages() {
86
106
  return this.repository.getMessages();
@@ -285,17 +305,65 @@ var AgUiThreadRuntimeCore = class {
285
305
  const openIds = pending.interrupts.map((i) => i.id);
286
306
  const missing = openIds.filter((id) => !responsesById.has(id));
287
307
  if (missing.length > 0) throw new Error(`[agui] submitInterruptResponses: missing responses for open interrupts: ${missing.join(", ")}`);
308
+ this.assertInterruptsAnswerable("submitInterruptResponses", pending.interrupts);
309
+ await this.resumeWithResponses(pending.messageId, openIds.map((id) => responsesById.get(id)));
310
+ }
311
+ /**
312
+ * Resumes with an already validated resume array. Answerability is checked
313
+ * once per submission: re-checking here would let a clock crossing reject a
314
+ * decision this runtime has already recorded, stranding the gate decided and
315
+ * unretryable.
316
+ */
317
+ async resumeWithResponses(messageId, resume) {
318
+ this.clearPendingInterrupts(messageId, resume);
319
+ await this.startRun(messageId, this.lastRunConfig, resume);
320
+ }
321
+ /**
322
+ * The core seams that reach `respondToToolApproval` discard the promise they
323
+ * receive, so a rejected decision would otherwise surface only as an
324
+ * unhandled rejection. A failure raised by the resumed run itself was already
325
+ * reported by `startRun` before it rethrew, so reporting it here again would
326
+ * give the consumer two notifications for one failure.
327
+ */
328
+ reportError(error) {
329
+ if (this.reportedErrors.has(error)) {
330
+ this.reportedErrors.delete(error);
331
+ return;
332
+ }
333
+ invokeRuntimeCallback("onError", this.onError, error instanceof Error ? error : new Error(String(error)));
334
+ }
335
+ assertInterruptsAnswerable(method, interrupts) {
288
336
  const now = Date.now();
289
- for (const interrupt of pending.interrupts) {
337
+ for (const interrupt of interrupts) {
290
338
  if (!interrupt.expiresAt) continue;
291
339
  const expiry = new Date(interrupt.expiresAt).getTime();
292
- if (Number.isNaN(expiry)) throw new Error(`[agui] submitInterruptResponses: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`);
293
- if (expiry <= now) throw new Error(`[agui] submitInterruptResponses: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`);
340
+ if (Number.isNaN(expiry)) throw new Error(`[agui] ${method}: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`);
341
+ if (expiry <= now) throw new Error(`[agui] ${method}: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`);
294
342
  }
295
- const resume = openIds.map((id) => responsesById.get(id));
296
- if (this.isRunningFlag) throw new Error("[agui] submitInterruptResponses: a run is already in progress");
297
- this.clearPendingInterrupts(pending.messageId);
298
- await this.startRun(pending.messageId, this.lastRunConfig, resume);
343
+ if (this.isRunningFlag) throw new Error(`[agui] ${method}: a run is already in progress`);
344
+ }
345
+ async respondToToolApproval(options) {
346
+ const pending = this.getPendingInterrupts();
347
+ if (!pending) throw new Error("[agui] respondToToolApproval: no pending interrupts on this thread");
348
+ const gatedMessage = this.tryGetMessage(pending.messageId)?.message;
349
+ 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}"`);
350
+ this.assertInterruptsAnswerable("respondToToolApproval", pending.interrupts);
351
+ if (!this.updateMessage(pending.messageId, (message) => {
352
+ if (message.role !== "assistant") return message;
353
+ const assistant = message;
354
+ const content = withToolApprovalDecision(assistant.content, options);
355
+ if (content === assistant.content) return assistant;
356
+ return {
357
+ ...assistant,
358
+ content
359
+ };
360
+ })) throw new Error(`[agui] respondToToolApproval: approval "${options.approvalId}" is already decided`);
361
+ this.notifyUpdate();
362
+ const assistant = this.tryGetMessage(pending.messageId)?.message;
363
+ if (!assistant) return;
364
+ const resume = buildToolApprovalResume(assistant.content, pending.interrupts);
365
+ if (!resume) return;
366
+ await this.resumeWithResponses(pending.messageId, resume);
299
367
  }
300
368
  async steerAway(message, responses) {
301
369
  const pending = this.getPendingInterrupts();
@@ -318,7 +386,7 @@ var AgUiThreadRuntimeCore = class {
318
386
  const resume = this.resolveSteerAwayResume(pending.interrupts, responses);
319
387
  if (this.isRunningFlag) throw new Error("[agui] steerAway: a run is already in progress");
320
388
  const normalized = this.toAppendMessage(message);
321
- this.clearPendingInterrupts(pending.messageId);
389
+ this.clearPendingInterrupts(pending.messageId, resume);
322
390
  const threadMessageId = this.appendEntry(normalized);
323
391
  await this.startRun(threadMessageId, normalized.runConfig, resume);
324
392
  }
@@ -367,7 +435,7 @@ var AgUiThreadRuntimeCore = class {
367
435
  startRun: message.startRun
368
436
  };
369
437
  }
370
- clearPendingInterrupts(messageId) {
438
+ clearPendingInterrupts(messageId, resume) {
371
439
  if (this.updateMessage(messageId, (message) => {
372
440
  if (message.role !== "assistant") return message;
373
441
  const assistant = message;
@@ -378,6 +446,7 @@ var AgUiThreadRuntimeCore = class {
378
446
  else delete newCustom["agui"];
379
447
  return {
380
448
  ...assistant,
449
+ content: withSettledToolApprovals(assistant.content, resume),
381
450
  status: {
382
451
  type: "complete",
383
452
  reason: "unknown"
@@ -530,8 +599,8 @@ var AgUiThreadRuntimeCore = class {
530
599
  }
531
600
  this.resetRepositoryHead(lastAppliedId);
532
601
  }
533
- this.recordedHistoryIds.clear();
534
- for (const { message } of this.getMessageRepository().messages) this.recordedHistoryIds.add(message.id);
602
+ this.snapshotHistoryIds.clear();
603
+ for (const { message } of this.getMessageRepository().messages) this.snapshotHistoryIds.add(message.id);
535
604
  this.notifyUpdate();
536
605
  }
537
606
  applyExternalMessageRepository(loaded) {
@@ -580,8 +649,9 @@ var AgUiThreadRuntimeCore = class {
580
649
  this.resetRepositoryHead(previousId);
581
650
  } else this.resetRepositoryHead(headId);
582
651
  this.assistantHistoryParents.clear();
583
- this.recordedHistoryIds.clear();
584
- for (const { message } of loaded.messages) this.recordedHistoryIds.add(message.id);
652
+ this.snapshotHistoryIds.clear();
653
+ this.persistedHistoryIds.clear();
654
+ for (const { message } of loaded.messages) this.persistedHistoryIds.add(message.id);
585
655
  this.notifyUpdate();
586
656
  }
587
657
  loadExternalState(state) {
@@ -602,6 +672,7 @@ var AgUiThreadRuntimeCore = class {
602
672
  const parent = parentId === null ? void 0 : this.tryGetMessage(parentId);
603
673
  const shouldEagerlyInsertAssistant = parentId !== null && parent !== void 0 && parentId !== this.repository.headId;
604
674
  const historicalMessages = [...shouldEagerlyInsertAssistant ? this.tryGetMessages(parentId) ?? this.repository.getMessages() : this.repository.getMessages()];
675
+ const runStartMessageIds = new Set(this.getMessageRepository().messages.map(({ message }) => message.id));
605
676
  this.pendingError = null;
606
677
  const assistantParentId = parent ? parentId : this.repository.headId;
607
678
  let assistantMessageId;
@@ -610,7 +681,8 @@ var AgUiThreadRuntimeCore = class {
610
681
  const cached = assistantMessageId;
611
682
  if (cached !== void 0 && this.tryGetMessage(cached)) return cached;
612
683
  if (cached !== void 0 && (assistantCollided || !allowRecreate)) return cached;
613
- const parentId = cached === void 0 && assistantParentId && this.hasMessage(assistantParentId) ? assistantParentId : this.repository.headId;
684
+ const repositoryHeadId = this.repository.headId;
685
+ const parentId = (cached === void 0 || shouldEagerlyInsertAssistant && (repositoryHeadId === null || runStartMessageIds.has(repositoryHeadId))) && assistantParentId && this.hasMessage(assistantParentId) ? assistantParentId : repositoryHeadId;
614
686
  const created = this.insertAssistantPlaceholder(parentId, cached);
615
687
  assistantMessageId = created;
616
688
  this.markPendingAssistantHistory(created, parentId);
@@ -618,8 +690,8 @@ var AgUiThreadRuntimeCore = class {
618
690
  };
619
691
  if (shouldEagerlyInsertAssistant) ensureAssistant();
620
692
  const applyUpdate = (update) => {
621
- const hasContent = Array.isArray(update.content) && update.content.length > 0;
622
- const resolved = this.updateAssistantMessage(ensureAssistant(hasContent), update);
693
+ const hasStreamContent = Array.isArray(update.content) && update.content.some((part) => part.type !== "data");
694
+ const resolved = this.updateAssistantMessage(ensureAssistant(hasStreamContent), update);
623
695
  if (resolved !== assistantMessageId) assistantMessageId = resolved;
624
696
  };
625
697
  const aggregator = new RunAggregator({
@@ -629,8 +701,15 @@ var AgUiThreadRuntimeCore = class {
629
701
  onServerMessageId: (serverId) => {
630
702
  const placeholder = ensureAssistant(true);
631
703
  if (placeholder === serverId) return;
632
- if (this.reassignAssistantId(placeholder, serverId)) assistantMessageId = serverId;
633
- else assistantCollided = true;
704
+ const reassigned = this.reassignAssistantId(placeholder, serverId);
705
+ const adoptsVisibleCollision = !reassigned && !runStartMessageIds.has(serverId) && this.repository.headId === serverId;
706
+ if (reassigned || adoptsVisibleCollision) {
707
+ assistantMessageId = serverId;
708
+ if (adoptsVisibleCollision) {
709
+ const parentId = this.tryGetMessage(serverId)?.parentId ?? null;
710
+ this.markPendingAssistantHistory(serverId, parentId);
711
+ }
712
+ } else assistantCollided = true;
634
713
  }
635
714
  });
636
715
  const dispatch = (event) => this.handleEvent(aggregator, event, assistantMessageId);
@@ -701,6 +780,7 @@ var AgUiThreadRuntimeCore = class {
701
780
  }
702
781
  if (this.pendingError) {
703
782
  const err = this.pendingError;
783
+ this.reportedErrors.add(err);
704
784
  this.pendingError = null;
705
785
  this.pendingResumeMessageId = null;
706
786
  this.pendingA2uiResume = false;
@@ -872,9 +952,24 @@ var AgUiThreadRuntimeCore = class {
872
952
  this.assistantHistoryParents.delete(oldId);
873
953
  if (!collidesWithExisting && !this.assistantHistoryParents.has(newId)) this.assistantHistoryParents.set(newId, pendingParent);
874
954
  }
875
- if (this.recordedHistoryIds.has(oldId)) {
876
- this.recordedHistoryIds.delete(oldId);
877
- if (!collidesWithExisting) this.recordedHistoryIds.add(newId);
955
+ for (const ids of [this.snapshotHistoryIds, this.persistedHistoryIds]) if (ids.has(oldId)) {
956
+ ids.delete(oldId);
957
+ if (!collidesWithExisting) ids.add(newId);
958
+ }
959
+ const pendingWrite = this.historyWrites.get(oldId);
960
+ if (pendingWrite) {
961
+ this.historyWrites.delete(oldId);
962
+ if (!collidesWithExisting) {
963
+ this.historyWrites.set(newId, pendingWrite);
964
+ const settle = () => {
965
+ if (this.historyWrites.get(newId) === pendingWrite) this.historyWrites.delete(newId);
966
+ };
967
+ pendingWrite.then(() => {
968
+ this.persistedHistoryIds.delete(oldId);
969
+ this.persistedHistoryIds.add(newId);
970
+ settle();
971
+ }, settle);
972
+ }
878
973
  }
879
974
  this.notifyUpdate();
880
975
  return !collidesWithExisting;
@@ -934,10 +1029,7 @@ var AgUiThreadRuntimeCore = class {
934
1029
  steps,
935
1030
  ...current.isOptimistic ? { isOptimistic: true } : {},
936
1031
  ...incoming.timing ? { timing: incoming.timing } : {},
937
- custom: incoming.custom ? {
938
- ...current.custom,
939
- ...incoming.custom
940
- } : current.custom
1032
+ custom: incoming.custom ? mergeAgUiNamespace(current.custom, incoming.custom) : current.custom
941
1033
  };
942
1034
  }
943
1035
  handleEvent(aggregator, event, activeAssistantId) {
@@ -1074,14 +1166,16 @@ var AgUiThreadRuntimeCore = class {
1074
1166
  } catch (error) {
1075
1167
  this.logger.error?.("[agui] failed to import message from snapshot", error);
1076
1168
  }
1077
- const snapshotHeadId = converted.at(-1)?.id ?? null;
1078
1169
  const snapshotContainsActiveAssistant = converted.some((message) => message.id === activeAssistant?.id);
1079
1170
  const preservesActiveAssistant = activeAssistant !== void 0 && !snapshotContainsActiveAssistant && (activeAssistant.metadata.isOptimistic !== true || converted.at(-1)?.role !== "assistant");
1080
1171
  if (preservesActiveAssistant) converted.push(activeAssistant);
1081
1172
  this.applyExternalMessages(converted);
1082
- if (preservesActiveAssistant) {
1083
- this.recordedHistoryIds.delete(activeAssistant.id);
1084
- this.markPendingAssistantHistory(activeAssistant.id, snapshotHeadId);
1173
+ if (activeAssistant !== void 0) {
1174
+ const activeItem = this.tryGetMessage(activeAssistant.id);
1175
+ if (activeItem) {
1176
+ if (preservesActiveAssistant) this.snapshotHistoryIds.delete(activeAssistant.id);
1177
+ this.markPendingAssistantHistory(activeAssistant.id, activeItem.parentId);
1178
+ }
1085
1179
  }
1086
1180
  } catch (error) {
1087
1181
  this.logger.error?.("[agui] failed to import messages snapshot", error);
@@ -1098,32 +1192,82 @@ var AgUiThreadRuntimeCore = class {
1098
1192
  return status?.type === "requires-action" && status.reason === "interrupt";
1099
1193
  }
1100
1194
  recordHistoryEntry(parentId, message) {
1101
- this.appendHistoryItem(parentId, message);
1195
+ this.appendHistoryItem(parentId, message)?.catch((error) => {
1196
+ this.logger.error?.("[agui] failed to append history entry", error);
1197
+ });
1102
1198
  }
1103
1199
  markPendingAssistantHistory(messageId, parentId) {
1104
1200
  if (!this.history) return;
1105
1201
  this.assistantHistoryParents.set(messageId, parentId);
1106
1202
  }
1107
1203
  persistAssistantHistory(messageId) {
1108
- if (!this.history) return;
1204
+ const history = this.history;
1205
+ if (!history) return;
1109
1206
  const parentId = this.assistantHistoryParents.get(messageId);
1110
1207
  if (parentId === void 0) return;
1111
1208
  const message = this.tryGetMessage(messageId)?.message;
1112
1209
  if (!message || message.role !== "assistant") return;
1113
1210
  if (!this.isPersistableStatus(message.status)) return;
1211
+ const wasPersisted = this.persistedHistoryIds.has(messageId);
1212
+ const update = history.update;
1213
+ if (update !== void 0 && (wasPersisted || this.snapshotHistoryIds.has(messageId))) {
1214
+ const write = this.chainHistoryWrite(messageId, () => update.call(history, {
1215
+ parentId,
1216
+ message
1217
+ }));
1218
+ this.assistantHistoryParents.delete(messageId);
1219
+ write.then(() => {
1220
+ this.persistedHistoryIds.add(messageId);
1221
+ }, (error) => {
1222
+ const pending = this.historyWrites.get(messageId);
1223
+ if (pending === void 0 || pending === write) this.assistantHistoryParents.set(messageId, parentId);
1224
+ this.logger.error?.("[agui] failed to update history entry", error);
1225
+ });
1226
+ return;
1227
+ }
1228
+ if (wasPersisted) {
1229
+ this.assistantHistoryParents.delete(messageId);
1230
+ return;
1231
+ }
1232
+ const write = this.appendHistoryItem(parentId, message);
1233
+ if (!write) return;
1114
1234
  this.assistantHistoryParents.delete(messageId);
1115
- this.appendHistoryItem(parentId, message);
1235
+ write.then(() => {}, (error) => {
1236
+ const pending = this.historyWrites.get(messageId);
1237
+ if (pending === void 0 || pending === write) this.assistantHistoryParents.set(messageId, parentId);
1238
+ this.logger.error?.("[agui] failed to append history entry", error);
1239
+ });
1116
1240
  }
1117
1241
  appendHistoryItem(parentId, message) {
1118
- if (!this.history || this.recordedHistoryIds.has(message.id)) return;
1119
- this.recordedHistoryIds.add(message.id);
1120
- this.history.append({
1242
+ if (!this.history || this.persistedHistoryIds.has(message.id)) return;
1243
+ const pending = this.historyWrites.get(message.id);
1244
+ if (pending) return pending;
1245
+ const append = this.history.append.bind(this.history);
1246
+ const write = this.chainHistoryWrite(message.id, () => append({
1121
1247
  parentId,
1122
1248
  message
1123
- }).catch((error) => {
1124
- this.recordedHistoryIds.delete(message.id);
1125
- this.logger.error?.("[agui] failed to append history entry", error);
1249
+ }));
1250
+ write.then(() => {
1251
+ this.persistedHistoryIds.add(message.id);
1252
+ }, () => {});
1253
+ return write;
1254
+ }
1255
+ chainHistoryWrite(id, write) {
1256
+ const pending = this.historyWrites.get(id);
1257
+ let next;
1258
+ if (pending) next = pending.then(write, write);
1259
+ else try {
1260
+ next = Promise.resolve(write());
1261
+ } catch (error) {
1262
+ next = Promise.reject(error);
1263
+ }
1264
+ this.historyWrites.set(id, next);
1265
+ next.then(() => {
1266
+ if (this.historyWrites.get(id) === next) this.historyWrites.delete(id);
1267
+ }, () => {
1268
+ if (this.historyWrites.get(id) === next) this.historyWrites.delete(id);
1126
1269
  });
1270
+ return next;
1127
1271
  }
1128
1272
  };
1129
1273
  //#endregion