@assistant-ui/react-ag-ui 0.0.54 → 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 (45) 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 +169 -35
  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 +58 -26
  10. package/dist/runtime/adapter/conversions.js.map +1 -1
  11. package/dist/runtime/adapter/run-aggregator.d.ts +5 -1
  12. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  13. package/dist/runtime/adapter/run-aggregator.js +26 -1
  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 +3 -0
  31. package/dist/useAgUiRuntime.js.map +1 -1
  32. package/package.json +7 -7
  33. package/src/index.ts +8 -2
  34. package/src/runtime/AgUiThreadRuntimeCore.ts +314 -41
  35. package/src/runtime/adapter/conversions.content.test.ts +61 -3
  36. package/src/runtime/adapter/conversions.ts +83 -39
  37. package/src/runtime/adapter/run-aggregator.approval.test.ts +103 -0
  38. package/src/runtime/adapter/run-aggregator.ts +37 -1
  39. package/src/runtime/adapter/subscriber.ts +14 -0
  40. package/src/runtime/adapter/tool-approval.test.ts +681 -0
  41. package/src/runtime/adapter/tool-approval.ts +366 -0
  42. package/src/runtime/event-parser.ts +10 -1
  43. package/src/runtime/interrupt-internals.ts +35 -0
  44. package/src/useAgUiRuntime.approval.test.tsx +885 -0
  45. package/src/useAgUiRuntime.ts +4 -0
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;;cAuDW;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";
@@ -51,6 +52,7 @@ var AgUiThreadRuntimeCore = class {
51
52
  onError;
52
53
  onCancel;
53
54
  notifyUpdate;
55
+ reportedErrors = /* @__PURE__ */ new WeakSet();
54
56
  runtime;
55
57
  repository = new MessageRepository();
56
58
  exportedRepository;
@@ -62,7 +64,9 @@ var AgUiThreadRuntimeCore = class {
62
64
  history;
63
65
  lastRunConfig;
64
66
  assistantHistoryParents = /* @__PURE__ */ new Map();
65
- recordedHistoryIds = /* @__PURE__ */ new Set();
67
+ snapshotHistoryIds = /* @__PURE__ */ new Set();
68
+ persistedHistoryIds = /* @__PURE__ */ new Set();
69
+ historyWrites = /* @__PURE__ */ new Map();
66
70
  _isLoading = false;
67
71
  _loadPromise;
68
72
  pendingResumeMessageId = null;
@@ -94,6 +98,9 @@ var AgUiThreadRuntimeCore = class {
94
98
  }
95
99
  detachRuntime() {
96
100
  this.runtime = void 0;
101
+ this.cancel().catch((error) => {
102
+ this.logger.error("[agui] failed to cancel run during teardown", error);
103
+ });
97
104
  }
98
105
  getMessages() {
99
106
  return this.repository.getMessages();
@@ -298,17 +305,65 @@ var AgUiThreadRuntimeCore = class {
298
305
  const openIds = pending.interrupts.map((i) => i.id);
299
306
  const missing = openIds.filter((id) => !responsesById.has(id));
300
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) {
301
336
  const now = Date.now();
302
- for (const interrupt of pending.interrupts) {
337
+ for (const interrupt of interrupts) {
303
338
  if (!interrupt.expiresAt) continue;
304
339
  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}`);
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}`);
307
342
  }
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);
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);
312
367
  }
313
368
  async steerAway(message, responses) {
314
369
  const pending = this.getPendingInterrupts();
@@ -331,7 +386,7 @@ var AgUiThreadRuntimeCore = class {
331
386
  const resume = this.resolveSteerAwayResume(pending.interrupts, responses);
332
387
  if (this.isRunningFlag) throw new Error("[agui] steerAway: a run is already in progress");
333
388
  const normalized = this.toAppendMessage(message);
334
- this.clearPendingInterrupts(pending.messageId);
389
+ this.clearPendingInterrupts(pending.messageId, resume);
335
390
  const threadMessageId = this.appendEntry(normalized);
336
391
  await this.startRun(threadMessageId, normalized.runConfig, resume);
337
392
  }
@@ -380,7 +435,7 @@ var AgUiThreadRuntimeCore = class {
380
435
  startRun: message.startRun
381
436
  };
382
437
  }
383
- clearPendingInterrupts(messageId) {
438
+ clearPendingInterrupts(messageId, resume) {
384
439
  if (this.updateMessage(messageId, (message) => {
385
440
  if (message.role !== "assistant") return message;
386
441
  const assistant = message;
@@ -391,6 +446,7 @@ var AgUiThreadRuntimeCore = class {
391
446
  else delete newCustom["agui"];
392
447
  return {
393
448
  ...assistant,
449
+ content: withSettledToolApprovals(assistant.content, resume),
394
450
  status: {
395
451
  type: "complete",
396
452
  reason: "unknown"
@@ -543,8 +599,8 @@ var AgUiThreadRuntimeCore = class {
543
599
  }
544
600
  this.resetRepositoryHead(lastAppliedId);
545
601
  }
546
- this.recordedHistoryIds.clear();
547
- 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);
548
604
  this.notifyUpdate();
549
605
  }
550
606
  applyExternalMessageRepository(loaded) {
@@ -593,8 +649,9 @@ var AgUiThreadRuntimeCore = class {
593
649
  this.resetRepositoryHead(previousId);
594
650
  } else this.resetRepositoryHead(headId);
595
651
  this.assistantHistoryParents.clear();
596
- this.recordedHistoryIds.clear();
597
- 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);
598
655
  this.notifyUpdate();
599
656
  }
600
657
  loadExternalState(state) {
@@ -615,6 +672,7 @@ var AgUiThreadRuntimeCore = class {
615
672
  const parent = parentId === null ? void 0 : this.tryGetMessage(parentId);
616
673
  const shouldEagerlyInsertAssistant = parentId !== null && parent !== void 0 && parentId !== this.repository.headId;
617
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));
618
676
  this.pendingError = null;
619
677
  const assistantParentId = parent ? parentId : this.repository.headId;
620
678
  let assistantMessageId;
@@ -623,7 +681,8 @@ var AgUiThreadRuntimeCore = class {
623
681
  const cached = assistantMessageId;
624
682
  if (cached !== void 0 && this.tryGetMessage(cached)) return cached;
625
683
  if (cached !== void 0 && (assistantCollided || !allowRecreate)) return cached;
626
- 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;
627
686
  const created = this.insertAssistantPlaceholder(parentId, cached);
628
687
  assistantMessageId = created;
629
688
  this.markPendingAssistantHistory(created, parentId);
@@ -631,8 +690,8 @@ var AgUiThreadRuntimeCore = class {
631
690
  };
632
691
  if (shouldEagerlyInsertAssistant) ensureAssistant();
633
692
  const applyUpdate = (update) => {
634
- const hasContent = Array.isArray(update.content) && update.content.length > 0;
635
- 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);
636
695
  if (resolved !== assistantMessageId) assistantMessageId = resolved;
637
696
  };
638
697
  const aggregator = new RunAggregator({
@@ -642,8 +701,15 @@ var AgUiThreadRuntimeCore = class {
642
701
  onServerMessageId: (serverId) => {
643
702
  const placeholder = ensureAssistant(true);
644
703
  if (placeholder === serverId) return;
645
- if (this.reassignAssistantId(placeholder, serverId)) assistantMessageId = serverId;
646
- 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;
647
713
  }
648
714
  });
649
715
  const dispatch = (event) => this.handleEvent(aggregator, event, assistantMessageId);
@@ -714,6 +780,7 @@ var AgUiThreadRuntimeCore = class {
714
780
  }
715
781
  if (this.pendingError) {
716
782
  const err = this.pendingError;
783
+ this.reportedErrors.add(err);
717
784
  this.pendingError = null;
718
785
  this.pendingResumeMessageId = null;
719
786
  this.pendingA2uiResume = false;
@@ -885,9 +952,24 @@ var AgUiThreadRuntimeCore = class {
885
952
  this.assistantHistoryParents.delete(oldId);
886
953
  if (!collidesWithExisting && !this.assistantHistoryParents.has(newId)) this.assistantHistoryParents.set(newId, pendingParent);
887
954
  }
888
- if (this.recordedHistoryIds.has(oldId)) {
889
- this.recordedHistoryIds.delete(oldId);
890
- 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
+ }
891
973
  }
892
974
  this.notifyUpdate();
893
975
  return !collidesWithExisting;
@@ -1084,14 +1166,16 @@ var AgUiThreadRuntimeCore = class {
1084
1166
  } catch (error) {
1085
1167
  this.logger.error?.("[agui] failed to import message from snapshot", error);
1086
1168
  }
1087
- const snapshotHeadId = converted.at(-1)?.id ?? null;
1088
1169
  const snapshotContainsActiveAssistant = converted.some((message) => message.id === activeAssistant?.id);
1089
1170
  const preservesActiveAssistant = activeAssistant !== void 0 && !snapshotContainsActiveAssistant && (activeAssistant.metadata.isOptimistic !== true || converted.at(-1)?.role !== "assistant");
1090
1171
  if (preservesActiveAssistant) converted.push(activeAssistant);
1091
1172
  this.applyExternalMessages(converted);
1092
- if (preservesActiveAssistant) {
1093
- this.recordedHistoryIds.delete(activeAssistant.id);
1094
- 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
+ }
1095
1179
  }
1096
1180
  } catch (error) {
1097
1181
  this.logger.error?.("[agui] failed to import messages snapshot", error);
@@ -1108,32 +1192,82 @@ var AgUiThreadRuntimeCore = class {
1108
1192
  return status?.type === "requires-action" && status.reason === "interrupt";
1109
1193
  }
1110
1194
  recordHistoryEntry(parentId, message) {
1111
- this.appendHistoryItem(parentId, message);
1195
+ this.appendHistoryItem(parentId, message)?.catch((error) => {
1196
+ this.logger.error?.("[agui] failed to append history entry", error);
1197
+ });
1112
1198
  }
1113
1199
  markPendingAssistantHistory(messageId, parentId) {
1114
1200
  if (!this.history) return;
1115
1201
  this.assistantHistoryParents.set(messageId, parentId);
1116
1202
  }
1117
1203
  persistAssistantHistory(messageId) {
1118
- if (!this.history) return;
1204
+ const history = this.history;
1205
+ if (!history) return;
1119
1206
  const parentId = this.assistantHistoryParents.get(messageId);
1120
1207
  if (parentId === void 0) return;
1121
1208
  const message = this.tryGetMessage(messageId)?.message;
1122
1209
  if (!message || message.role !== "assistant") return;
1123
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;
1124
1234
  this.assistantHistoryParents.delete(messageId);
1125
- 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
+ });
1126
1240
  }
1127
1241
  appendHistoryItem(parentId, message) {
1128
- if (!this.history || this.recordedHistoryIds.has(message.id)) return;
1129
- this.recordedHistoryIds.add(message.id);
1130
- 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({
1131
1247
  parentId,
1132
1248
  message
1133
- }).catch((error) => {
1134
- this.recordedHistoryIds.delete(message.id);
1135
- 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);
1136
1269
  });
1270
+ return next;
1137
1271
  }
1138
1272
  };
1139
1273
  //#endregion