@assistant-ui/react-ag-ui 0.0.44 → 0.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/agUiExtras.d.ts +0 -1
  2. package/dist/agUiExtras.d.ts.map +1 -1
  3. package/dist/hooks.d.ts +15 -9
  4. package/dist/hooks.d.ts.map +1 -1
  5. package/dist/hooks.js +17 -8
  6. package/dist/hooks.js.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +28 -4
  10. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  11. package/dist/runtime/AgUiThreadRuntimeCore.js +379 -114
  12. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  13. package/dist/runtime/adapter/conversions.d.ts +0 -1
  14. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  15. package/dist/runtime/adapter/conversions.js +12 -12
  16. package/dist/runtime/adapter/conversions.js.map +1 -1
  17. package/dist/runtime/adapter/run-aggregator.d.ts +3 -2
  18. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  19. package/dist/runtime/adapter/run-aggregator.js +48 -11
  20. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  21. package/dist/runtime/adapter/subscriber.d.ts +0 -1
  22. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  23. package/dist/runtime/event-parser.d.ts +0 -1
  24. package/dist/runtime/event-parser.d.ts.map +1 -1
  25. package/dist/runtime/event-parser.js +23 -11
  26. package/dist/runtime/event-parser.js.map +1 -1
  27. package/dist/runtime/logger.d.ts.map +1 -1
  28. package/dist/runtime/mcp-tool-result.d.ts +11 -0
  29. package/dist/runtime/mcp-tool-result.d.ts.map +1 -0
  30. package/dist/runtime/mcp-tool-result.js +20 -0
  31. package/dist/runtime/mcp-tool-result.js.map +1 -0
  32. package/dist/runtime/types.d.ts +15 -1
  33. package/dist/runtime/types.d.ts.map +1 -1
  34. package/dist/useAgUiRuntime.d.ts +0 -1
  35. package/dist/useAgUiRuntime.d.ts.map +1 -1
  36. package/dist/useAgUiRuntime.js +8 -2
  37. package/dist/useAgUiRuntime.js.map +1 -1
  38. package/package.json +7 -7
  39. package/src/hooks.ts +29 -7
  40. package/src/index.ts +2 -0
  41. package/src/runtime/AgUiThreadRuntimeCore.ts +547 -147
  42. package/src/runtime/adapter/conversions.ts +26 -17
  43. package/src/runtime/adapter/run-aggregator.ts +84 -7
  44. package/src/runtime/event-parser.ts +4 -1
  45. package/src/runtime/mcp-tool-result.ts +26 -0
  46. package/src/runtime/types.ts +19 -0
  47. package/src/useAgUiRuntime.ts +8 -1
@@ -1,6 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { generateId, fromThreadMessageLike } from "@assistant-ui/core";
3
+ import {
4
+ generateId,
5
+ fromThreadMessageLike,
6
+ isMcpAppUri,
7
+ } from "@assistant-ui/core";
4
8
  import type {
5
9
  AddToolResultOptions,
6
10
  AppendMessage,
@@ -8,12 +12,14 @@ import type {
8
12
  AssistantRuntime,
9
13
  ChatModelRunOptions,
10
14
  ChatModelRunResult,
15
+ ExportedMessageRepository,
11
16
  MessageStatus,
12
17
  ThreadAssistantMessage,
13
18
  ThreadHistoryAdapter,
14
19
  ThreadMessage,
15
20
  ToolCallMessagePart,
16
21
  } from "@assistant-ui/core";
22
+ import { MessageRepository } from "@assistant-ui/core/internal";
17
23
  import type { AbstractAgent } from "@ag-ui/client";
18
24
  import jsonpatch, { type Operation } from "fast-json-patch";
19
25
  import type { Logger } from "./logger";
@@ -22,6 +28,8 @@ import type { ReadonlyJSONValue } from "assistant-stream/utils";
22
28
  import {
23
29
  AG_UI_METADATA_NAMESPACE,
24
30
  type AgUiCustomMetadata,
31
+ isPlainObject,
32
+ MCP_APPS_ACTIVITY_TYPE,
25
33
  RunAggregator,
26
34
  tryParseJSON,
27
35
  } from "./adapter/run-aggregator";
@@ -36,6 +44,11 @@ const optimisticPrefix = "__optimistic__";
36
44
  const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;
37
45
  const isOptimisticId = (id: string) => id.startsWith(optimisticPrefix);
38
46
 
47
+ const isResolvedToolCall = (
48
+ part: ThreadAssistantMessage["content"][number],
49
+ ): boolean =>
50
+ part.type === "tool-call" && "result" in part && part.result !== undefined;
51
+
39
52
  const symbolResumeShim = Symbol("agui-resume-shim");
40
53
 
41
54
  type RunConfig = NonNullable<AppendMessage["runConfig"]>;
@@ -53,6 +66,7 @@ type CoreOptions = {
53
66
  agent: AbstractAgent;
54
67
  logger: Logger;
55
68
  showThinking: boolean;
69
+ autoCancelPendingToolCalls?: boolean | undefined;
56
70
  onError?: (error: Error) => void;
57
71
  onCancel?: () => void;
58
72
  history?: ThreadHistoryAdapter;
@@ -65,12 +79,14 @@ export class AgUiThreadRuntimeCore {
65
79
  private agent: AbstractAgent;
66
80
  private logger: Logger;
67
81
  private showThinking: boolean;
82
+ private autoCancelPendingToolCalls: boolean | undefined;
68
83
  private onError: ((error: Error) => void) | undefined;
69
84
  private onCancel: (() => void) | undefined;
70
85
  private readonly notifyUpdate: () => void;
71
86
 
72
87
  private runtime: AssistantRuntime | undefined;
73
- private messages: ThreadMessage[] = [];
88
+ private readonly repository = new MessageRepository();
89
+ private exportedRepository: ExportedMessageRepository | undefined;
74
90
  private isRunningFlag = false;
75
91
  private abortController: AbortController | null = null;
76
92
  private stateSnapshot: ReadonlyJSONValue | undefined;
@@ -87,6 +103,7 @@ export class AgUiThreadRuntimeCore {
87
103
  this.agent = options.agent;
88
104
  this.logger = options.logger;
89
105
  this.showThinking = options.showThinking;
106
+ this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
90
107
  this.onError = options.onError;
91
108
  this.onCancel = options.onCancel;
92
109
  this.history = options.history;
@@ -98,6 +115,7 @@ export class AgUiThreadRuntimeCore {
98
115
  this.agent = options.agent;
99
116
  this.logger = options.logger;
100
117
  this.showThinking = options.showThinking;
118
+ this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
101
119
  this.onError = options.onError;
102
120
  this.onCancel = options.onCancel;
103
121
  this.history = options.history;
@@ -113,7 +131,97 @@ export class AgUiThreadRuntimeCore {
113
131
  }
114
132
 
115
133
  getMessages(): readonly ThreadMessage[] {
116
- return this.messages;
134
+ return this.repository.getMessages();
135
+ }
136
+
137
+ getMessageRepository(): ExportedMessageRepository {
138
+ if (this.exportedRepository) return this.exportedRepository;
139
+
140
+ const exportedRepository = this.repository.export();
141
+ let parentId: string | null = null;
142
+ for (const message of this.repository.getMessages()) {
143
+ if (message.metadata.isOptimistic) {
144
+ exportedRepository.messages.push({ parentId, message });
145
+ }
146
+ parentId = message.id;
147
+ }
148
+
149
+ this.exportedRepository = {
150
+ ...exportedRepository,
151
+ headId: this.repository.headId,
152
+ };
153
+ return this.exportedRepository;
154
+ }
155
+
156
+ private tryGetMessage(messageId: string) {
157
+ try {
158
+ return this.repository.getMessage(messageId);
159
+ } catch {
160
+ return undefined;
161
+ }
162
+ }
163
+
164
+ private tryGetMessages(
165
+ messageId: string,
166
+ ): readonly ThreadMessage[] | undefined {
167
+ try {
168
+ return this.repository.getMessages(messageId);
169
+ } catch {
170
+ return undefined;
171
+ }
172
+ }
173
+
174
+ private hasMessage(messageId: string): boolean {
175
+ return this.tryGetMessage(messageId) !== undefined;
176
+ }
177
+
178
+ private addOrUpdateMessage(
179
+ parentId: string | null,
180
+ message: ThreadMessage,
181
+ ): void {
182
+ this.repository.addOrUpdateMessage(parentId, message);
183
+ this.exportedRepository = undefined;
184
+ }
185
+
186
+ private deleteMessage(
187
+ messageId: string,
188
+ replacementId?: string | null,
189
+ ): void {
190
+ this.repository.deleteMessage(messageId, replacementId);
191
+ this.exportedRepository = undefined;
192
+ }
193
+
194
+ private tryDeleteMessage(messageId: string): boolean {
195
+ if (!this.hasMessage(messageId)) return false;
196
+ this.deleteMessage(messageId);
197
+ return true;
198
+ }
199
+
200
+ private switchToBranch(messageId: string): void {
201
+ this.repository.switchToBranch(messageId);
202
+ this.exportedRepository = undefined;
203
+ }
204
+
205
+ private resetRepositoryHead(messageId: string | null): void {
206
+ this.repository.resetHead(messageId);
207
+ this.exportedRepository = undefined;
208
+ }
209
+
210
+ private clearRepository(): void {
211
+ this.repository.clear();
212
+ this.exportedRepository = undefined;
213
+ }
214
+
215
+ private updateMessage(
216
+ messageId: string,
217
+ updater: (message: ThreadMessage) => ThreadMessage,
218
+ ): boolean {
219
+ const item = this.tryGetMessage(messageId);
220
+ if (!item) return false;
221
+ const message = updater(item.message);
222
+ if (message === item.message) return false;
223
+ this.addOrUpdateMessage(item.parentId, message);
224
+ return true;
117
225
  }
118
226
 
119
227
  getState(): ReadonlyJSONValue | undefined {
@@ -139,15 +247,14 @@ export class AgUiThreadRuntimeCore {
139
247
  .then(async (repo) => {
140
248
  if (!repo) return;
141
249
 
142
- const messages = repo.messages.map((item) => item.message);
143
- this.applyExternalMessages(messages);
250
+ this.applyExternalMessageRepository(repo);
144
251
 
145
252
  if (repo.state !== undefined) {
146
253
  this.loadExternalState(repo.state);
147
254
  }
148
255
 
149
256
  if (repo.unstable_resume) {
150
- const parentId = repo.headId ?? messages.at(-1)?.id ?? null;
257
+ const parentId = repo.headId ?? this.repository.headId;
151
258
  const resumeStream = this.history?.resume?.bind(this.history);
152
259
  await this.startRun(
153
260
  parentId,
@@ -174,24 +281,37 @@ export class AgUiThreadRuntimeCore {
174
281
 
175
282
  async append(message: AppendMessage): Promise<void> {
176
283
  const startRun = message.startRun ?? message.role === "user";
177
- if (startRun) this.assertNoPendingInterrupts();
284
+ if (startRun) {
285
+ this.assertNoPendingInterrupts();
286
+ this.maybeAutoCancelPendingToolCalls();
287
+ }
178
288
  const threadMessageId = this.appendEntry(message);
179
289
  if (!startRun) return;
180
290
  await this.startRun(threadMessageId, message.runConfig);
181
291
  }
182
292
 
293
+ private maybeAutoCancelPendingToolCalls(): void {
294
+ if (this.autoCancelPendingToolCalls === false) return;
295
+ const pending = this.getPendingToolCalls();
296
+ if (!pending) return;
297
+ this.cancelUnresolvedToolCalls(pending.messageId);
298
+ this.maybeCompleteAfterToolResults(pending.messageId);
299
+ }
300
+
183
301
  private appendEntry(message: AppendMessage): string {
184
- if (message.sourceId) {
185
- this.messages = this.messages.filter(
186
- (entry) => entry.id !== message.sourceId,
187
- );
188
- }
189
- this.resetHead(message.parentId);
302
+ if (message.sourceId) this.tryDeleteMessage(message.sourceId);
190
303
 
191
304
  const threadMessage = this.toThreadMessage(message);
192
- this.messages = [...this.messages, threadMessage];
305
+ const parentId =
306
+ message.parentId === null
307
+ ? null
308
+ : message.parentId && this.hasMessage(message.parentId)
309
+ ? message.parentId
310
+ : this.repository.headId;
311
+ this.addOrUpdateMessage(parentId, threadMessage);
312
+ this.switchToBranch(threadMessage.id);
193
313
  this.notifyUpdate();
194
- this.recordHistoryEntry(message.parentId ?? null, threadMessage);
314
+ this.recordHistoryEntry(parentId, threadMessage);
195
315
  return threadMessage.id;
196
316
  }
197
317
 
@@ -204,8 +324,7 @@ export class AgUiThreadRuntimeCore {
204
324
  config: { runConfig?: RunConfig } = {},
205
325
  ): Promise<void> {
206
326
  this.assertNoPendingInterrupts();
207
- this.resetHead(parentId);
208
- this.notifyUpdate();
327
+ this.maybeAutoCancelPendingToolCalls();
209
328
  await this.startRun(parentId, config.runConfig);
210
329
  }
211
330
 
@@ -255,20 +374,28 @@ export class AgUiThreadRuntimeCore {
255
374
  );
256
375
  }
257
376
 
258
- getPendingInterrupts(): {
259
- messageId: string;
260
- interrupts: readonly AgUiInterrupt[];
261
- } | null {
262
- const assistant = this.messages.findLast((m) => m.role === "assistant") as
263
- | ThreadAssistantMessage
264
- | undefined;
377
+ private findRequiresActionAssistant(
378
+ reason: "interrupt" | "tool-calls",
379
+ ): ThreadAssistantMessage | null {
380
+ const assistant = this.getMessages().findLast(
381
+ (message) => message.role === "assistant",
382
+ ) as ThreadAssistantMessage | undefined;
265
383
  if (
266
384
  !assistant ||
267
385
  assistant.status?.type !== "requires-action" ||
268
- assistant.status.reason !== "interrupt"
386
+ assistant.status.reason !== reason
269
387
  ) {
270
388
  return null;
271
389
  }
390
+ return assistant;
391
+ }
392
+
393
+ getPendingInterrupts(): {
394
+ messageId: string;
395
+ interrupts: readonly AgUiInterrupt[];
396
+ } | null {
397
+ const assistant = this.findRequiresActionAssistant("interrupt");
398
+ if (!assistant) return null;
272
399
  const stored = (
273
400
  assistant.metadata.custom[AG_UI_METADATA_NAMESPACE] as
274
401
  | AgUiCustomMetadata
@@ -278,6 +405,22 @@ export class AgUiThreadRuntimeCore {
278
405
  return { messageId: assistant.id, interrupts: stored };
279
406
  }
280
407
 
408
+ getPendingToolCalls(): {
409
+ messageId: string;
410
+ toolCallIds: string[];
411
+ } | null {
412
+ const assistant = this.findRequiresActionAssistant("tool-calls");
413
+ if (!assistant) return null;
414
+ const toolCallIds: string[] = [];
415
+ for (const part of assistant.content) {
416
+ if (part.type !== "tool-call") continue;
417
+ if (isResolvedToolCall(part)) continue;
418
+ toolCallIds.push(part.toolCallId);
419
+ }
420
+ if (toolCallIds.length === 0) return null;
421
+ return { messageId: assistant.id, toolCallIds };
422
+ }
423
+
281
424
  async submitInterruptResponses(
282
425
  responses: readonly AgUiResumeEntry[],
283
426
  ): Promise<void> {
@@ -288,25 +431,11 @@ export class AgUiThreadRuntimeCore {
288
431
  );
289
432
  }
290
433
 
291
- const responsesById = new Map<string, AgUiResumeEntry>();
292
- for (const entry of responses) {
293
- if (!entry || typeof entry.interruptId !== "string") {
294
- throw new Error(
295
- "[agui] submitInterruptResponses: every entry must have an interruptId",
296
- );
297
- }
298
- if (entry.status !== "resolved" && entry.status !== "cancelled") {
299
- throw new Error(
300
- `[agui] submitInterruptResponses: invalid status "${entry.status}" for interrupt ${entry.interruptId}`,
301
- );
302
- }
303
- if (responsesById.has(entry.interruptId)) {
304
- throw new Error(
305
- `[agui] submitInterruptResponses: duplicate response for interrupt ${entry.interruptId}`,
306
- );
307
- }
308
- responsesById.set(entry.interruptId, entry);
309
- }
434
+ const responsesById = this.collectInterruptResponses(
435
+ "submitInterruptResponses",
436
+ pending.interrupts,
437
+ responses,
438
+ );
310
439
 
311
440
  const openIds = pending.interrupts.map((i) => i.id);
312
441
  const missing = openIds.filter((id) => !responsesById.has(id));
@@ -315,13 +444,6 @@ export class AgUiThreadRuntimeCore {
315
444
  `[agui] submitInterruptResponses: missing responses for open interrupts: ${missing.join(", ")}`,
316
445
  );
317
446
  }
318
- const known = new Set(openIds);
319
- const unknownIds = [...responsesById.keys()].filter((id) => !known.has(id));
320
- if (unknownIds.length > 0) {
321
- throw new Error(
322
- `[agui] submitInterruptResponses: unknown interrupt ids: ${unknownIds.join(", ")}`,
323
- );
324
- }
325
447
 
326
448
  const now = Date.now();
327
449
  for (const interrupt of pending.interrupts) {
@@ -339,8 +461,8 @@ export class AgUiThreadRuntimeCore {
339
461
  }
340
462
  }
341
463
 
342
- const resume: AgUiResumeEntry[] = openIds.map(
343
- (id) => responsesById.get(id)!,
464
+ const resume: AgUiResumeEntry[] = openIds.map((id) =>
465
+ responsesById.get(id)!,
344
466
  );
345
467
 
346
468
  if (this.isRunningFlag) {
@@ -359,6 +481,23 @@ export class AgUiThreadRuntimeCore {
359
481
  ): Promise<void> {
360
482
  const pending = this.getPendingInterrupts();
361
483
  if (!pending) {
484
+ const pendingTools = this.getPendingToolCalls();
485
+ if (pendingTools) {
486
+ if (responses?.length) {
487
+ throw new Error(
488
+ "[agui] steerAway: responses are only valid for pending interrupts",
489
+ );
490
+ }
491
+ if (this.isRunningFlag) {
492
+ throw new Error("[agui] steerAway: a run is already in progress");
493
+ }
494
+ this.cancelUnresolvedToolCalls(pendingTools.messageId);
495
+ this.maybeCompleteAfterToolResults(pendingTools.messageId);
496
+ const normalized = this.toAppendMessage(message);
497
+ const threadMessageId = this.appendEntry(normalized);
498
+ await this.startRun(threadMessageId, normalized.runConfig);
499
+ return;
500
+ }
362
501
  if (responses?.length) {
363
502
  throw new Error(
364
503
  "[agui] steerAway: no pending interrupts on this thread",
@@ -375,9 +514,6 @@ export class AgUiThreadRuntimeCore {
375
514
  }
376
515
 
377
516
  const normalized = this.toAppendMessage(message);
378
- // Clear before appendEntry so the interrupted assistant is still in
379
- // this.messages when its status is flipped; a parentId that points at an
380
- // ancestor would otherwise truncate it away before it can be cleared.
381
517
  this.clearPendingInterrupts(pending.messageId);
382
518
  const threadMessageId = this.appendEntry(normalized);
383
519
  await this.startRun(threadMessageId, normalized.runConfig, resume);
@@ -387,42 +523,58 @@ export class AgUiThreadRuntimeCore {
387
523
  interrupts: readonly AgUiInterrupt[],
388
524
  responses: readonly AgUiResumeEntry[] | undefined,
389
525
  ): AgUiResumeEntry[] {
390
- const openIds = interrupts.map((interrupt) => interrupt.id);
391
- const known = new Set(openIds);
526
+ const responsesById = this.collectInterruptResponses(
527
+ "steerAway",
528
+ interrupts,
529
+ responses ?? [],
530
+ );
531
+ return interrupts.map(
532
+ (interrupt) =>
533
+ responsesById.get(interrupt.id) ?? {
534
+ interruptId: interrupt.id,
535
+ status: "cancelled",
536
+ },
537
+ );
538
+ }
539
+
540
+ private collectInterruptResponses(
541
+ method: "submitInterruptResponses" | "steerAway",
542
+ interrupts: readonly AgUiInterrupt[],
543
+ responses: readonly AgUiResumeEntry[],
544
+ ): Map<string, AgUiResumeEntry> {
545
+ const known = new Set(interrupts.map((interrupt) => interrupt.id));
392
546
  const responsesById = new Map<string, AgUiResumeEntry>();
393
- for (const entry of responses ?? []) {
547
+ for (const entry of responses) {
394
548
  if (!entry || typeof entry.interruptId !== "string") {
395
549
  throw new Error(
396
- "[agui] steerAway: every response must have an interruptId",
550
+ `[agui] ${method}: every entry must have an interruptId`,
397
551
  );
398
552
  }
399
553
  if (entry.status !== "resolved" && entry.status !== "cancelled") {
400
554
  throw new Error(
401
- `[agui] steerAway: invalid status "${entry.status}" for interrupt ${entry.interruptId}`,
555
+ `[agui] ${method}: invalid status "${entry.status}" for interrupt ${entry.interruptId}`,
402
556
  );
403
557
  }
404
558
  if (!known.has(entry.interruptId)) {
405
559
  throw new Error(
406
- `[agui] steerAway: unknown interrupt id ${entry.interruptId}`,
560
+ `[agui] ${method}: unknown interrupt id ${entry.interruptId}`,
407
561
  );
408
562
  }
409
563
  if (responsesById.has(entry.interruptId)) {
410
564
  throw new Error(
411
- `[agui] steerAway: duplicate response for interrupt ${entry.interruptId}`,
565
+ `[agui] ${method}: duplicate response for interrupt ${entry.interruptId}`,
412
566
  );
413
567
  }
414
568
  responsesById.set(entry.interruptId, entry);
415
569
  }
416
- return openIds.map(
417
- (id) => responsesById.get(id) ?? { interruptId: id, status: "cancelled" },
418
- );
570
+ return responsesById;
419
571
  }
420
572
 
421
573
  private toAppendMessage(message: CreateAppendMessage): AppendMessage {
422
574
  if (typeof message === "string") {
423
575
  return {
424
576
  createdAt: new Date(),
425
- parentId: this.messages.at(-1)?.id ?? null,
577
+ parentId: this.repository.headId,
426
578
  sourceId: null,
427
579
  runConfig: {},
428
580
  role: "user",
@@ -433,7 +585,7 @@ export class AgUiThreadRuntimeCore {
433
585
  }
434
586
  return {
435
587
  createdAt: message.createdAt ?? new Date(),
436
- parentId: message.parentId ?? this.messages.at(-1)?.id ?? null,
588
+ parentId: message.parentId ?? this.repository.headId,
437
589
  sourceId: message.sourceId ?? null,
438
590
  role: message.role ?? "user",
439
591
  content: message.content,
@@ -445,10 +597,8 @@ export class AgUiThreadRuntimeCore {
445
597
  }
446
598
 
447
599
  private clearPendingInterrupts(messageId: string): void {
448
- let touched = false;
449
- this.messages = this.messages.map((message) => {
450
- if (message.id !== messageId || message.role !== "assistant")
451
- return message;
600
+ const touched = this.updateMessage(messageId, (message) => {
601
+ if (message.role !== "assistant") return message;
452
602
  const assistant = message as ThreadAssistantMessage;
453
603
  if (
454
604
  assistant.status?.type !== "requires-action" ||
@@ -456,7 +606,6 @@ export class AgUiThreadRuntimeCore {
456
606
  ) {
457
607
  return assistant;
458
608
  }
459
- touched = true;
460
609
  const aguiMeta = assistant.metadata.custom[AG_UI_METADATA_NAMESPACE] as
461
610
  | AgUiCustomMetadata
462
611
  | undefined;
@@ -473,18 +622,21 @@ export class AgUiThreadRuntimeCore {
473
622
  metadata: { ...assistant.metadata, custom: newCustom },
474
623
  };
475
624
  });
476
- if (touched) this.notifyUpdate();
625
+ if (touched) {
626
+ this.notifyUpdate();
627
+ }
477
628
  }
478
629
 
479
630
  findMessageIdForToolCall(toolCallId: string): string | undefined {
480
631
  let fallbackMessageId: string | undefined;
481
- for (let index = this.messages.length - 1; index >= 0; index--) {
482
- const message = this.messages[index];
632
+ const messages = this.getMessages();
633
+ for (let index = messages.length - 1; index >= 0; index--) {
634
+ const message = messages[index];
483
635
  if (!message || message.role !== "assistant") continue;
484
636
  for (const part of message.content) {
485
637
  if (part.type !== "tool-call" || part.toolCallId !== toolCallId)
486
638
  continue;
487
- if (!("result" in part) || part.result === undefined) {
639
+ if (!isResolvedToolCall(part)) {
488
640
  return message.id;
489
641
  }
490
642
  fallbackMessageId ??= message.id;
@@ -493,11 +645,26 @@ export class AgUiThreadRuntimeCore {
493
645
  return fallbackMessageId;
494
646
  }
495
647
 
648
+ private cancelUnresolvedToolCalls(messageId: string): void {
649
+ const updated = this.updateMessage(messageId, (message) => {
650
+ if (message.role !== "assistant") return message;
651
+ const assistant = message as ThreadAssistantMessage;
652
+ const content = assistant.content.map((part) => {
653
+ if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
654
+ return {
655
+ ...part,
656
+ result: { error: "Tool call cancelled by user" },
657
+ isError: true,
658
+ };
659
+ });
660
+ return { ...assistant, content };
661
+ });
662
+ if (updated) this.notifyUpdate();
663
+ }
664
+
496
665
  addToolResult(options: AddToolResultOptions): void {
497
- let updated = false;
498
- this.messages = this.messages.map((message) => {
499
- if (message.id !== options.messageId || message.role !== "assistant")
500
- return message;
666
+ const updated = this.updateMessage(options.messageId, (message) => {
667
+ if (message.role !== "assistant") return message;
501
668
  const assistant = message as ThreadAssistantMessage;
502
669
  let matchedToolCall = false;
503
670
  const content = assistant.content.map((part) => {
@@ -512,7 +679,6 @@ export class AgUiThreadRuntimeCore {
512
679
  };
513
680
  });
514
681
  if (!matchedToolCall) return message;
515
- updated = true;
516
682
  return { ...assistant, content };
517
683
  });
518
684
 
@@ -537,7 +703,7 @@ export class AgUiThreadRuntimeCore {
537
703
  }
538
704
 
539
705
  private maybeCompleteAfterToolResults(messageId: string): boolean {
540
- const message = this.messages.find((m) => m.id === messageId);
706
+ const message = this.tryGetMessage(messageId)?.message;
541
707
  if (!message || message.role !== "assistant") return false;
542
708
  const assistant = message as ThreadAssistantMessage;
543
709
  if (
@@ -547,20 +713,19 @@ export class AgUiThreadRuntimeCore {
547
713
  return false;
548
714
  }
549
715
  const allResolved = assistant.content.every(
550
- (part) =>
551
- part.type !== "tool-call" ||
552
- ("result" in part && part.result !== undefined),
716
+ (part) => part.type !== "tool-call" || isResolvedToolCall(part),
553
717
  );
554
718
  if (!allResolved) return false;
555
719
 
556
- this.messages = this.messages.map((m) =>
557
- m.id === messageId && m.role === "assistant"
720
+ const updated = this.updateMessage(messageId, (current) =>
721
+ current.role === "assistant"
558
722
  ? {
559
- ...(m as ThreadAssistantMessage),
723
+ ...(current as ThreadAssistantMessage),
560
724
  status: { type: "complete" as const, reason: "unknown" as const },
561
725
  }
562
- : m,
726
+ : current,
563
727
  );
728
+ if (!updated) return false;
564
729
  this.notifyUpdate();
565
730
  this.persistAssistantHistory(messageId);
566
731
  return true;
@@ -574,9 +739,111 @@ export class AgUiThreadRuntimeCore {
574
739
 
575
740
  applyExternalMessages(messages: readonly ThreadMessage[]): void {
576
741
  this.assistantHistoryParents.clear();
577
- this.messages = [...messages];
742
+
743
+ if (messages.length === 0) {
744
+ this.clearRepository();
745
+ } else {
746
+ let expectedParentId: string | null = null;
747
+ let lastAppliedId: string | null = null;
748
+ let hardReplace = false;
749
+ const seen = new Set<string>();
750
+
751
+ for (const message of messages) {
752
+ if (seen.has(message.id)) continue;
753
+ seen.add(message.id);
754
+ const existing = this.tryGetMessage(message.id);
755
+ if (existing && existing.parentId !== expectedParentId) {
756
+ hardReplace = true;
757
+ break;
758
+ }
759
+ this.addOrUpdateMessage(expectedParentId, message);
760
+ expectedParentId = message.id;
761
+ lastAppliedId = message.id;
762
+ }
763
+
764
+ if (hardReplace) {
765
+ this.clearRepository();
766
+ expectedParentId = null;
767
+ lastAppliedId = null;
768
+ seen.clear();
769
+ for (const message of messages) {
770
+ if (seen.has(message.id)) continue;
771
+ seen.add(message.id);
772
+ this.addOrUpdateMessage(expectedParentId, message);
773
+ expectedParentId = message.id;
774
+ lastAppliedId = message.id;
775
+ }
776
+ }
777
+
778
+ this.resetRepositoryHead(lastAppliedId);
779
+ }
780
+
781
+ this.recordedHistoryIds.clear();
782
+ for (const { message } of this.getMessageRepository().messages) {
783
+ this.recordedHistoryIds.add(message.id);
784
+ }
785
+ this.notifyUpdate();
786
+ }
787
+
788
+ private applyExternalMessageRepository(
789
+ loaded: ExportedMessageRepository,
790
+ ): void {
791
+ const headId = loaded.headId ?? loaded.messages.at(-1)?.message.id ?? null;
792
+ const ids = new Set<string>();
793
+ let degenerate = false;
794
+ for (const { message } of loaded.messages) {
795
+ if (ids.has(message.id)) {
796
+ degenerate = true;
797
+ break;
798
+ }
799
+ ids.add(message.id);
800
+ }
801
+ if (headId !== null && !ids.has(headId)) degenerate = true;
802
+
803
+ if (!degenerate) {
804
+ this.clearRepository();
805
+ let pending = [...loaded.messages];
806
+ const importedIds = new Set<string>();
807
+
808
+ while (pending.length > 0) {
809
+ const unresolved: typeof pending = [];
810
+ let progressed = false;
811
+ for (const item of pending) {
812
+ if (item.parentId !== null && !importedIds.has(item.parentId)) {
813
+ unresolved.push(item);
814
+ continue;
815
+ }
816
+ this.addOrUpdateMessage(item.parentId, item.message);
817
+ importedIds.add(item.message.id);
818
+ progressed = true;
819
+ }
820
+ if (!progressed) {
821
+ degenerate = true;
822
+ break;
823
+ }
824
+ pending = unresolved;
825
+ }
826
+ }
827
+
828
+ if (degenerate) {
829
+ this.clearRepository();
830
+ let previousId: string | null = null;
831
+ for (const { message } of loaded.messages) {
832
+ const existing = this.tryGetMessage(message.id);
833
+ this.addOrUpdateMessage(
834
+ existing ? existing.parentId : previousId,
835
+ message,
836
+ );
837
+ previousId = message.id;
838
+ }
839
+ this.resetRepositoryHead(previousId);
840
+ } else {
841
+ this.resetRepositoryHead(headId);
842
+ }
843
+
844
+ this.assistantHistoryParents.clear();
578
845
  this.recordedHistoryIds.clear();
579
- for (const message of this.messages) {
846
+ for (const { message } of loaded.messages) {
580
847
  this.recordedHistoryIds.add(message.id);
581
848
  }
582
849
  this.notifyUpdate();
@@ -587,6 +854,21 @@ export class AgUiThreadRuntimeCore {
587
854
  this.notifyUpdate();
588
855
  }
589
856
 
857
+ setState(
858
+ next:
859
+ | ReadonlyJSONValue
860
+ | ((prev: ReadonlyJSONValue | undefined) => ReadonlyJSONValue),
861
+ ): void {
862
+ this.stateSnapshot =
863
+ typeof next === "function" ? next(this.stateSnapshot) : next;
864
+ this.notifyUpdate();
865
+ }
866
+
867
+ resetState(): void {
868
+ this.stateSnapshot = undefined;
869
+ this.notifyUpdate();
870
+ }
871
+
590
872
  private async startRun(
591
873
  parentId: string | null,
592
874
  runConfig?: RunConfig,
@@ -595,20 +877,32 @@ export class AgUiThreadRuntimeCore {
595
877
  ): Promise<void> {
596
878
  const normalizedRunConfig = runConfig ?? {};
597
879
  this.lastRunConfig = normalizedRunConfig;
598
- this.resetHead(parentId);
599
- const historicalMessages = [...this.messages];
880
+ const parent = parentId === null ? undefined : this.tryGetMessage(parentId);
881
+ const shouldEagerlyInsertAssistant =
882
+ parentId !== null &&
883
+ parent !== undefined &&
884
+ parentId !== this.repository.headId;
885
+ const historicalMessages = [
886
+ ...(shouldEagerlyInsertAssistant
887
+ ? (this.tryGetMessages(parentId) ?? this.repository.getMessages())
888
+ : this.repository.getMessages()),
889
+ ];
600
890
 
601
891
  this.pendingError = null;
602
- const assistantParentId = parentId ?? this.messages.at(-1)?.id ?? null;
892
+ const assistantParentId = parent ? parentId : this.repository.headId;
603
893
  let assistantMessageId: string | undefined;
604
894
  const ensureAssistant = () => {
605
895
  if (assistantMessageId) return assistantMessageId;
606
- const created = this.insertAssistantPlaceholder();
896
+ const created = this.insertAssistantPlaceholder(
897
+ assistantParentId ?? null,
898
+ );
607
899
  assistantMessageId = created;
608
900
  this.markPendingAssistantHistory(created, assistantParentId ?? null);
609
901
  return created;
610
902
  };
611
903
 
904
+ if (shouldEagerlyInsertAssistant) ensureAssistant();
905
+
612
906
  const applyUpdate = (update: ChatModelRunResult) => {
613
907
  const resolved = this.updateAssistantMessage(ensureAssistant(), update);
614
908
  if (resolved !== assistantMessageId) {
@@ -623,8 +917,9 @@ export class AgUiThreadRuntimeCore {
623
917
  onServerMessageId: (serverId) => {
624
918
  const placeholder = ensureAssistant();
625
919
  if (placeholder === serverId) return;
626
- this.reassignAssistantId(placeholder, serverId);
627
- assistantMessageId = serverId;
920
+ if (this.reassignAssistantId(placeholder, serverId)) {
921
+ assistantMessageId = serverId;
922
+ }
628
923
  },
629
924
  });
630
925
  const dispatch = (event: AgUiEvent) => this.handleEvent(aggregator, event);
@@ -744,7 +1039,7 @@ export class AgUiThreadRuntimeCore {
744
1039
  unstable_threadId: ctx.threadId,
745
1040
  unstable_parentId: ctx.parentId,
746
1041
  unstable_getMessage: () => {
747
- const message = this.messages.find((m) => m.id === currentId());
1042
+ const message = this.tryGetMessage(currentId())?.message;
748
1043
  if (!message) {
749
1044
  throw new Error(
750
1045
  "[agui] resume stream requested the assistant message before it existed",
@@ -771,7 +1066,7 @@ export class AgUiThreadRuntimeCore {
771
1066
  }
772
1067
 
773
1068
  if (ctx.abortSignal.aborted) return;
774
- const current = this.messages.find((m) => m.id === currentId());
1069
+ const current = this.tryGetMessage(currentId())?.message;
775
1070
  if (!current || current.status?.type === "running") {
776
1071
  ctx.applyUpdate({ status: { type: "complete", reason: "unknown" } });
777
1072
  }
@@ -784,7 +1079,9 @@ export class AgUiThreadRuntimeCore {
784
1079
  resume?: AgUiResumeEntry[],
785
1080
  ) {
786
1081
  const threadId = this.agent.threadId || "main";
787
- const messages = toAgUiMessages(historyMessages ?? this.messages);
1082
+ const messages = toAgUiMessages(
1083
+ historyMessages ?? this.repository.getMessages(),
1084
+ );
788
1085
  const context = this.runtime?.thread.getModelContext();
789
1086
  return {
790
1087
  threadId,
@@ -837,7 +1134,7 @@ export class AgUiThreadRuntimeCore {
837
1134
  this.setRunning(false);
838
1135
  }
839
1136
 
840
- private insertAssistantPlaceholder(): string {
1137
+ private insertAssistantPlaceholder(parentId: string | null): string {
841
1138
  const id = generateOptimisticId();
842
1139
  const assistant: ThreadAssistantMessage = {
843
1140
  id,
@@ -854,56 +1151,62 @@ export class AgUiThreadRuntimeCore {
854
1151
  custom: {},
855
1152
  },
856
1153
  };
857
- this.messages = [...this.messages, assistant];
1154
+ this.addOrUpdateMessage(parentId ?? this.repository.headId, assistant);
1155
+ this.switchToBranch(id);
858
1156
  this.notifyUpdate();
859
1157
  return id;
860
1158
  }
861
1159
 
862
- private reassignAssistantId(oldId: string, newId: string): void {
863
- if (oldId === newId) return;
1160
+ private reassignAssistantId(oldId: string, newId: string): boolean {
1161
+ if (oldId === newId) return true;
1162
+ const oldItem = this.tryGetMessage(oldId);
1163
+ if (!oldItem) return false;
864
1164
 
865
- const collidesWithExisting = this.messages.some((m) => m.id === newId);
1165
+ const collidesWithExisting = this.hasMessage(newId);
866
1166
 
867
1167
  if (collidesWithExisting) {
868
1168
  this.logger.debug?.(
869
- "[agui] reassignAssistantId: server id already present in messages, dropping placeholder",
1169
+ "[agui] reassignAssistantId: server id already present in messages or repository, dropping placeholder",
870
1170
  { oldId, newId },
871
1171
  );
872
- this.messages = this.messages.filter((m) => m.id !== oldId);
1172
+ this.deleteMessage(oldId, oldItem.parentId ?? null);
873
1173
  } else {
874
- this.messages = this.messages.map((m) => {
875
- if (m.id !== oldId) return m;
876
- const { isOptimistic: _, ...metadata } = m.metadata;
877
- return { ...m, id: newId, metadata } as ThreadMessage;
878
- });
1174
+ const { isOptimistic: _, ...metadata } = oldItem.message.metadata;
1175
+ this.addOrUpdateMessage(oldItem.parentId, {
1176
+ ...oldItem.message,
1177
+ id: newId,
1178
+ metadata,
1179
+ } as ThreadMessage);
1180
+ this.switchToBranch(newId);
1181
+ this.tryDeleteMessage(oldId);
879
1182
  }
880
1183
 
881
1184
  const pendingParent = this.assistantHistoryParents.get(oldId);
882
1185
  if (pendingParent !== undefined) {
883
1186
  this.assistantHistoryParents.delete(oldId);
884
- if (!this.assistantHistoryParents.has(newId)) {
1187
+ if (!collidesWithExisting && !this.assistantHistoryParents.has(newId)) {
885
1188
  this.assistantHistoryParents.set(newId, pendingParent);
886
1189
  }
887
1190
  }
888
1191
 
889
1192
  if (this.recordedHistoryIds.has(oldId)) {
890
1193
  this.recordedHistoryIds.delete(oldId);
891
- this.recordedHistoryIds.add(newId);
1194
+ if (!collidesWithExisting) {
1195
+ this.recordedHistoryIds.add(newId);
1196
+ }
892
1197
  }
893
1198
 
894
1199
  this.notifyUpdate();
1200
+ return !collidesWithExisting;
895
1201
  }
896
1202
 
897
1203
  private updateAssistantMessage(
898
1204
  messageId: string,
899
1205
  update: ChatModelRunResult,
900
1206
  ): string {
901
- let touched = false;
902
1207
  let latestStatus: MessageStatus | undefined;
903
- this.messages = this.messages.map((message) => {
904
- if (message.id !== messageId || message.role !== "assistant")
905
- return message;
906
- touched = true;
1208
+ const touched = this.updateMessage(messageId, (message) => {
1209
+ if (message.role !== "assistant") return message;
907
1210
  const assistant = message as ThreadAssistantMessage;
908
1211
  const metadata = update.metadata
909
1212
  ? this.mergeAssistantMetadata(assistant.metadata, update.metadata)
@@ -952,11 +1255,7 @@ export class AgUiThreadRuntimeCore {
952
1255
  ): ThreadAssistantMessage["content"] {
953
1256
  const resolved = new Map<string, ToolCallMessagePart>();
954
1257
  for (const part of previous) {
955
- if (
956
- part.type === "tool-call" &&
957
- "result" in part &&
958
- part.result !== undefined
959
- ) {
1258
+ if (part.type === "tool-call" && isResolvedToolCall(part)) {
960
1259
  resolved.set(part.toolCallId, part);
961
1260
  }
962
1261
  }
@@ -964,8 +1263,7 @@ export class AgUiThreadRuntimeCore {
964
1263
 
965
1264
  let changed = false;
966
1265
  const merged = next.map((part) => {
967
- if (part.type !== "tool-call") return part;
968
- if ("result" in part && part.result !== undefined) return part;
1266
+ if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
969
1267
  const prior = resolved.get(part.toolCallId);
970
1268
  if (!prior) return part;
971
1269
  changed = true;
@@ -1047,6 +1345,22 @@ export class AgUiThreadRuntimeCore {
1047
1345
  aggregator.handle(event);
1048
1346
  return;
1049
1347
  }
1348
+ case "ACTIVITY_SNAPSHOT": {
1349
+ const toolCallId = event.content["toolCallId"];
1350
+ if (
1351
+ event.activityType === MCP_APPS_ACTIVITY_TYPE &&
1352
+ typeof toolCallId === "string" &&
1353
+ !aggregator.hasToolCall(toolCallId)
1354
+ ) {
1355
+ const messageId = this.findMessageIdForToolCall(toolCallId);
1356
+ if (messageId !== undefined) {
1357
+ this.applyCrossRunActivitySnapshot(messageId, toolCallId, event);
1358
+ return;
1359
+ }
1360
+ }
1361
+ aggregator.handle(event);
1362
+ return;
1363
+ }
1050
1364
  default:
1051
1365
  aggregator.handle(event);
1052
1366
  }
@@ -1056,27 +1370,63 @@ export class AgUiThreadRuntimeCore {
1056
1370
  messageId: string,
1057
1371
  event: Extract<AgUiEvent, { type: "TOOL_CALL_RESULT" }>,
1058
1372
  ): void {
1059
- let updated = false;
1060
- this.messages = this.messages.map((message) => {
1061
- if (message.id !== messageId || message.role !== "assistant")
1062
- return message;
1373
+ const updated = this.updateMessage(messageId, (message) => {
1374
+ if (message.role !== "assistant") return message;
1063
1375
  const assistant = message as ThreadAssistantMessage;
1064
1376
  let matchedToolCall = false;
1065
1377
  const content = assistant.content.map((part) => {
1066
1378
  if (part.type !== "tool-call" || part.toolCallId !== event.toolCallId)
1067
1379
  return part;
1068
1380
  matchedToolCall = true;
1381
+ // An applied activity snapshot owns part.result; a later result only
1382
+ // fills what is missing, mirroring the aggregator's finishToolCall.
1383
+ // The aggregator's flag is set only when the snapshot carried a
1384
+ // result, so app presence alone is not enough: the current result
1385
+ // must be CallToolResult-shaped (a required content array).
1386
+ const snapshotResultApplied =
1387
+ part.mcp?.app !== undefined &&
1388
+ isPlainObject(part.result) &&
1389
+ Array.isArray((part.result as Record<string, unknown>)["content"]);
1390
+ if (snapshotResultApplied) {
1391
+ return {
1392
+ ...part,
1393
+ ...(part.modelContent === undefined && event.content
1394
+ ? {
1395
+ modelContent: [
1396
+ { type: "text" as const, text: event.content },
1397
+ ],
1398
+ }
1399
+ : {}),
1400
+ ...(part.isError === undefined
1401
+ ? typeof event.mcpResult?.isError === "boolean"
1402
+ ? { isError: event.mcpResult.isError }
1403
+ : event.role === "tool"
1404
+ ? { isError: false }
1405
+ : {}
1406
+ : {}),
1407
+ ...(event.messageId
1408
+ ? { unstable_toolMessageId: event.messageId }
1409
+ : {}),
1410
+ };
1411
+ }
1069
1412
  return {
1070
1413
  ...part,
1071
- result: tryParseJSON(event.content ?? "") as ReadonlyJSONValue,
1072
- ...(event.role === "tool" ? { isError: false } : {}),
1414
+ result: (event.mcpResult ??
1415
+ tryParseJSON(event.content ?? "")) as ReadonlyJSONValue,
1416
+ ...(event.mcpResult !== undefined
1417
+ ? { modelContent: [{ type: "text" as const, text: event.content }] }
1418
+ : {}),
1419
+ ...(typeof event.mcpResult?.isError === "boolean"
1420
+ ? { isError: event.mcpResult.isError }
1421
+ : event.role === "tool"
1422
+ ? { isError: false }
1423
+ : {}),
1073
1424
  ...(event.messageId
1074
1425
  ? { unstable_toolMessageId: event.messageId }
1075
1426
  : {}),
1076
1427
  };
1077
1428
  });
1078
1429
  if (!matchedToolCall) return message;
1079
- updated = true;
1080
1430
  return { ...assistant, content };
1081
1431
  });
1082
1432
 
@@ -1087,6 +1437,68 @@ export class AgUiThreadRuntimeCore {
1087
1437
  this.maybeCompleteAfterToolResults(messageId);
1088
1438
  }
1089
1439
 
1440
+ private applyCrossRunActivitySnapshot(
1441
+ messageId: string,
1442
+ toolCallId: string,
1443
+ event: Extract<AgUiEvent, { type: "ACTIVITY_SNAPSHOT" }>,
1444
+ ): void {
1445
+ const resourceUri = event.content["resourceUri"];
1446
+ if (typeof resourceUri !== "string" || !isMcpAppUri(resourceUri)) return;
1447
+ const serverId = event.content["serverId"];
1448
+ const serverHash = event.content["serverHash"];
1449
+ const appServerId =
1450
+ typeof serverId === "string" && serverId.length > 0
1451
+ ? serverId
1452
+ : typeof serverHash === "string" && serverHash.length > 0
1453
+ ? serverHash
1454
+ : undefined;
1455
+ const result = event.content["result"];
1456
+ const updated = this.updateMessage(messageId, (message) => {
1457
+ if (message.role !== "assistant") return message;
1458
+ const assistant = message as ThreadAssistantMessage;
1459
+ let matchedToolCall = false;
1460
+ const content = assistant.content.map((part) => {
1461
+ if (part.type !== "tool-call" || part.toolCallId !== toolCallId)
1462
+ return part;
1463
+ matchedToolCall = true;
1464
+ return {
1465
+ ...part,
1466
+ mcp: {
1467
+ app: {
1468
+ resourceUri,
1469
+ ...(appServerId ? { serverId: appServerId } : {}),
1470
+ },
1471
+ },
1472
+ ...(isPlainObject(result)
1473
+ ? {
1474
+ ...(part.result !== undefined && part.modelContent === undefined
1475
+ ? {
1476
+ modelContent: [
1477
+ {
1478
+ type: "text" as const,
1479
+ text:
1480
+ typeof part.result === "string"
1481
+ ? part.result
1482
+ : JSON.stringify(part.result),
1483
+ },
1484
+ ],
1485
+ }
1486
+ : {}),
1487
+ result: result as ReadonlyJSONValue,
1488
+ isError: result["isError"] === true,
1489
+ }
1490
+ : {}),
1491
+ };
1492
+ });
1493
+ if (!matchedToolCall) return message;
1494
+ return { ...assistant, content };
1495
+ });
1496
+
1497
+ if (!updated) return;
1498
+ this.notifyUpdate();
1499
+ this.maybeCompleteAfterToolResults(messageId);
1500
+ }
1501
+
1090
1502
  private importMessagesSnapshot(rawMessages: readonly unknown[]) {
1091
1503
  try {
1092
1504
  const normalized = fromAgUiMessages(rawMessages, {
@@ -1119,18 +1531,6 @@ export class AgUiThreadRuntimeCore {
1119
1531
  );
1120
1532
  }
1121
1533
 
1122
- private resetHead(parentId: string | null | undefined) {
1123
- if (!parentId) {
1124
- if (this.messages.length) {
1125
- this.messages = [];
1126
- }
1127
- return;
1128
- }
1129
- const idx = this.messages.findIndex((message) => message.id === parentId);
1130
- if (idx === -1) return;
1131
- this.messages = this.messages.slice(0, idx + 1);
1132
- }
1133
-
1134
1534
  private isTerminalStatus(status?: MessageStatus): boolean {
1135
1535
  return status?.type === "complete" || status?.type === "incomplete";
1136
1536
  }
@@ -1156,7 +1556,7 @@ export class AgUiThreadRuntimeCore {
1156
1556
  if (!this.history) return;
1157
1557
  const parentId = this.assistantHistoryParents.get(messageId);
1158
1558
  if (parentId === undefined) return;
1159
- const message = this.messages.find((m) => m.id === messageId);
1559
+ const message = this.tryGetMessage(messageId)?.message;
1160
1560
  if (!message || message.role !== "assistant") return;
1161
1561
  if (!this.isPersistableStatus(message.status)) return;
1162
1562
  this.assistantHistoryParents.delete(messageId);