@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,13 +1,15 @@
1
1
  "use client";
2
- import { AG_UI_METADATA_NAMESPACE, RunAggregator, tryParseJSON } from "./adapter/run-aggregator.js";
2
+ import { AG_UI_METADATA_NAMESPACE, RunAggregator, isPlainObject, 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 { fromThreadMessageLike, generateId } from "@assistant-ui/core";
5
+ import { MessageRepository } from "@assistant-ui/core/internal";
6
+ import { fromThreadMessageLike, generateId, isMcpAppUri } 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,22 +241,28 @@ 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");
160
- const responsesById = /* @__PURE__ */ new Map();
161
- for (const entry of responses) {
162
- if (!entry || typeof entry.interruptId !== "string") throw new Error("[agui] submitInterruptResponses: every entry must have an interruptId");
163
- if (entry.status !== "resolved" && entry.status !== "cancelled") throw new Error(`[agui] submitInterruptResponses: invalid status "${entry.status}" for interrupt ${entry.interruptId}`);
164
- if (responsesById.has(entry.interruptId)) throw new Error(`[agui] submitInterruptResponses: duplicate response for interrupt ${entry.interruptId}`);
165
- responsesById.set(entry.interruptId, entry);
166
- }
262
+ const responsesById = this.collectInterruptResponses("submitInterruptResponses", pending.interrupts, responses);
167
263
  const openIds = pending.interrupts.map((i) => i.id);
168
264
  const missing = openIds.filter((id) => !responsesById.has(id));
169
265
  if (missing.length > 0) throw new Error(`[agui] submitInterruptResponses: missing responses for open interrupts: ${missing.join(", ")}`);
170
- const known = new Set(openIds);
171
- const unknownIds = [...responsesById.keys()].filter((id) => !known.has(id));
172
- if (unknownIds.length > 0) throw new Error(`[agui] submitInterruptResponses: unknown interrupt ids: ${unknownIds.join(", ")}`);
173
266
  const now = Date.now();
174
267
  for (const interrupt of pending.interrupts) {
175
268
  if (!interrupt.expiresAt) continue;
@@ -185,6 +278,17 @@ var AgUiThreadRuntimeCore = class {
185
278
  async steerAway(message, responses) {
186
279
  const pending = this.getPendingInterrupts();
187
280
  if (!pending) {
281
+ const pendingTools = this.getPendingToolCalls();
282
+ if (pendingTools) {
283
+ if (responses?.length) throw new Error("[agui] steerAway: responses are only valid for pending interrupts");
284
+ if (this.isRunningFlag) throw new Error("[agui] steerAway: a run is already in progress");
285
+ this.cancelUnresolvedToolCalls(pendingTools.messageId);
286
+ this.maybeCompleteAfterToolResults(pendingTools.messageId);
287
+ const normalized = this.toAppendMessage(message);
288
+ const threadMessageId = this.appendEntry(normalized);
289
+ await this.startRun(threadMessageId, normalized.runConfig);
290
+ return;
291
+ }
188
292
  if (responses?.length) throw new Error("[agui] steerAway: no pending interrupts on this thread");
189
293
  await this.append(this.toAppendMessage(message));
190
294
  return;
@@ -197,25 +301,28 @@ var AgUiThreadRuntimeCore = class {
197
301
  await this.startRun(threadMessageId, normalized.runConfig, resume);
198
302
  }
199
303
  resolveSteerAwayResume(interrupts, responses) {
200
- const openIds = interrupts.map((interrupt) => interrupt.id);
201
- const known = new Set(openIds);
304
+ const responsesById = this.collectInterruptResponses("steerAway", interrupts, responses ?? []);
305
+ return interrupts.map((interrupt) => responsesById.get(interrupt.id) ?? {
306
+ interruptId: interrupt.id,
307
+ status: "cancelled"
308
+ });
309
+ }
310
+ collectInterruptResponses(method, interrupts, responses) {
311
+ const known = new Set(interrupts.map((interrupt) => interrupt.id));
202
312
  const responsesById = /* @__PURE__ */ new Map();
203
- for (const entry of responses ?? []) {
204
- if (!entry || typeof entry.interruptId !== "string") throw new Error("[agui] steerAway: every response must have an interruptId");
205
- if (entry.status !== "resolved" && entry.status !== "cancelled") throw new Error(`[agui] steerAway: invalid status "${entry.status}" for interrupt ${entry.interruptId}`);
206
- if (!known.has(entry.interruptId)) throw new Error(`[agui] steerAway: unknown interrupt id ${entry.interruptId}`);
207
- if (responsesById.has(entry.interruptId)) throw new Error(`[agui] steerAway: duplicate response for interrupt ${entry.interruptId}`);
313
+ for (const entry of responses) {
314
+ if (!entry || typeof entry.interruptId !== "string") throw new Error(`[agui] ${method}: every entry must have an interruptId`);
315
+ if (entry.status !== "resolved" && entry.status !== "cancelled") throw new Error(`[agui] ${method}: invalid status "${entry.status}" for interrupt ${entry.interruptId}`);
316
+ if (!known.has(entry.interruptId)) throw new Error(`[agui] ${method}: unknown interrupt id ${entry.interruptId}`);
317
+ if (responsesById.has(entry.interruptId)) throw new Error(`[agui] ${method}: duplicate response for interrupt ${entry.interruptId}`);
208
318
  responsesById.set(entry.interruptId, entry);
209
319
  }
210
- return openIds.map((id) => responsesById.get(id) ?? {
211
- interruptId: id,
212
- status: "cancelled"
213
- });
320
+ return responsesById;
214
321
  }
215
322
  toAppendMessage(message) {
216
323
  if (typeof message === "string") return {
217
324
  createdAt: /* @__PURE__ */ new Date(),
218
- parentId: this.messages.at(-1)?.id ?? null,
325
+ parentId: this.repository.headId,
219
326
  sourceId: null,
220
327
  runConfig: {},
221
328
  role: "user",
@@ -228,7 +335,7 @@ var AgUiThreadRuntimeCore = class {
228
335
  };
229
336
  return {
230
337
  createdAt: message.createdAt ?? /* @__PURE__ */ new Date(),
231
- parentId: message.parentId ?? this.messages.at(-1)?.id ?? null,
338
+ parentId: message.parentId ?? this.repository.headId,
232
339
  sourceId: message.sourceId ?? null,
233
340
  role: message.role ?? "user",
234
341
  content: message.content,
@@ -239,16 +346,14 @@ var AgUiThreadRuntimeCore = class {
239
346
  };
240
347
  }
241
348
  clearPendingInterrupts(messageId) {
242
- let touched = false;
243
- this.messages = this.messages.map((message) => {
244
- if (message.id !== messageId || message.role !== "assistant") return message;
349
+ if (this.updateMessage(messageId, (message) => {
350
+ if (message.role !== "assistant") return message;
245
351
  const assistant = message;
246
352
  if (assistant.status?.type !== "requires-action" || assistant.status.reason !== "interrupt") return assistant;
247
- touched = true;
248
353
  const { interrupts: _drop, ...restAgui } = assistant.metadata.custom["agui"] ?? {};
249
354
  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];
355
+ if (Object.keys(restAgui).length > 0) newCustom["agui"] = restAgui;
356
+ else delete newCustom["agui"];
252
357
  return {
253
358
  ...assistant,
254
359
  status: {
@@ -260,26 +365,43 @@ var AgUiThreadRuntimeCore = class {
260
365
  custom: newCustom
261
366
  }
262
367
  };
263
- });
264
- if (touched) this.notifyUpdate();
368
+ })) this.notifyUpdate();
265
369
  }
266
370
  findMessageIdForToolCall(toolCallId) {
267
371
  let fallbackMessageId;
268
- for (let index = this.messages.length - 1; index >= 0; index--) {
269
- const message = this.messages[index];
372
+ const messages = this.getMessages();
373
+ for (let index = messages.length - 1; index >= 0; index--) {
374
+ const message = messages[index];
270
375
  if (!message || message.role !== "assistant") continue;
271
376
  for (const part of message.content) {
272
377
  if (part.type !== "tool-call" || part.toolCallId !== toolCallId) continue;
273
- if (!("result" in part) || part.result === void 0) return message.id;
378
+ if (!isResolvedToolCall(part)) return message.id;
274
379
  fallbackMessageId ??= message.id;
275
380
  }
276
381
  }
277
382
  return fallbackMessageId;
278
383
  }
384
+ cancelUnresolvedToolCalls(messageId) {
385
+ if (this.updateMessage(messageId, (message) => {
386
+ if (message.role !== "assistant") return message;
387
+ const assistant = message;
388
+ const content = assistant.content.map((part) => {
389
+ if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
390
+ return {
391
+ ...part,
392
+ result: { error: "Tool call cancelled by user" },
393
+ isError: true
394
+ };
395
+ });
396
+ return {
397
+ ...assistant,
398
+ content
399
+ };
400
+ })) this.notifyUpdate();
401
+ }
279
402
  addToolResult(options) {
280
- let updated = false;
281
- this.messages = this.messages.map((message) => {
282
- if (message.id !== options.messageId || message.role !== "assistant") return message;
403
+ if (!this.updateMessage(options.messageId, (message) => {
404
+ if (message.role !== "assistant") return message;
283
405
  const assistant = message;
284
406
  let matchedToolCall = false;
285
407
  const content = assistant.content.map((part) => {
@@ -293,13 +415,11 @@ var AgUiThreadRuntimeCore = class {
293
415
  };
294
416
  });
295
417
  if (!matchedToolCall) return message;
296
- updated = true;
297
418
  return {
298
419
  ...assistant,
299
420
  content
300
421
  };
301
- });
302
- if (!updated) return;
422
+ })) return;
303
423
  this.notifyUpdate();
304
424
  this.maybeResumeAfterToolResults(options.messageId);
305
425
  }
@@ -312,18 +432,18 @@ var AgUiThreadRuntimeCore = class {
312
432
  this.startResumeRun(messageId);
313
433
  }
314
434
  maybeCompleteAfterToolResults(messageId) {
315
- const message = this.messages.find((m) => m.id === messageId);
435
+ const message = this.tryGetMessage(messageId)?.message;
316
436
  if (!message || message.role !== "assistant") return false;
317
437
  const assistant = message;
318
438
  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,
439
+ if (!assistant.content.every((part) => part.type !== "tool-call" || isResolvedToolCall(part))) return false;
440
+ if (!this.updateMessage(messageId, (current) => current.role === "assistant" ? {
441
+ ...current,
322
442
  status: {
323
443
  type: "complete",
324
444
  reason: "unknown"
325
445
  }
326
- } : m);
446
+ } : current)) return false;
327
447
  this.notifyUpdate();
328
448
  this.persistAssistantHistory(messageId);
329
449
  return true;
@@ -335,30 +455,122 @@ var AgUiThreadRuntimeCore = class {
335
455
  }
336
456
  applyExternalMessages(messages) {
337
457
  this.assistantHistoryParents.clear();
338
- this.messages = [...messages];
458
+ if (messages.length === 0) this.clearRepository();
459
+ else {
460
+ let expectedParentId = null;
461
+ let lastAppliedId = null;
462
+ let hardReplace = false;
463
+ const seen = /* @__PURE__ */ new Set();
464
+ for (const message of messages) {
465
+ if (seen.has(message.id)) continue;
466
+ seen.add(message.id);
467
+ const existing = this.tryGetMessage(message.id);
468
+ if (existing && existing.parentId !== expectedParentId) {
469
+ hardReplace = true;
470
+ break;
471
+ }
472
+ this.addOrUpdateMessage(expectedParentId, message);
473
+ expectedParentId = message.id;
474
+ lastAppliedId = message.id;
475
+ }
476
+ if (hardReplace) {
477
+ this.clearRepository();
478
+ expectedParentId = null;
479
+ lastAppliedId = null;
480
+ seen.clear();
481
+ for (const message of messages) {
482
+ if (seen.has(message.id)) continue;
483
+ seen.add(message.id);
484
+ this.addOrUpdateMessage(expectedParentId, message);
485
+ expectedParentId = message.id;
486
+ lastAppliedId = message.id;
487
+ }
488
+ }
489
+ this.resetRepositoryHead(lastAppliedId);
490
+ }
491
+ this.recordedHistoryIds.clear();
492
+ for (const { message } of this.getMessageRepository().messages) this.recordedHistoryIds.add(message.id);
493
+ this.notifyUpdate();
494
+ }
495
+ applyExternalMessageRepository(loaded) {
496
+ const headId = loaded.headId ?? loaded.messages.at(-1)?.message.id ?? null;
497
+ const ids = /* @__PURE__ */ new Set();
498
+ let degenerate = false;
499
+ for (const { message } of loaded.messages) {
500
+ if (ids.has(message.id)) {
501
+ degenerate = true;
502
+ break;
503
+ }
504
+ ids.add(message.id);
505
+ }
506
+ if (headId !== null && !ids.has(headId)) degenerate = true;
507
+ if (!degenerate) {
508
+ this.clearRepository();
509
+ let pending = [...loaded.messages];
510
+ const importedIds = /* @__PURE__ */ new Set();
511
+ while (pending.length > 0) {
512
+ const unresolved = [];
513
+ let progressed = false;
514
+ for (const item of pending) {
515
+ if (item.parentId !== null && !importedIds.has(item.parentId)) {
516
+ unresolved.push(item);
517
+ continue;
518
+ }
519
+ this.addOrUpdateMessage(item.parentId, item.message);
520
+ importedIds.add(item.message.id);
521
+ progressed = true;
522
+ }
523
+ if (!progressed) {
524
+ degenerate = true;
525
+ break;
526
+ }
527
+ pending = unresolved;
528
+ }
529
+ }
530
+ if (degenerate) {
531
+ this.clearRepository();
532
+ let previousId = null;
533
+ for (const { message } of loaded.messages) {
534
+ const existing = this.tryGetMessage(message.id);
535
+ this.addOrUpdateMessage(existing ? existing.parentId : previousId, message);
536
+ previousId = message.id;
537
+ }
538
+ this.resetRepositoryHead(previousId);
539
+ } else this.resetRepositoryHead(headId);
540
+ this.assistantHistoryParents.clear();
339
541
  this.recordedHistoryIds.clear();
340
- for (const message of this.messages) this.recordedHistoryIds.add(message.id);
542
+ for (const { message } of loaded.messages) this.recordedHistoryIds.add(message.id);
341
543
  this.notifyUpdate();
342
544
  }
343
545
  loadExternalState(state) {
344
546
  this.stateSnapshot = state;
345
547
  this.notifyUpdate();
346
548
  }
549
+ setState(next) {
550
+ this.stateSnapshot = typeof next === "function" ? next(this.stateSnapshot) : next;
551
+ this.notifyUpdate();
552
+ }
553
+ resetState() {
554
+ this.stateSnapshot = void 0;
555
+ this.notifyUpdate();
556
+ }
347
557
  async startRun(parentId, runConfig, resume, resumeStream) {
348
558
  const normalizedRunConfig = runConfig ?? {};
349
559
  this.lastRunConfig = normalizedRunConfig;
350
- this.resetHead(parentId);
351
- const historicalMessages = [...this.messages];
560
+ const parent = parentId === null ? void 0 : this.tryGetMessage(parentId);
561
+ const shouldEagerlyInsertAssistant = parentId !== null && parent !== void 0 && parentId !== this.repository.headId;
562
+ const historicalMessages = [...shouldEagerlyInsertAssistant ? this.tryGetMessages(parentId) ?? this.repository.getMessages() : this.repository.getMessages()];
352
563
  this.pendingError = null;
353
- const assistantParentId = parentId ?? this.messages.at(-1)?.id ?? null;
564
+ const assistantParentId = parent ? parentId : this.repository.headId;
354
565
  let assistantMessageId;
355
566
  const ensureAssistant = () => {
356
567
  if (assistantMessageId) return assistantMessageId;
357
- const created = this.insertAssistantPlaceholder();
568
+ const created = this.insertAssistantPlaceholder(assistantParentId ?? null);
358
569
  assistantMessageId = created;
359
570
  this.markPendingAssistantHistory(created, assistantParentId ?? null);
360
571
  return created;
361
572
  };
573
+ if (shouldEagerlyInsertAssistant) ensureAssistant();
362
574
  const applyUpdate = (update) => {
363
575
  const resolved = this.updateAssistantMessage(ensureAssistant(), update);
364
576
  if (resolved !== assistantMessageId) assistantMessageId = resolved;
@@ -370,8 +582,7 @@ var AgUiThreadRuntimeCore = class {
370
582
  onServerMessageId: (serverId) => {
371
583
  const placeholder = ensureAssistant();
372
584
  if (placeholder === serverId) return;
373
- this.reassignAssistantId(placeholder, serverId);
374
- assistantMessageId = serverId;
585
+ if (this.reassignAssistantId(placeholder, serverId)) assistantMessageId = serverId;
375
586
  }
376
587
  });
377
588
  const dispatch = (event) => this.handleEvent(aggregator, event);
@@ -461,7 +672,7 @@ var AgUiThreadRuntimeCore = class {
461
672
  unstable_threadId: ctx.threadId,
462
673
  unstable_parentId: ctx.parentId,
463
674
  unstable_getMessage: () => {
464
- const message = this.messages.find((m) => m.id === currentId());
675
+ const message = this.tryGetMessage(currentId())?.message;
465
676
  if (!message) throw new Error("[agui] resume stream requested the assistant message before it existed");
466
677
  return message;
467
678
  }
@@ -484,7 +695,7 @@ var AgUiThreadRuntimeCore = class {
484
695
  return;
485
696
  }
486
697
  if (ctx.abortSignal.aborted) return;
487
- const current = this.messages.find((m) => m.id === currentId());
698
+ const current = this.tryGetMessage(currentId())?.message;
488
699
  if (!current || current.status?.type === "running") ctx.applyUpdate({ status: {
489
700
  type: "complete",
490
701
  reason: "unknown"
@@ -492,7 +703,7 @@ var AgUiThreadRuntimeCore = class {
492
703
  }
493
704
  buildRunInput(runId, runConfig, historyMessages, resume) {
494
705
  const threadId = this.agent.threadId || "main";
495
- const messages = toAgUiMessages(historyMessages ?? this.messages);
706
+ const messages = toAgUiMessages(historyMessages ?? this.repository.getMessages());
496
707
  const context = this.runtime?.thread.getModelContext();
497
708
  return {
498
709
  threadId,
@@ -535,7 +746,7 @@ var AgUiThreadRuntimeCore = class {
535
746
  if (this.abortController === controller) this.abortController = null;
536
747
  this.setRunning(false);
537
748
  }
538
- insertAssistantPlaceholder() {
749
+ insertAssistantPlaceholder(parentId) {
539
750
  const id = generateOptimisticId();
540
751
  const assistant = {
541
752
  id,
@@ -552,44 +763,48 @@ var AgUiThreadRuntimeCore = class {
552
763
  custom: {}
553
764
  }
554
765
  };
555
- this.messages = [...this.messages, assistant];
766
+ this.addOrUpdateMessage(parentId ?? this.repository.headId, assistant);
767
+ this.switchToBranch(id);
556
768
  this.notifyUpdate();
557
769
  return id;
558
770
  }
559
771
  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", {
772
+ if (oldId === newId) return true;
773
+ const oldItem = this.tryGetMessage(oldId);
774
+ if (!oldItem) return false;
775
+ const collidesWithExisting = this.hasMessage(newId);
776
+ if (collidesWithExisting) {
777
+ this.logger.debug?.("[agui] reassignAssistantId: server id already present in messages or repository, dropping placeholder", {
563
778
  oldId,
564
779
  newId
565
780
  });
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,
781
+ this.deleteMessage(oldId, oldItem.parentId ?? null);
782
+ } else {
783
+ const { isOptimistic: _, ...metadata } = oldItem.message.metadata;
784
+ this.addOrUpdateMessage(oldItem.parentId, {
785
+ ...oldItem.message,
572
786
  id: newId,
573
787
  metadata
574
- };
575
- });
788
+ });
789
+ this.switchToBranch(newId);
790
+ this.tryDeleteMessage(oldId);
791
+ }
576
792
  const pendingParent = this.assistantHistoryParents.get(oldId);
577
793
  if (pendingParent !== void 0) {
578
794
  this.assistantHistoryParents.delete(oldId);
579
- if (!this.assistantHistoryParents.has(newId)) this.assistantHistoryParents.set(newId, pendingParent);
795
+ if (!collidesWithExisting && !this.assistantHistoryParents.has(newId)) this.assistantHistoryParents.set(newId, pendingParent);
580
796
  }
581
797
  if (this.recordedHistoryIds.has(oldId)) {
582
798
  this.recordedHistoryIds.delete(oldId);
583
- this.recordedHistoryIds.add(newId);
799
+ if (!collidesWithExisting) this.recordedHistoryIds.add(newId);
584
800
  }
585
801
  this.notifyUpdate();
802
+ return !collidesWithExisting;
586
803
  }
587
804
  updateAssistantMessage(messageId, update) {
588
- let touched = false;
589
805
  let latestStatus;
590
- this.messages = this.messages.map((message) => {
591
- if (message.id !== messageId || message.role !== "assistant") return message;
592
- touched = true;
806
+ if (!this.updateMessage(messageId, (message) => {
807
+ if (message.role !== "assistant") return message;
593
808
  const assistant = message;
594
809
  const metadata = update.metadata ? this.mergeAssistantMetadata(assistant.metadata, update.metadata) : assistant.metadata;
595
810
  latestStatus = update.status ?? assistant.status;
@@ -600,8 +815,7 @@ var AgUiThreadRuntimeCore = class {
600
815
  status: latestStatus,
601
816
  metadata
602
817
  };
603
- });
604
- if (!touched) return messageId;
818
+ })) return messageId;
605
819
  let resolvedMessageId = messageId;
606
820
  if (latestStatus !== void 0 && latestStatus.type !== "running" && isOptimisticId(messageId)) {
607
821
  const stableId = generateId();
@@ -614,12 +828,11 @@ var AgUiThreadRuntimeCore = class {
614
828
  }
615
829
  preserveToolResults(previous, next) {
616
830
  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);
831
+ for (const part of previous) if (part.type === "tool-call" && isResolvedToolCall(part)) resolved.set(part.toolCallId, part);
618
832
  if (resolved.size === 0) return next;
619
833
  let changed = false;
620
834
  const merged = next.map((part) => {
621
- if (part.type !== "tool-call") return part;
622
- if ("result" in part && part.result !== void 0) return part;
835
+ if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
623
836
  const prior = resolved.get(part.toolCallId);
624
837
  if (!prior) return part;
625
838
  changed = true;
@@ -679,33 +892,94 @@ var AgUiThreadRuntimeCore = class {
679
892
  }
680
893
  aggregator.handle(event);
681
894
  return;
895
+ case "ACTIVITY_SNAPSHOT": {
896
+ const toolCallId = event.content["toolCallId"];
897
+ if (event.activityType === "mcp-apps" && typeof toolCallId === "string" && !aggregator.hasToolCall(toolCallId)) {
898
+ const messageId = this.findMessageIdForToolCall(toolCallId);
899
+ if (messageId !== void 0) {
900
+ this.applyCrossRunActivitySnapshot(messageId, toolCallId, event);
901
+ return;
902
+ }
903
+ }
904
+ aggregator.handle(event);
905
+ return;
906
+ }
682
907
  default: aggregator.handle(event);
683
908
  }
684
909
  }
685
910
  applyCrossRunToolResult(messageId, event) {
686
- let updated = false;
687
- this.messages = this.messages.map((message) => {
688
- if (message.id !== messageId || message.role !== "assistant") return message;
911
+ if (!this.updateMessage(messageId, (message) => {
912
+ if (message.role !== "assistant") return message;
689
913
  const assistant = message;
690
914
  let matchedToolCall = false;
691
915
  const content = assistant.content.map((part) => {
692
916
  if (part.type !== "tool-call" || part.toolCallId !== event.toolCallId) return part;
693
917
  matchedToolCall = true;
918
+ if (part.mcp?.app !== void 0 && isPlainObject(part.result) && Array.isArray(part.result["content"])) return {
919
+ ...part,
920
+ ...part.modelContent === void 0 && event.content ? { modelContent: [{
921
+ type: "text",
922
+ text: event.content
923
+ }] } : {},
924
+ ...part.isError === void 0 ? typeof event.mcpResult?.isError === "boolean" ? { isError: event.mcpResult.isError } : event.role === "tool" ? { isError: false } : {} : {},
925
+ ...event.messageId ? { unstable_toolMessageId: event.messageId } : {}
926
+ };
694
927
  return {
695
928
  ...part,
696
- result: tryParseJSON(event.content ?? ""),
697
- ...event.role === "tool" ? { isError: false } : {},
929
+ result: event.mcpResult ?? tryParseJSON(event.content ?? ""),
930
+ ...event.mcpResult !== void 0 ? { modelContent: [{
931
+ type: "text",
932
+ text: event.content
933
+ }] } : {},
934
+ ...typeof event.mcpResult?.isError === "boolean" ? { isError: event.mcpResult.isError } : event.role === "tool" ? { isError: false } : {},
698
935
  ...event.messageId ? { unstable_toolMessageId: event.messageId } : {}
699
936
  };
700
937
  });
701
938
  if (!matchedToolCall) return message;
702
- updated = true;
703
939
  return {
704
940
  ...assistant,
705
941
  content
706
942
  };
707
- });
708
- if (!updated) return;
943
+ })) return;
944
+ this.notifyUpdate();
945
+ this.maybeCompleteAfterToolResults(messageId);
946
+ }
947
+ applyCrossRunActivitySnapshot(messageId, toolCallId, event) {
948
+ const resourceUri = event.content["resourceUri"];
949
+ if (typeof resourceUri !== "string" || !isMcpAppUri(resourceUri)) return;
950
+ const serverId = event.content["serverId"];
951
+ const serverHash = event.content["serverHash"];
952
+ const appServerId = typeof serverId === "string" && serverId.length > 0 ? serverId : typeof serverHash === "string" && serverHash.length > 0 ? serverHash : void 0;
953
+ const result = event.content["result"];
954
+ if (!this.updateMessage(messageId, (message) => {
955
+ if (message.role !== "assistant") return message;
956
+ const assistant = message;
957
+ let matchedToolCall = false;
958
+ const content = assistant.content.map((part) => {
959
+ if (part.type !== "tool-call" || part.toolCallId !== toolCallId) return part;
960
+ matchedToolCall = true;
961
+ return {
962
+ ...part,
963
+ mcp: { app: {
964
+ resourceUri,
965
+ ...appServerId ? { serverId: appServerId } : {}
966
+ } },
967
+ ...isPlainObject(result) ? {
968
+ ...part.result !== void 0 && part.modelContent === void 0 ? { modelContent: [{
969
+ type: "text",
970
+ text: typeof part.result === "string" ? part.result : JSON.stringify(part.result)
971
+ }] } : {},
972
+ result,
973
+ isError: result["isError"] === true
974
+ } : {}
975
+ };
976
+ });
977
+ if (!matchedToolCall) return message;
978
+ return {
979
+ ...assistant,
980
+ content
981
+ };
982
+ })) return;
709
983
  this.notifyUpdate();
710
984
  this.maybeCompleteAfterToolResults(messageId);
711
985
  }
@@ -726,15 +1000,6 @@ var AgUiThreadRuntimeCore = class {
726
1000
  toThreadMessage(message) {
727
1001
  return fromThreadMessageLike(message, generateId(), FALLBACK_USER_STATUS);
728
1002
  }
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
1003
  isTerminalStatus(status) {
739
1004
  return status?.type === "complete" || status?.type === "incomplete";
740
1005
  }
@@ -753,7 +1018,7 @@ var AgUiThreadRuntimeCore = class {
753
1018
  if (!this.history) return;
754
1019
  const parentId = this.assistantHistoryParents.get(messageId);
755
1020
  if (parentId === void 0) return;
756
- const message = this.messages.find((m) => m.id === messageId);
1021
+ const message = this.tryGetMessage(messageId)?.message;
757
1022
  if (!message || message.role !== "assistant") return;
758
1023
  if (!this.isPersistableStatus(message.status)) return;
759
1024
  this.assistantHistoryParents.delete(messageId);