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

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 (46) hide show
  1. package/README.md +16 -0
  2. package/dist/index.d.ts +2 -2
  3. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +19 -8
  4. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  5. package/dist/runtime/AgUiThreadRuntimeCore.js +118 -86
  6. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  7. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  8. package/dist/runtime/adapter/conversions.js +34 -11
  9. package/dist/runtime/adapter/conversions.js.map +1 -1
  10. package/dist/runtime/adapter/run-aggregator.d.ts +13 -3
  11. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  12. package/dist/runtime/adapter/run-aggregator.js +385 -119
  13. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  14. package/dist/runtime/adapter/subscriber.d.ts +9 -0
  15. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  16. package/dist/runtime/adapter/subscriber.js +4 -1
  17. package/dist/runtime/adapter/subscriber.js.map +1 -1
  18. package/dist/runtime/adapter/tool-approval.d.ts +3 -1
  19. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
  20. package/dist/runtime/adapter/tool-approval.js +11 -12
  21. package/dist/runtime/adapter/tool-approval.js.map +1 -1
  22. package/dist/runtime/event-parser.d.ts.map +1 -1
  23. package/dist/runtime/event-parser.js +176 -45
  24. package/dist/runtime/event-parser.js.map +1 -1
  25. package/dist/runtime/types.d.ts +67 -1
  26. package/dist/runtime/types.d.ts.map +1 -1
  27. package/dist/useAgUiRuntime.d.ts.map +1 -1
  28. package/dist/useAgUiRuntime.js +15 -5
  29. package/dist/useAgUiRuntime.js.map +1 -1
  30. package/package.json +9 -9
  31. package/src/index.ts +1 -0
  32. package/src/runtime/AgUiThreadRuntimeCore.test.ts +18 -0
  33. package/src/runtime/AgUiThreadRuntimeCore.ts +204 -120
  34. package/src/runtime/adapter/conversions.ts +87 -17
  35. package/src/runtime/adapter/run-aggregator.approval.test.ts +128 -0
  36. package/src/runtime/adapter/run-aggregator.ts +595 -139
  37. package/src/runtime/adapter/subscriber.ts +10 -1
  38. package/src/runtime/adapter/tool-approval.test.ts +75 -0
  39. package/src/runtime/adapter/tool-approval.ts +17 -12
  40. package/src/runtime/event-parser.ts +216 -36
  41. package/src/runtime/types.ts +96 -12
  42. package/src/useAgUiRuntime.approval.test.tsx +34 -24
  43. package/src/useAgUiRuntime.queue.test.tsx +54 -0
  44. package/src/useAgUiRuntime.thread-switch.test.tsx +199 -0
  45. package/src/useAgUiRuntime.toolDeferral.test.tsx +186 -0
  46. package/src/useAgUiRuntime.ts +15 -3
package/README.md CHANGED
@@ -33,6 +33,22 @@ export function Provider({ children }: { children: React.ReactNode }) {
33
33
  }
34
34
  ```
35
35
 
36
+ ## Subagents
37
+
38
+ An AG-UI backend that runs subagents (the agents-as-tools pattern) emits `SUBAGENT_STARTED` / `SUBAGENT_FINISHED` / `SUBAGENT_ERROR` plus a `subagentRunId` on the events each subagent produces. This adapter groups that activity into one nested assistant message per subagent run and attaches it to the spawning tool call as `ToolCallMessagePart.messages`, joined on `SUBAGENT_STARTED.parentToolCallId`. `PartPrimitive.Messages` renders it without extra wiring, and the subagent's name, description, result, and error code ride on that message's `agui` metadata.
39
+
40
+ A subagent that names no reachable spawning call (`parentToolCallId` is optional, may name a call this run never saw, or two runs may name each other) has nowhere to nest, so its output renders in the parent thread instead of being dropped. This matches the downgrade the protocol's own pre-subagent compatibility middleware performs.
41
+
42
+ A subagent's frontend-executed tool calls work the same way the root agent's do: a call nested on `ToolCallMessagePart.messages` is reachable by `getPendingToolCalls()`, resolves through `addToolResult`, and its result rides the resume as a `tool` record on the spawning assistant record, the same flattened wire shape these calls had before subagent attribution.
43
+
44
+ Approval gates cover nested calls too: a gate that names a subagent-scoped call projects onto the nested part, the frontend tool stays unexecuted while the gate is open, decisions recorded through `respondToToolApproval` land on the nested part, and an undecided gate's result is never exported to the backend.
45
+
46
+ Two limitations are worth knowing before you rely on this:
47
+
48
+ - **Nested structure does not survive a reload.** Thread restore reads the flattened wire shape, so a restored subagent tool call comes back as a root-level part rather than nested under its spawning call. Results and decisions are preserved; only the nesting is not.
49
+
50
+ - **Nested human-in-the-loop is not wired up.** A `SUBAGENT_FINISHED` with a `suspended` outcome marks the nested message `requires-action`, and its `interruptIds` are preserved on the message metadata, but there is no resume path that answers them yet.
51
+
36
52
  ## See also
37
53
 
38
54
  - `@assistant-ui/react-a2a` for the A2A v1.0 protocol.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { AgUiInterrupt, AgUiInterruptReason, AgUiResumeEntry, AgUiRunFinishedOutcome, UseAgUiRuntimeAdapters, UseAgUiRuntimeOptions, UseAgUiThreadListAdapter } from "./runtime/types.js";
1
+ import { AgUiInterrupt, AgUiInterruptReason, AgUiResumeEntry, AgUiResumeTranscript, 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
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 };
5
+ export { type AgUiAssistantRuntime, type AgUiInterrupt, type AgUiInterruptReason, type AgUiMessage, type AgUiResumeEntry, type AgUiResumeTranscript, type AgUiRunFinishedOutcome, type FromAgUiMessagesOptions, type UseAgUiRuntimeAdapters, type UseAgUiRuntimeOptions, type UseAgUiThreadListAdapter, fromAgUiMessages, toAgUiMessages, useAgUiInterrupts, useAgUiRuntime, useAgUiSendA2uiAction, useAgUiSetState, useAgUiState, useAgUiSteerAway, useAgUiSubmitInterruptResponses };
@@ -1,5 +1,5 @@
1
1
  import { Logger } from "./logger.js";
2
- import { AgUiInterrupt, AgUiResumeEntry } from "./types.js";
2
+ import { AgUiInterrupt, AgUiResumeEntry, AgUiResumeTranscript } from "./types.js";
3
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";
@@ -16,6 +16,7 @@ type CoreOptions = {
16
16
  agent: AbstractAgent;
17
17
  logger: Logger;
18
18
  showThinking: boolean;
19
+ resumeTranscript?: AgUiResumeTranscript | undefined;
19
20
  autoCancelPendingToolCalls?: boolean | undefined;
20
21
  onError?: (error: Error) => void;
21
22
  onCancel?: () => void;
@@ -26,6 +27,7 @@ declare class AgUiThreadRuntimeCore {
26
27
  private agent;
27
28
  private logger;
28
29
  private showThinking;
30
+ private resumeTranscript;
29
31
  private autoCancelPendingToolCalls;
30
32
  private onError;
31
33
  private onCancel;
@@ -37,7 +39,6 @@ declare class AgUiThreadRuntimeCore {
37
39
  private abortController;
38
40
  private activeRunAgent;
39
41
  private stateSnapshot;
40
- private pendingError;
41
42
  private history;
42
43
  private lastRunConfig;
43
44
  private readonly assistantHistoryParents;
@@ -47,8 +48,8 @@ declare class AgUiThreadRuntimeCore {
47
48
  private _isLoading;
48
49
  private _loadPromise;
49
50
  private _loadRequested;
50
- private pendingResumeMessageId;
51
- private pendingA2uiResume;
51
+ private pendingResume;
52
+ private pendingA2uiResumeOwner;
52
53
  private pendingA2uiAction;
53
54
  constructor(options: CoreOptions);
54
55
  updateOptions(options: Omit<CoreOptions, "notifyUpdate">): void;
@@ -68,6 +69,8 @@ declare class AgUiThreadRuntimeCore {
68
69
  runConfig?: RunConfig;
69
70
  }): Promise<void>;
70
71
  cancel(): Promise<void>;
72
+ private abortActiveRun;
73
+ private supersedeActiveRun;
71
74
  resume(config: ResumeRunConfig): Promise<void>;
72
75
  resumeInFlightRun(messages: readonly ThreadMessage[]): Promise<void>;
73
76
  private assertNoPendingInterrupts;
@@ -89,9 +92,9 @@ declare class AgUiThreadRuntimeCore {
89
92
  */
90
93
  private resumeWithResponses;
91
94
  /**
92
- * The core seams that reach `respondToToolApproval` discard the promise they
93
- * receive, so a rejected decision would otherwise surface only as an
94
- * unhandled rejection. A failure raised by the resumed run itself was already
95
+ * A rejected decision reaches the caller through the approval seam, and
96
+ * `onError` in addition, because a consumer watching only the runtime would
97
+ * otherwise miss it. A failure raised by the resumed run itself was already
95
98
  * reported by `startRun` before it rethrew, so reporting it here again would
96
99
  * give the consumer two notifications for one failure.
97
100
  */
@@ -117,7 +120,15 @@ declare class AgUiThreadRuntimeCore {
117
120
  private startRun;
118
121
  private consumeResumeStream;
119
122
  private buildRunInput;
120
- private installResumeShim;
123
+ /**
124
+ * An anchor the thread no longer carries falls back to the whole transcript.
125
+ * The anchor is missing exactly when a snapshot import replaced the messages
126
+ * this resume was raised against, and a host that expects a delta tolerates
127
+ * being told more than it knows, while one rebuilding the run from the
128
+ * transcript cannot recover from being told less.
129
+ */
130
+ private selectRunMessages;
131
+ private clearDeferredContinuations;
121
132
  private setRunning;
122
133
  private finishRun;
123
134
  private insertAssistantPlaceholder;
@@ -1 +1 @@
1
- {"version":3,"file":"AgUiThreadRuntimeCore.d.ts","names":[],"sources":["../../src/runtime/AgUiThreadRuntimeCore.ts"],"mappings":";;;;;;KA8EK,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;;cAoCW;UACH;UACA;UACA;UACA;UACA;UACA;mBACS;mBACA;UAET;mBACS;UAgBT;UACA;UAGA;UACA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;UACT;UACA;UACA;UACA;UACA;UACA;EAEI,YAAA,SAAS;EAYrB,cAAc,SAAS,KAAK;EAsB5B,cAAc,SAAS;EAIvB;EAOA,wBAAwB;EAIxB,wBAAwB;EAIxB,YAAY;EAIZ;MAII;EAIJ,mBAAmB;EAsDb,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;EAsEG,UACJ,SAAS,qBACT,qBAAqB,oBACpB;UAyCK;UAkBA;UAiCA;UA0BA;EAmCR,yBAAyB;UAkBjB;EAiBR,cAAc,SAAS;EAyBvB,eAAe,QAAQ;UA4Bf;UAYA;UA6BA;EAUR,sBAAsB,mBAAmB;EAkDzC,kBAAkB,OAAO;EAKzB,SACE,MACI,sBACE,MAAM,kCAAkC;EAOhD;UAKc;UAyPA;UA2DN;UAkCA;UAqBA;UAKA;UAQA;UAyBA;UAkEA;UAiDA;UA4BA;UA6BA;UAgEA;UA2EA;UA8DA;UAuDA;UAQA;UAIA;UAKA;UAMA;UAQA;UAsDA;UAqBA"}
1
+ {"version":3,"file":"AgUiThreadRuntimeCore.d.ts","names":[],"sources":["../../src/runtime/AgUiThreadRuntimeCore.ts"],"mappings":";;;;;;KAmFK,YAAY,YAAY;KACxB,gBACH,SAAS,wBACN,eAAe;KACf;EACH;EACA;EACA,WAAW;EACX,SAAS;;KAaN;EACH,OAAO;EACP,QAAQ;EACR;EACA,mBAAmB;EACnB;EACA,WAAW,OAAO;EAClB;EACA,UAAU;EACV;;cAoCW;UACH;UACA;UACA;UACA;UACA;UACA;UACA;mBACS;mBACA;UAET;mBACS;UAgBT;UACA;UAGA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;UACT;UACA;UACA;UACA;UAEA;UACA;EAEI,YAAA,SAAS;EAYrB,cAAc,SAAS,KAAK;EAqB5B,cAAc,SAAS;EAIvB;EAOA,wBAAwB;EAIxB,wBAAwB;EAIxB,YAAY;EAIZ;MAII;EAIJ,mBAAmB;EAsDb,OAAO,SAAS,gBAAgB;UAgB9B;UAQA;EAiBF,KAAK,SAAS,gBAAgB;EAI9B,OACJ,yBACA;IAAU,YAAY;MACrB;EAMG,UAAU;UAWR;UAiBA;EAaF,OAAO,QAAQ,kBAAkB;EAUjC,kBAAkB,mBAAmB,kBAAkB;UAwBrD;UAOA;EAgBR;IACE;IACA,qBAAqB;;EAavB;IACE;IACA;;EAaI,yBACJ,oBAAoB,oBACnB;;;;;;;UAuCW;;;;;;;;EAkBd,YAAY;UAYJ;EAwBF,sBACJ,SAAS,+BACR;EAsEG,UACJ,SAAS,qBACT,qBAAqB,oBACpB;UA4CK;UAkBA;UAiCA;UA2BA;EAmCR,yBAAyB;UAiBjB;EAiBR,cAAc,SAAS;EA+BvB,eAAe,QAAQ;UA6Bf;UAaA;UAiCA;EAUR,sBAAsB,mBAAmB;EA6DzC,kBAAkB,OAAO;EAKzB,SACE,MACI,sBACE,MAAM,kCAAkC;EAOhD;UAKc;UA0PA;UA2DN;;;;;;;;UA4CA;UAaA;UAUA;UAKA;UAOA;UAyBA;UAkEA;UAiDA;UA0BA;UA6BA;UAgEA;UA0EA;UA6DA;UAuDA;UAQA;UAIA;UAKA;UAMA;UAQA;UAsDA;UAqBA"}
@@ -5,14 +5,13 @@ import { AG_UI_METADATA_NAMESPACE, RunAggregator, isPlainObject, tryParseJSON }
5
5
  import { fromAgUiMessages, toAgUiMessages, toAgUiTools } from "./adapter/conversions.js";
6
6
  import { createAgUiSubscriber } from "./adapter/subscriber.js";
7
7
  import { fromThreadMessageLike, generateId, isMcpAppUri } from "@assistant-ui/core";
8
- import { createMessageRepositorySession, invokeUserCallback } from "@assistant-ui/core/internal";
8
+ import { createMessageRepositorySession, invokeUserCallback, iterateToolCallParts, mapToolCallPartsDeep } from "@assistant-ui/core/internal";
9
9
  import jsonpatch from "fast-json-patch";
10
10
  //#region src/runtime/AgUiThreadRuntimeCore.ts
11
11
  const optimisticPrefix = "__optimistic__";
12
12
  const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;
13
13
  const isOptimisticId = (id) => id.startsWith(optimisticPrefix);
14
14
  const isResolvedToolCall = (part) => part.type === "tool-call" && "result" in part && part.result !== void 0;
15
- const symbolResumeShim = Symbol("agui-resume-shim");
16
15
  const FALLBACK_USER_STATUS = {
17
16
  type: "complete",
18
17
  reason: "unknown"
@@ -37,6 +36,7 @@ var AgUiThreadRuntimeCore = class {
37
36
  agent;
38
37
  logger;
39
38
  showThinking;
39
+ resumeTranscript;
40
40
  autoCancelPendingToolCalls;
41
41
  onError;
42
42
  onCancel;
@@ -61,7 +61,6 @@ var AgUiThreadRuntimeCore = class {
61
61
  abortController = null;
62
62
  activeRunAgent = null;
63
63
  stateSnapshot;
64
- pendingError = null;
65
64
  history;
66
65
  lastRunConfig;
67
66
  assistantHistoryParents = /* @__PURE__ */ new Map();
@@ -71,30 +70,30 @@ var AgUiThreadRuntimeCore = class {
71
70
  _isLoading = false;
72
71
  _loadPromise;
73
72
  _loadRequested = false;
74
- pendingResumeMessageId = null;
75
- pendingA2uiResume = false;
73
+ pendingResume = null;
74
+ pendingA2uiResumeOwner = null;
76
75
  pendingA2uiAction;
77
76
  constructor(options) {
78
77
  this.agent = options.agent;
79
78
  this.logger = options.logger;
80
79
  this.showThinking = options.showThinking;
80
+ this.resumeTranscript = options.resumeTranscript ?? "full";
81
81
  this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
82
82
  this.onError = options.onError;
83
83
  this.onCancel = options.onCancel;
84
84
  this.history = options.history;
85
85
  this.notifyUpdate = options.notifyUpdate;
86
- this.installResumeShim();
87
86
  }
88
87
  updateOptions(options) {
89
88
  this.agent = options.agent;
90
89
  this.logger = options.logger;
91
90
  this.showThinking = options.showThinking;
91
+ this.resumeTranscript = options.resumeTranscript ?? "full";
92
92
  this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
93
93
  this.onError = options.onError;
94
94
  this.onCancel = options.onCancel;
95
95
  const previousHistory = this.history;
96
96
  this.history = options.history;
97
- this.installResumeShim();
98
97
  if (this._loadRequested && !this._loadPromise && !previousHistory && options.history && this.session.getMessages().length === 0) this.__internal_load();
99
98
  }
100
99
  attachRuntime(runtime) {
@@ -153,12 +152,14 @@ var AgUiThreadRuntimeCore = class {
153
152
  }
154
153
  async append(message) {
155
154
  const startRun = message.startRun ?? message.role === "user";
155
+ let ownsThread = true;
156
156
  if (startRun) {
157
157
  this.assertNoPendingInterrupts();
158
158
  this.maybeAutoCancelPendingToolCalls();
159
+ ownsThread = this.supersedeActiveRun();
159
160
  }
160
161
  const threadMessageId = this.appendEntry(message);
161
- if (!startRun) return;
162
+ if (!startRun || !ownsThread) return;
162
163
  await this.startRun(threadMessageId, message.runConfig);
163
164
  }
164
165
  maybeAutoCancelPendingToolCalls() {
@@ -187,12 +188,26 @@ var AgUiThreadRuntimeCore = class {
187
188
  await this.startRun(parentId, config.runConfig);
188
189
  }
189
190
  async cancel() {
190
- if (!this.abortController) return;
191
+ this.abortActiveRun();
192
+ }
193
+ abortActiveRun() {
194
+ const controller = this.abortController;
195
+ if (!controller) return;
191
196
  try {
192
197
  (this.activeRunAgent ?? this.agent).abortRun();
193
198
  } finally {
194
- this.abortController.abort();
199
+ controller.abort();
200
+ }
201
+ }
202
+ supersedeActiveRun() {
203
+ try {
204
+ this.abortActiveRun();
205
+ } catch (error) {
206
+ this.logger.error?.("[agui] agent abortRun failed", error);
195
207
  }
208
+ if (this.abortController === null) return true;
209
+ this.logger.debug?.("[agui] onCancel started a replacement run; dropping the superseding send");
210
+ return false;
196
211
  }
197
212
  async resume(config) {
198
213
  this.assertNoPendingInterrupts();
@@ -234,8 +249,7 @@ var AgUiThreadRuntimeCore = class {
234
249
  const assistant = this.findRequiresActionAssistant("tool-calls");
235
250
  if (!assistant) return null;
236
251
  const toolCallIds = [];
237
- for (const part of assistant.content) {
238
- if (part.type !== "tool-call") continue;
252
+ for (const part of iterateToolCallParts(assistant.content)) {
239
253
  if (isResolvedToolCall(part)) continue;
240
254
  toolCallIds.push(part.toolCallId);
241
255
  }
@@ -263,12 +277,15 @@ var AgUiThreadRuntimeCore = class {
263
277
  */
264
278
  async resumeWithResponses(messageId, resume) {
265
279
  this.clearPendingInterrupts(messageId, resume);
266
- await this.startRun(messageId, this.lastRunConfig, resume);
280
+ await this.startRun(messageId, this.lastRunConfig, {
281
+ entries: resume,
282
+ interruptedMessageId: messageId
283
+ });
267
284
  }
268
285
  /**
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
286
+ * A rejected decision reaches the caller through the approval seam, and
287
+ * `onError` in addition, because a consumer watching only the runtime would
288
+ * otherwise miss it. A failure raised by the resumed run itself was already
272
289
  * reported by `startRun` before it rethrew, so reporting it here again would
273
290
  * give the consumer two notifications for one failure.
274
291
  */
@@ -293,7 +310,7 @@ var AgUiThreadRuntimeCore = class {
293
310
  const pending = this.getPendingInterrupts();
294
311
  if (!pending) throw new Error("[agui] respondToToolApproval: no pending interrupts on this thread");
295
312
  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}"`);
313
+ if (![...projectAgUiToolApprovals(pending.interrupts, new Set(Array.from(iterateToolCallParts(gatedMessage?.content ?? [])).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
314
  this.assertInterruptsAnswerable("respondToToolApproval", pending.interrupts);
298
315
  if (!this.session.updateMessage(pending.messageId, (message) => {
299
316
  if (message.role !== "assistant") return message;
@@ -335,7 +352,10 @@ var AgUiThreadRuntimeCore = class {
335
352
  const normalized = this.toAppendMessage(message);
336
353
  this.clearPendingInterrupts(pending.messageId, resume);
337
354
  const threadMessageId = this.appendEntry(normalized);
338
- await this.startRun(threadMessageId, normalized.runConfig, resume);
355
+ await this.startRun(threadMessageId, normalized.runConfig, {
356
+ entries: resume,
357
+ interruptedMessageId: pending.messageId
358
+ });
339
359
  }
340
360
  resolveSteerAwayResume(interrupts, responses) {
341
361
  const responsesById = this.collectInterruptResponses("steerAway", interrupts, responses ?? []);
@@ -372,7 +392,7 @@ var AgUiThreadRuntimeCore = class {
372
392
  };
373
393
  return {
374
394
  createdAt: message.createdAt ?? /* @__PURE__ */ new Date(),
375
- parentId: message.parentId ?? this.session.headId,
395
+ parentId: message.parentId === void 0 ? this.session.headId : message.parentId,
376
396
  sourceId: message.sourceId ?? null,
377
397
  role: message.role ?? "user",
378
398
  content: message.content,
@@ -411,8 +431,8 @@ var AgUiThreadRuntimeCore = class {
411
431
  for (let index = messages.length - 1; index >= 0; index--) {
412
432
  const message = messages[index];
413
433
  if (!message || message.role !== "assistant") continue;
414
- for (const part of message.content) {
415
- if (part.type !== "tool-call" || part.toolCallId !== toolCallId) continue;
434
+ for (const part of iterateToolCallParts(message.content)) {
435
+ if (part.toolCallId !== toolCallId) continue;
416
436
  if (!isResolvedToolCall(part)) return message.id;
417
437
  fallbackMessageId ??= message.id;
418
438
  }
@@ -423,8 +443,8 @@ var AgUiThreadRuntimeCore = class {
423
443
  if (this.session.updateMessage(messageId, (message) => {
424
444
  if (message.role !== "assistant") return message;
425
445
  const assistant = message;
426
- const content = assistant.content.map((part) => {
427
- if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
446
+ const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
447
+ if (isResolvedToolCall(part)) return part;
428
448
  return {
429
449
  ...part,
430
450
  result: { error: "Tool call cancelled by user" },
@@ -438,12 +458,14 @@ var AgUiThreadRuntimeCore = class {
438
458
  })) this.notifyUpdate();
439
459
  }
440
460
  addToolResult(options) {
441
- if (!this.session.updateMessage(options.messageId, (message) => {
461
+ const sessionMessageId = this.session.tryGetMessage(options.messageId) ? options.messageId : this.findMessageIdForToolCall(options.toolCallId);
462
+ if (sessionMessageId === void 0) return;
463
+ if (!this.session.updateMessage(sessionMessageId, (message) => {
442
464
  if (message.role !== "assistant") return message;
443
465
  const assistant = message;
444
466
  let matchedToolCall = false;
445
- const content = assistant.content.map((part) => {
446
- if (part.type !== "tool-call" || part.toolCallId !== options.toolCallId) return part;
467
+ const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
468
+ if (part.toolCallId !== options.toolCallId) return part;
447
469
  matchedToolCall = true;
448
470
  return {
449
471
  ...part,
@@ -459,7 +481,7 @@ var AgUiThreadRuntimeCore = class {
459
481
  };
460
482
  })) return;
461
483
  this.notifyUpdate();
462
- this.maybeResumeAfterToolResults(options.messageId);
484
+ this.maybeResumeAfterToolResults(sessionMessageId);
463
485
  }
464
486
  sendA2uiAction(action) {
465
487
  this.assertNoPendingInterrupts();
@@ -473,16 +495,21 @@ var AgUiThreadRuntimeCore = class {
473
495
  delete userAction.type;
474
496
  if (!("timestamp" in userAction)) userAction.timestamp = (/* @__PURE__ */ new Date()).toISOString();
475
497
  this.pendingA2uiAction = userAction;
476
- if (this.isRunningFlag) {
477
- this.pendingA2uiResume = true;
498
+ const owner = this.abortController;
499
+ if (owner) {
500
+ this.pendingA2uiResumeOwner = owner;
478
501
  return;
479
502
  }
480
503
  this.startResumeRun(parentId);
481
504
  }
482
505
  maybeResumeAfterToolResults(messageId) {
483
506
  if (!this.maybeCompleteAfterToolResults(messageId)) return;
484
- if (this.isRunningFlag) {
485
- this.pendingResumeMessageId = messageId;
507
+ const owner = this.abortController;
508
+ if (owner) {
509
+ this.pendingResume = {
510
+ owner,
511
+ messageId
512
+ };
486
513
  return;
487
514
  }
488
515
  this.startResumeRun(messageId);
@@ -492,7 +519,12 @@ var AgUiThreadRuntimeCore = class {
492
519
  if (!message || message.role !== "assistant") return false;
493
520
  const assistant = message;
494
521
  if (assistant.status?.type !== "requires-action" || assistant.status.reason !== "tool-calls") return false;
495
- if (!assistant.content.every((part) => part.type !== "tool-call" || isResolvedToolCall(part))) return false;
522
+ let allResolved = true;
523
+ for (const part of iterateToolCallParts(assistant.content)) if (!isResolvedToolCall(part)) {
524
+ allResolved = false;
525
+ break;
526
+ }
527
+ if (!allResolved) return false;
496
528
  if (!this.session.updateMessage(messageId, (current) => current.role === "assistant" ? {
497
529
  ...current,
498
530
  status: {
@@ -510,7 +542,7 @@ var AgUiThreadRuntimeCore = class {
510
542
  });
511
543
  }
512
544
  applyExternalMessages(messages) {
513
- this.pendingA2uiResume = false;
545
+ this.pendingA2uiResumeOwner = null;
514
546
  this.pendingA2uiAction = void 0;
515
547
  this.assistantHistoryParents.clear();
516
548
  if (messages.length === 0) this.session.clear();
@@ -548,6 +580,8 @@ var AgUiThreadRuntimeCore = class {
548
580
  }
549
581
  this.snapshotHistoryIds.clear();
550
582
  for (const { message } of this.getMessageRepository().messages) this.snapshotHistoryIds.add(message.id);
583
+ const resumeTarget = this.pendingResume?.messageId;
584
+ if (resumeTarget !== void 0 && !this.session.getMessages().some((message) => message.id === resumeTarget)) this.pendingResume = null;
551
585
  this.notifyUpdate();
552
586
  }
553
587
  loadExternalState(state) {
@@ -563,13 +597,14 @@ var AgUiThreadRuntimeCore = class {
563
597
  this.notifyUpdate();
564
598
  }
565
599
  async startRun(parentId, runConfig, resume, resumeStream) {
600
+ if (!this.supersedeActiveRun()) return;
566
601
  const normalizedRunConfig = runConfig ?? {};
567
602
  this.lastRunConfig = normalizedRunConfig;
568
603
  const parent = parentId === null ? void 0 : this.session.tryGetMessage(parentId);
569
604
  const shouldEagerlyInsertAssistant = parentId !== null && parent !== void 0 && parentId !== this.session.headId;
570
605
  const historicalMessages = [...shouldEagerlyInsertAssistant ? this.session.tryGetMessages(parentId) ?? this.session.getMessages() : this.session.getMessages()];
571
606
  const runStartMessageIds = new Set(this.getMessageRepository().messages.map(({ message }) => message.id));
572
- this.pendingError = null;
607
+ let pendingError = null;
573
608
  const assistantParentId = parent ? parentId : this.session.headId;
574
609
  let assistantMessageId;
575
610
  let assistantCollided = false;
@@ -644,7 +679,7 @@ var AgUiThreadRuntimeCore = class {
644
679
  type: "incomplete",
645
680
  reason: "cancelled"
646
681
  } });
647
- await this.consumeResumeStream(resumeStream, {
682
+ pendingError = await this.consumeResumeStream(resumeStream, {
648
683
  runConfig: normalizedRunConfig,
649
684
  threadId: this.agent.threadId || "main",
650
685
  parentId: assistantParentId,
@@ -653,7 +688,7 @@ var AgUiThreadRuntimeCore = class {
653
688
  ensureAssistant,
654
689
  applyUpdate,
655
690
  getAssistantMessageId: () => assistantMessageId
656
- });
691
+ }) ?? null;
657
692
  } else {
658
693
  const runId = generateId();
659
694
  aggregator.handle({
@@ -667,7 +702,7 @@ var AgUiThreadRuntimeCore = class {
667
702
  logger: this.logger,
668
703
  onRunFailed: (error) => {
669
704
  if (abortSignal.aborted) return;
670
- this.pendingError = error;
705
+ pendingError = error;
671
706
  invokeRuntimeCallback("onError", this.onError, error);
672
707
  }
673
708
  });
@@ -686,28 +721,24 @@ var AgUiThreadRuntimeCore = class {
686
721
  message: err.message
687
722
  });
688
723
  invokeRuntimeCallback("onError", this.onError, err);
689
- this.pendingError = this.pendingError ?? err;
724
+ pendingError ??= err;
690
725
  }
691
726
  } finally {
692
727
  this.finishRun(abortController);
693
728
  }
694
- if (this.pendingError) {
695
- const err = this.pendingError;
729
+ if (pendingError) {
730
+ const err = pendingError;
696
731
  this.reportedErrors.add(err);
697
- this.pendingError = null;
698
- this.pendingResumeMessageId = null;
699
- this.pendingA2uiResume = false;
700
- this.pendingA2uiAction = void 0;
732
+ this.clearDeferredContinuations(abortController);
701
733
  throw err;
702
734
  }
703
- if (this.pendingResumeMessageId !== null) {
704
- const resumeMessageId = this.pendingResumeMessageId;
705
- this.pendingResumeMessageId = null;
706
- if (!abortSignal.aborted) this.startResumeRun(resumeMessageId);
707
- else this.pendingA2uiAction = void 0;
735
+ if (this.pendingResume?.owner === abortController) {
736
+ const { messageId } = this.pendingResume;
737
+ this.pendingResume = null;
738
+ if (!abortSignal.aborted) this.startResumeRun(messageId);
708
739
  }
709
- if (this.pendingA2uiResume) {
710
- this.pendingA2uiResume = false;
740
+ if (this.pendingA2uiResumeOwner === abortController) {
741
+ this.pendingA2uiResumeOwner = null;
711
742
  if (!abortSignal.aborted && this.pendingA2uiAction !== void 0) {
712
743
  if (this.getPendingInterrupts()) {
713
744
  this.pendingA2uiAction = void 0;
@@ -726,7 +757,7 @@ var AgUiThreadRuntimeCore = class {
726
757
  }
727
758
  async consumeResumeStream(stream, ctx) {
728
759
  this.pendingA2uiAction = void 0;
729
- this.pendingA2uiResume = false;
760
+ this.pendingA2uiResumeOwner = null;
730
761
  const assistantId = ctx.ensureAssistant();
731
762
  const currentId = () => ctx.getAssistantMessageId() ?? assistantId;
732
763
  const options = {
@@ -745,11 +776,11 @@ var AgUiThreadRuntimeCore = class {
745
776
  };
746
777
  try {
747
778
  for await (const result of stream(options)) {
748
- if (ctx.abortSignal.aborted) return;
779
+ if (ctx.abortSignal.aborted) return void 0;
749
780
  ctx.applyUpdate(result);
750
781
  }
751
782
  } catch (error) {
752
- if (ctx.abortSignal.aborted) return;
783
+ if (ctx.abortSignal.aborted) return void 0;
753
784
  const err = error instanceof Error ? error : new Error(String(error));
754
785
  ctx.applyUpdate({ status: {
755
786
  type: "incomplete",
@@ -757,10 +788,9 @@ var AgUiThreadRuntimeCore = class {
757
788
  error: err.message
758
789
  } });
759
790
  invokeRuntimeCallback("onError", this.onError, err);
760
- this.pendingError = this.pendingError ?? err;
761
- return;
791
+ return err;
762
792
  }
763
- if (ctx.abortSignal.aborted) return;
793
+ if (ctx.abortSignal.aborted) return void 0;
764
794
  const current = this.session.tryGetMessage(currentId())?.message;
765
795
  if (!current || current.status?.type === "running") ctx.applyUpdate({ status: {
766
796
  type: "complete",
@@ -769,7 +799,7 @@ var AgUiThreadRuntimeCore = class {
769
799
  }
770
800
  buildRunInput(runId, runConfig, historyMessages, resume) {
771
801
  const threadId = this.agent.threadId || "main";
772
- const messages = toAgUiMessages(historyMessages ?? this.session.getMessages());
802
+ const messages = toAgUiMessages(this.selectRunMessages(historyMessages ?? this.session.getMessages(), resume));
773
803
  const context = this.runtime?.thread.getModelContext();
774
804
  const input = {
775
805
  threadId,
@@ -787,35 +817,39 @@ var AgUiThreadRuntimeCore = class {
787
817
  ...runConfig?.custom ? { runConfig: runConfig.custom } : {},
788
818
  ...this.pendingA2uiAction ? { a2uiAction: { userAction: this.pendingA2uiAction } } : {}
789
819
  },
790
- ...resume !== void 0 ? { resume } : {}
820
+ ...resume !== void 0 ? { resume: resume.entries } : {}
791
821
  };
792
822
  this.pendingA2uiAction = void 0;
793
823
  return input;
794
824
  }
795
- installResumeShim() {
796
- const agent = this.agent;
797
- if (agent[symbolResumeShim]) return;
798
- agent[symbolResumeShim] = true;
799
- const original = Object.hasOwn(agent, "prepareRunAgentInput") ? agent.prepareRunAgentInput : Object.getPrototypeOf(agent)?.prepareRunAgentInput;
800
- if (typeof original !== "function") return;
801
- agent.prepareRunAgentInput = function(params) {
802
- const input = original.call(this, params);
803
- if (params?.resume !== void 0 && input && typeof input === "object") return {
804
- ...input,
805
- resume: params.resume
806
- };
807
- return input;
808
- };
825
+ /**
826
+ * An anchor the thread no longer carries falls back to the whole transcript.
827
+ * The anchor is missing exactly when a snapshot import replaced the messages
828
+ * this resume was raised against, and a host that expects a delta tolerates
829
+ * being told more than it knows, while one rebuilding the run from the
830
+ * transcript cannot recover from being told less.
831
+ */
832
+ selectRunMessages(messages, resume) {
833
+ if (resume === void 0 || this.resumeTranscript !== "appended") return messages;
834
+ const anchor = messages.findIndex((message) => message.id === resume.interruptedMessageId);
835
+ if (anchor === -1) return messages;
836
+ return messages.slice(anchor + 1);
837
+ }
838
+ clearDeferredContinuations(owner) {
839
+ if (this.pendingResume?.owner === owner) this.pendingResume = null;
840
+ if (this.pendingA2uiResumeOwner === owner) {
841
+ this.pendingA2uiResumeOwner = null;
842
+ this.pendingA2uiAction = void 0;
843
+ }
809
844
  }
810
845
  setRunning(running) {
811
846
  this.isRunningFlag = running;
812
847
  this.notifyUpdate();
813
848
  }
814
849
  finishRun(controller) {
815
- if (this.abortController === controller) {
816
- this.abortController = null;
817
- this.activeRunAgent = null;
818
- }
850
+ if (this.abortController !== controller) return;
851
+ this.abortController = null;
852
+ this.activeRunAgent = null;
819
853
  this.setRunning(false);
820
854
  }
821
855
  insertAssistantPlaceholder(parentId, id = generateOptimisticId()) {
@@ -914,14 +948,12 @@ var AgUiThreadRuntimeCore = class {
914
948
  }
915
949
  preserveToolResults(previous, next) {
916
950
  const resolved = /* @__PURE__ */ new Map();
917
- for (const part of previous) if (part.type === "tool-call" && isResolvedToolCall(part)) resolved.set(part.toolCallId, part);
951
+ for (const part of iterateToolCallParts(previous)) if (isResolvedToolCall(part)) resolved.set(part.toolCallId, part);
918
952
  if (resolved.size === 0) return next;
919
- let changed = false;
920
- const merged = next.map((part) => {
921
- if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
953
+ const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
954
+ if (isResolvedToolCall(part)) return part;
922
955
  const prior = resolved.get(part.toolCallId);
923
956
  if (!prior) return part;
924
- changed = true;
925
957
  return {
926
958
  ...part,
927
959
  result: prior.result,
@@ -996,8 +1028,8 @@ var AgUiThreadRuntimeCore = class {
996
1028
  const assistant = message;
997
1029
  const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
998
1030
  let matchedToolCall = false;
999
- const content = assistant.content.map((part) => {
1000
- if (part.type !== "tool-call" || part.toolCallId !== event.toolCallId) return part;
1031
+ const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
1032
+ if (part.toolCallId !== event.toolCallId) return part;
1001
1033
  matchedToolCall = true;
1002
1034
  if (part.mcp?.app !== void 0 && isPlainObject(part.result) && Array.isArray(part.result["content"])) return {
1003
1035
  ...part,
@@ -1040,8 +1072,8 @@ var AgUiThreadRuntimeCore = class {
1040
1072
  if (message.role !== "assistant") return message;
1041
1073
  const assistant = message;
1042
1074
  let matchedToolCall = false;
1043
- const content = assistant.content.map((part) => {
1044
- if (part.type !== "tool-call" || part.toolCallId !== toolCallId) return part;
1075
+ const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
1076
+ if (part.toolCallId !== toolCallId) return part;
1045
1077
  matchedToolCall = true;
1046
1078
  return {
1047
1079
  ...part,