@assistant-ui/react-ag-ui 0.0.30 → 0.0.32

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 (39) hide show
  1. package/dist/index.d.ts +3 -4
  2. package/dist/index.js +2 -2
  3. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +85 -82
  4. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  5. package/dist/runtime/AgUiThreadRuntimeCore.js +542 -627
  6. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  7. package/dist/runtime/adapter/conversions.d.ts +39 -36
  8. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  9. package/dist/runtime/adapter/conversions.js +294 -390
  10. package/dist/runtime/adapter/conversions.js.map +1 -1
  11. package/dist/runtime/adapter/run-aggregator.d.ts +54 -47
  12. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  13. package/dist/runtime/adapter/run-aggregator.js +367 -364
  14. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  15. package/dist/runtime/adapter/subscriber.d.ts +94 -91
  16. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  17. package/dist/runtime/adapter/subscriber.js +77 -86
  18. package/dist/runtime/adapter/subscriber.js.map +1 -1
  19. package/dist/runtime/event-parser.d.ts +9 -5
  20. package/dist/runtime/event-parser.d.ts.map +1 -1
  21. package/dist/runtime/event-parser.js +172 -177
  22. package/dist/runtime/event-parser.js.map +1 -1
  23. package/dist/runtime/logger.d.ts +7 -4
  24. package/dist/runtime/logger.d.ts.map +1 -1
  25. package/dist/runtime/logger.js +7 -3
  26. package/dist/runtime/logger.js.map +1 -1
  27. package/dist/runtime/types.d.ts +121 -117
  28. package/dist/runtime/types.d.ts.map +1 -1
  29. package/dist/runtime/types.js +0 -2
  30. package/dist/useAgUiRuntime.d.ts +10 -6
  31. package/dist/useAgUiRuntime.d.ts.map +1 -1
  32. package/dist/useAgUiRuntime.js +136 -142
  33. package/dist/useAgUiRuntime.js.map +1 -1
  34. package/package.json +9 -8
  35. package/src/runtime/AgUiThreadRuntimeCore.ts +1 -0
  36. package/src/runtime/adapter/run-aggregator.ts +111 -45
  37. package/dist/index.d.ts.map +0 -1
  38. package/dist/index.js.map +0 -1
  39. package/dist/runtime/types.js.map +0 -1
@@ -1,631 +1,546 @@
1
1
  "use client";
2
- import { generateId, generateOptimisticId, isOptimisticId, fromThreadMessageLike, } from "@assistant-ui/core/internal";
3
- import { AG_UI_METADATA_NAMESPACE, RunAggregator, } from "./adapter/run-aggregator.js";
4
- import { fromAgUiMessages, toAgUiMessages, toAgUiTools, } from "./adapter/conversions.js";
2
+ import { AG_UI_METADATA_NAMESPACE, RunAggregator } from "./adapter/run-aggregator.js";
3
+ import { fromAgUiMessages, toAgUiMessages, toAgUiTools } from "./adapter/conversions.js";
5
4
  import { createAgUiSubscriber } from "./adapter/subscriber.js";
5
+ import { fromThreadMessageLike, generateId, generateOptimisticId, isOptimisticId } from "@assistant-ui/core/internal";
6
+ //#region src/runtime/AgUiThreadRuntimeCore.ts
6
7
  const symbolResumeShim = Symbol("agui-resume-shim");
7
- const FALLBACK_USER_STATUS = { type: "complete", reason: "unknown" };
8
- export class AgUiThreadRuntimeCore {
9
- agent;
10
- logger;
11
- showThinking;
12
- onError;
13
- onCancel;
14
- notifyUpdate;
15
- runtime;
16
- messages = [];
17
- isRunningFlag = false;
18
- abortController = null;
19
- stateSnapshot;
20
- pendingError = null;
21
- history;
22
- lastRunConfig;
23
- assistantHistoryParents = new Map();
24
- recordedHistoryIds = new Set();
25
- _isLoading = false;
26
- _loadPromise;
27
- constructor(options) {
28
- this.agent = options.agent;
29
- this.logger = options.logger;
30
- this.showThinking = options.showThinking;
31
- this.onError = options.onError;
32
- this.onCancel = options.onCancel;
33
- this.history = options.history;
34
- this.notifyUpdate = options.notifyUpdate;
35
- this.installResumeShim();
36
- }
37
- updateOptions(options) {
38
- this.agent = options.agent;
39
- this.logger = options.logger;
40
- this.showThinking = options.showThinking;
41
- this.onError = options.onError;
42
- this.onCancel = options.onCancel;
43
- this.history = options.history;
44
- this.installResumeShim();
45
- }
46
- attachRuntime(runtime) {
47
- this.runtime = runtime;
48
- }
49
- detachRuntime() {
50
- this.runtime = undefined;
51
- }
52
- getMessages() {
53
- return this.messages;
54
- }
55
- getState() {
56
- return this.stateSnapshot;
57
- }
58
- isRunning() {
59
- return this.isRunningFlag;
60
- }
61
- get isLoading() {
62
- return this._isLoading;
63
- }
64
- __internal_load() {
65
- if (this._loadPromise)
66
- return this._loadPromise;
67
- const promise = this.history?.load() ?? Promise.resolve(null);
68
- this._isLoading = true;
69
- this._loadPromise = promise
70
- .then(async (repo) => {
71
- if (!repo)
72
- return;
73
- const messages = repo.messages.map((item) => item.message);
74
- this.applyExternalMessages(messages);
75
- if (repo.unstable_resume) {
76
- const parentId = repo.headId ?? messages.at(-1)?.id ?? null;
77
- await this.startRun(parentId, this.lastRunConfig);
78
- }
79
- })
80
- .catch((error) => {
81
- this.logger.error?.("[agui] failed to load history", error);
82
- this.onError?.(error instanceof Error ? error : new Error(String(error)));
83
- })
84
- .finally(() => {
85
- this._isLoading = false;
86
- this.notifyUpdate();
87
- });
88
- this.notifyUpdate();
89
- return this._loadPromise;
90
- }
91
- async append(message) {
92
- const startRun = message.startRun ?? message.role === "user";
93
- if (startRun)
94
- this.assertNoPendingInterrupts();
95
- if (message.sourceId) {
96
- this.messages = this.messages.filter((entry) => entry.id !== message.sourceId);
97
- }
98
- this.resetHead(message.parentId);
99
- const threadMessage = this.toThreadMessage(message);
100
- this.messages = [...this.messages, threadMessage];
101
- this.notifyUpdate();
102
- this.recordHistoryEntry(message.parentId ?? null, threadMessage);
103
- if (!startRun)
104
- return;
105
- await this.startRun(threadMessage.id, message.runConfig);
106
- }
107
- async edit(message) {
108
- await this.append(message);
109
- }
110
- async reload(parentId, config = {}) {
111
- this.assertNoPendingInterrupts();
112
- this.resetHead(parentId);
113
- this.notifyUpdate();
114
- await this.startRun(parentId, config.runConfig);
115
- }
116
- async cancel() {
117
- if (!this.abortController)
118
- return;
119
- this.abortController.abort();
120
- }
121
- async resume(config) {
122
- this.assertNoPendingInterrupts();
123
- if (config.stream) {
124
- this.logger.debug?.("[agui] resume stream is not supported, falling back to regular run");
125
- }
126
- await this.startRun(config.parentId, config.runConfig ?? this.lastRunConfig);
127
- }
128
- assertNoPendingInterrupts() {
129
- if (!this.getPendingInterrupts())
130
- return;
131
- throw new Error("[agui] cannot start a new run while interrupts are pending; resolve them with submitInterruptResponses()");
132
- }
133
- getPendingInterrupts() {
134
- const assistant = this.messages.findLast((m) => m.role === "assistant");
135
- if (!assistant ||
136
- assistant.status?.type !== "requires-action" ||
137
- assistant.status.reason !== "interrupt") {
138
- return null;
139
- }
140
- const stored = assistant.metadata.custom[AG_UI_METADATA_NAMESPACE]?.interrupts;
141
- if (!stored?.length)
142
- return null;
143
- return { messageId: assistant.id, interrupts: stored };
144
- }
145
- async submitInterruptResponses(responses) {
146
- const pending = this.getPendingInterrupts();
147
- if (!pending) {
148
- throw new Error("[agui] submitInterruptResponses: no pending interrupts on this thread");
149
- }
150
- const responsesById = new Map();
151
- for (const entry of responses) {
152
- if (!entry || typeof entry.interruptId !== "string") {
153
- throw new Error("[agui] submitInterruptResponses: every entry must have an interruptId");
154
- }
155
- if (entry.status !== "resolved" && entry.status !== "cancelled") {
156
- throw new Error(`[agui] submitInterruptResponses: invalid status "${entry.status}" for interrupt ${entry.interruptId}`);
157
- }
158
- if (responsesById.has(entry.interruptId)) {
159
- throw new Error(`[agui] submitInterruptResponses: duplicate response for interrupt ${entry.interruptId}`);
160
- }
161
- responsesById.set(entry.interruptId, entry);
162
- }
163
- const openIds = pending.interrupts.map((i) => i.id);
164
- const missing = openIds.filter((id) => !responsesById.has(id));
165
- if (missing.length > 0) {
166
- throw new Error(`[agui] submitInterruptResponses: missing responses for open interrupts: ${missing.join(", ")}`);
167
- }
168
- const known = new Set(openIds);
169
- const unknownIds = [...responsesById.keys()].filter((id) => !known.has(id));
170
- if (unknownIds.length > 0) {
171
- throw new Error(`[agui] submitInterruptResponses: unknown interrupt ids: ${unknownIds.join(", ")}`);
172
- }
173
- const now = Date.now();
174
- for (const interrupt of pending.interrupts) {
175
- if (!interrupt.expiresAt)
176
- continue;
177
- const expiry = new Date(interrupt.expiresAt).getTime();
178
- if (Number.isNaN(expiry)) {
179
- throw new Error(`[agui] submitInterruptResponses: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`);
180
- }
181
- if (expiry <= now) {
182
- throw new Error(`[agui] submitInterruptResponses: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`);
183
- }
184
- }
185
- const resume = openIds.map((id) => responsesById.get(id));
186
- if (this.isRunningFlag) {
187
- throw new Error("[agui] submitInterruptResponses: a run is already in progress");
188
- }
189
- this.clearPendingInterrupts(pending.messageId);
190
- await this.startRun(pending.messageId, this.lastRunConfig, resume);
191
- }
192
- clearPendingInterrupts(messageId) {
193
- let touched = false;
194
- this.messages = this.messages.map((message) => {
195
- if (message.id !== messageId || message.role !== "assistant")
196
- return message;
197
- const assistant = message;
198
- if (assistant.status?.type !== "requires-action" ||
199
- assistant.status.reason !== "interrupt") {
200
- return assistant;
201
- }
202
- touched = true;
203
- const aguiMeta = assistant.metadata.custom[AG_UI_METADATA_NAMESPACE];
204
- const { interrupts: _drop, ...restAgui } = aguiMeta ?? {};
205
- const newCustom = { ...assistant.metadata.custom };
206
- if (Object.keys(restAgui).length > 0) {
207
- newCustom[AG_UI_METADATA_NAMESPACE] = restAgui;
208
- }
209
- else {
210
- delete newCustom[AG_UI_METADATA_NAMESPACE];
211
- }
212
- return {
213
- ...assistant,
214
- status: { type: "complete", reason: "unknown" },
215
- metadata: { ...assistant.metadata, custom: newCustom },
216
- };
217
- });
218
- if (touched)
219
- this.notifyUpdate();
220
- }
221
- findMessageIdForToolCall(toolCallId) {
222
- let fallbackMessageId;
223
- for (let index = this.messages.length - 1; index >= 0; index--) {
224
- const message = this.messages[index];
225
- if (!message || message.role !== "assistant")
226
- continue;
227
- for (const part of message.content) {
228
- if (part.type !== "tool-call" || part.toolCallId !== toolCallId)
229
- continue;
230
- if (!("result" in part) || part.result === undefined) {
231
- return message.id;
232
- }
233
- fallbackMessageId ??= message.id;
234
- }
235
- }
236
- return fallbackMessageId;
237
- }
238
- addToolResult(options) {
239
- let updated = false;
240
- let shouldResume = false;
241
- this.messages = this.messages.map((message) => {
242
- if (message.id !== options.messageId || message.role !== "assistant")
243
- return message;
244
- const assistant = message;
245
- let matchedToolCall = false;
246
- const content = assistant.content.map((part) => {
247
- if (part.type !== "tool-call" || part.toolCallId !== options.toolCallId)
248
- return part;
249
- matchedToolCall = true;
250
- return {
251
- ...part,
252
- result: options.result,
253
- artifact: options.artifact,
254
- isError: options.isError,
255
- };
256
- });
257
- if (!matchedToolCall)
258
- return message;
259
- updated = true;
260
- if (assistant.status?.type === "requires-action" &&
261
- assistant.status.reason === "tool-calls" &&
262
- content.every((part) => part.type !== "tool-call" ||
263
- ("result" in part && part.result !== undefined))) {
264
- shouldResume = true;
265
- return {
266
- ...assistant,
267
- content,
268
- status: { type: "complete", reason: "unknown" },
269
- };
270
- }
271
- return {
272
- ...assistant,
273
- content,
274
- };
275
- });
276
- if (updated) {
277
- this.notifyUpdate();
278
- if (shouldResume) {
279
- this.persistAssistantHistory(options.messageId);
280
- if (!this.isRunningFlag) {
281
- void this.startRun(options.messageId, this.lastRunConfig).catch((error) => {
282
- this.onError?.(error instanceof Error ? error : new Error(String(error)));
283
- });
284
- }
285
- }
286
- }
287
- }
288
- applyExternalMessages(messages) {
289
- this.assistantHistoryParents.clear();
290
- this.messages = [...messages];
291
- this.recordedHistoryIds.clear();
292
- for (const message of this.messages) {
293
- this.recordedHistoryIds.add(message.id);
294
- }
295
- this.notifyUpdate();
296
- }
297
- loadExternalState(state) {
298
- this.stateSnapshot = state;
299
- this.notifyUpdate();
300
- }
301
- async startRun(parentId, runConfig, resume) {
302
- const normalizedRunConfig = runConfig ?? {};
303
- this.lastRunConfig = normalizedRunConfig;
304
- this.resetHead(parentId);
305
- const historicalMessages = [...this.messages];
306
- const runId = generateId();
307
- this.pendingError = null;
308
- const input = this.buildRunInput(runId, normalizedRunConfig, historicalMessages, resume);
309
- const assistantParentId = parentId ?? this.messages.at(-1)?.id ?? null;
310
- let assistantMessageId;
311
- const ensureAssistant = () => {
312
- if (assistantMessageId)
313
- return assistantMessageId;
314
- const created = this.insertAssistantPlaceholder();
315
- assistantMessageId = created;
316
- this.markPendingAssistantHistory(created, assistantParentId ?? null);
317
- return created;
318
- };
319
- const aggregator = new RunAggregator({
320
- showThinking: this.showThinking,
321
- logger: this.logger,
322
- emit: (update) => {
323
- const resolved = this.updateAssistantMessage(ensureAssistant(), update);
324
- if (resolved !== assistantMessageId) {
325
- assistantMessageId = resolved;
326
- }
327
- },
328
- onServerMessageId: (serverId) => {
329
- const placeholder = ensureAssistant();
330
- if (placeholder === serverId)
331
- return;
332
- this.reassignAssistantId(placeholder, serverId);
333
- assistantMessageId = serverId;
334
- },
335
- });
336
- const dispatch = (event) => this.handleEvent(aggregator, event);
337
- const abortController = new AbortController();
338
- const abortSignal = abortController.signal;
339
- this.abortController = abortController;
340
- abortSignal.addEventListener("abort", () => {
341
- dispatch({ type: "RUN_CANCELLED" });
342
- this.finishRun(abortController);
343
- this.onCancel?.();
344
- }, { once: true });
345
- const subscriber = createAgUiSubscriber({
346
- dispatch,
347
- runId,
348
- logger: this.logger,
349
- onRunFailed: (error) => {
350
- this.pendingError = error;
351
- this.onError?.(error);
352
- },
353
- });
354
- aggregator.handle({ type: "RUN_STARTED", runId });
355
- this.setRunning(true);
356
- try {
357
- try {
358
- this.agent.messages = input.messages;
359
- this.agent.threadId = input.threadId;
360
- this.agent.state = input.state ?? null;
361
- }
362
- catch {
363
- // ignore
364
- }
365
- await this.agent.runAgent(input, subscriber, {
366
- signal: abortSignal,
367
- });
368
- }
369
- catch (error) {
370
- if (!abortSignal.aborted) {
371
- const err = error instanceof Error ? error : new Error(String(error));
372
- dispatch({ type: "RUN_ERROR", message: err.message });
373
- this.onError?.(err);
374
- this.pendingError = this.pendingError ?? err;
375
- }
376
- }
377
- finally {
378
- this.finishRun(abortController);
379
- }
380
- if (this.pendingError) {
381
- const err = this.pendingError;
382
- this.pendingError = null;
383
- throw err;
384
- }
385
- }
386
- buildRunInput(runId, runConfig, historyMessages, resume) {
387
- const threadId = this.agent.threadId || "main";
388
- const messages = toAgUiMessages(historyMessages ?? this.messages);
389
- const context = this.runtime?.thread.getModelContext();
390
- return {
391
- threadId,
392
- runId,
393
- state: this.stateSnapshot ?? null,
394
- messages,
395
- tools: toAgUiTools(context?.tools),
396
- context: context?.system
397
- ? [{ description: "system", value: context.system }]
398
- : [],
399
- forwardedProps: {
400
- ...(context?.callSettings ?? {}),
401
- ...(context?.config ?? {}),
402
- ...(runConfig?.custom ? { runConfig: runConfig.custom } : {}),
403
- },
404
- ...(resume !== undefined ? { resume } : {}),
405
- };
406
- }
407
- installResumeShim() {
408
- const agent = this.agent;
409
- if (agent[symbolResumeShim])
410
- return;
411
- agent[symbolResumeShim] = true;
412
- const onInstance = Object.hasOwn(agent, "prepareRunAgentInput");
413
- const original = onInstance
414
- ? agent.prepareRunAgentInput
415
- : Object.getPrototypeOf(agent)?.prepareRunAgentInput;
416
- if (typeof original !== "function")
417
- return;
418
- agent.prepareRunAgentInput = function (params) {
419
- const input = original.call(this, params);
420
- if (params?.resume !== undefined && input && typeof input === "object") {
421
- return { ...input, resume: params.resume };
422
- }
423
- return input;
424
- };
425
- }
426
- setRunning(running) {
427
- this.isRunningFlag = running;
428
- this.notifyUpdate();
429
- }
430
- finishRun(controller) {
431
- if (this.abortController === controller) {
432
- this.abortController = null;
433
- }
434
- this.setRunning(false);
435
- }
436
- insertAssistantPlaceholder() {
437
- const id = generateOptimisticId();
438
- const assistant = {
439
- id,
440
- role: "assistant",
441
- createdAt: new Date(),
442
- status: { type: "running" },
443
- content: [],
444
- metadata: {
445
- unstable_state: this.stateSnapshot ?? null,
446
- unstable_annotations: [],
447
- unstable_data: [],
448
- steps: [],
449
- custom: {},
450
- },
451
- };
452
- this.messages = [...this.messages, assistant];
453
- this.notifyUpdate();
454
- return id;
455
- }
456
- reassignAssistantId(oldId, newId) {
457
- if (oldId === newId)
458
- return;
459
- const collidesWithExisting = this.messages.some((m) => m.id === newId);
460
- if (collidesWithExisting) {
461
- this.logger.debug?.("[agui] reassignAssistantId: server id already present in messages, dropping placeholder", { oldId, newId });
462
- this.messages = this.messages.filter((m) => m.id !== oldId);
463
- }
464
- else {
465
- this.messages = this.messages.map((m) => m.id === oldId ? { ...m, id: newId } : m);
466
- }
467
- const pendingParent = this.assistantHistoryParents.get(oldId);
468
- if (pendingParent !== undefined) {
469
- this.assistantHistoryParents.delete(oldId);
470
- if (!this.assistantHistoryParents.has(newId)) {
471
- this.assistantHistoryParents.set(newId, pendingParent);
472
- }
473
- }
474
- if (this.recordedHistoryIds.has(oldId)) {
475
- this.recordedHistoryIds.delete(oldId);
476
- this.recordedHistoryIds.add(newId);
477
- }
478
- this.notifyUpdate();
479
- }
480
- updateAssistantMessage(messageId, update) {
481
- let touched = false;
482
- let latestStatus;
483
- this.messages = this.messages.map((message) => {
484
- if (message.id !== messageId || message.role !== "assistant")
485
- return message;
486
- touched = true;
487
- const assistant = message;
488
- const metadata = update.metadata
489
- ? this.mergeAssistantMetadata(assistant.metadata, update.metadata)
490
- : assistant.metadata;
491
- latestStatus = update.status ?? assistant.status;
492
- return {
493
- ...assistant,
494
- content: (update.content ??
495
- assistant.content),
496
- status: latestStatus,
497
- metadata,
498
- };
499
- });
500
- if (!touched)
501
- return messageId;
502
- let resolvedMessageId = messageId;
503
- const isSettled = latestStatus !== undefined && latestStatus.type !== "running";
504
- if (isSettled && isOptimisticId(messageId)) {
505
- const stableId = generateId();
506
- this.reassignAssistantId(messageId, stableId);
507
- resolvedMessageId = stableId;
508
- }
509
- else {
510
- this.notifyUpdate();
511
- }
512
- if (this.isPersistableStatus(latestStatus)) {
513
- this.persistAssistantHistory(resolvedMessageId);
514
- }
515
- return resolvedMessageId;
516
- }
517
- mergeAssistantMetadata(current, incoming) {
518
- const annotations = incoming.unstable_annotations
519
- ? [...current.unstable_annotations, ...incoming.unstable_annotations]
520
- : current.unstable_annotations;
521
- const data = incoming.unstable_data
522
- ? [...current.unstable_data, ...incoming.unstable_data]
523
- : current.unstable_data;
524
- const steps = incoming.steps
525
- ? [...current.steps, ...incoming.steps]
526
- : current.steps;
527
- return {
528
- unstable_state: incoming.unstable_state !== undefined
529
- ? incoming.unstable_state
530
- : current.unstable_state,
531
- unstable_annotations: annotations,
532
- unstable_data: data,
533
- steps,
534
- custom: incoming.custom
535
- ? { ...current.custom, ...incoming.custom }
536
- : current.custom,
537
- };
538
- }
539
- handleEvent(aggregator, event) {
540
- switch (event.type) {
541
- case "STATE_SNAPSHOT": {
542
- this.stateSnapshot = event.snapshot;
543
- this.notifyUpdate();
544
- return;
545
- }
546
- case "STATE_DELTA": {
547
- this.logger.debug?.("[agui] state delta event ignored", event.delta);
548
- return;
549
- }
550
- case "MESSAGES_SNAPSHOT": {
551
- this.importMessagesSnapshot(event.messages);
552
- return;
553
- }
554
- default:
555
- aggregator.handle(event);
556
- }
557
- }
558
- importMessagesSnapshot(rawMessages) {
559
- try {
560
- const normalized = fromAgUiMessages(rawMessages);
561
- const converted = [];
562
- for (const message of normalized) {
563
- try {
564
- converted.push(fromThreadMessageLike(message, generateId(), FALLBACK_USER_STATUS));
565
- }
566
- catch (error) {
567
- this.logger.error?.("[agui] failed to import message from snapshot", error);
568
- }
569
- }
570
- this.applyExternalMessages(converted);
571
- }
572
- catch (error) {
573
- this.logger.error?.("[agui] failed to import messages snapshot", error);
574
- }
575
- }
576
- toThreadMessage(message) {
577
- return fromThreadMessageLike(message, generateId(), FALLBACK_USER_STATUS);
578
- }
579
- resetHead(parentId) {
580
- if (!parentId) {
581
- if (this.messages.length) {
582
- this.messages = [];
583
- }
584
- return;
585
- }
586
- const idx = this.messages.findIndex((message) => message.id === parentId);
587
- if (idx === -1)
588
- return;
589
- this.messages = this.messages.slice(0, idx + 1);
590
- }
591
- isTerminalStatus(status) {
592
- return status?.type === "complete" || status?.type === "incomplete";
593
- }
594
- isPersistableStatus(status) {
595
- if (this.isTerminalStatus(status))
596
- return true;
597
- return status?.type === "requires-action" && status.reason === "interrupt";
598
- }
599
- recordHistoryEntry(parentId, message) {
600
- this.appendHistoryItem(parentId, message);
601
- }
602
- markPendingAssistantHistory(messageId, parentId) {
603
- if (!this.history)
604
- return;
605
- this.assistantHistoryParents.set(messageId, parentId);
606
- }
607
- persistAssistantHistory(messageId) {
608
- if (!this.history)
609
- return;
610
- const parentId = this.assistantHistoryParents.get(messageId);
611
- if (parentId === undefined)
612
- return;
613
- const message = this.messages.find((m) => m.id === messageId);
614
- if (!message || message.role !== "assistant")
615
- return;
616
- if (!this.isPersistableStatus(message.status))
617
- return;
618
- this.assistantHistoryParents.delete(messageId);
619
- this.appendHistoryItem(parentId, message);
620
- }
621
- appendHistoryItem(parentId, message) {
622
- if (!this.history || this.recordedHistoryIds.has(message.id))
623
- return;
624
- this.recordedHistoryIds.add(message.id);
625
- void this.history.append({ parentId, message }).catch((error) => {
626
- this.recordedHistoryIds.delete(message.id);
627
- this.logger.error?.("[agui] failed to append history entry", error);
628
- });
629
- }
630
- }
8
+ const FALLBACK_USER_STATUS = {
9
+ type: "complete",
10
+ reason: "unknown"
11
+ };
12
+ var AgUiThreadRuntimeCore = class {
13
+ agent;
14
+ logger;
15
+ showThinking;
16
+ onError;
17
+ onCancel;
18
+ notifyUpdate;
19
+ runtime;
20
+ messages = [];
21
+ isRunningFlag = false;
22
+ abortController = null;
23
+ stateSnapshot;
24
+ pendingError = null;
25
+ history;
26
+ lastRunConfig;
27
+ assistantHistoryParents = /* @__PURE__ */ new Map();
28
+ recordedHistoryIds = /* @__PURE__ */ new Set();
29
+ _isLoading = false;
30
+ _loadPromise;
31
+ constructor(options) {
32
+ this.agent = options.agent;
33
+ this.logger = options.logger;
34
+ this.showThinking = options.showThinking;
35
+ this.onError = options.onError;
36
+ this.onCancel = options.onCancel;
37
+ this.history = options.history;
38
+ this.notifyUpdate = options.notifyUpdate;
39
+ this.installResumeShim();
40
+ }
41
+ updateOptions(options) {
42
+ this.agent = options.agent;
43
+ this.logger = options.logger;
44
+ this.showThinking = options.showThinking;
45
+ this.onError = options.onError;
46
+ this.onCancel = options.onCancel;
47
+ this.history = options.history;
48
+ this.installResumeShim();
49
+ }
50
+ attachRuntime(runtime) {
51
+ this.runtime = runtime;
52
+ }
53
+ detachRuntime() {
54
+ this.runtime = void 0;
55
+ }
56
+ getMessages() {
57
+ return this.messages;
58
+ }
59
+ getState() {
60
+ return this.stateSnapshot;
61
+ }
62
+ isRunning() {
63
+ return this.isRunningFlag;
64
+ }
65
+ get isLoading() {
66
+ return this._isLoading;
67
+ }
68
+ __internal_load() {
69
+ if (this._loadPromise) return this._loadPromise;
70
+ const promise = this.history?.load() ?? Promise.resolve(null);
71
+ this._isLoading = true;
72
+ this._loadPromise = promise.then(async (repo) => {
73
+ if (!repo) return;
74
+ const messages = repo.messages.map((item) => item.message);
75
+ this.applyExternalMessages(messages);
76
+ if (repo.unstable_resume) {
77
+ const parentId = repo.headId ?? messages.at(-1)?.id ?? null;
78
+ await this.startRun(parentId, this.lastRunConfig);
79
+ }
80
+ }).catch((error) => {
81
+ this.logger.error?.("[agui] failed to load history", error);
82
+ this.onError?.(error instanceof Error ? error : new Error(String(error)));
83
+ }).finally(() => {
84
+ this._isLoading = false;
85
+ this.notifyUpdate();
86
+ });
87
+ this.notifyUpdate();
88
+ return this._loadPromise;
89
+ }
90
+ async append(message) {
91
+ const startRun = message.startRun ?? message.role === "user";
92
+ if (startRun) this.assertNoPendingInterrupts();
93
+ if (message.sourceId) this.messages = this.messages.filter((entry) => entry.id !== message.sourceId);
94
+ this.resetHead(message.parentId);
95
+ const threadMessage = this.toThreadMessage(message);
96
+ this.messages = [...this.messages, threadMessage];
97
+ this.notifyUpdate();
98
+ this.recordHistoryEntry(message.parentId ?? null, threadMessage);
99
+ if (!startRun) return;
100
+ await this.startRun(threadMessage.id, message.runConfig);
101
+ }
102
+ async edit(message) {
103
+ await this.append(message);
104
+ }
105
+ async reload(parentId, config = {}) {
106
+ this.assertNoPendingInterrupts();
107
+ this.resetHead(parentId);
108
+ this.notifyUpdate();
109
+ await this.startRun(parentId, config.runConfig);
110
+ }
111
+ async cancel() {
112
+ if (!this.abortController) return;
113
+ this.abortController.abort();
114
+ }
115
+ async resume(config) {
116
+ this.assertNoPendingInterrupts();
117
+ if (config.stream) this.logger.debug?.("[agui] resume stream is not supported, falling back to regular run");
118
+ await this.startRun(config.parentId, config.runConfig ?? this.lastRunConfig);
119
+ }
120
+ assertNoPendingInterrupts() {
121
+ if (!this.getPendingInterrupts()) return;
122
+ throw new Error("[agui] cannot start a new run while interrupts are pending; resolve them with submitInterruptResponses()");
123
+ }
124
+ getPendingInterrupts() {
125
+ const assistant = this.messages.findLast((m) => m.role === "assistant");
126
+ if (!assistant || assistant.status?.type !== "requires-action" || assistant.status.reason !== "interrupt") return null;
127
+ const stored = assistant.metadata.custom[AG_UI_METADATA_NAMESPACE]?.interrupts;
128
+ if (!stored?.length) return null;
129
+ return {
130
+ messageId: assistant.id,
131
+ interrupts: stored
132
+ };
133
+ }
134
+ async submitInterruptResponses(responses) {
135
+ const pending = this.getPendingInterrupts();
136
+ if (!pending) throw new Error("[agui] submitInterruptResponses: no pending interrupts on this thread");
137
+ const responsesById = /* @__PURE__ */ new Map();
138
+ for (const entry of responses) {
139
+ if (!entry || typeof entry.interruptId !== "string") throw new Error("[agui] submitInterruptResponses: every entry must have an interruptId");
140
+ if (entry.status !== "resolved" && entry.status !== "cancelled") throw new Error(`[agui] submitInterruptResponses: invalid status "${entry.status}" for interrupt ${entry.interruptId}`);
141
+ if (responsesById.has(entry.interruptId)) throw new Error(`[agui] submitInterruptResponses: duplicate response for interrupt ${entry.interruptId}`);
142
+ responsesById.set(entry.interruptId, entry);
143
+ }
144
+ const openIds = pending.interrupts.map((i) => i.id);
145
+ const missing = openIds.filter((id) => !responsesById.has(id));
146
+ if (missing.length > 0) throw new Error(`[agui] submitInterruptResponses: missing responses for open interrupts: ${missing.join(", ")}`);
147
+ const known = new Set(openIds);
148
+ const unknownIds = [...responsesById.keys()].filter((id) => !known.has(id));
149
+ if (unknownIds.length > 0) throw new Error(`[agui] submitInterruptResponses: unknown interrupt ids: ${unknownIds.join(", ")}`);
150
+ const now = Date.now();
151
+ for (const interrupt of pending.interrupts) {
152
+ if (!interrupt.expiresAt) continue;
153
+ const expiry = new Date(interrupt.expiresAt).getTime();
154
+ if (Number.isNaN(expiry)) throw new Error(`[agui] submitInterruptResponses: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`);
155
+ if (expiry <= now) throw new Error(`[agui] submitInterruptResponses: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`);
156
+ }
157
+ const resume = openIds.map((id) => responsesById.get(id));
158
+ if (this.isRunningFlag) throw new Error("[agui] submitInterruptResponses: a run is already in progress");
159
+ this.clearPendingInterrupts(pending.messageId);
160
+ await this.startRun(pending.messageId, this.lastRunConfig, resume);
161
+ }
162
+ clearPendingInterrupts(messageId) {
163
+ let touched = false;
164
+ this.messages = this.messages.map((message) => {
165
+ if (message.id !== messageId || message.role !== "assistant") return message;
166
+ const assistant = message;
167
+ if (assistant.status?.type !== "requires-action" || assistant.status.reason !== "interrupt") return assistant;
168
+ touched = true;
169
+ const { interrupts: _drop, ...restAgui } = assistant.metadata.custom["agui"] ?? {};
170
+ const newCustom = { ...assistant.metadata.custom };
171
+ if (Object.keys(restAgui).length > 0) newCustom[AG_UI_METADATA_NAMESPACE] = restAgui;
172
+ else delete newCustom[AG_UI_METADATA_NAMESPACE];
173
+ return {
174
+ ...assistant,
175
+ status: {
176
+ type: "complete",
177
+ reason: "unknown"
178
+ },
179
+ metadata: {
180
+ ...assistant.metadata,
181
+ custom: newCustom
182
+ }
183
+ };
184
+ });
185
+ if (touched) this.notifyUpdate();
186
+ }
187
+ findMessageIdForToolCall(toolCallId) {
188
+ let fallbackMessageId;
189
+ for (let index = this.messages.length - 1; index >= 0; index--) {
190
+ const message = this.messages[index];
191
+ if (!message || message.role !== "assistant") continue;
192
+ for (const part of message.content) {
193
+ if (part.type !== "tool-call" || part.toolCallId !== toolCallId) continue;
194
+ if (!("result" in part) || part.result === void 0) return message.id;
195
+ fallbackMessageId ??= message.id;
196
+ }
197
+ }
198
+ return fallbackMessageId;
199
+ }
200
+ addToolResult(options) {
201
+ let updated = false;
202
+ let shouldResume = false;
203
+ this.messages = this.messages.map((message) => {
204
+ if (message.id !== options.messageId || message.role !== "assistant") return message;
205
+ const assistant = message;
206
+ let matchedToolCall = false;
207
+ const content = assistant.content.map((part) => {
208
+ if (part.type !== "tool-call" || part.toolCallId !== options.toolCallId) return part;
209
+ matchedToolCall = true;
210
+ return {
211
+ ...part,
212
+ result: options.result,
213
+ artifact: options.artifact,
214
+ isError: options.isError
215
+ };
216
+ });
217
+ if (!matchedToolCall) return message;
218
+ updated = true;
219
+ if (assistant.status?.type === "requires-action" && assistant.status.reason === "tool-calls" && content.every((part) => part.type !== "tool-call" || "result" in part && part.result !== void 0)) {
220
+ shouldResume = true;
221
+ return {
222
+ ...assistant,
223
+ content,
224
+ status: {
225
+ type: "complete",
226
+ reason: "unknown"
227
+ }
228
+ };
229
+ }
230
+ return {
231
+ ...assistant,
232
+ content
233
+ };
234
+ });
235
+ if (updated) {
236
+ this.notifyUpdate();
237
+ if (shouldResume) {
238
+ this.persistAssistantHistory(options.messageId);
239
+ if (!this.isRunningFlag) this.startRun(options.messageId, this.lastRunConfig).catch((error) => {
240
+ this.onError?.(error instanceof Error ? error : new Error(String(error)));
241
+ });
242
+ }
243
+ }
244
+ }
245
+ applyExternalMessages(messages) {
246
+ this.assistantHistoryParents.clear();
247
+ this.messages = [...messages];
248
+ this.recordedHistoryIds.clear();
249
+ for (const message of this.messages) this.recordedHistoryIds.add(message.id);
250
+ this.notifyUpdate();
251
+ }
252
+ loadExternalState(state) {
253
+ this.stateSnapshot = state;
254
+ this.notifyUpdate();
255
+ }
256
+ async startRun(parentId, runConfig, resume) {
257
+ const normalizedRunConfig = runConfig ?? {};
258
+ this.lastRunConfig = normalizedRunConfig;
259
+ this.resetHead(parentId);
260
+ const historicalMessages = [...this.messages];
261
+ const runId = generateId();
262
+ this.pendingError = null;
263
+ const input = this.buildRunInput(runId, normalizedRunConfig, historicalMessages, resume);
264
+ const assistantParentId = parentId ?? this.messages.at(-1)?.id ?? null;
265
+ let assistantMessageId;
266
+ const ensureAssistant = () => {
267
+ if (assistantMessageId) return assistantMessageId;
268
+ const created = this.insertAssistantPlaceholder();
269
+ assistantMessageId = created;
270
+ this.markPendingAssistantHistory(created, assistantParentId ?? null);
271
+ return created;
272
+ };
273
+ const aggregator = new RunAggregator({
274
+ showThinking: this.showThinking,
275
+ logger: this.logger,
276
+ emit: (update) => {
277
+ const resolved = this.updateAssistantMessage(ensureAssistant(), update);
278
+ if (resolved !== assistantMessageId) assistantMessageId = resolved;
279
+ },
280
+ onServerMessageId: (serverId) => {
281
+ const placeholder = ensureAssistant();
282
+ if (placeholder === serverId) return;
283
+ this.reassignAssistantId(placeholder, serverId);
284
+ assistantMessageId = serverId;
285
+ }
286
+ });
287
+ const dispatch = (event) => this.handleEvent(aggregator, event);
288
+ const abortController = new AbortController();
289
+ const abortSignal = abortController.signal;
290
+ this.abortController = abortController;
291
+ abortSignal.addEventListener("abort", () => {
292
+ dispatch({ type: "RUN_CANCELLED" });
293
+ this.finishRun(abortController);
294
+ this.onCancel?.();
295
+ }, { once: true });
296
+ const subscriber = createAgUiSubscriber({
297
+ dispatch,
298
+ runId,
299
+ logger: this.logger,
300
+ onRunFailed: (error) => {
301
+ this.pendingError = error;
302
+ this.onError?.(error);
303
+ }
304
+ });
305
+ aggregator.handle({
306
+ type: "RUN_STARTED",
307
+ runId
308
+ });
309
+ this.setRunning(true);
310
+ try {
311
+ try {
312
+ this.agent.messages = input.messages;
313
+ this.agent.threadId = input.threadId;
314
+ this.agent.state = input.state ?? null;
315
+ } catch {}
316
+ await this.agent.runAgent(input, subscriber, { signal: abortSignal });
317
+ } catch (error) {
318
+ if (!abortSignal.aborted) {
319
+ const err = error instanceof Error ? error : new Error(String(error));
320
+ dispatch({
321
+ type: "RUN_ERROR",
322
+ message: err.message
323
+ });
324
+ this.onError?.(err);
325
+ this.pendingError = this.pendingError ?? err;
326
+ }
327
+ } finally {
328
+ this.finishRun(abortController);
329
+ }
330
+ if (this.pendingError) {
331
+ const err = this.pendingError;
332
+ this.pendingError = null;
333
+ throw err;
334
+ }
335
+ }
336
+ buildRunInput(runId, runConfig, historyMessages, resume) {
337
+ const threadId = this.agent.threadId || "main";
338
+ const messages = toAgUiMessages(historyMessages ?? this.messages);
339
+ const context = this.runtime?.thread.getModelContext();
340
+ return {
341
+ threadId,
342
+ runId,
343
+ state: this.stateSnapshot ?? null,
344
+ messages,
345
+ tools: toAgUiTools(context?.tools),
346
+ context: context?.system ? [{
347
+ description: "system",
348
+ value: context.system
349
+ }] : [],
350
+ forwardedProps: {
351
+ ...context?.callSettings ?? {},
352
+ ...context?.config ?? {},
353
+ ...runConfig?.custom ? { runConfig: runConfig.custom } : {}
354
+ },
355
+ ...resume !== void 0 ? { resume } : {}
356
+ };
357
+ }
358
+ installResumeShim() {
359
+ const agent = this.agent;
360
+ if (agent[symbolResumeShim]) return;
361
+ agent[symbolResumeShim] = true;
362
+ const original = Object.hasOwn(agent, "prepareRunAgentInput") ? agent.prepareRunAgentInput : Object.getPrototypeOf(agent)?.prepareRunAgentInput;
363
+ if (typeof original !== "function") return;
364
+ agent.prepareRunAgentInput = function(params) {
365
+ const input = original.call(this, params);
366
+ if (params?.resume !== void 0 && input && typeof input === "object") return {
367
+ ...input,
368
+ resume: params.resume
369
+ };
370
+ return input;
371
+ };
372
+ }
373
+ setRunning(running) {
374
+ this.isRunningFlag = running;
375
+ this.notifyUpdate();
376
+ }
377
+ finishRun(controller) {
378
+ if (this.abortController === controller) this.abortController = null;
379
+ this.setRunning(false);
380
+ }
381
+ insertAssistantPlaceholder() {
382
+ const id = generateOptimisticId();
383
+ const assistant = {
384
+ id,
385
+ role: "assistant",
386
+ createdAt: /* @__PURE__ */ new Date(),
387
+ status: { type: "running" },
388
+ content: [],
389
+ metadata: {
390
+ unstable_state: this.stateSnapshot ?? null,
391
+ unstable_annotations: [],
392
+ unstable_data: [],
393
+ steps: [],
394
+ custom: {}
395
+ }
396
+ };
397
+ this.messages = [...this.messages, assistant];
398
+ this.notifyUpdate();
399
+ return id;
400
+ }
401
+ reassignAssistantId(oldId, newId) {
402
+ if (oldId === newId) return;
403
+ if (this.messages.some((m) => m.id === newId)) {
404
+ this.logger.debug?.("[agui] reassignAssistantId: server id already present in messages, dropping placeholder", {
405
+ oldId,
406
+ newId
407
+ });
408
+ this.messages = this.messages.filter((m) => m.id !== oldId);
409
+ } else this.messages = this.messages.map((m) => m.id === oldId ? {
410
+ ...m,
411
+ id: newId
412
+ } : m);
413
+ const pendingParent = this.assistantHistoryParents.get(oldId);
414
+ if (pendingParent !== void 0) {
415
+ this.assistantHistoryParents.delete(oldId);
416
+ if (!this.assistantHistoryParents.has(newId)) this.assistantHistoryParents.set(newId, pendingParent);
417
+ }
418
+ if (this.recordedHistoryIds.has(oldId)) {
419
+ this.recordedHistoryIds.delete(oldId);
420
+ this.recordedHistoryIds.add(newId);
421
+ }
422
+ this.notifyUpdate();
423
+ }
424
+ updateAssistantMessage(messageId, update) {
425
+ let touched = false;
426
+ let latestStatus;
427
+ this.messages = this.messages.map((message) => {
428
+ if (message.id !== messageId || message.role !== "assistant") return message;
429
+ touched = true;
430
+ const assistant = message;
431
+ const metadata = update.metadata ? this.mergeAssistantMetadata(assistant.metadata, update.metadata) : assistant.metadata;
432
+ latestStatus = update.status ?? assistant.status;
433
+ return {
434
+ ...assistant,
435
+ content: update.content ?? assistant.content,
436
+ status: latestStatus,
437
+ metadata
438
+ };
439
+ });
440
+ if (!touched) return messageId;
441
+ let resolvedMessageId = messageId;
442
+ if (latestStatus !== void 0 && latestStatus.type !== "running" && isOptimisticId(messageId)) {
443
+ const stableId = generateId();
444
+ this.reassignAssistantId(messageId, stableId);
445
+ resolvedMessageId = stableId;
446
+ } else this.notifyUpdate();
447
+ if (this.isPersistableStatus(latestStatus)) this.persistAssistantHistory(resolvedMessageId);
448
+ return resolvedMessageId;
449
+ }
450
+ mergeAssistantMetadata(current, incoming) {
451
+ const annotations = incoming.unstable_annotations ? [...current.unstable_annotations, ...incoming.unstable_annotations] : current.unstable_annotations;
452
+ const data = incoming.unstable_data ? [...current.unstable_data, ...incoming.unstable_data] : current.unstable_data;
453
+ const steps = incoming.steps ? [...current.steps, ...incoming.steps] : current.steps;
454
+ return {
455
+ unstable_state: incoming.unstable_state !== void 0 ? incoming.unstable_state : current.unstable_state,
456
+ unstable_annotations: annotations,
457
+ unstable_data: data,
458
+ steps,
459
+ ...incoming.timing ? { timing: incoming.timing } : {},
460
+ custom: incoming.custom ? {
461
+ ...current.custom,
462
+ ...incoming.custom
463
+ } : current.custom
464
+ };
465
+ }
466
+ handleEvent(aggregator, event) {
467
+ switch (event.type) {
468
+ case "STATE_SNAPSHOT":
469
+ this.stateSnapshot = event.snapshot;
470
+ this.notifyUpdate();
471
+ return;
472
+ case "STATE_DELTA":
473
+ this.logger.debug?.("[agui] state delta event ignored", event.delta);
474
+ return;
475
+ case "MESSAGES_SNAPSHOT":
476
+ this.importMessagesSnapshot(event.messages);
477
+ return;
478
+ default: aggregator.handle(event);
479
+ }
480
+ }
481
+ importMessagesSnapshot(rawMessages) {
482
+ try {
483
+ const normalized = fromAgUiMessages(rawMessages);
484
+ const converted = [];
485
+ for (const message of normalized) try {
486
+ converted.push(fromThreadMessageLike(message, generateId(), FALLBACK_USER_STATUS));
487
+ } catch (error) {
488
+ this.logger.error?.("[agui] failed to import message from snapshot", error);
489
+ }
490
+ this.applyExternalMessages(converted);
491
+ } catch (error) {
492
+ this.logger.error?.("[agui] failed to import messages snapshot", error);
493
+ }
494
+ }
495
+ toThreadMessage(message) {
496
+ return fromThreadMessageLike(message, generateId(), FALLBACK_USER_STATUS);
497
+ }
498
+ resetHead(parentId) {
499
+ if (!parentId) {
500
+ if (this.messages.length) this.messages = [];
501
+ return;
502
+ }
503
+ const idx = this.messages.findIndex((message) => message.id === parentId);
504
+ if (idx === -1) return;
505
+ this.messages = this.messages.slice(0, idx + 1);
506
+ }
507
+ isTerminalStatus(status) {
508
+ return status?.type === "complete" || status?.type === "incomplete";
509
+ }
510
+ isPersistableStatus(status) {
511
+ if (this.isTerminalStatus(status)) return true;
512
+ return status?.type === "requires-action" && status.reason === "interrupt";
513
+ }
514
+ recordHistoryEntry(parentId, message) {
515
+ this.appendHistoryItem(parentId, message);
516
+ }
517
+ markPendingAssistantHistory(messageId, parentId) {
518
+ if (!this.history) return;
519
+ this.assistantHistoryParents.set(messageId, parentId);
520
+ }
521
+ persistAssistantHistory(messageId) {
522
+ if (!this.history) return;
523
+ const parentId = this.assistantHistoryParents.get(messageId);
524
+ if (parentId === void 0) return;
525
+ const message = this.messages.find((m) => m.id === messageId);
526
+ if (!message || message.role !== "assistant") return;
527
+ if (!this.isPersistableStatus(message.status)) return;
528
+ this.assistantHistoryParents.delete(messageId);
529
+ this.appendHistoryItem(parentId, message);
530
+ }
531
+ appendHistoryItem(parentId, message) {
532
+ if (!this.history || this.recordedHistoryIds.has(message.id)) return;
533
+ this.recordedHistoryIds.add(message.id);
534
+ this.history.append({
535
+ parentId,
536
+ message
537
+ }).catch((error) => {
538
+ this.recordedHistoryIds.delete(message.id);
539
+ this.logger.error?.("[agui] failed to append history entry", error);
540
+ });
541
+ }
542
+ };
543
+ //#endregion
544
+ export { AgUiThreadRuntimeCore };
545
+
631
546
  //# sourceMappingURL=AgUiThreadRuntimeCore.js.map