@assistant-ui/react-ag-ui 0.0.42 → 0.0.44
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/agUiExtras.d.ts +7 -0
- package/dist/agUiExtras.d.ts.map +1 -0
- package/dist/agUiExtras.js +7 -0
- package/dist/agUiExtras.js.map +1 -0
- package/dist/hooks.d.ts +27 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +35 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +5 -1
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +62 -2
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +29 -2
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +1 -0
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +24 -4
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/types.d.ts +7 -2
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.d.ts +9 -0
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +7 -0
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +6 -5
- package/src/agUiExtras.ts +5 -0
- package/src/hooks.ts +42 -0
- package/src/index.ts +5 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +99 -3
- package/src/runtime/adapter/conversions.ts +69 -0
- package/src/runtime/adapter/run-aggregator.ts +30 -1
- package/src/runtime/types.ts +12 -0
- package/src/useAgUiRuntime.ts +19 -0
|
@@ -4,6 +4,7 @@ import { generateId, fromThreadMessageLike } from "@assistant-ui/core";
|
|
|
4
4
|
import type {
|
|
5
5
|
AddToolResultOptions,
|
|
6
6
|
AppendMessage,
|
|
7
|
+
CreateAppendMessage,
|
|
7
8
|
AssistantRuntime,
|
|
8
9
|
ChatModelRunOptions,
|
|
9
10
|
ChatModelRunResult,
|
|
@@ -174,6 +175,12 @@ export class AgUiThreadRuntimeCore {
|
|
|
174
175
|
async append(message: AppendMessage): Promise<void> {
|
|
175
176
|
const startRun = message.startRun ?? message.role === "user";
|
|
176
177
|
if (startRun) this.assertNoPendingInterrupts();
|
|
178
|
+
const threadMessageId = this.appendEntry(message);
|
|
179
|
+
if (!startRun) return;
|
|
180
|
+
await this.startRun(threadMessageId, message.runConfig);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private appendEntry(message: AppendMessage): string {
|
|
177
184
|
if (message.sourceId) {
|
|
178
185
|
this.messages = this.messages.filter(
|
|
179
186
|
(entry) => entry.id !== message.sourceId,
|
|
@@ -185,9 +192,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
185
192
|
this.messages = [...this.messages, threadMessage];
|
|
186
193
|
this.notifyUpdate();
|
|
187
194
|
this.recordHistoryEntry(message.parentId ?? null, threadMessage);
|
|
188
|
-
|
|
189
|
-
if (!startRun) return;
|
|
190
|
-
await this.startRun(threadMessage.id, message.runConfig);
|
|
195
|
+
return threadMessage.id;
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
async edit(message: AppendMessage): Promise<void> {
|
|
@@ -348,6 +353,97 @@ export class AgUiThreadRuntimeCore {
|
|
|
348
353
|
await this.startRun(pending.messageId, this.lastRunConfig, resume);
|
|
349
354
|
}
|
|
350
355
|
|
|
356
|
+
async steerAway(
|
|
357
|
+
message: CreateAppendMessage,
|
|
358
|
+
responses?: readonly AgUiResumeEntry[],
|
|
359
|
+
): Promise<void> {
|
|
360
|
+
const pending = this.getPendingInterrupts();
|
|
361
|
+
if (!pending) {
|
|
362
|
+
if (responses?.length) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
"[agui] steerAway: no pending interrupts on this thread",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
await this.append(this.toAppendMessage(message));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const resume = this.resolveSteerAwayResume(pending.interrupts, responses);
|
|
372
|
+
|
|
373
|
+
if (this.isRunningFlag) {
|
|
374
|
+
throw new Error("[agui] steerAway: a run is already in progress");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const normalized = this.toAppendMessage(message);
|
|
378
|
+
// Clear before appendEntry so the interrupted assistant is still in
|
|
379
|
+
// this.messages when its status is flipped; a parentId that points at an
|
|
380
|
+
// ancestor would otherwise truncate it away before it can be cleared.
|
|
381
|
+
this.clearPendingInterrupts(pending.messageId);
|
|
382
|
+
const threadMessageId = this.appendEntry(normalized);
|
|
383
|
+
await this.startRun(threadMessageId, normalized.runConfig, resume);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private resolveSteerAwayResume(
|
|
387
|
+
interrupts: readonly AgUiInterrupt[],
|
|
388
|
+
responses: readonly AgUiResumeEntry[] | undefined,
|
|
389
|
+
): AgUiResumeEntry[] {
|
|
390
|
+
const openIds = interrupts.map((interrupt) => interrupt.id);
|
|
391
|
+
const known = new Set(openIds);
|
|
392
|
+
const responsesById = new Map<string, AgUiResumeEntry>();
|
|
393
|
+
for (const entry of responses ?? []) {
|
|
394
|
+
if (!entry || typeof entry.interruptId !== "string") {
|
|
395
|
+
throw new Error(
|
|
396
|
+
"[agui] steerAway: every response must have an interruptId",
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
if (entry.status !== "resolved" && entry.status !== "cancelled") {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`[agui] steerAway: invalid status "${entry.status}" for interrupt ${entry.interruptId}`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (!known.has(entry.interruptId)) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`[agui] steerAway: unknown interrupt id ${entry.interruptId}`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
if (responsesById.has(entry.interruptId)) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`[agui] steerAway: duplicate response for interrupt ${entry.interruptId}`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
responsesById.set(entry.interruptId, entry);
|
|
415
|
+
}
|
|
416
|
+
return openIds.map(
|
|
417
|
+
(id) => responsesById.get(id) ?? { interruptId: id, status: "cancelled" },
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private toAppendMessage(message: CreateAppendMessage): AppendMessage {
|
|
422
|
+
if (typeof message === "string") {
|
|
423
|
+
return {
|
|
424
|
+
createdAt: new Date(),
|
|
425
|
+
parentId: this.messages.at(-1)?.id ?? null,
|
|
426
|
+
sourceId: null,
|
|
427
|
+
runConfig: {},
|
|
428
|
+
role: "user",
|
|
429
|
+
content: [{ type: "text", text: message }],
|
|
430
|
+
attachments: [],
|
|
431
|
+
metadata: { custom: {} },
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
createdAt: message.createdAt ?? new Date(),
|
|
436
|
+
parentId: message.parentId ?? this.messages.at(-1)?.id ?? null,
|
|
437
|
+
sourceId: message.sourceId ?? null,
|
|
438
|
+
role: message.role ?? "user",
|
|
439
|
+
content: message.content,
|
|
440
|
+
attachments: message.attachments ?? [],
|
|
441
|
+
metadata: message.metadata ?? { custom: {} },
|
|
442
|
+
runConfig: message.runConfig ?? {},
|
|
443
|
+
startRun: message.startRun,
|
|
444
|
+
} as AppendMessage;
|
|
445
|
+
}
|
|
446
|
+
|
|
351
447
|
private clearPendingInterrupts(messageId: string): void {
|
|
352
448
|
let touched = false;
|
|
353
449
|
this.messages = this.messages.map((message) => {
|
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import type { InputContent } from "@ag-ui/client";
|
|
4
4
|
import type { ThreadMessageLike as CoreThreadMessageLike } from "@assistant-ui/core";
|
|
5
|
+
import { getAutoStatus } from "@assistant-ui/core/internal";
|
|
5
6
|
import { type Tool, toToolsJSONSchema } from "assistant-stream";
|
|
6
7
|
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
8
|
+
import {
|
|
9
|
+
AG_UI_METADATA_NAMESPACE,
|
|
10
|
+
type AgUiCustomMetadata,
|
|
11
|
+
} from "./run-aggregator";
|
|
12
|
+
import type { AgUiInterrupt } from "../types";
|
|
7
13
|
|
|
8
14
|
export type { InputContent };
|
|
9
15
|
|
|
@@ -439,6 +445,28 @@ function extractAssistantToolCalls(
|
|
|
439
445
|
return parts;
|
|
440
446
|
}
|
|
441
447
|
|
|
448
|
+
function validateInterrupts(value: unknown): AgUiInterrupt[] | undefined {
|
|
449
|
+
if (!Array.isArray(value)) return undefined;
|
|
450
|
+
const valid = value.filter(
|
|
451
|
+
(entry): entry is AgUiInterrupt =>
|
|
452
|
+
isObject(entry) &&
|
|
453
|
+
typeof entry.id === "string" &&
|
|
454
|
+
typeof entry.reason === "string",
|
|
455
|
+
);
|
|
456
|
+
return valid.length > 0 ? valid : undefined;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function readPersistedInterrupts(
|
|
460
|
+
metadata: unknown,
|
|
461
|
+
): AgUiInterrupt[] | undefined {
|
|
462
|
+
if (!isObject(metadata)) return undefined;
|
|
463
|
+
const custom = metadata.custom;
|
|
464
|
+
if (!isObject(custom)) return undefined;
|
|
465
|
+
const namespaced = custom[AG_UI_METADATA_NAMESPACE];
|
|
466
|
+
if (!isObject(namespaced)) return undefined;
|
|
467
|
+
return validateInterrupts(namespaced.interrupts);
|
|
468
|
+
}
|
|
469
|
+
|
|
442
470
|
function toAssistantSnapshotMessage(
|
|
443
471
|
rawMessage: Record<string, unknown>,
|
|
444
472
|
): CoreThreadMessageLike {
|
|
@@ -449,11 +477,23 @@ function toAssistantSnapshotMessage(
|
|
|
449
477
|
...toolCallParts,
|
|
450
478
|
];
|
|
451
479
|
const messageName = getString(rawMessage, "name");
|
|
480
|
+
const interrupts = readPersistedInterrupts(rawMessage.metadata);
|
|
452
481
|
return {
|
|
453
482
|
id: getString(rawMessage, "id") ?? generateId(),
|
|
454
483
|
role: "assistant",
|
|
455
484
|
content: assistantContent.length > 0 ? assistantContent : "",
|
|
456
485
|
...(messageName !== undefined ? { name: messageName } : {}),
|
|
486
|
+
...(interrupts
|
|
487
|
+
? {
|
|
488
|
+
metadata: {
|
|
489
|
+
custom: {
|
|
490
|
+
[AG_UI_METADATA_NAMESPACE]: {
|
|
491
|
+
interrupts,
|
|
492
|
+
} satisfies AgUiCustomMetadata,
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
}
|
|
496
|
+
: {}),
|
|
457
497
|
};
|
|
458
498
|
}
|
|
459
499
|
|
|
@@ -605,6 +645,35 @@ export function fromAgUiMessages(
|
|
|
605
645
|
}
|
|
606
646
|
}
|
|
607
647
|
|
|
648
|
+
for (let i = 0; i < converted.length; i++) {
|
|
649
|
+
const message = converted[i]!;
|
|
650
|
+
if (message.role !== "assistant") continue;
|
|
651
|
+
|
|
652
|
+
const hasInterrupt =
|
|
653
|
+
readPersistedInterrupts(message.metadata) !== undefined;
|
|
654
|
+
const hasPendingToolCall =
|
|
655
|
+
Array.isArray(message.content) &&
|
|
656
|
+
message.content.some(
|
|
657
|
+
(part) =>
|
|
658
|
+
isObject(part) &&
|
|
659
|
+
part.type === "tool-call" &&
|
|
660
|
+
part.result === undefined,
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
if (hasInterrupt || hasPendingToolCall) {
|
|
664
|
+
converted[i] = {
|
|
665
|
+
...message,
|
|
666
|
+
status: getAutoStatus(
|
|
667
|
+
false,
|
|
668
|
+
false,
|
|
669
|
+
hasInterrupt,
|
|
670
|
+
hasPendingToolCall,
|
|
671
|
+
undefined,
|
|
672
|
+
),
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
608
677
|
return converted;
|
|
609
678
|
}
|
|
610
679
|
|
|
@@ -334,7 +334,7 @@ export class RunAggregator {
|
|
|
334
334
|
(part) => part.kind === "tool-call" && part.toolCallId === id,
|
|
335
335
|
)
|
|
336
336
|
) {
|
|
337
|
-
this.
|
|
337
|
+
this.insertToolPart(id, parentMessageId);
|
|
338
338
|
}
|
|
339
339
|
const state: ToolCallState = {
|
|
340
340
|
toolCallId: id,
|
|
@@ -350,6 +350,35 @@ export class RunAggregator {
|
|
|
350
350
|
this.toolCalls.set(id, state);
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
// The message and tool-call channels are unordered on the wire, so anchor a
|
|
354
|
+
// tool call under its parentMessageId text part instead of appending it.
|
|
355
|
+
private insertToolPart(id: string, parentMessageId?: string) {
|
|
356
|
+
const entry = { kind: "tool-call", toolCallId: id } as const;
|
|
357
|
+
if (parentMessageId) {
|
|
358
|
+
const parentIndex = this.partOrder.findIndex(
|
|
359
|
+
(part) => part.kind === "text" && part.key === parentMessageId,
|
|
360
|
+
);
|
|
361
|
+
if (parentIndex !== -1) {
|
|
362
|
+
let insertAt = parentIndex + 1;
|
|
363
|
+
while (insertAt < this.partOrder.length) {
|
|
364
|
+
const part = this.partOrder[insertAt];
|
|
365
|
+
if (
|
|
366
|
+
part?.kind === "tool-call" &&
|
|
367
|
+
this.toolCalls.get(part.toolCallId)?.parentMessageId ===
|
|
368
|
+
parentMessageId
|
|
369
|
+
) {
|
|
370
|
+
insertAt += 1;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
this.partOrder.splice(insertAt, 0, entry);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
this.partOrder.push(entry);
|
|
380
|
+
}
|
|
381
|
+
|
|
353
382
|
private appendToolArgs(id: string | undefined, delta: string) {
|
|
354
383
|
const entry = id ? this.toolCalls.get(id) : undefined;
|
|
355
384
|
if (!entry) return;
|
package/src/runtime/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
CreateAppendMessage,
|
|
2
3
|
AttachmentAdapter,
|
|
3
4
|
DictationAdapter,
|
|
4
5
|
ExternalStoreSharedOptions,
|
|
@@ -85,6 +86,17 @@ export type AgUiResumeEntry = {
|
|
|
85
86
|
payload?: unknown;
|
|
86
87
|
};
|
|
87
88
|
|
|
89
|
+
export type AgUiRuntimeExtras = {
|
|
90
|
+
interrupts: readonly AgUiInterrupt[];
|
|
91
|
+
submitInterruptResponses: (
|
|
92
|
+
responses: readonly AgUiResumeEntry[],
|
|
93
|
+
) => Promise<void>;
|
|
94
|
+
steerAway: (
|
|
95
|
+
message: CreateAppendMessage,
|
|
96
|
+
responses?: readonly AgUiResumeEntry[],
|
|
97
|
+
) => Promise<void>;
|
|
98
|
+
};
|
|
99
|
+
|
|
88
100
|
export type AgUiRunFinishedOutcome =
|
|
89
101
|
| { type: "success" }
|
|
90
102
|
| { type: "interrupt"; interrupts: AgUiInterrupt[] };
|
package/src/useAgUiRuntime.ts
CHANGED
|
@@ -21,9 +21,21 @@ import type {
|
|
|
21
21
|
UseAgUiRuntimeOptions,
|
|
22
22
|
} from "./runtime/types";
|
|
23
23
|
import { AgUiThreadRuntimeCore } from "./runtime/AgUiThreadRuntimeCore";
|
|
24
|
+
import { agUiExtras } from "./agUiExtras";
|
|
25
|
+
|
|
26
|
+
const EMPTY_INTERRUPTS: readonly AgUiInterrupt[] = [];
|
|
24
27
|
|
|
25
28
|
export type AgUiAssistantRuntime = AssistantRuntime & {
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated Use the `useAgUiInterrupts()` hook instead. This method is kept
|
|
31
|
+
* for backward compatibility and will be removed in a future major release.
|
|
32
|
+
*/
|
|
26
33
|
unstable_getPendingInterrupts: () => readonly AgUiInterrupt[];
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated Use the `useAgUiSubmitInterruptResponses()` hook instead. This
|
|
36
|
+
* method is kept for backward compatibility and will be removed in a future
|
|
37
|
+
* major release.
|
|
38
|
+
*/
|
|
27
39
|
unstable_submitInterruptResponses: (
|
|
28
40
|
responses: readonly AgUiResumeEntry[],
|
|
29
41
|
) => Promise<void>;
|
|
@@ -127,6 +139,13 @@ export function useAgUiRuntime(
|
|
|
127
139
|
messages: core.getMessages(),
|
|
128
140
|
state: core.getState(),
|
|
129
141
|
isRunning: core.isRunning() || hasExecutingTools,
|
|
142
|
+
extras: agUiExtras.provide({
|
|
143
|
+
interrupts:
|
|
144
|
+
core.getPendingInterrupts()?.interrupts ?? EMPTY_INTERRUPTS,
|
|
145
|
+
submitInterruptResponses: (responses) =>
|
|
146
|
+
core.submitInterruptResponses(responses),
|
|
147
|
+
steerAway: (message, responses) => core.steerAway(message, responses),
|
|
148
|
+
}),
|
|
130
149
|
unstable_enableToolInvocations: true,
|
|
131
150
|
setToolStatuses,
|
|
132
151
|
onNew: (message: AppendMessage) => core.append(message),
|