@assistant-ui/react-ag-ui 0.0.36 → 0.0.39
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 +5 -2
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +74 -13
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +95 -1
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +3 -2
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +13 -10
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +10 -0
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/types.d.ts +18 -13
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +4 -2
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +4 -4
- package/src/runtime/AgUiThreadRuntimeCore.ts +100 -22
- package/src/runtime/adapter/conversions.ts +112 -0
- package/src/runtime/adapter/run-aggregator.ts +14 -10
- package/src/runtime/adapter/subscriber.ts +15 -0
- package/src/runtime/types.ts +24 -11
- package/src/useAgUiRuntime.ts +9 -2
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
ThreadMessage,
|
|
14
14
|
ToolCallMessagePart,
|
|
15
15
|
} from "@assistant-ui/core";
|
|
16
|
-
import type {
|
|
16
|
+
import type { AbstractAgent } from "@ag-ui/client";
|
|
17
17
|
import jsonpatch, { type Operation } from "fast-json-patch";
|
|
18
18
|
import type { Logger } from "./logger";
|
|
19
19
|
import type { AgUiEvent, AgUiInterrupt, AgUiResumeEntry } from "./types";
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
AG_UI_METADATA_NAMESPACE,
|
|
23
23
|
type AgUiCustomMetadata,
|
|
24
24
|
RunAggregator,
|
|
25
|
+
tryParseJSON,
|
|
25
26
|
} from "./adapter/run-aggregator";
|
|
26
27
|
import {
|
|
27
28
|
fromAgUiMessages,
|
|
@@ -48,7 +49,7 @@ type ResumeRunConfig = {
|
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
type CoreOptions = {
|
|
51
|
-
agent:
|
|
52
|
+
agent: AbstractAgent;
|
|
52
53
|
logger: Logger;
|
|
53
54
|
showThinking: boolean;
|
|
54
55
|
onError?: (error: Error) => void;
|
|
@@ -60,7 +61,7 @@ type CoreOptions = {
|
|
|
60
61
|
const FALLBACK_USER_STATUS = { type: "complete", reason: "unknown" } as const;
|
|
61
62
|
|
|
62
63
|
export class AgUiThreadRuntimeCore {
|
|
63
|
-
private agent:
|
|
64
|
+
private agent: AbstractAgent;
|
|
64
65
|
private logger: Logger;
|
|
65
66
|
private showThinking: boolean;
|
|
66
67
|
private onError: ((error: Error) => void) | undefined;
|
|
@@ -214,6 +215,30 @@ export class AgUiThreadRuntimeCore {
|
|
|
214
215
|
);
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
async resumeInFlightRun(messages: readonly ThreadMessage[]): Promise<void> {
|
|
219
|
+
// Without a resume stream startRun would re-run the agent from scratch.
|
|
220
|
+
const resumeStream = this.history?.resume?.bind(this.history);
|
|
221
|
+
if (!resumeStream) {
|
|
222
|
+
const error = new Error(
|
|
223
|
+
"[agui] unstable_resume requires a ThreadHistoryAdapter with a resume() method; skipping resume after thread switch",
|
|
224
|
+
);
|
|
225
|
+
this.logger.error?.(error.message);
|
|
226
|
+
this.onError?.(error);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const parentId = messages.at(-1)?.id ?? null;
|
|
230
|
+
try {
|
|
231
|
+
await this.startRun(
|
|
232
|
+
parentId,
|
|
233
|
+
this.lastRunConfig,
|
|
234
|
+
undefined,
|
|
235
|
+
resumeStream,
|
|
236
|
+
);
|
|
237
|
+
} catch {
|
|
238
|
+
// startRun already reported via onError; don't reject the switch.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
217
242
|
private assertNoPendingInterrupts(): void {
|
|
218
243
|
if (!this.getPendingInterrupts()) return;
|
|
219
244
|
throw new Error(
|
|
@@ -396,27 +421,37 @@ export class AgUiThreadRuntimeCore {
|
|
|
396
421
|
this.maybeResumeAfterToolResults(options.messageId);
|
|
397
422
|
}
|
|
398
423
|
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// (status flips to requires-action only later, while a run is still draining)
|
|
403
|
-
// or after it. Idempotent: once the message is marked complete it no-ops.
|
|
424
|
+
// The continuation fires whether the frontend result lands before
|
|
425
|
+
// RUN_FINISHED (the status flips to requires-action only later, while the
|
|
426
|
+
// run is still draining) or after it.
|
|
404
427
|
private maybeResumeAfterToolResults(messageId: string): void {
|
|
428
|
+
if (!this.maybeCompleteAfterToolResults(messageId)) return;
|
|
429
|
+
|
|
430
|
+
if (this.isRunningFlag) {
|
|
431
|
+
// A run is still draining (RUN_FINISHED arrived but the stream has not
|
|
432
|
+
// closed). Defer until startRun's tail so we never start two runs.
|
|
433
|
+
this.pendingResumeMessageId = messageId;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
this.startResumeRun(messageId);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private maybeCompleteAfterToolResults(messageId: string): boolean {
|
|
405
440
|
const message = this.messages.find((m) => m.id === messageId);
|
|
406
|
-
if (!message || message.role !== "assistant") return;
|
|
441
|
+
if (!message || message.role !== "assistant") return false;
|
|
407
442
|
const assistant = message as ThreadAssistantMessage;
|
|
408
443
|
if (
|
|
409
444
|
assistant.status?.type !== "requires-action" ||
|
|
410
445
|
assistant.status.reason !== "tool-calls"
|
|
411
446
|
) {
|
|
412
|
-
return;
|
|
447
|
+
return false;
|
|
413
448
|
}
|
|
414
449
|
const allResolved = assistant.content.every(
|
|
415
450
|
(part) =>
|
|
416
451
|
part.type !== "tool-call" ||
|
|
417
452
|
("result" in part && part.result !== undefined),
|
|
418
453
|
);
|
|
419
|
-
if (!allResolved) return;
|
|
454
|
+
if (!allResolved) return false;
|
|
420
455
|
|
|
421
456
|
this.messages = this.messages.map((m) =>
|
|
422
457
|
m.id === messageId && m.role === "assistant"
|
|
@@ -428,14 +463,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
428
463
|
);
|
|
429
464
|
this.notifyUpdate();
|
|
430
465
|
this.persistAssistantHistory(messageId);
|
|
431
|
-
|
|
432
|
-
if (this.isRunningFlag) {
|
|
433
|
-
// A run is still draining (RUN_FINISHED arrived but the stream has not
|
|
434
|
-
// closed). Defer until startRun's tail so we never start two runs.
|
|
435
|
-
this.pendingResumeMessageId = messageId;
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
this.startResumeRun(messageId);
|
|
466
|
+
return true;
|
|
439
467
|
}
|
|
440
468
|
|
|
441
469
|
private startResumeRun(messageId: string): void {
|
|
@@ -722,6 +750,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
722
750
|
unstable_annotations: [],
|
|
723
751
|
unstable_data: [],
|
|
724
752
|
steps: [],
|
|
753
|
+
isOptimistic: true,
|
|
725
754
|
custom: {},
|
|
726
755
|
},
|
|
727
756
|
};
|
|
@@ -742,9 +771,11 @@ export class AgUiThreadRuntimeCore {
|
|
|
742
771
|
);
|
|
743
772
|
this.messages = this.messages.filter((m) => m.id !== oldId);
|
|
744
773
|
} else {
|
|
745
|
-
this.messages = this.messages.map((m) =>
|
|
746
|
-
m.id
|
|
747
|
-
|
|
774
|
+
this.messages = this.messages.map((m) => {
|
|
775
|
+
if (m.id !== oldId) return m;
|
|
776
|
+
const { isOptimistic: _, ...metadata } = m.metadata;
|
|
777
|
+
return { ...m, id: newId, metadata } as ThreadMessage;
|
|
778
|
+
});
|
|
748
779
|
}
|
|
749
780
|
|
|
750
781
|
const pendingParent = this.assistantHistoryParents.get(oldId);
|
|
@@ -869,6 +900,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
869
900
|
unstable_annotations: annotations,
|
|
870
901
|
unstable_data: data,
|
|
871
902
|
steps,
|
|
903
|
+
...(current.isOptimistic ? { isOptimistic: true } : {}),
|
|
872
904
|
...(incoming.timing ? { timing: incoming.timing } : {}),
|
|
873
905
|
custom: incoming.custom
|
|
874
906
|
? { ...current.custom, ...incoming.custom }
|
|
@@ -904,11 +936,57 @@ export class AgUiThreadRuntimeCore {
|
|
|
904
936
|
this.importMessagesSnapshot(event.messages);
|
|
905
937
|
return;
|
|
906
938
|
}
|
|
939
|
+
case "TOOL_CALL_RESULT": {
|
|
940
|
+
if (!aggregator.hasToolCall(event.toolCallId)) {
|
|
941
|
+
const messageId = this.findMessageIdForToolCall(event.toolCallId);
|
|
942
|
+
if (messageId !== undefined) {
|
|
943
|
+
this.applyCrossRunToolResult(messageId, event);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
aggregator.handle(event);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
907
950
|
default:
|
|
908
951
|
aggregator.handle(event);
|
|
909
952
|
}
|
|
910
953
|
}
|
|
911
954
|
|
|
955
|
+
private applyCrossRunToolResult(
|
|
956
|
+
messageId: string,
|
|
957
|
+
event: Extract<AgUiEvent, { type: "TOOL_CALL_RESULT" }>,
|
|
958
|
+
): void {
|
|
959
|
+
let updated = false;
|
|
960
|
+
this.messages = this.messages.map((message) => {
|
|
961
|
+
if (message.id !== messageId || message.role !== "assistant")
|
|
962
|
+
return message;
|
|
963
|
+
const assistant = message as ThreadAssistantMessage;
|
|
964
|
+
let matchedToolCall = false;
|
|
965
|
+
const content = assistant.content.map((part) => {
|
|
966
|
+
if (part.type !== "tool-call" || part.toolCallId !== event.toolCallId)
|
|
967
|
+
return part;
|
|
968
|
+
matchedToolCall = true;
|
|
969
|
+
return {
|
|
970
|
+
...part,
|
|
971
|
+
result: tryParseJSON(event.content ?? "") as ReadonlyJSONValue,
|
|
972
|
+
...(event.role === "tool" ? { isError: false } : {}),
|
|
973
|
+
...(event.messageId
|
|
974
|
+
? { unstable_toolMessageId: event.messageId }
|
|
975
|
+
: {}),
|
|
976
|
+
};
|
|
977
|
+
});
|
|
978
|
+
if (!matchedToolCall) return message;
|
|
979
|
+
updated = true;
|
|
980
|
+
return { ...assistant, content };
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
if (!updated) return;
|
|
984
|
+
this.notifyUpdate();
|
|
985
|
+
// Not maybeResumeAfterToolResults: the delivering run is already in
|
|
986
|
+
// flight, and a resume from the owner would reset the head past it.
|
|
987
|
+
this.maybeCompleteAfterToolResults(messageId);
|
|
988
|
+
}
|
|
989
|
+
|
|
912
990
|
private importMessagesSnapshot(rawMessages: readonly unknown[]) {
|
|
913
991
|
try {
|
|
914
992
|
const normalized = fromAgUiMessages(rawMessages, {
|
|
@@ -204,6 +204,115 @@ function toInputContent(
|
|
|
204
204
|
return null;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
type SnapshotAttachment = NonNullable<
|
|
208
|
+
CoreThreadMessageLike["attachments"]
|
|
209
|
+
>[number];
|
|
210
|
+
|
|
211
|
+
const mediaInputTypes = new Set(["image", "audio", "video", "document"]);
|
|
212
|
+
|
|
213
|
+
// Inverse of buildInputSource.
|
|
214
|
+
function inputSourceToString(
|
|
215
|
+
value: unknown,
|
|
216
|
+
): { value: string; mimeType?: string } | null {
|
|
217
|
+
if (!isObject(value)) return null;
|
|
218
|
+
const sourceValue = getString(value, "value");
|
|
219
|
+
if (sourceValue === undefined) return null;
|
|
220
|
+
const mimeType = getString(value, "mimeType");
|
|
221
|
+
const type = getString(value, "type");
|
|
222
|
+
if (type === "url") {
|
|
223
|
+
return { value: sourceValue, ...(mimeType !== undefined && { mimeType }) };
|
|
224
|
+
}
|
|
225
|
+
if (type === "data") {
|
|
226
|
+
const resolvedMimeType = mimeType ?? "application/octet-stream";
|
|
227
|
+
return {
|
|
228
|
+
value: `data:${resolvedMimeType};base64,${sourceValue}`,
|
|
229
|
+
mimeType: resolvedMimeType,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Mirrors @ag-ui/client's BackwardCompatibility_0_0_47 middleware.
|
|
236
|
+
function upgradeBinaryInputPart(
|
|
237
|
+
part: Record<string, unknown>,
|
|
238
|
+
): Record<string, unknown> | null {
|
|
239
|
+
const mimeType = getString(part, "mimeType");
|
|
240
|
+
if (mimeType === undefined) return null;
|
|
241
|
+
const data = getString(part, "data");
|
|
242
|
+
const url = getString(part, "url");
|
|
243
|
+
const source = data
|
|
244
|
+
? { type: "data", value: data, mimeType }
|
|
245
|
+
: url
|
|
246
|
+
? { type: "url", value: url, mimeType }
|
|
247
|
+
: null;
|
|
248
|
+
if (!source) return null;
|
|
249
|
+
const filename = getString(part, "filename");
|
|
250
|
+
return {
|
|
251
|
+
type: mediaTypeForMime(mimeType),
|
|
252
|
+
source,
|
|
253
|
+
...(filename && { metadata: { filename } }),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function toSnapshotAttachments(content: unknown): SnapshotAttachment[] {
|
|
258
|
+
if (!Array.isArray(content)) return [];
|
|
259
|
+
|
|
260
|
+
const attachments: SnapshotAttachment[] = [];
|
|
261
|
+
for (const rawPart of content) {
|
|
262
|
+
if (!isObject(rawPart)) continue;
|
|
263
|
+
const part =
|
|
264
|
+
getString(rawPart, "type") === "binary"
|
|
265
|
+
? upgradeBinaryInputPart(rawPart)
|
|
266
|
+
: rawPart;
|
|
267
|
+
if (!part) continue;
|
|
268
|
+
const type = getString(part, "type");
|
|
269
|
+
if (type === undefined || !mediaInputTypes.has(type)) continue;
|
|
270
|
+
const source = inputSourceToString(part.source);
|
|
271
|
+
if (!source) continue;
|
|
272
|
+
|
|
273
|
+
const filename = isObject(part.metadata)
|
|
274
|
+
? getString(part.metadata, "filename")
|
|
275
|
+
: undefined;
|
|
276
|
+
const id = attachments.length.toString();
|
|
277
|
+
|
|
278
|
+
if (type === "image") {
|
|
279
|
+
attachments.push({
|
|
280
|
+
id,
|
|
281
|
+
type: "image",
|
|
282
|
+
name: filename ?? "image",
|
|
283
|
+
...(source.mimeType !== undefined && { contentType: source.mimeType }),
|
|
284
|
+
status: { type: "complete" },
|
|
285
|
+
content: [
|
|
286
|
+
{
|
|
287
|
+
type: "image",
|
|
288
|
+
image: source.value,
|
|
289
|
+
...(filename !== undefined && { filename }),
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const mimeType = source.mimeType ?? "application/octet-stream";
|
|
297
|
+
attachments.push({
|
|
298
|
+
id,
|
|
299
|
+
type: type === "document" ? "document" : "file",
|
|
300
|
+
name: filename ?? "file",
|
|
301
|
+
contentType: mimeType,
|
|
302
|
+
status: { type: "complete" },
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: "file",
|
|
306
|
+
data: source.value,
|
|
307
|
+
mimeType,
|
|
308
|
+
...(filename !== undefined && { filename }),
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return attachments;
|
|
314
|
+
}
|
|
315
|
+
|
|
207
316
|
function buildUserContent(message: ThreadMessageLike): string | InputContent[] {
|
|
208
317
|
// File parts in message.content are intentionally skipped: the canonical
|
|
209
318
|
// binary payload for files always flows through message.attachments.
|
|
@@ -353,11 +462,14 @@ function toUserOrSystemSnapshotMessage(
|
|
|
353
462
|
rawMessage: Record<string, unknown>,
|
|
354
463
|
): CoreThreadMessageLike {
|
|
355
464
|
const messageName = getString(rawMessage, "name");
|
|
465
|
+
const attachments =
|
|
466
|
+
role === "user" ? toSnapshotAttachments(rawMessage.content) : [];
|
|
356
467
|
return {
|
|
357
468
|
id: getString(rawMessage, "id") ?? generateId(),
|
|
358
469
|
role,
|
|
359
470
|
content: extractText(rawMessage.content),
|
|
360
471
|
...(messageName !== undefined ? { name: messageName } : {}),
|
|
472
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
361
473
|
};
|
|
362
474
|
}
|
|
363
475
|
|
|
@@ -82,6 +82,10 @@ export class RunAggregator {
|
|
|
82
82
|
this.onServerMessageId = options.onServerMessageId;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
hasToolCall(toolCallId: string): boolean {
|
|
86
|
+
return this.toolCalls.has(toolCallId);
|
|
87
|
+
}
|
|
88
|
+
|
|
85
89
|
handle(event: AgUiEvent): void {
|
|
86
90
|
switch (event.type) {
|
|
87
91
|
case "RUN_STARTED": {
|
|
@@ -388,7 +392,7 @@ export class RunAggregator {
|
|
|
388
392
|
) {
|
|
389
393
|
this.partOrder.push({ kind: "tool-call", toolCallId: id });
|
|
390
394
|
}
|
|
391
|
-
entry.result =
|
|
395
|
+
entry.result = tryParseJSON(content);
|
|
392
396
|
entry.isError = isError;
|
|
393
397
|
if (toolMessageId) {
|
|
394
398
|
entry.toolMessageId = toolMessageId;
|
|
@@ -396,15 +400,6 @@ export class RunAggregator {
|
|
|
396
400
|
this.lastResolvedToolCallId = id;
|
|
397
401
|
}
|
|
398
402
|
|
|
399
|
-
private tryParseJSON(value: string): unknown {
|
|
400
|
-
if (!value) return value;
|
|
401
|
-
try {
|
|
402
|
-
return JSON.parse(value);
|
|
403
|
-
} catch {
|
|
404
|
-
return value;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
403
|
private emit(): void {
|
|
409
404
|
const snapshot: ThreadAssistantMessagePart[] = [];
|
|
410
405
|
|
|
@@ -542,3 +537,12 @@ export class RunAggregator {
|
|
|
542
537
|
this.emit();
|
|
543
538
|
}
|
|
544
539
|
}
|
|
540
|
+
|
|
541
|
+
export function tryParseJSON(value: string): unknown {
|
|
542
|
+
if (!value) return value;
|
|
543
|
+
try {
|
|
544
|
+
return JSON.parse(value);
|
|
545
|
+
} catch {
|
|
546
|
+
return value;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
@@ -38,6 +38,16 @@ type Subscriber = {
|
|
|
38
38
|
onRunFailed?: (payload: { error: Error }) => void;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
const isAbortError = (error: Error): boolean => {
|
|
42
|
+
if (error.name === "AbortError") return true;
|
|
43
|
+
const message = typeof error.message === "string" ? error.message : "";
|
|
44
|
+
return (
|
|
45
|
+
message === "Fetch is aborted" ||
|
|
46
|
+
message === "signal is aborted without reason" ||
|
|
47
|
+
message === "component unmounted"
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
41
51
|
const ensureEvent = (
|
|
42
52
|
raw: unknown,
|
|
43
53
|
type: AgUiEvent["type"],
|
|
@@ -143,7 +153,12 @@ export const createAgUiSubscriber = (
|
|
|
143
153
|
dispatch({ type: "RUN_FINISHED", runId });
|
|
144
154
|
},
|
|
145
155
|
onRunFailed: ({ error }) => {
|
|
156
|
+
runFinishedDispatched = true;
|
|
146
157
|
onRunFailed?.(error);
|
|
158
|
+
if (isAbortError(error)) {
|
|
159
|
+
dispatch({ type: "RUN_CANCELLED" } satisfies AgUiEvent);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
147
162
|
const message =
|
|
148
163
|
typeof error.message === "string" ? error.message : "Run failed";
|
|
149
164
|
const code =
|
package/src/runtime/types.ts
CHANGED
|
@@ -2,29 +2,42 @@ import type {
|
|
|
2
2
|
AttachmentAdapter,
|
|
3
3
|
DictationAdapter,
|
|
4
4
|
ExternalStoreSharedOptions,
|
|
5
|
+
ExternalStoreThreadListAdapter,
|
|
5
6
|
FeedbackAdapter,
|
|
6
7
|
RealtimeVoiceAdapter,
|
|
7
8
|
SpeechSynthesisAdapter,
|
|
8
9
|
ThreadHistoryAdapter,
|
|
9
10
|
ThreadMessage,
|
|
10
11
|
} from "@assistant-ui/core";
|
|
11
|
-
import type {
|
|
12
|
+
import type { AbstractAgent } from "@ag-ui/client";
|
|
12
13
|
import type { Logger } from "./logger";
|
|
13
14
|
import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* @experimental This API is still under active development and might change without notice.
|
|
18
|
+
*
|
|
19
|
+
* Same as ExternalStoreThreadListAdapter, except `onSwitchToThread` returns
|
|
20
|
+
* the messages (and optional state) to hydrate the thread with.
|
|
17
21
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
type SwitchToThreadResult = {
|
|
23
|
+
messages: readonly ThreadMessage[];
|
|
24
|
+
state?: ReadonlyJSONValue;
|
|
25
|
+
/**
|
|
26
|
+
* Set when the thread has a run in flight. The runtime resumes the run
|
|
27
|
+
* after hydrating, the same way `ThreadHistoryAdapter.load()` does when it
|
|
28
|
+
* returns `unstable_resume: true`.
|
|
29
|
+
*/
|
|
30
|
+
unstable_resume?: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type UseAgUiThreadListAdapter = Omit<
|
|
34
|
+
ExternalStoreThreadListAdapter,
|
|
35
|
+
"onSwitchToThread"
|
|
36
|
+
> & {
|
|
21
37
|
onSwitchToThread?:
|
|
22
|
-
| ((
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
state?: ReadonlyJSONValue;
|
|
26
|
-
}>
|
|
27
|
-
| { messages: readonly ThreadMessage[]; state?: ReadonlyJSONValue })
|
|
38
|
+
| ((
|
|
39
|
+
threadId: string,
|
|
40
|
+
) => Promise<SwitchToThreadResult> | SwitchToThreadResult)
|
|
28
41
|
| undefined;
|
|
29
42
|
};
|
|
30
43
|
|
|
@@ -42,7 +55,7 @@ export type UseAgUiRuntimeAdapters = {
|
|
|
42
55
|
};
|
|
43
56
|
|
|
44
57
|
export type UseAgUiRuntimeOptions = ExternalStoreSharedOptions & {
|
|
45
|
-
agent:
|
|
58
|
+
agent: AbstractAgent;
|
|
46
59
|
logger?: Partial<Logger>;
|
|
47
60
|
showThinking?: boolean;
|
|
48
61
|
onError?: (e: Error) => void;
|
package/src/useAgUiRuntime.ts
CHANGED
|
@@ -74,10 +74,11 @@ export function useAgUiRuntime(
|
|
|
74
74
|
const threadList = useMemo(() => {
|
|
75
75
|
if (!threadListAdapter) return undefined;
|
|
76
76
|
|
|
77
|
-
const { onSwitchToNewThread, onSwitchToThread } =
|
|
77
|
+
const { onSwitchToNewThread, onSwitchToThread, ...rest } =
|
|
78
|
+
threadListAdapter;
|
|
78
79
|
|
|
79
80
|
return {
|
|
80
|
-
|
|
81
|
+
...rest,
|
|
81
82
|
onSwitchToNewThread: onSwitchToNewThread
|
|
82
83
|
? async () => {
|
|
83
84
|
await onSwitchToNewThread();
|
|
@@ -86,11 +87,17 @@ export function useAgUiRuntime(
|
|
|
86
87
|
: undefined,
|
|
87
88
|
onSwitchToThread: onSwitchToThread
|
|
88
89
|
? async (threadId: string) => {
|
|
90
|
+
// Clear before the thread id flips, or the old messages leak
|
|
91
|
+
// into the new thread as a sibling branch.
|
|
92
|
+
core.applyExternalMessages([]);
|
|
89
93
|
const result = await onSwitchToThread(threadId);
|
|
90
94
|
core.applyExternalMessages(result.messages);
|
|
91
95
|
if (result.state) {
|
|
92
96
|
core.loadExternalState(result.state);
|
|
93
97
|
}
|
|
98
|
+
if (result.unstable_resume) {
|
|
99
|
+
void core.resumeInFlightRun(result.messages);
|
|
100
|
+
}
|
|
94
101
|
}
|
|
95
102
|
: undefined,
|
|
96
103
|
};
|