@assistant-ui/react-ag-ui 0.0.65 → 0.0.66
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.
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +19 -3
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +193 -39
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +11 -12
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +4 -16
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +80 -15
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/types.d.ts +22 -5
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js +8 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +76 -33
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +6 -6
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +380 -6
- package/src/runtime/AgUiThreadRuntimeCore.ts +314 -70
- package/src/runtime/adapter/conversions.content.test.ts +41 -0
- package/src/runtime/adapter/conversions.ts +16 -19
- package/src/runtime/adapter/run-aggregator.reasoning.test.ts +143 -0
- package/src/runtime/adapter/run-aggregator.ts +128 -33
- package/src/runtime/types.ts +25 -5
- package/src/useAgUiRuntime.external-store.test.tsx +51 -0
- package/src/useAgUiRuntime.thread-switch.test.tsx +587 -3
- package/src/useAgUiRuntime.ts +98 -44
|
@@ -38,17 +38,16 @@ import jsonpatch, { type Operation } from "fast-json-patch";
|
|
|
38
38
|
import type { Logger } from "./logger";
|
|
39
39
|
import { readMcpAppResourceUri } from "./mcp-tool-result";
|
|
40
40
|
import type {
|
|
41
|
+
AgUiCustomMetadata,
|
|
41
42
|
AgUiEvent,
|
|
42
43
|
AgUiInterrupt,
|
|
43
44
|
AgUiResumeEntry,
|
|
44
45
|
AgUiResumeTranscript,
|
|
45
46
|
} from "./types";
|
|
47
|
+
import { AG_UI_METADATA_NAMESPACE, MCP_APPS_ACTIVITY_TYPE } from "./types";
|
|
46
48
|
import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
47
49
|
import {
|
|
48
|
-
AG_UI_METADATA_NAMESPACE,
|
|
49
|
-
type AgUiCustomMetadata,
|
|
50
50
|
isPlainObject,
|
|
51
|
-
MCP_APPS_ACTIVITY_TYPE,
|
|
52
51
|
RunAggregator,
|
|
53
52
|
tryParseJSON,
|
|
54
53
|
} from "./adapter/run-aggregator";
|
|
@@ -83,6 +82,9 @@ const isResolvedToolCall = (
|
|
|
83
82
|
): boolean =>
|
|
84
83
|
part.type === "tool-call" && "result" in part && part.result !== undefined;
|
|
85
84
|
|
|
85
|
+
const isActivityPart = (part: ThreadAssistantMessage["content"][number]) =>
|
|
86
|
+
part.type === "data" && part.name.startsWith("agui-activity/");
|
|
87
|
+
|
|
86
88
|
type RunConfig = NonNullable<AppendMessage["runConfig"]>;
|
|
87
89
|
type ResumeStream = (
|
|
88
90
|
options: ChatModelRunOptions,
|
|
@@ -113,7 +115,7 @@ type CoreOptions = {
|
|
|
113
115
|
onError?: (error: Error) => void;
|
|
114
116
|
onCancel?: () => void;
|
|
115
117
|
history?: ThreadHistoryAdapter;
|
|
116
|
-
|
|
118
|
+
isThreadSwitching?: () => boolean;
|
|
117
119
|
};
|
|
118
120
|
|
|
119
121
|
const FALLBACK_USER_STATUS = { type: "complete", reason: "unknown" } as const;
|
|
@@ -157,7 +159,16 @@ export class AgUiThreadRuntimeCore {
|
|
|
157
159
|
private autoCancelPendingToolCalls: boolean | undefined;
|
|
158
160
|
private onError: ((error: Error) => void) | undefined;
|
|
159
161
|
private onCancel: (() => void) | undefined;
|
|
160
|
-
private readonly
|
|
162
|
+
private readonly listeners = new Set<() => void>();
|
|
163
|
+
private snapshot:
|
|
164
|
+
| {
|
|
165
|
+
isLoading: boolean;
|
|
166
|
+
isRunning: boolean;
|
|
167
|
+
messageRepository: ExportedMessageRepository;
|
|
168
|
+
state: ReadonlyJSONValue | undefined;
|
|
169
|
+
}
|
|
170
|
+
| undefined;
|
|
171
|
+
private readonly isThreadSwitching: (() => boolean) | undefined;
|
|
161
172
|
private readonly reportedErrors = new WeakSet<object>();
|
|
162
173
|
|
|
163
174
|
private runtime: AssistantRuntime | undefined;
|
|
@@ -190,6 +201,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
190
201
|
private readonly persistedHistoryIds = new Set<string>();
|
|
191
202
|
private readonly historyWrites = new Map<string, Promise<void>>();
|
|
192
203
|
private _isLoading = false;
|
|
204
|
+
private _historyImportPromise: Promise<void> | undefined;
|
|
205
|
+
private historyMutationGeneration = 0;
|
|
193
206
|
private _loadPromise: Promise<void> | undefined;
|
|
194
207
|
private _loadRequested = false;
|
|
195
208
|
private pendingResume: { owner: AbortController; messageId: string } | null =
|
|
@@ -206,10 +219,28 @@ export class AgUiThreadRuntimeCore {
|
|
|
206
219
|
this.onError = options.onError;
|
|
207
220
|
this.onCancel = options.onCancel;
|
|
208
221
|
this.history = options.history;
|
|
209
|
-
this.
|
|
222
|
+
this.isThreadSwitching = options.isThreadSwitching;
|
|
210
223
|
}
|
|
211
224
|
|
|
212
|
-
|
|
225
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
226
|
+
this.listeners.add(listener);
|
|
227
|
+
return () => this.listeners.delete(listener);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
getSnapshot = () =>
|
|
231
|
+
(this.snapshot ??= {
|
|
232
|
+
isLoading: this._isLoading,
|
|
233
|
+
isRunning: this.isRunningFlag,
|
|
234
|
+
messageRepository: this.session.export(),
|
|
235
|
+
state: this.stateSnapshot,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
private publishUpdate(): void {
|
|
239
|
+
this.snapshot = undefined;
|
|
240
|
+
for (const listener of this.listeners) listener();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
updateOptions(options: CoreOptions) {
|
|
213
244
|
this.agent = options.agent;
|
|
214
245
|
this.logger = options.logger;
|
|
215
246
|
this.showThinking = options.showThinking;
|
|
@@ -266,28 +297,38 @@ export class AgUiThreadRuntimeCore {
|
|
|
266
297
|
if (this._loadPromise) return this._loadPromise;
|
|
267
298
|
if (!this.history) return Promise.resolve();
|
|
268
299
|
|
|
269
|
-
const promise = this.history.load()
|
|
300
|
+
const promise = this.history.load().then((repo) => {
|
|
301
|
+
if (!repo) return;
|
|
302
|
+
|
|
303
|
+
this.session.applyExternalMessageRepository(repo);
|
|
304
|
+
this.assistantHistoryParents.clear();
|
|
305
|
+
this.snapshotHistoryIds.clear();
|
|
306
|
+
this.persistedHistoryIds.clear();
|
|
307
|
+
for (const { message } of repo.messages) {
|
|
308
|
+
this.persistedHistoryIds.add(message.id);
|
|
309
|
+
}
|
|
310
|
+
this.publishUpdate();
|
|
311
|
+
|
|
312
|
+
if (repo.state !== undefined) {
|
|
313
|
+
this.loadExternalState(repo.state);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return repo;
|
|
317
|
+
});
|
|
270
318
|
|
|
271
319
|
this._isLoading = true;
|
|
320
|
+
this._historyImportPromise = promise.then(
|
|
321
|
+
() => {
|
|
322
|
+
this._historyImportPromise = undefined;
|
|
323
|
+
},
|
|
324
|
+
() => {
|
|
325
|
+
this._historyImportPromise = undefined;
|
|
326
|
+
},
|
|
327
|
+
);
|
|
272
328
|
|
|
273
329
|
this._loadPromise = promise
|
|
274
330
|
.then(async (repo) => {
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
this.session.applyExternalMessageRepository(repo);
|
|
278
|
-
this.assistantHistoryParents.clear();
|
|
279
|
-
this.snapshotHistoryIds.clear();
|
|
280
|
-
this.persistedHistoryIds.clear();
|
|
281
|
-
for (const { message } of repo.messages) {
|
|
282
|
-
this.persistedHistoryIds.add(message.id);
|
|
283
|
-
}
|
|
284
|
-
this.notifyUpdate();
|
|
285
|
-
|
|
286
|
-
if (repo.state !== undefined) {
|
|
287
|
-
this.loadExternalState(repo.state);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (repo.unstable_resume) {
|
|
331
|
+
if (repo?.unstable_resume) {
|
|
291
332
|
const parentId = repo.headId ?? this.session.headId;
|
|
292
333
|
const resumeStream = this.history?.resume?.bind(this.history);
|
|
293
334
|
await this.startRun(
|
|
@@ -304,14 +345,60 @@ export class AgUiThreadRuntimeCore {
|
|
|
304
345
|
})
|
|
305
346
|
.finally(() => {
|
|
306
347
|
this._isLoading = false;
|
|
307
|
-
this.
|
|
348
|
+
this.publishUpdate();
|
|
308
349
|
});
|
|
309
350
|
|
|
310
|
-
this.
|
|
351
|
+
this.publishUpdate();
|
|
311
352
|
return this._loadPromise;
|
|
312
353
|
}
|
|
313
354
|
|
|
355
|
+
private waitForHistoryImport(): Promise<boolean> | undefined {
|
|
356
|
+
const historyImport = this._historyImportPromise;
|
|
357
|
+
if (!historyImport) return undefined;
|
|
358
|
+
const generation = this.historyMutationGeneration;
|
|
359
|
+
return historyImport.then(
|
|
360
|
+
() =>
|
|
361
|
+
generation === this.historyMutationGeneration &&
|
|
362
|
+
!this.isThreadSwitching?.(),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private deferUntilHistoryImported(callback: () => void): boolean {
|
|
367
|
+
const historyImport = this._historyImportPromise;
|
|
368
|
+
if (!historyImport) return false;
|
|
369
|
+
const generation = this.historyMutationGeneration;
|
|
370
|
+
void historyImport.then(() => {
|
|
371
|
+
if (
|
|
372
|
+
generation !== this.historyMutationGeneration ||
|
|
373
|
+
this.isThreadSwitching?.()
|
|
374
|
+
) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
callback();
|
|
379
|
+
} catch (error) {
|
|
380
|
+
this.reportError(error);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
|
|
314
386
|
async append(message: AppendMessage): Promise<void> {
|
|
387
|
+
const historyImport = this._historyImportPromise;
|
|
388
|
+
if (historyImport) {
|
|
389
|
+
const wasAtTail = message.parentId === this.session.headId;
|
|
390
|
+
const generation = this.historyMutationGeneration;
|
|
391
|
+
await historyImport;
|
|
392
|
+
if (
|
|
393
|
+
generation !== this.historyMutationGeneration ||
|
|
394
|
+
this.isThreadSwitching?.()
|
|
395
|
+
)
|
|
396
|
+
return;
|
|
397
|
+
if (wasAtTail) {
|
|
398
|
+
message = { ...message, parentId: this.session.headId };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
315
402
|
const startRun = message.startRun ?? message.role === "user";
|
|
316
403
|
let ownsThread = true;
|
|
317
404
|
if (startRun) {
|
|
@@ -331,7 +418,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
331
418
|
const parentId = this.session.headId;
|
|
332
419
|
this.session.addOrUpdateMessage(parentId, message);
|
|
333
420
|
this.session.switchToBranch(message.id);
|
|
334
|
-
this.
|
|
421
|
+
this.publishUpdate();
|
|
335
422
|
this.recordHistoryEntry(parentId, message);
|
|
336
423
|
}
|
|
337
424
|
|
|
@@ -353,7 +440,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
353
440
|
: this.session.headId;
|
|
354
441
|
this.session.addOrUpdateMessage(parentId, threadMessage);
|
|
355
442
|
this.session.switchToBranch(threadMessage.id);
|
|
356
|
-
this.
|
|
443
|
+
this.publishUpdate();
|
|
357
444
|
this.recordHistoryEntry(parentId, threadMessage);
|
|
358
445
|
return threadMessage.id;
|
|
359
446
|
}
|
|
@@ -366,12 +453,15 @@ export class AgUiThreadRuntimeCore {
|
|
|
366
453
|
parentId: string | null,
|
|
367
454
|
config: { runConfig?: RunConfig } = {},
|
|
368
455
|
): Promise<void> {
|
|
456
|
+
const historyImport = this.waitForHistoryImport();
|
|
457
|
+
if (historyImport && !(await historyImport)) return;
|
|
369
458
|
this.assertNoPendingInterrupts();
|
|
370
459
|
this.maybeAutoCancelPendingToolCalls();
|
|
371
460
|
await this.startRun(parentId, config.runConfig);
|
|
372
461
|
}
|
|
373
462
|
|
|
374
463
|
async cancel(): Promise<void> {
|
|
464
|
+
this.historyMutationGeneration++;
|
|
375
465
|
this.abortActiveRun();
|
|
376
466
|
}
|
|
377
467
|
|
|
@@ -413,6 +503,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
413
503
|
}
|
|
414
504
|
|
|
415
505
|
async resume(config: ResumeRunConfig): Promise<void> {
|
|
506
|
+
const historyImport = this.waitForHistoryImport();
|
|
507
|
+
if (historyImport && !(await historyImport)) return;
|
|
416
508
|
this.assertNoPendingInterrupts();
|
|
417
509
|
await this.startRun(
|
|
418
510
|
config.parentId,
|
|
@@ -423,6 +515,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
423
515
|
}
|
|
424
516
|
|
|
425
517
|
async resumeInFlightRun(messages: readonly ThreadMessage[]): Promise<void> {
|
|
518
|
+
const historyImport = this.waitForHistoryImport();
|
|
519
|
+
if (historyImport && !(await historyImport)) return;
|
|
426
520
|
// Without a resume stream startRun would re-run the agent from scratch.
|
|
427
521
|
const resumeStream = this.history?.resume?.bind(this.history);
|
|
428
522
|
if (!resumeStream) {
|
|
@@ -502,6 +596,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
502
596
|
async submitInterruptResponses(
|
|
503
597
|
responses: readonly AgUiResumeEntry[],
|
|
504
598
|
): Promise<void> {
|
|
599
|
+
const historyImport = this.waitForHistoryImport();
|
|
600
|
+
if (historyImport && !(await historyImport)) return;
|
|
505
601
|
const pending = this.getPendingInterrupts();
|
|
506
602
|
if (!pending) {
|
|
507
603
|
throw new Error(
|
|
@@ -597,6 +693,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
597
693
|
async respondToToolApproval(
|
|
598
694
|
options: RespondToToolApprovalOptions,
|
|
599
695
|
): Promise<void> {
|
|
696
|
+
const historyImport = this.waitForHistoryImport();
|
|
697
|
+
if (historyImport && !(await historyImport)) return;
|
|
600
698
|
const pending = this.getPendingInterrupts();
|
|
601
699
|
if (!pending) {
|
|
602
700
|
throw new Error(
|
|
@@ -648,7 +746,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
648
746
|
`[agui] respondToToolApproval: approval "${options.approvalId}" is already decided`,
|
|
649
747
|
);
|
|
650
748
|
}
|
|
651
|
-
this.
|
|
749
|
+
this.publishUpdate();
|
|
652
750
|
|
|
653
751
|
const assistant = this.session.tryGetMessage(pending.messageId)?.message as
|
|
654
752
|
| ThreadAssistantMessage
|
|
@@ -670,6 +768,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
670
768
|
message: CreateAppendMessage,
|
|
671
769
|
responses?: readonly AgUiResumeEntry[],
|
|
672
770
|
): Promise<void> {
|
|
771
|
+
const historyImport = this.waitForHistoryImport();
|
|
772
|
+
if (historyImport && !(await historyImport)) return;
|
|
673
773
|
const pending = this.getPendingInterrupts();
|
|
674
774
|
if (!pending) {
|
|
675
775
|
const pendingTools = this.getPendingToolCalls();
|
|
@@ -822,7 +922,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
822
922
|
};
|
|
823
923
|
});
|
|
824
924
|
if (touched) {
|
|
825
|
-
this.
|
|
925
|
+
this.publishUpdate();
|
|
826
926
|
}
|
|
827
927
|
}
|
|
828
928
|
|
|
@@ -857,10 +957,12 @@ export class AgUiThreadRuntimeCore {
|
|
|
857
957
|
});
|
|
858
958
|
return { ...assistant, content };
|
|
859
959
|
});
|
|
860
|
-
if (updated) this.
|
|
960
|
+
if (updated) this.publishUpdate();
|
|
861
961
|
}
|
|
862
962
|
|
|
863
963
|
addToolResult(options: AddToolResultOptions): void {
|
|
964
|
+
if (this.deferUntilHistoryImported(() => this.addToolResult(options)))
|
|
965
|
+
return;
|
|
864
966
|
// Core's ToolInvocationTracker resolves a nested call to the nested
|
|
865
967
|
// subagent message's id, which is not a session message; re-anchor on the
|
|
866
968
|
// top-level message that owns the tree so the update can land.
|
|
@@ -893,7 +995,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
893
995
|
});
|
|
894
996
|
|
|
895
997
|
if (!updated) return;
|
|
896
|
-
this.
|
|
998
|
+
this.publishUpdate();
|
|
897
999
|
this.maybeResumeAfterToolResults(sessionMessageId);
|
|
898
1000
|
}
|
|
899
1001
|
|
|
@@ -902,6 +1004,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
902
1004
|
toolCallId: string;
|
|
903
1005
|
interaction: Unstable_ToolInteraction;
|
|
904
1006
|
}): Promise<void> {
|
|
1007
|
+
const historyImport = this.waitForHistoryImport();
|
|
1008
|
+
if (historyImport && !(await historyImport)) return;
|
|
905
1009
|
const sessionMessageId = this.session.tryGetMessage(options.messageId)
|
|
906
1010
|
? options.messageId
|
|
907
1011
|
: this.findMessageIdForToolCall(options.toolCallId);
|
|
@@ -941,7 +1045,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
941
1045
|
);
|
|
942
1046
|
}
|
|
943
1047
|
|
|
944
|
-
this.
|
|
1048
|
+
this.publishUpdate();
|
|
945
1049
|
|
|
946
1050
|
const message = this.session.tryGetMessage(sessionMessageId)?.message;
|
|
947
1051
|
const history = this.history;
|
|
@@ -959,6 +1063,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
959
1063
|
}
|
|
960
1064
|
|
|
961
1065
|
sendA2uiAction(action: Record<string, unknown>): void {
|
|
1066
|
+
if (this.deferUntilHistoryImported(() => this.sendA2uiAction(action)))
|
|
1067
|
+
return;
|
|
962
1068
|
this.assertNoPendingInterrupts();
|
|
963
1069
|
this.maybeAutoCancelPendingToolCalls();
|
|
964
1070
|
const parentId = this.session.headId;
|
|
@@ -1028,7 +1134,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1028
1134
|
: current,
|
|
1029
1135
|
);
|
|
1030
1136
|
if (!updated) return false;
|
|
1031
|
-
this.
|
|
1137
|
+
this.publishUpdate();
|
|
1032
1138
|
this.persistAssistantHistory(messageId);
|
|
1033
1139
|
return true;
|
|
1034
1140
|
}
|
|
@@ -1108,12 +1214,12 @@ export class AgUiThreadRuntimeCore {
|
|
|
1108
1214
|
) {
|
|
1109
1215
|
this.pendingResume = null;
|
|
1110
1216
|
}
|
|
1111
|
-
this.
|
|
1217
|
+
this.publishUpdate();
|
|
1112
1218
|
}
|
|
1113
1219
|
|
|
1114
1220
|
loadExternalState(state: ReadonlyJSONValue): void {
|
|
1115
1221
|
this.stateSnapshot = state;
|
|
1116
|
-
this.
|
|
1222
|
+
this.publishUpdate();
|
|
1117
1223
|
}
|
|
1118
1224
|
|
|
1119
1225
|
setState(
|
|
@@ -1123,15 +1229,16 @@ export class AgUiThreadRuntimeCore {
|
|
|
1123
1229
|
): void {
|
|
1124
1230
|
this.stateSnapshot =
|
|
1125
1231
|
typeof next === "function" ? next(this.stateSnapshot) : next;
|
|
1126
|
-
this.
|
|
1232
|
+
this.publishUpdate();
|
|
1127
1233
|
}
|
|
1128
1234
|
|
|
1129
1235
|
resetState(): void {
|
|
1130
1236
|
this.stateSnapshot = undefined;
|
|
1131
|
-
this.
|
|
1237
|
+
this.publishUpdate();
|
|
1132
1238
|
}
|
|
1133
1239
|
|
|
1134
1240
|
resetThreadState(): void {
|
|
1241
|
+
this.historyMutationGeneration++;
|
|
1135
1242
|
const controller = this.abortController;
|
|
1136
1243
|
const activeRunAgent = this.activeRunAgent;
|
|
1137
1244
|
|
|
@@ -1152,7 +1259,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1152
1259
|
controller.abort();
|
|
1153
1260
|
}
|
|
1154
1261
|
} else {
|
|
1155
|
-
this.
|
|
1262
|
+
this.publishUpdate();
|
|
1156
1263
|
}
|
|
1157
1264
|
}
|
|
1158
1265
|
|
|
@@ -1162,6 +1269,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1162
1269
|
resume?: ResumeDispatch,
|
|
1163
1270
|
resumeStream?: ResumeStream,
|
|
1164
1271
|
): Promise<void> {
|
|
1272
|
+
if (this.isThreadSwitching?.()) return;
|
|
1165
1273
|
// A default AG-UI run supersedes the active run; the hook's opt-in message
|
|
1166
1274
|
// queue serializes sends instead. append supersedes earlier, before it links
|
|
1167
1275
|
// its message; this covers the entry points that start a run without one.
|
|
@@ -1553,7 +1661,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1553
1661
|
|
|
1554
1662
|
private setRunning(running: boolean) {
|
|
1555
1663
|
this.isRunningFlag = running;
|
|
1556
|
-
this.
|
|
1664
|
+
this.publishUpdate();
|
|
1557
1665
|
}
|
|
1558
1666
|
|
|
1559
1667
|
private finishRun(controller: AbortController | null) {
|
|
@@ -1584,7 +1692,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1584
1692
|
};
|
|
1585
1693
|
this.session.addOrUpdateMessage(parentId ?? this.session.headId, assistant);
|
|
1586
1694
|
this.session.switchToBranch(id);
|
|
1587
|
-
this.
|
|
1695
|
+
this.publishUpdate();
|
|
1588
1696
|
return id;
|
|
1589
1697
|
}
|
|
1590
1698
|
|
|
@@ -1650,7 +1758,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1650
1758
|
}
|
|
1651
1759
|
}
|
|
1652
1760
|
|
|
1653
|
-
this.
|
|
1761
|
+
this.publishUpdate();
|
|
1654
1762
|
return !collidesWithExisting;
|
|
1655
1763
|
}
|
|
1656
1764
|
|
|
@@ -1690,7 +1798,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1690
1798
|
this.reassignAssistantId(messageId, stableId);
|
|
1691
1799
|
resolvedMessageId = stableId;
|
|
1692
1800
|
} else {
|
|
1693
|
-
this.
|
|
1801
|
+
this.publishUpdate();
|
|
1694
1802
|
}
|
|
1695
1803
|
if (this.isPersistableStatus(latestStatus)) {
|
|
1696
1804
|
this.persistAssistantHistory(resolvedMessageId);
|
|
@@ -1760,6 +1868,128 @@ export class AgUiThreadRuntimeCore {
|
|
|
1760
1868
|
return changed ? content : next;
|
|
1761
1869
|
}
|
|
1762
1870
|
|
|
1871
|
+
// A snapshot built from user, assistant, and tool records alone carries no
|
|
1872
|
+
// reasoning or activity of its own, so the parts streamed locally would be
|
|
1873
|
+
// lost on every refresh; keep the local ones where they streamed when the
|
|
1874
|
+
// snapshot has none of that kind, matching the merge rule of @ag-ui/client.
|
|
1875
|
+
private mergeSnapshotAssistantContent(
|
|
1876
|
+
previous: ThreadAssistantMessage["content"],
|
|
1877
|
+
next: ThreadAssistantMessage["content"],
|
|
1878
|
+
snapshotHasReasoning: boolean,
|
|
1879
|
+
snapshotHasActivity: boolean,
|
|
1880
|
+
): ThreadAssistantMessage["content"] {
|
|
1881
|
+
const shouldKeep = (part: ThreadAssistantMessage["content"][number]) =>
|
|
1882
|
+
part.type === "reasoning"
|
|
1883
|
+
? this.showThinking && !snapshotHasReasoning
|
|
1884
|
+
: isActivityPart(part) && !snapshotHasActivity;
|
|
1885
|
+
const kept = previous.filter(shouldKeep);
|
|
1886
|
+
const merged = this.reconcileSnapshotNestedContent(
|
|
1887
|
+
previous,
|
|
1888
|
+
this.preserveToolInteractions(previous, next),
|
|
1889
|
+
);
|
|
1890
|
+
if (kept.length === 0) return merged;
|
|
1891
|
+
|
|
1892
|
+
const mergedByType = new Map<string, number[]>();
|
|
1893
|
+
const mergedTools = new Map<string, number>();
|
|
1894
|
+
for (const [index, part] of merged.entries()) {
|
|
1895
|
+
if (part.type === "tool-call") {
|
|
1896
|
+
mergedTools.set(part.toolCallId, index);
|
|
1897
|
+
} else {
|
|
1898
|
+
const indexes = mergedByType.get(part.type) ?? [];
|
|
1899
|
+
indexes.push(index);
|
|
1900
|
+
mergedByType.set(part.type, indexes);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const previousOrdinals = new Map<string, number>();
|
|
1905
|
+
const insertions: (typeof kept)[] = Array.from(
|
|
1906
|
+
{ length: merged.length + 1 },
|
|
1907
|
+
() => [],
|
|
1908
|
+
);
|
|
1909
|
+
let predecessorIndex = -1;
|
|
1910
|
+
for (const part of previous) {
|
|
1911
|
+
if (shouldKeep(part)) {
|
|
1912
|
+
insertions[predecessorIndex + 1]!.push(part);
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
let matchedIndex: number | undefined;
|
|
1917
|
+
if (part.type === "tool-call") {
|
|
1918
|
+
matchedIndex = mergedTools.get(part.toolCallId);
|
|
1919
|
+
} else {
|
|
1920
|
+
const ordinal = previousOrdinals.get(part.type) ?? 0;
|
|
1921
|
+
matchedIndex = mergedByType.get(part.type)?.[ordinal];
|
|
1922
|
+
previousOrdinals.set(part.type, ordinal + 1);
|
|
1923
|
+
}
|
|
1924
|
+
if (matchedIndex !== undefined) predecessorIndex = matchedIndex;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
const result: ThreadAssistantMessage["content"][number][] = [];
|
|
1928
|
+
for (const [index, part] of merged.entries()) {
|
|
1929
|
+
result.push(...insertions[index]!, part);
|
|
1930
|
+
}
|
|
1931
|
+
result.push(...insertions[merged.length]!);
|
|
1932
|
+
return result;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
private reconcileSnapshotNestedContent(
|
|
1936
|
+
previous: ThreadAssistantMessage["content"],
|
|
1937
|
+
next: ThreadAssistantMessage["content"],
|
|
1938
|
+
): ThreadAssistantMessage["content"] {
|
|
1939
|
+
const nestedIds = new Set<string>();
|
|
1940
|
+
for (const part of previous) {
|
|
1941
|
+
if (part.type !== "tool-call" || !part.messages) continue;
|
|
1942
|
+
for (const nested of part.messages) {
|
|
1943
|
+
if (nested.role !== "assistant") continue;
|
|
1944
|
+
for (const call of iterateToolCallParts(nested.content)) {
|
|
1945
|
+
nestedIds.add(call.toolCallId);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
const flattened = new Map<string, ToolCallMessagePart>();
|
|
1951
|
+
for (const part of next) {
|
|
1952
|
+
if (part.type === "tool-call" && nestedIds.has(part.toolCallId)) {
|
|
1953
|
+
flattened.set(part.toolCallId, part);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
const carried =
|
|
1957
|
+
flattened.size === 0
|
|
1958
|
+
? previous
|
|
1959
|
+
: mapToolCallPartsDeep(previous, (part) => {
|
|
1960
|
+
const snapshot = flattened.get(part.toolCallId);
|
|
1961
|
+
if (!snapshot) return part;
|
|
1962
|
+
return {
|
|
1963
|
+
...part,
|
|
1964
|
+
...(snapshot.result !== undefined
|
|
1965
|
+
? { result: snapshot.result }
|
|
1966
|
+
: {}),
|
|
1967
|
+
...(snapshot.isError !== undefined
|
|
1968
|
+
? { isError: snapshot.isError }
|
|
1969
|
+
: {}),
|
|
1970
|
+
...(snapshot.artifact !== undefined
|
|
1971
|
+
? { artifact: snapshot.artifact }
|
|
1972
|
+
: {}),
|
|
1973
|
+
};
|
|
1974
|
+
}).content;
|
|
1975
|
+
const previousCalls = new Map<string, ToolCallMessagePart>();
|
|
1976
|
+
for (const part of carried) {
|
|
1977
|
+
if (part.type !== "tool-call") continue;
|
|
1978
|
+
previousCalls.set(part.toolCallId, part);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
return next.flatMap<ThreadAssistantMessage["content"][number]>((part) => {
|
|
1982
|
+
if (part.type === "tool-call" && flattened.has(part.toolCallId)) {
|
|
1983
|
+
return [];
|
|
1984
|
+
}
|
|
1985
|
+
if (part.type !== "tool-call") return [part];
|
|
1986
|
+
const prior = previousCalls.get(part.toolCallId);
|
|
1987
|
+
return prior?.messages === undefined
|
|
1988
|
+
? [part]
|
|
1989
|
+
: [{ ...part, messages: prior.messages }];
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1763
1993
|
private mergeAssistantMetadata(
|
|
1764
1994
|
current: ThreadAssistantMessage["metadata"],
|
|
1765
1995
|
incoming: NonNullable<ChatModelRunResult["metadata"]>,
|
|
@@ -1894,7 +2124,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1894
2124
|
};
|
|
1895
2125
|
});
|
|
1896
2126
|
}
|
|
1897
|
-
this.
|
|
2127
|
+
this.publishUpdate();
|
|
1898
2128
|
}
|
|
1899
2129
|
|
|
1900
2130
|
private applyCrossRunToolResult(
|
|
@@ -1965,7 +2195,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1965
2195
|
});
|
|
1966
2196
|
|
|
1967
2197
|
if (!updated) return;
|
|
1968
|
-
this.
|
|
2198
|
+
this.publishUpdate();
|
|
1969
2199
|
// Not maybeResumeAfterToolResults: the delivering run is already in
|
|
1970
2200
|
// flight, and a resume from the owner would reset the head past it.
|
|
1971
2201
|
this.maybeCompleteAfterToolResults(messageId);
|
|
@@ -2028,7 +2258,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2028
2258
|
});
|
|
2029
2259
|
|
|
2030
2260
|
if (!updated) return;
|
|
2031
|
-
this.
|
|
2261
|
+
this.publishUpdate();
|
|
2032
2262
|
this.maybeCompleteAfterToolResults(messageId);
|
|
2033
2263
|
}
|
|
2034
2264
|
|
|
@@ -2037,9 +2267,11 @@ export class AgUiThreadRuntimeCore {
|
|
|
2037
2267
|
activeAssistantId: string | undefined,
|
|
2038
2268
|
) {
|
|
2039
2269
|
try {
|
|
2040
|
-
const
|
|
2041
|
-
? this.session.tryGetMessage(activeAssistantId)
|
|
2270
|
+
const activeAssistantItem = activeAssistantId
|
|
2271
|
+
? this.session.tryGetMessage(activeAssistantId)
|
|
2042
2272
|
: undefined;
|
|
2273
|
+
const activeMessage = activeAssistantItem?.message;
|
|
2274
|
+
const activeAssistantParentId = activeAssistantItem?.parentId;
|
|
2043
2275
|
const activeAssistant =
|
|
2044
2276
|
activeMessage?.role === "assistant" ? activeMessage : undefined;
|
|
2045
2277
|
const normalized = fromAgUiMessages(rawMessages, {
|
|
@@ -2048,28 +2280,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
2048
2280
|
const converted: ThreadMessage[] = [];
|
|
2049
2281
|
for (const message of normalized) {
|
|
2050
2282
|
try {
|
|
2051
|
-
|
|
2052
|
-
message,
|
|
2053
|
-
generateId(),
|
|
2054
|
-
FALLBACK_USER_STATUS,
|
|
2283
|
+
converted.push(
|
|
2284
|
+
fromThreadMessageLike(message, generateId(), FALLBACK_USER_STATUS),
|
|
2055
2285
|
);
|
|
2056
|
-
const existing = this.session.tryGetMessage(
|
|
2057
|
-
convertedMessage.id,
|
|
2058
|
-
)?.message;
|
|
2059
|
-
if (
|
|
2060
|
-
convertedMessage.role === "assistant" &&
|
|
2061
|
-
existing?.role === "assistant"
|
|
2062
|
-
) {
|
|
2063
|
-
converted.push({
|
|
2064
|
-
...convertedMessage,
|
|
2065
|
-
content: this.preserveToolInteractions(
|
|
2066
|
-
existing.content,
|
|
2067
|
-
convertedMessage.content,
|
|
2068
|
-
),
|
|
2069
|
-
});
|
|
2070
|
-
} else {
|
|
2071
|
-
converted.push(convertedMessage);
|
|
2072
|
-
}
|
|
2073
2286
|
} catch (error) {
|
|
2074
2287
|
this.logger.error?.(
|
|
2075
2288
|
"[agui] failed to import message from snapshot",
|
|
@@ -2077,6 +2290,35 @@ export class AgUiThreadRuntimeCore {
|
|
|
2077
2290
|
);
|
|
2078
2291
|
}
|
|
2079
2292
|
}
|
|
2293
|
+
const snapshotHasReasoning = converted.some(
|
|
2294
|
+
(message) =>
|
|
2295
|
+
message.role === "assistant" &&
|
|
2296
|
+
message.content.some((part) => part.type === "reasoning"),
|
|
2297
|
+
);
|
|
2298
|
+
const snapshotHasActivity = converted.some(
|
|
2299
|
+
(message) =>
|
|
2300
|
+
message.role === "assistant" && message.content.some(isActivityPart),
|
|
2301
|
+
);
|
|
2302
|
+
for (let index = 0; index < converted.length; index++) {
|
|
2303
|
+
const convertedMessage = converted[index]!;
|
|
2304
|
+
const existing = this.session.tryGetMessage(
|
|
2305
|
+
convertedMessage.id,
|
|
2306
|
+
)?.message;
|
|
2307
|
+
if (
|
|
2308
|
+
convertedMessage.role === "assistant" &&
|
|
2309
|
+
existing?.role === "assistant"
|
|
2310
|
+
) {
|
|
2311
|
+
converted[index] = {
|
|
2312
|
+
...convertedMessage,
|
|
2313
|
+
content: this.mergeSnapshotAssistantContent(
|
|
2314
|
+
existing.content,
|
|
2315
|
+
convertedMessage.content,
|
|
2316
|
+
snapshotHasReasoning,
|
|
2317
|
+
snapshotHasActivity,
|
|
2318
|
+
),
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2080
2322
|
const snapshotContainsActiveAssistant = converted.some(
|
|
2081
2323
|
(message) => message.id === activeAssistant?.id,
|
|
2082
2324
|
);
|
|
@@ -2084,7 +2326,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
2084
2326
|
activeAssistant !== undefined &&
|
|
2085
2327
|
!snapshotContainsActiveAssistant &&
|
|
2086
2328
|
(activeAssistant.metadata.isOptimistic !== true ||
|
|
2087
|
-
converted.at(-1)?.role !== "assistant"
|
|
2329
|
+
converted.at(-1)?.role !== "assistant" ||
|
|
2330
|
+
(activeAssistantParentId !== undefined &&
|
|
2331
|
+
converted.at(-1)?.id === activeAssistantParentId));
|
|
2088
2332
|
if (preservesActiveAssistant) {
|
|
2089
2333
|
converted.push(activeAssistant);
|
|
2090
2334
|
}
|