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

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 (41) 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 +26 -4
  10. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  11. package/dist/runtime/AgUiThreadRuntimeCore.js +296 -89
  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 +2 -1
  16. package/dist/runtime/adapter/conversions.js.map +1 -1
  17. package/dist/runtime/adapter/run-aggregator.d.ts +0 -1
  18. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  19. package/dist/runtime/adapter/run-aggregator.js +36 -7
  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 +18 -9
  26. package/dist/runtime/event-parser.js.map +1 -1
  27. package/dist/runtime/logger.d.ts.map +1 -1
  28. package/dist/runtime/types.d.ts +13 -1
  29. package/dist/runtime/types.d.ts.map +1 -1
  30. package/dist/useAgUiRuntime.d.ts +0 -1
  31. package/dist/useAgUiRuntime.d.ts.map +1 -1
  32. package/dist/useAgUiRuntime.js +8 -2
  33. package/dist/useAgUiRuntime.js.map +1 -1
  34. package/package.json +6 -6
  35. package/src/hooks.ts +29 -7
  36. package/src/index.ts +2 -0
  37. package/src/runtime/AgUiThreadRuntimeCore.ts +390 -108
  38. package/src/runtime/adapter/conversions.ts +11 -4
  39. package/src/runtime/adapter/run-aggregator.ts +69 -5
  40. package/src/runtime/types.ts +17 -0
  41. package/src/useAgUiRuntime.ts +8 -1
@@ -2,12 +2,14 @@
2
2
  import { AG_UI_METADATA_NAMESPACE, RunAggregator, tryParseJSON } from "./adapter/run-aggregator.js";
3
3
  import { fromAgUiMessages, toAgUiMessages, toAgUiTools } from "./adapter/conversions.js";
4
4
  import { createAgUiSubscriber } from "./adapter/subscriber.js";
5
+ import { MessageRepository } from "@assistant-ui/core/internal";
5
6
  import { fromThreadMessageLike, generateId } from "@assistant-ui/core";
6
7
  import jsonpatch from "fast-json-patch";
7
8
  //#region src/runtime/AgUiThreadRuntimeCore.ts
8
9
  const optimisticPrefix = "__optimistic__";
9
10
  const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;
10
11
  const isOptimisticId = (id) => id.startsWith(optimisticPrefix);
12
+ const isResolvedToolCall = (part) => part.type === "tool-call" && "result" in part && part.result !== void 0;
11
13
  const symbolResumeShim = Symbol("agui-resume-shim");
12
14
  const FALLBACK_USER_STATUS = {
13
15
  type: "complete",
@@ -17,11 +19,13 @@ var AgUiThreadRuntimeCore = class {
17
19
  agent;
18
20
  logger;
19
21
  showThinking;
22
+ autoCancelPendingToolCalls;
20
23
  onError;
21
24
  onCancel;
22
25
  notifyUpdate;
23
26
  runtime;
24
- messages = [];
27
+ repository = new MessageRepository();
28
+ exportedRepository;
25
29
  isRunningFlag = false;
26
30
  abortController = null;
27
31
  stateSnapshot;
@@ -37,6 +41,7 @@ var AgUiThreadRuntimeCore = class {
37
41
  this.agent = options.agent;
38
42
  this.logger = options.logger;
39
43
  this.showThinking = options.showThinking;
44
+ this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
40
45
  this.onError = options.onError;
41
46
  this.onCancel = options.onCancel;
42
47
  this.history = options.history;
@@ -47,6 +52,7 @@ var AgUiThreadRuntimeCore = class {
47
52
  this.agent = options.agent;
48
53
  this.logger = options.logger;
49
54
  this.showThinking = options.showThinking;
55
+ this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
50
56
  this.onError = options.onError;
51
57
  this.onCancel = options.onCancel;
52
58
  this.history = options.history;
@@ -59,7 +65,74 @@ var AgUiThreadRuntimeCore = class {
59
65
  this.runtime = void 0;
60
66
  }
61
67
  getMessages() {
62
- return this.messages;
68
+ return this.repository.getMessages();
69
+ }
70
+ getMessageRepository() {
71
+ if (this.exportedRepository) return this.exportedRepository;
72
+ const exportedRepository = this.repository.export();
73
+ let parentId = null;
74
+ for (const message of this.repository.getMessages()) {
75
+ if (message.metadata.isOptimistic) exportedRepository.messages.push({
76
+ parentId,
77
+ message
78
+ });
79
+ parentId = message.id;
80
+ }
81
+ this.exportedRepository = {
82
+ ...exportedRepository,
83
+ headId: this.repository.headId
84
+ };
85
+ return this.exportedRepository;
86
+ }
87
+ tryGetMessage(messageId) {
88
+ try {
89
+ return this.repository.getMessage(messageId);
90
+ } catch {
91
+ return;
92
+ }
93
+ }
94
+ tryGetMessages(messageId) {
95
+ try {
96
+ return this.repository.getMessages(messageId);
97
+ } catch {
98
+ return;
99
+ }
100
+ }
101
+ hasMessage(messageId) {
102
+ return this.tryGetMessage(messageId) !== void 0;
103
+ }
104
+ addOrUpdateMessage(parentId, message) {
105
+ this.repository.addOrUpdateMessage(parentId, message);
106
+ this.exportedRepository = void 0;
107
+ }
108
+ deleteMessage(messageId, replacementId) {
109
+ this.repository.deleteMessage(messageId, replacementId);
110
+ this.exportedRepository = void 0;
111
+ }
112
+ tryDeleteMessage(messageId) {
113
+ if (!this.hasMessage(messageId)) return false;
114
+ this.deleteMessage(messageId);
115
+ return true;
116
+ }
117
+ switchToBranch(messageId) {
118
+ this.repository.switchToBranch(messageId);
119
+ this.exportedRepository = void 0;
120
+ }
121
+ resetRepositoryHead(messageId) {
122
+ this.repository.resetHead(messageId);
123
+ this.exportedRepository = void 0;
124
+ }
125
+ clearRepository() {
126
+ this.repository.clear();
127
+ this.exportedRepository = void 0;
128
+ }
129
+ updateMessage(messageId, updater) {
130
+ const item = this.tryGetMessage(messageId);
131
+ if (!item) return false;
132
+ const message = updater(item.message);
133
+ if (message === item.message) return false;
134
+ this.addOrUpdateMessage(item.parentId, message);
135
+ return true;
63
136
  }
64
137
  getState() {
65
138
  return this.stateSnapshot;
@@ -76,11 +149,10 @@ var AgUiThreadRuntimeCore = class {
76
149
  this._isLoading = true;
77
150
  this._loadPromise = promise.then(async (repo) => {
78
151
  if (!repo) return;
79
- const messages = repo.messages.map((item) => item.message);
80
- this.applyExternalMessages(messages);
152
+ this.applyExternalMessageRepository(repo);
81
153
  if (repo.state !== void 0) this.loadExternalState(repo.state);
82
154
  if (repo.unstable_resume) {
83
- const parentId = repo.headId ?? messages.at(-1)?.id ?? null;
155
+ const parentId = repo.headId ?? this.repository.headId;
84
156
  const resumeStream = this.history?.resume?.bind(this.history);
85
157
  await this.startRun(parentId, this.lastRunConfig, void 0, resumeStream);
86
158
  }
@@ -96,18 +168,29 @@ var AgUiThreadRuntimeCore = class {
96
168
  }
97
169
  async append(message) {
98
170
  const startRun = message.startRun ?? message.role === "user";
99
- if (startRun) this.assertNoPendingInterrupts();
171
+ if (startRun) {
172
+ this.assertNoPendingInterrupts();
173
+ this.maybeAutoCancelPendingToolCalls();
174
+ }
100
175
  const threadMessageId = this.appendEntry(message);
101
176
  if (!startRun) return;
102
177
  await this.startRun(threadMessageId, message.runConfig);
103
178
  }
179
+ maybeAutoCancelPendingToolCalls() {
180
+ if (this.autoCancelPendingToolCalls === false) return;
181
+ const pending = this.getPendingToolCalls();
182
+ if (!pending) return;
183
+ this.cancelUnresolvedToolCalls(pending.messageId);
184
+ this.maybeCompleteAfterToolResults(pending.messageId);
185
+ }
104
186
  appendEntry(message) {
105
- if (message.sourceId) this.messages = this.messages.filter((entry) => entry.id !== message.sourceId);
106
- this.resetHead(message.parentId);
187
+ if (message.sourceId) this.tryDeleteMessage(message.sourceId);
107
188
  const threadMessage = this.toThreadMessage(message);
108
- this.messages = [...this.messages, threadMessage];
189
+ const parentId = message.parentId === null ? null : message.parentId && this.hasMessage(message.parentId) ? message.parentId : this.repository.headId;
190
+ this.addOrUpdateMessage(parentId, threadMessage);
191
+ this.switchToBranch(threadMessage.id);
109
192
  this.notifyUpdate();
110
- this.recordHistoryEntry(message.parentId ?? null, threadMessage);
193
+ this.recordHistoryEntry(parentId, threadMessage);
111
194
  return threadMessage.id;
112
195
  }
113
196
  async edit(message) {
@@ -115,8 +198,7 @@ var AgUiThreadRuntimeCore = class {
115
198
  }
116
199
  async reload(parentId, config = {}) {
117
200
  this.assertNoPendingInterrupts();
118
- this.resetHead(parentId);
119
- this.notifyUpdate();
201
+ this.maybeAutoCancelPendingToolCalls();
120
202
  await this.startRun(parentId, config.runConfig);
121
203
  }
122
204
  async cancel() {
@@ -144,9 +226,14 @@ var AgUiThreadRuntimeCore = class {
144
226
  if (!this.getPendingInterrupts()) return;
145
227
  throw new Error("[agui] cannot start a new run while interrupts are pending; resolve them with submitInterruptResponses()");
146
228
  }
229
+ findRequiresActionAssistant(reason) {
230
+ const assistant = this.getMessages().findLast((message) => message.role === "assistant");
231
+ if (!assistant || assistant.status?.type !== "requires-action" || assistant.status.reason !== reason) return null;
232
+ return assistant;
233
+ }
147
234
  getPendingInterrupts() {
148
- const assistant = this.messages.findLast((m) => m.role === "assistant");
149
- if (!assistant || assistant.status?.type !== "requires-action" || assistant.status.reason !== "interrupt") return null;
235
+ const assistant = this.findRequiresActionAssistant("interrupt");
236
+ if (!assistant) return null;
150
237
  const stored = assistant.metadata.custom[AG_UI_METADATA_NAMESPACE]?.interrupts;
151
238
  if (!stored?.length) return null;
152
239
  return {
@@ -154,6 +241,21 @@ var AgUiThreadRuntimeCore = class {
154
241
  interrupts: stored
155
242
  };
156
243
  }
244
+ getPendingToolCalls() {
245
+ const assistant = this.findRequiresActionAssistant("tool-calls");
246
+ if (!assistant) return null;
247
+ const toolCallIds = [];
248
+ for (const part of assistant.content) {
249
+ if (part.type !== "tool-call") continue;
250
+ if (isResolvedToolCall(part)) continue;
251
+ toolCallIds.push(part.toolCallId);
252
+ }
253
+ if (toolCallIds.length === 0) return null;
254
+ return {
255
+ messageId: assistant.id,
256
+ toolCallIds
257
+ };
258
+ }
157
259
  async submitInterruptResponses(responses) {
158
260
  const pending = this.getPendingInterrupts();
159
261
  if (!pending) throw new Error("[agui] submitInterruptResponses: no pending interrupts on this thread");
@@ -185,6 +287,17 @@ var AgUiThreadRuntimeCore = class {
185
287
  async steerAway(message, responses) {
186
288
  const pending = this.getPendingInterrupts();
187
289
  if (!pending) {
290
+ const pendingTools = this.getPendingToolCalls();
291
+ if (pendingTools) {
292
+ if (responses?.length) throw new Error("[agui] steerAway: responses are only valid for pending interrupts");
293
+ if (this.isRunningFlag) throw new Error("[agui] steerAway: a run is already in progress");
294
+ this.cancelUnresolvedToolCalls(pendingTools.messageId);
295
+ this.maybeCompleteAfterToolResults(pendingTools.messageId);
296
+ const normalized = this.toAppendMessage(message);
297
+ const threadMessageId = this.appendEntry(normalized);
298
+ await this.startRun(threadMessageId, normalized.runConfig);
299
+ return;
300
+ }
188
301
  if (responses?.length) throw new Error("[agui] steerAway: no pending interrupts on this thread");
189
302
  await this.append(this.toAppendMessage(message));
190
303
  return;
@@ -215,7 +328,7 @@ var AgUiThreadRuntimeCore = class {
215
328
  toAppendMessage(message) {
216
329
  if (typeof message === "string") return {
217
330
  createdAt: /* @__PURE__ */ new Date(),
218
- parentId: this.messages.at(-1)?.id ?? null,
331
+ parentId: this.repository.headId,
219
332
  sourceId: null,
220
333
  runConfig: {},
221
334
  role: "user",
@@ -228,7 +341,7 @@ var AgUiThreadRuntimeCore = class {
228
341
  };
229
342
  return {
230
343
  createdAt: message.createdAt ?? /* @__PURE__ */ new Date(),
231
- parentId: message.parentId ?? this.messages.at(-1)?.id ?? null,
344
+ parentId: message.parentId ?? this.repository.headId,
232
345
  sourceId: message.sourceId ?? null,
233
346
  role: message.role ?? "user",
234
347
  content: message.content,
@@ -239,16 +352,14 @@ var AgUiThreadRuntimeCore = class {
239
352
  };
240
353
  }
241
354
  clearPendingInterrupts(messageId) {
242
- let touched = false;
243
- this.messages = this.messages.map((message) => {
244
- if (message.id !== messageId || message.role !== "assistant") return message;
355
+ if (this.updateMessage(messageId, (message) => {
356
+ if (message.role !== "assistant") return message;
245
357
  const assistant = message;
246
358
  if (assistant.status?.type !== "requires-action" || assistant.status.reason !== "interrupt") return assistant;
247
- touched = true;
248
359
  const { interrupts: _drop, ...restAgui } = assistant.metadata.custom["agui"] ?? {};
249
360
  const newCustom = { ...assistant.metadata.custom };
250
- if (Object.keys(restAgui).length > 0) newCustom[AG_UI_METADATA_NAMESPACE] = restAgui;
251
- else delete newCustom[AG_UI_METADATA_NAMESPACE];
361
+ if (Object.keys(restAgui).length > 0) newCustom["agui"] = restAgui;
362
+ else delete newCustom["agui"];
252
363
  return {
253
364
  ...assistant,
254
365
  status: {
@@ -260,26 +371,43 @@ var AgUiThreadRuntimeCore = class {
260
371
  custom: newCustom
261
372
  }
262
373
  };
263
- });
264
- if (touched) this.notifyUpdate();
374
+ })) this.notifyUpdate();
265
375
  }
266
376
  findMessageIdForToolCall(toolCallId) {
267
377
  let fallbackMessageId;
268
- for (let index = this.messages.length - 1; index >= 0; index--) {
269
- const message = this.messages[index];
378
+ const messages = this.getMessages();
379
+ for (let index = messages.length - 1; index >= 0; index--) {
380
+ const message = messages[index];
270
381
  if (!message || message.role !== "assistant") continue;
271
382
  for (const part of message.content) {
272
383
  if (part.type !== "tool-call" || part.toolCallId !== toolCallId) continue;
273
- if (!("result" in part) || part.result === void 0) return message.id;
384
+ if (!isResolvedToolCall(part)) return message.id;
274
385
  fallbackMessageId ??= message.id;
275
386
  }
276
387
  }
277
388
  return fallbackMessageId;
278
389
  }
390
+ cancelUnresolvedToolCalls(messageId) {
391
+ if (this.updateMessage(messageId, (message) => {
392
+ if (message.role !== "assistant") return message;
393
+ const assistant = message;
394
+ const content = assistant.content.map((part) => {
395
+ if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
396
+ return {
397
+ ...part,
398
+ result: { error: "Tool call cancelled by user" },
399
+ isError: true
400
+ };
401
+ });
402
+ return {
403
+ ...assistant,
404
+ content
405
+ };
406
+ })) this.notifyUpdate();
407
+ }
279
408
  addToolResult(options) {
280
- let updated = false;
281
- this.messages = this.messages.map((message) => {
282
- if (message.id !== options.messageId || message.role !== "assistant") return message;
409
+ if (!this.updateMessage(options.messageId, (message) => {
410
+ if (message.role !== "assistant") return message;
283
411
  const assistant = message;
284
412
  let matchedToolCall = false;
285
413
  const content = assistant.content.map((part) => {
@@ -293,13 +421,11 @@ var AgUiThreadRuntimeCore = class {
293
421
  };
294
422
  });
295
423
  if (!matchedToolCall) return message;
296
- updated = true;
297
424
  return {
298
425
  ...assistant,
299
426
  content
300
427
  };
301
- });
302
- if (!updated) return;
428
+ })) return;
303
429
  this.notifyUpdate();
304
430
  this.maybeResumeAfterToolResults(options.messageId);
305
431
  }
@@ -312,18 +438,18 @@ var AgUiThreadRuntimeCore = class {
312
438
  this.startResumeRun(messageId);
313
439
  }
314
440
  maybeCompleteAfterToolResults(messageId) {
315
- const message = this.messages.find((m) => m.id === messageId);
441
+ const message = this.tryGetMessage(messageId)?.message;
316
442
  if (!message || message.role !== "assistant") return false;
317
443
  const assistant = message;
318
444
  if (assistant.status?.type !== "requires-action" || assistant.status.reason !== "tool-calls") return false;
319
- if (!assistant.content.every((part) => part.type !== "tool-call" || "result" in part && part.result !== void 0)) return false;
320
- this.messages = this.messages.map((m) => m.id === messageId && m.role === "assistant" ? {
321
- ...m,
445
+ if (!assistant.content.every((part) => part.type !== "tool-call" || isResolvedToolCall(part))) return false;
446
+ if (!this.updateMessage(messageId, (current) => current.role === "assistant" ? {
447
+ ...current,
322
448
  status: {
323
449
  type: "complete",
324
450
  reason: "unknown"
325
451
  }
326
- } : m);
452
+ } : current)) return false;
327
453
  this.notifyUpdate();
328
454
  this.persistAssistantHistory(messageId);
329
455
  return true;
@@ -335,30 +461,122 @@ var AgUiThreadRuntimeCore = class {
335
461
  }
336
462
  applyExternalMessages(messages) {
337
463
  this.assistantHistoryParents.clear();
338
- this.messages = [...messages];
464
+ if (messages.length === 0) this.clearRepository();
465
+ else {
466
+ let expectedParentId = null;
467
+ let lastAppliedId = null;
468
+ let hardReplace = false;
469
+ const seen = /* @__PURE__ */ new Set();
470
+ for (const message of messages) {
471
+ if (seen.has(message.id)) continue;
472
+ seen.add(message.id);
473
+ const existing = this.tryGetMessage(message.id);
474
+ if (existing && existing.parentId !== expectedParentId) {
475
+ hardReplace = true;
476
+ break;
477
+ }
478
+ this.addOrUpdateMessage(expectedParentId, message);
479
+ expectedParentId = message.id;
480
+ lastAppliedId = message.id;
481
+ }
482
+ if (hardReplace) {
483
+ this.clearRepository();
484
+ expectedParentId = null;
485
+ lastAppliedId = null;
486
+ seen.clear();
487
+ for (const message of messages) {
488
+ if (seen.has(message.id)) continue;
489
+ seen.add(message.id);
490
+ this.addOrUpdateMessage(expectedParentId, message);
491
+ expectedParentId = message.id;
492
+ lastAppliedId = message.id;
493
+ }
494
+ }
495
+ this.resetRepositoryHead(lastAppliedId);
496
+ }
497
+ this.recordedHistoryIds.clear();
498
+ for (const { message } of this.getMessageRepository().messages) this.recordedHistoryIds.add(message.id);
499
+ this.notifyUpdate();
500
+ }
501
+ applyExternalMessageRepository(loaded) {
502
+ const headId = loaded.headId ?? loaded.messages.at(-1)?.message.id ?? null;
503
+ const ids = /* @__PURE__ */ new Set();
504
+ let degenerate = false;
505
+ for (const { message } of loaded.messages) {
506
+ if (ids.has(message.id)) {
507
+ degenerate = true;
508
+ break;
509
+ }
510
+ ids.add(message.id);
511
+ }
512
+ if (headId !== null && !ids.has(headId)) degenerate = true;
513
+ if (!degenerate) {
514
+ this.clearRepository();
515
+ let pending = [...loaded.messages];
516
+ const importedIds = /* @__PURE__ */ new Set();
517
+ while (pending.length > 0) {
518
+ const unresolved = [];
519
+ let progressed = false;
520
+ for (const item of pending) {
521
+ if (item.parentId !== null && !importedIds.has(item.parentId)) {
522
+ unresolved.push(item);
523
+ continue;
524
+ }
525
+ this.addOrUpdateMessage(item.parentId, item.message);
526
+ importedIds.add(item.message.id);
527
+ progressed = true;
528
+ }
529
+ if (!progressed) {
530
+ degenerate = true;
531
+ break;
532
+ }
533
+ pending = unresolved;
534
+ }
535
+ }
536
+ if (degenerate) {
537
+ this.clearRepository();
538
+ let previousId = null;
539
+ for (const { message } of loaded.messages) {
540
+ const existing = this.tryGetMessage(message.id);
541
+ this.addOrUpdateMessage(existing ? existing.parentId : previousId, message);
542
+ previousId = message.id;
543
+ }
544
+ this.resetRepositoryHead(previousId);
545
+ } else this.resetRepositoryHead(headId);
546
+ this.assistantHistoryParents.clear();
339
547
  this.recordedHistoryIds.clear();
340
- for (const message of this.messages) this.recordedHistoryIds.add(message.id);
548
+ for (const { message } of loaded.messages) this.recordedHistoryIds.add(message.id);
341
549
  this.notifyUpdate();
342
550
  }
343
551
  loadExternalState(state) {
344
552
  this.stateSnapshot = state;
345
553
  this.notifyUpdate();
346
554
  }
555
+ setState(next) {
556
+ this.stateSnapshot = typeof next === "function" ? next(this.stateSnapshot) : next;
557
+ this.notifyUpdate();
558
+ }
559
+ resetState() {
560
+ this.stateSnapshot = void 0;
561
+ this.notifyUpdate();
562
+ }
347
563
  async startRun(parentId, runConfig, resume, resumeStream) {
348
564
  const normalizedRunConfig = runConfig ?? {};
349
565
  this.lastRunConfig = normalizedRunConfig;
350
- this.resetHead(parentId);
351
- const historicalMessages = [...this.messages];
566
+ const parent = parentId === null ? void 0 : this.tryGetMessage(parentId);
567
+ const shouldEagerlyInsertAssistant = parentId !== null && parent !== void 0 && parentId !== this.repository.headId;
568
+ const historicalMessages = [...shouldEagerlyInsertAssistant ? this.tryGetMessages(parentId) ?? this.repository.getMessages() : this.repository.getMessages()];
352
569
  this.pendingError = null;
353
- const assistantParentId = parentId ?? this.messages.at(-1)?.id ?? null;
570
+ const assistantParentId = parent ? parentId : this.repository.headId;
354
571
  let assistantMessageId;
355
572
  const ensureAssistant = () => {
356
573
  if (assistantMessageId) return assistantMessageId;
357
- const created = this.insertAssistantPlaceholder();
574
+ const created = this.insertAssistantPlaceholder(assistantParentId ?? null);
358
575
  assistantMessageId = created;
359
576
  this.markPendingAssistantHistory(created, assistantParentId ?? null);
360
577
  return created;
361
578
  };
579
+ if (shouldEagerlyInsertAssistant) ensureAssistant();
362
580
  const applyUpdate = (update) => {
363
581
  const resolved = this.updateAssistantMessage(ensureAssistant(), update);
364
582
  if (resolved !== assistantMessageId) assistantMessageId = resolved;
@@ -370,8 +588,7 @@ var AgUiThreadRuntimeCore = class {
370
588
  onServerMessageId: (serverId) => {
371
589
  const placeholder = ensureAssistant();
372
590
  if (placeholder === serverId) return;
373
- this.reassignAssistantId(placeholder, serverId);
374
- assistantMessageId = serverId;
591
+ if (this.reassignAssistantId(placeholder, serverId)) assistantMessageId = serverId;
375
592
  }
376
593
  });
377
594
  const dispatch = (event) => this.handleEvent(aggregator, event);
@@ -461,7 +678,7 @@ var AgUiThreadRuntimeCore = class {
461
678
  unstable_threadId: ctx.threadId,
462
679
  unstable_parentId: ctx.parentId,
463
680
  unstable_getMessage: () => {
464
- const message = this.messages.find((m) => m.id === currentId());
681
+ const message = this.tryGetMessage(currentId())?.message;
465
682
  if (!message) throw new Error("[agui] resume stream requested the assistant message before it existed");
466
683
  return message;
467
684
  }
@@ -484,7 +701,7 @@ var AgUiThreadRuntimeCore = class {
484
701
  return;
485
702
  }
486
703
  if (ctx.abortSignal.aborted) return;
487
- const current = this.messages.find((m) => m.id === currentId());
704
+ const current = this.tryGetMessage(currentId())?.message;
488
705
  if (!current || current.status?.type === "running") ctx.applyUpdate({ status: {
489
706
  type: "complete",
490
707
  reason: "unknown"
@@ -492,7 +709,7 @@ var AgUiThreadRuntimeCore = class {
492
709
  }
493
710
  buildRunInput(runId, runConfig, historyMessages, resume) {
494
711
  const threadId = this.agent.threadId || "main";
495
- const messages = toAgUiMessages(historyMessages ?? this.messages);
712
+ const messages = toAgUiMessages(historyMessages ?? this.repository.getMessages());
496
713
  const context = this.runtime?.thread.getModelContext();
497
714
  return {
498
715
  threadId,
@@ -535,7 +752,7 @@ var AgUiThreadRuntimeCore = class {
535
752
  if (this.abortController === controller) this.abortController = null;
536
753
  this.setRunning(false);
537
754
  }
538
- insertAssistantPlaceholder() {
755
+ insertAssistantPlaceholder(parentId) {
539
756
  const id = generateOptimisticId();
540
757
  const assistant = {
541
758
  id,
@@ -552,44 +769,48 @@ var AgUiThreadRuntimeCore = class {
552
769
  custom: {}
553
770
  }
554
771
  };
555
- this.messages = [...this.messages, assistant];
772
+ this.addOrUpdateMessage(parentId ?? this.repository.headId, assistant);
773
+ this.switchToBranch(id);
556
774
  this.notifyUpdate();
557
775
  return id;
558
776
  }
559
777
  reassignAssistantId(oldId, newId) {
560
- if (oldId === newId) return;
561
- if (this.messages.some((m) => m.id === newId)) {
562
- this.logger.debug?.("[agui] reassignAssistantId: server id already present in messages, dropping placeholder", {
778
+ if (oldId === newId) return true;
779
+ const oldItem = this.tryGetMessage(oldId);
780
+ if (!oldItem) return false;
781
+ const collidesWithExisting = this.hasMessage(newId);
782
+ if (collidesWithExisting) {
783
+ this.logger.debug?.("[agui] reassignAssistantId: server id already present in messages or repository, dropping placeholder", {
563
784
  oldId,
564
785
  newId
565
786
  });
566
- this.messages = this.messages.filter((m) => m.id !== oldId);
567
- } else this.messages = this.messages.map((m) => {
568
- if (m.id !== oldId) return m;
569
- const { isOptimistic: _, ...metadata } = m.metadata;
570
- return {
571
- ...m,
787
+ this.deleteMessage(oldId, oldItem.parentId ?? null);
788
+ } else {
789
+ const { isOptimistic: _, ...metadata } = oldItem.message.metadata;
790
+ this.addOrUpdateMessage(oldItem.parentId, {
791
+ ...oldItem.message,
572
792
  id: newId,
573
793
  metadata
574
- };
575
- });
794
+ });
795
+ this.switchToBranch(newId);
796
+ this.tryDeleteMessage(oldId);
797
+ }
576
798
  const pendingParent = this.assistantHistoryParents.get(oldId);
577
799
  if (pendingParent !== void 0) {
578
800
  this.assistantHistoryParents.delete(oldId);
579
- if (!this.assistantHistoryParents.has(newId)) this.assistantHistoryParents.set(newId, pendingParent);
801
+ if (!collidesWithExisting && !this.assistantHistoryParents.has(newId)) this.assistantHistoryParents.set(newId, pendingParent);
580
802
  }
581
803
  if (this.recordedHistoryIds.has(oldId)) {
582
804
  this.recordedHistoryIds.delete(oldId);
583
- this.recordedHistoryIds.add(newId);
805
+ if (!collidesWithExisting) this.recordedHistoryIds.add(newId);
584
806
  }
585
807
  this.notifyUpdate();
808
+ return !collidesWithExisting;
586
809
  }
587
810
  updateAssistantMessage(messageId, update) {
588
- let touched = false;
589
811
  let latestStatus;
590
- this.messages = this.messages.map((message) => {
591
- if (message.id !== messageId || message.role !== "assistant") return message;
592
- touched = true;
812
+ if (!this.updateMessage(messageId, (message) => {
813
+ if (message.role !== "assistant") return message;
593
814
  const assistant = message;
594
815
  const metadata = update.metadata ? this.mergeAssistantMetadata(assistant.metadata, update.metadata) : assistant.metadata;
595
816
  latestStatus = update.status ?? assistant.status;
@@ -600,8 +821,7 @@ var AgUiThreadRuntimeCore = class {
600
821
  status: latestStatus,
601
822
  metadata
602
823
  };
603
- });
604
- if (!touched) return messageId;
824
+ })) return messageId;
605
825
  let resolvedMessageId = messageId;
606
826
  if (latestStatus !== void 0 && latestStatus.type !== "running" && isOptimisticId(messageId)) {
607
827
  const stableId = generateId();
@@ -614,12 +834,11 @@ var AgUiThreadRuntimeCore = class {
614
834
  }
615
835
  preserveToolResults(previous, next) {
616
836
  const resolved = /* @__PURE__ */ new Map();
617
- for (const part of previous) if (part.type === "tool-call" && "result" in part && part.result !== void 0) resolved.set(part.toolCallId, part);
837
+ for (const part of previous) if (part.type === "tool-call" && isResolvedToolCall(part)) resolved.set(part.toolCallId, part);
618
838
  if (resolved.size === 0) return next;
619
839
  let changed = false;
620
840
  const merged = next.map((part) => {
621
- if (part.type !== "tool-call") return part;
622
- if ("result" in part && part.result !== void 0) return part;
841
+ if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
623
842
  const prior = resolved.get(part.toolCallId);
624
843
  if (!prior) return part;
625
844
  changed = true;
@@ -683,9 +902,8 @@ var AgUiThreadRuntimeCore = class {
683
902
  }
684
903
  }
685
904
  applyCrossRunToolResult(messageId, event) {
686
- let updated = false;
687
- this.messages = this.messages.map((message) => {
688
- if (message.id !== messageId || message.role !== "assistant") return message;
905
+ if (!this.updateMessage(messageId, (message) => {
906
+ if (message.role !== "assistant") return message;
689
907
  const assistant = message;
690
908
  let matchedToolCall = false;
691
909
  const content = assistant.content.map((part) => {
@@ -699,13 +917,11 @@ var AgUiThreadRuntimeCore = class {
699
917
  };
700
918
  });
701
919
  if (!matchedToolCall) return message;
702
- updated = true;
703
920
  return {
704
921
  ...assistant,
705
922
  content
706
923
  };
707
- });
708
- if (!updated) return;
924
+ })) return;
709
925
  this.notifyUpdate();
710
926
  this.maybeCompleteAfterToolResults(messageId);
711
927
  }
@@ -726,15 +942,6 @@ var AgUiThreadRuntimeCore = class {
726
942
  toThreadMessage(message) {
727
943
  return fromThreadMessageLike(message, generateId(), FALLBACK_USER_STATUS);
728
944
  }
729
- resetHead(parentId) {
730
- if (!parentId) {
731
- if (this.messages.length) this.messages = [];
732
- return;
733
- }
734
- const idx = this.messages.findIndex((message) => message.id === parentId);
735
- if (idx === -1) return;
736
- this.messages = this.messages.slice(0, idx + 1);
737
- }
738
945
  isTerminalStatus(status) {
739
946
  return status?.type === "complete" || status?.type === "incomplete";
740
947
  }
@@ -753,7 +960,7 @@ var AgUiThreadRuntimeCore = class {
753
960
  if (!this.history) return;
754
961
  const parentId = this.assistantHistoryParents.get(messageId);
755
962
  if (parentId === void 0) return;
756
- const message = this.messages.find((m) => m.id === messageId);
963
+ const message = this.tryGetMessage(messageId)?.message;
757
964
  if (!message || message.role !== "assistant") return;
758
965
  if (!this.isPersistableStatus(message.status)) return;
759
966
  this.assistantHistoryParents.delete(messageId);