@assistant-ui/react-ag-ui 0.0.58 → 0.0.60

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/agUiExtras.d.ts +1 -2
  2. package/dist/agUiExtras.d.ts.map +1 -1
  3. package/dist/hooks.d.ts +6 -7
  4. package/dist/hooks.d.ts.map +1 -1
  5. package/dist/index.d.ts +2 -2
  6. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +17 -6
  7. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  8. package/dist/runtime/AgUiThreadRuntimeCore.js +95 -47
  9. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  10. package/dist/runtime/adapter/conversions.d.ts +11 -11
  11. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  12. package/dist/runtime/adapter/conversions.js +114 -17
  13. package/dist/runtime/adapter/conversions.js.map +1 -1
  14. package/dist/runtime/adapter/run-aggregator.d.ts +9 -10
  15. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  16. package/dist/runtime/adapter/run-aggregator.js +59 -0
  17. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  18. package/dist/runtime/adapter/subscriber.d.ts +2 -3
  19. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  20. package/dist/runtime/adapter/tool-approval.d.ts +4 -5
  21. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
  22. package/dist/runtime/event-parser.d.ts +2 -3
  23. package/dist/runtime/event-parser.d.ts.map +1 -1
  24. package/dist/runtime/interrupt-internals.d.ts +3 -4
  25. package/dist/runtime/interrupt-internals.d.ts.map +1 -1
  26. package/dist/runtime/logger.d.ts +2 -3
  27. package/dist/runtime/logger.d.ts.map +1 -1
  28. package/dist/runtime/mcp-tool-result.d.ts +3 -4
  29. package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
  30. package/dist/runtime/types.d.ts +35 -11
  31. package/dist/runtime/types.d.ts.map +1 -1
  32. package/dist/useAgUiRuntime.d.ts +2 -3
  33. package/dist/useAgUiRuntime.d.ts.map +1 -1
  34. package/dist/useAgUiRuntime.js +14 -5
  35. package/dist/useAgUiRuntime.js.map +1 -1
  36. package/package.json +12 -11
  37. package/src/index.ts +1 -0
  38. package/src/runtime/AgUiThreadRuntimeCore.test.ts +18 -0
  39. package/src/runtime/AgUiThreadRuntimeCore.ts +176 -63
  40. package/src/runtime/adapter/conversions.ts +200 -26
  41. package/src/runtime/adapter/run-aggregator.ts +85 -0
  42. package/src/runtime/types.ts +26 -0
  43. package/src/useAgUiRuntime.queue.test.tsx +54 -0
  44. package/src/useAgUiRuntime.thread-switch.test.tsx +199 -0
  45. package/src/useAgUiRuntime.ts +14 -3
@@ -35,7 +35,12 @@ import type {
35
35
  import jsonpatch, { type Operation } from "fast-json-patch";
36
36
  import type { Logger } from "./logger";
37
37
  import { readMcpAppResourceUri } from "./mcp-tool-result";
38
- import type { AgUiEvent, AgUiInterrupt, AgUiResumeEntry } from "./types";
38
+ import type {
39
+ AgUiEvent,
40
+ AgUiInterrupt,
41
+ AgUiResumeEntry,
42
+ AgUiResumeTranscript,
43
+ } from "./types";
39
44
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
40
45
  import {
41
46
  AG_UI_METADATA_NAMESPACE,
@@ -87,10 +92,21 @@ type ResumeRunConfig = {
87
92
  stream?: ResumeStream;
88
93
  };
89
94
 
95
+ /**
96
+ * A resume submission, pairing the entries that answer the open interrupts with
97
+ * the assistant message they were opened on. That message anchors the resume
98
+ * transcript: under `resumeTranscript: "appended"` only what follows it is sent.
99
+ */
100
+ type ResumeDispatch = {
101
+ entries: AgUiResumeEntry[];
102
+ interruptedMessageId: string;
103
+ };
104
+
90
105
  type CoreOptions = {
91
106
  agent: AbstractAgent;
92
107
  logger: Logger;
93
108
  showThinking: boolean;
109
+ resumeTranscript?: AgUiResumeTranscript | undefined;
94
110
  autoCancelPendingToolCalls?: boolean | undefined;
95
111
  onError?: (error: Error) => void;
96
112
  onCancel?: () => void;
@@ -135,6 +151,7 @@ export class AgUiThreadRuntimeCore {
135
151
  private agent: AbstractAgent;
136
152
  private logger: Logger;
137
153
  private showThinking: boolean;
154
+ private resumeTranscript: AgUiResumeTranscript;
138
155
  private autoCancelPendingToolCalls: boolean | undefined;
139
156
  private onError: ((error: Error) => void) | undefined;
140
157
  private onCancel: (() => void) | undefined;
@@ -164,7 +181,6 @@ export class AgUiThreadRuntimeCore {
164
181
  // mid-run, and cancelling has to reach the agent holding the live request.
165
182
  private activeRunAgent: AbstractAgent | null = null;
166
183
  private stateSnapshot: ReadonlyJSONValue | undefined;
167
- private pendingError: Error | null = null;
168
184
  private history: ThreadHistoryAdapter | undefined;
169
185
  private lastRunConfig: RunConfig | undefined;
170
186
  private readonly assistantHistoryParents = new Map<string, string | null>();
@@ -174,14 +190,16 @@ export class AgUiThreadRuntimeCore {
174
190
  private _isLoading = false;
175
191
  private _loadPromise: Promise<void> | undefined;
176
192
  private _loadRequested = false;
177
- private pendingResumeMessageId: string | null = null;
178
- private pendingA2uiResume = false;
193
+ private pendingResume: { owner: AbortController; messageId: string } | null =
194
+ null;
195
+ private pendingA2uiResumeOwner: AbortController | null = null;
179
196
  private pendingA2uiAction: Record<string, unknown> | undefined;
180
197
 
181
198
  constructor(options: CoreOptions) {
182
199
  this.agent = options.agent;
183
200
  this.logger = options.logger;
184
201
  this.showThinking = options.showThinking;
202
+ this.resumeTranscript = options.resumeTranscript ?? "full";
185
203
  this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
186
204
  this.onError = options.onError;
187
205
  this.onCancel = options.onCancel;
@@ -193,6 +211,7 @@ export class AgUiThreadRuntimeCore {
193
211
  this.agent = options.agent;
194
212
  this.logger = options.logger;
195
213
  this.showThinking = options.showThinking;
214
+ this.resumeTranscript = options.resumeTranscript ?? "full";
196
215
  this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
197
216
  this.onError = options.onError;
198
217
  this.onCancel = options.onCancel;
@@ -296,12 +315,17 @@ export class AgUiThreadRuntimeCore {
296
315
 
297
316
  async append(message: AppendMessage): Promise<void> {
298
317
  const startRun = message.startRun ?? message.role === "user";
318
+ let ownsThread = true;
299
319
  if (startRun) {
300
320
  this.assertNoPendingInterrupts();
301
321
  this.maybeAutoCancelPendingToolCalls();
322
+ // Before the message is linked: callers parent it on the in-flight
323
+ // assistant, and settling that assistant reassigns its optimistic id,
324
+ // which would evict it and reparent this message onto a branch.
325
+ ownsThread = this.supersedeActiveRun();
302
326
  }
303
327
  const threadMessageId = this.appendEntry(message);
304
- if (!startRun) return;
328
+ if (!startRun || !ownsThread) return;
305
329
  await this.startRun(threadMessageId, message.runConfig);
306
330
  }
307
331
 
@@ -344,17 +368,44 @@ export class AgUiThreadRuntimeCore {
344
368
  }
345
369
 
346
370
  async cancel(): Promise<void> {
347
- if (!this.abortController) return;
348
- // Before the local abort, whose listener runs onCancel synchronously: a
349
- // callback that starts another run replaces the agent's controller, and
350
- // aborting afterwards would kill that replacement and leave this run live.
351
- // The local abort is unconditional because abortRun is a user subclass's
352
- // code, and a throw there would otherwise strand the thread as running.
371
+ this.abortActiveRun();
372
+ }
373
+
374
+ // abortRun is what stops an upstream agent: AbstractAgent.runAgent declares
375
+ // two parameters and HttpAgent binds its request to a controller of its own,
376
+ // so the run options object reaches only subclasses that read it. The local
377
+ // abort comes last and is unconditional: its listener runs onCancel
378
+ // synchronously, so a callback that starts another run must keep the
379
+ // controller it installed, and a throw from a subclass's abortRun would
380
+ // otherwise strand the thread as running.
381
+ private abortActiveRun(): void {
382
+ const controller = this.abortController;
383
+ if (!controller) return;
353
384
  try {
354
385
  (this.activeRunAgent ?? this.agent).abortRun();
355
386
  } finally {
356
- this.abortController.abort();
387
+ controller.abort();
388
+ }
389
+ }
390
+
391
+ // Starting a run supersedes the active one, and reports whether the caller
392
+ // still owns the thread afterwards: the local abort runs onCancel
393
+ // synchronously, so a callback that starts its own run keeps it. abortRun is a
394
+ // subclass's code, and a throw there must not abandon the run being started,
395
+ // unlike cancel(), where the failed operation is the caller's own. Calling
396
+ // this twice for one run is harmless, because the second call finds no active
397
+ // run to supersede.
398
+ private supersedeActiveRun(): boolean {
399
+ try {
400
+ this.abortActiveRun();
401
+ } catch (error) {
402
+ this.logger.error?.("[agui] agent abortRun failed", error);
357
403
  }
404
+ if (this.abortController === null) return true;
405
+ this.logger.debug?.(
406
+ "[agui] onCancel started a replacement run; dropping the superseding send",
407
+ );
408
+ return false;
358
409
  }
359
410
 
360
411
  async resume(config: ResumeRunConfig): Promise<void> {
@@ -490,7 +541,10 @@ export class AgUiThreadRuntimeCore {
490
541
  resume: AgUiResumeEntry[],
491
542
  ): Promise<void> {
492
543
  this.clearPendingInterrupts(messageId, resume);
493
- await this.startRun(messageId, this.lastRunConfig, resume);
544
+ await this.startRun(messageId, this.lastRunConfig, {
545
+ entries: resume,
546
+ interruptedMessageId: messageId,
547
+ });
494
548
  }
495
549
 
496
550
  /**
@@ -649,7 +703,10 @@ export class AgUiThreadRuntimeCore {
649
703
  const normalized = this.toAppendMessage(message);
650
704
  this.clearPendingInterrupts(pending.messageId, resume);
651
705
  const threadMessageId = this.appendEntry(normalized);
652
- await this.startRun(threadMessageId, normalized.runConfig, resume);
706
+ await this.startRun(threadMessageId, normalized.runConfig, {
707
+ entries: resume,
708
+ interruptedMessageId: pending.messageId,
709
+ });
653
710
  }
654
711
 
655
712
  private resolveSteerAwayResume(
@@ -718,7 +775,8 @@ export class AgUiThreadRuntimeCore {
718
775
  }
719
776
  return {
720
777
  createdAt: message.createdAt ?? new Date(),
721
- parentId: message.parentId ?? this.session.headId,
778
+ parentId:
779
+ message.parentId === undefined ? this.session.headId : message.parentId,
722
780
  sourceId: message.sourceId ?? null,
723
781
  role: message.role ?? "user",
724
782
  content: message.content,
@@ -813,11 +871,17 @@ export class AgUiThreadRuntimeCore {
813
871
  const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
814
872
  if (part.toolCallId !== options.toolCallId) return part;
815
873
  matchedToolCall = true;
874
+ // artifact and modelContent are optional; only override when supplied
875
+ // so a later result that omits them keeps a stored value, and
876
+ // emitToolResult can forward the model-facing content to the agent.
816
877
  return {
817
878
  ...part,
818
879
  result: options.result,
819
- artifact: options.artifact,
820
880
  isError: options.isError,
881
+ ...(options.artifact !== undefined && { artifact: options.artifact }),
882
+ ...(options.modelContent !== undefined && {
883
+ modelContent: options.modelContent,
884
+ }),
821
885
  };
822
886
  });
823
887
  if (!matchedToolCall) return message;
@@ -847,8 +911,9 @@ export class AgUiThreadRuntimeCore {
847
911
  }
848
912
  this.pendingA2uiAction = userAction;
849
913
 
850
- if (this.isRunningFlag) {
851
- this.pendingA2uiResume = true;
914
+ const owner = this.abortController;
915
+ if (owner) {
916
+ this.pendingA2uiResumeOwner = owner;
852
917
  return;
853
918
  }
854
919
  this.startResumeRun(parentId);
@@ -860,10 +925,11 @@ export class AgUiThreadRuntimeCore {
860
925
  private maybeResumeAfterToolResults(messageId: string): void {
861
926
  if (!this.maybeCompleteAfterToolResults(messageId)) return;
862
927
 
863
- if (this.isRunningFlag) {
928
+ const owner = this.abortController;
929
+ if (owner) {
864
930
  // A run is still draining (RUN_FINISHED arrived but the stream has not
865
931
  // closed). Defer until startRun's tail so we never start two runs.
866
- this.pendingResumeMessageId = messageId;
932
+ this.pendingResume = { owner, messageId };
867
933
  return;
868
934
  }
869
935
  this.startResumeRun(messageId);
@@ -913,7 +979,7 @@ export class AgUiThreadRuntimeCore {
913
979
  }
914
980
 
915
981
  applyExternalMessages(messages: readonly ThreadMessage[]): void {
916
- this.pendingA2uiResume = false;
982
+ this.pendingA2uiResumeOwner = null;
917
983
  this.pendingA2uiAction = undefined;
918
984
  this.assistantHistoryParents.clear();
919
985
 
@@ -959,6 +1025,17 @@ export class AgUiThreadRuntimeCore {
959
1025
  for (const { message } of this.getMessageRepository().messages) {
960
1026
  this.snapshotHistoryIds.add(message.id);
961
1027
  }
1028
+ // MESSAGES_SNAPSHOT re-appends the active assistant, so a parked
1029
+ // continuation whose target survived the snapshot is still answerable.
1030
+ // Membership is the head branch, not the repository: a soft merge leaves
1031
+ // the messages it dropped behind as off-branch nodes.
1032
+ const resumeTarget = this.pendingResume?.messageId;
1033
+ if (
1034
+ resumeTarget !== undefined &&
1035
+ !this.session.getMessages().some((message) => message.id === resumeTarget)
1036
+ ) {
1037
+ this.pendingResume = null;
1038
+ }
962
1039
  this.notifyUpdate();
963
1040
  }
964
1041
 
@@ -985,9 +1062,14 @@ export class AgUiThreadRuntimeCore {
985
1062
  private async startRun(
986
1063
  parentId: string | null,
987
1064
  runConfig?: RunConfig,
988
- resume?: AgUiResumeEntry[],
1065
+ resume?: ResumeDispatch,
989
1066
  resumeStream?: ResumeStream,
990
1067
  ): Promise<void> {
1068
+ // A default AG-UI run supersedes the active run; the hook's opt-in message
1069
+ // queue serializes sends instead. append supersedes earlier, before it links
1070
+ // its message; this covers the entry points that start a run without one.
1071
+ if (!this.supersedeActiveRun()) return;
1072
+
991
1073
  const normalizedRunConfig = runConfig ?? {};
992
1074
  this.lastRunConfig = normalizedRunConfig;
993
1075
  const parent =
@@ -1005,7 +1087,7 @@ export class AgUiThreadRuntimeCore {
1005
1087
  this.getMessageRepository().messages.map(({ message }) => message.id),
1006
1088
  );
1007
1089
 
1008
- this.pendingError = null;
1090
+ let pendingError: Error | null = null;
1009
1091
  const assistantParentId = parent ? parentId : this.session.headId;
1010
1092
  let assistantMessageId: string | undefined;
1011
1093
  // A snapshot the preserve gate declines still evicts the in-flight
@@ -1131,16 +1213,17 @@ export class AgUiThreadRuntimeCore {
1131
1213
  // Cancel flips only the status; an aggregator RUN_CANCELLED would emit an empty snapshot and wipe the replayed content.
1132
1214
  cancelRun = () =>
1133
1215
  applyUpdate({ status: { type: "incomplete", reason: "cancelled" } });
1134
- await this.consumeResumeStream(resumeStream, {
1135
- runConfig: normalizedRunConfig,
1136
- threadId: this.agent.threadId || "main",
1137
- parentId: assistantParentId,
1138
- historicalMessages,
1139
- abortSignal,
1140
- ensureAssistant,
1141
- applyUpdate,
1142
- getAssistantMessageId: () => assistantMessageId,
1143
- });
1216
+ pendingError =
1217
+ (await this.consumeResumeStream(resumeStream, {
1218
+ runConfig: normalizedRunConfig,
1219
+ threadId: this.agent.threadId || "main",
1220
+ parentId: assistantParentId,
1221
+ historicalMessages,
1222
+ abortSignal,
1223
+ ensureAssistant,
1224
+ applyUpdate,
1225
+ getAssistantMessageId: () => assistantMessageId,
1226
+ })) ?? null;
1144
1227
  } else {
1145
1228
  const runId = generateId();
1146
1229
  aggregator.handle({ type: "RUN_STARTED", runId });
@@ -1156,7 +1239,7 @@ export class AgUiThreadRuntimeCore {
1156
1239
  logger: this.logger,
1157
1240
  onRunFailed: (error) => {
1158
1241
  if (abortSignal.aborted) return;
1159
- this.pendingError = error;
1242
+ pendingError = error;
1160
1243
  invokeRuntimeCallback("onError", this.onError, error);
1161
1244
  },
1162
1245
  });
@@ -1176,36 +1259,31 @@ export class AgUiThreadRuntimeCore {
1176
1259
  const err = error instanceof Error ? error : new Error(String(error));
1177
1260
  dispatch({ type: "RUN_ERROR", message: err.message });
1178
1261
  invokeRuntimeCallback("onError", this.onError, err);
1179
- this.pendingError = this.pendingError ?? err;
1262
+ pendingError ??= err;
1180
1263
  }
1181
1264
  } finally {
1182
1265
  this.finishRun(abortController);
1183
1266
  }
1184
1267
 
1185
- if (this.pendingError) {
1186
- const err = this.pendingError;
1268
+ if (pendingError) {
1269
+ const err = pendingError;
1187
1270
  this.reportedErrors.add(err);
1188
- this.pendingError = null;
1189
- this.pendingResumeMessageId = null;
1190
- this.pendingA2uiResume = false;
1191
- this.pendingA2uiAction = undefined;
1271
+ this.clearDeferredContinuations(abortController);
1192
1272
  throw err;
1193
1273
  }
1194
1274
 
1195
1275
  // A tool result that landed before the run settled deferred its
1196
1276
  // continuation here so a second run never overlaps the first.
1197
- if (this.pendingResumeMessageId !== null) {
1198
- const resumeMessageId = this.pendingResumeMessageId;
1199
- this.pendingResumeMessageId = null;
1277
+ if (this.pendingResume?.owner === abortController) {
1278
+ const { messageId } = this.pendingResume;
1279
+ this.pendingResume = null;
1200
1280
  if (!abortSignal.aborted) {
1201
- this.startResumeRun(resumeMessageId);
1202
- } else {
1203
- this.pendingA2uiAction = undefined;
1281
+ this.startResumeRun(messageId);
1204
1282
  }
1205
1283
  }
1206
1284
 
1207
- if (this.pendingA2uiResume) {
1208
- this.pendingA2uiResume = false;
1285
+ if (this.pendingA2uiResumeOwner === abortController) {
1286
+ this.pendingA2uiResumeOwner = null;
1209
1287
  if (!abortSignal.aborted && this.pendingA2uiAction !== undefined) {
1210
1288
  if (this.getPendingInterrupts()) {
1211
1289
  this.pendingA2uiAction = undefined;
@@ -1243,9 +1321,9 @@ export class AgUiThreadRuntimeCore {
1243
1321
  applyUpdate: (update: ChatModelRunResult) => void;
1244
1322
  getAssistantMessageId: () => string | undefined;
1245
1323
  },
1246
- ): Promise<void> {
1324
+ ): Promise<Error | undefined> {
1247
1325
  this.pendingA2uiAction = undefined;
1248
- this.pendingA2uiResume = false;
1326
+ this.pendingA2uiResumeOwner = null;
1249
1327
  const assistantId = ctx.ensureAssistant();
1250
1328
  const currentId = () => ctx.getAssistantMessageId() ?? assistantId;
1251
1329
  const options: ChatModelRunOptions = {
@@ -1269,36 +1347,39 @@ export class AgUiThreadRuntimeCore {
1269
1347
 
1270
1348
  try {
1271
1349
  for await (const result of stream(options)) {
1272
- if (ctx.abortSignal.aborted) return;
1350
+ if (ctx.abortSignal.aborted) return undefined;
1273
1351
  ctx.applyUpdate(result);
1274
1352
  }
1275
1353
  } catch (error) {
1276
- if (ctx.abortSignal.aborted) return;
1354
+ if (ctx.abortSignal.aborted) return undefined;
1277
1355
  const err = error instanceof Error ? error : new Error(String(error));
1278
1356
  ctx.applyUpdate({
1279
1357
  status: { type: "incomplete", reason: "error", error: err.message },
1280
1358
  });
1281
1359
  invokeRuntimeCallback("onError", this.onError, err);
1282
- this.pendingError = this.pendingError ?? err;
1283
- return;
1360
+ return err;
1284
1361
  }
1285
1362
 
1286
- if (ctx.abortSignal.aborted) return;
1363
+ if (ctx.abortSignal.aborted) return undefined;
1287
1364
  const current = this.session.tryGetMessage(currentId())?.message;
1288
1365
  if (!current || current.status?.type === "running") {
1289
1366
  ctx.applyUpdate({ status: { type: "complete", reason: "unknown" } });
1290
1367
  }
1368
+ return undefined;
1291
1369
  }
1292
1370
 
1293
1371
  private buildRunInput(
1294
1372
  runId: string,
1295
1373
  runConfig: RunConfig | undefined,
1296
1374
  historyMessages: readonly ThreadMessage[] | undefined,
1297
- resume?: AgUiResumeEntry[],
1375
+ resume?: ResumeDispatch,
1298
1376
  ) {
1299
1377
  const threadId = this.agent.threadId || "main";
1300
1378
  const messages = toAgUiMessages(
1301
- historyMessages ?? this.session.getMessages(),
1379
+ this.selectRunMessages(
1380
+ historyMessages ?? this.session.getMessages(),
1381
+ resume,
1382
+ ),
1302
1383
  );
1303
1384
  const context = this.runtime?.thread.getModelContext();
1304
1385
  const input = {
@@ -1318,22 +1399,51 @@ export class AgUiThreadRuntimeCore {
1318
1399
  ? { a2uiAction: { userAction: this.pendingA2uiAction } }
1319
1400
  : {}),
1320
1401
  },
1321
- ...(resume !== undefined ? { resume } : {}),
1402
+ ...(resume !== undefined ? { resume: resume.entries } : {}),
1322
1403
  };
1323
1404
  this.pendingA2uiAction = undefined;
1324
1405
  return input;
1325
1406
  }
1326
1407
 
1408
+ /**
1409
+ * An anchor the thread no longer carries falls back to the whole transcript.
1410
+ * The anchor is missing exactly when a snapshot import replaced the messages
1411
+ * this resume was raised against, and a host that expects a delta tolerates
1412
+ * being told more than it knows, while one rebuilding the run from the
1413
+ * transcript cannot recover from being told less.
1414
+ */
1415
+ private selectRunMessages(
1416
+ messages: readonly ThreadMessage[],
1417
+ resume: ResumeDispatch | undefined,
1418
+ ): readonly ThreadMessage[] {
1419
+ if (resume === undefined || this.resumeTranscript !== "appended")
1420
+ return messages;
1421
+ const anchor = messages.findIndex(
1422
+ (message) => message.id === resume.interruptedMessageId,
1423
+ );
1424
+ if (anchor === -1) return messages;
1425
+ return messages.slice(anchor + 1);
1426
+ }
1427
+
1428
+ private clearDeferredContinuations(owner: AbortController): void {
1429
+ if (this.pendingResume?.owner === owner) {
1430
+ this.pendingResume = null;
1431
+ }
1432
+ if (this.pendingA2uiResumeOwner === owner) {
1433
+ this.pendingA2uiResumeOwner = null;
1434
+ this.pendingA2uiAction = undefined;
1435
+ }
1436
+ }
1437
+
1327
1438
  private setRunning(running: boolean) {
1328
1439
  this.isRunningFlag = running;
1329
1440
  this.notifyUpdate();
1330
1441
  }
1331
1442
 
1332
1443
  private finishRun(controller: AbortController | null) {
1333
- if (this.abortController === controller) {
1334
- this.abortController = null;
1335
- this.activeRunAgent = null;
1336
- }
1444
+ if (this.abortController !== controller) return;
1445
+ this.abortController = null;
1446
+ this.activeRunAgent = null;
1337
1447
  this.setRunning(false);
1338
1448
  }
1339
1449
 
@@ -1498,6 +1608,9 @@ export class AgUiThreadRuntimeCore {
1498
1608
  result: prior.result,
1499
1609
  ...(prior.artifact !== undefined ? { artifact: prior.artifact } : {}),
1500
1610
  ...(prior.isError !== undefined ? { isError: prior.isError } : {}),
1611
+ ...(prior.modelContent !== undefined
1612
+ ? { modelContent: prior.modelContent }
1613
+ : {}),
1501
1614
  };
1502
1615
  });
1503
1616
  return changed ? merged : next;