@assistant-ui/react-ag-ui 0.0.42 → 0.0.43
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 +10 -1
- 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 +20 -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,6 +2,7 @@
|
|
|
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";
|
|
7
8
|
|
|
@@ -605,6 +606,25 @@ export function fromAgUiMessages(
|
|
|
605
606
|
}
|
|
606
607
|
}
|
|
607
608
|
|
|
609
|
+
for (let i = 0; i < converted.length; i++) {
|
|
610
|
+
const message = converted[i]!;
|
|
611
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const hasPendingToolCall = message.content.some(
|
|
615
|
+
(part) =>
|
|
616
|
+
isObject(part) &&
|
|
617
|
+
part.type === "tool-call" &&
|
|
618
|
+
part.result === undefined,
|
|
619
|
+
);
|
|
620
|
+
if (hasPendingToolCall) {
|
|
621
|
+
converted[i] = {
|
|
622
|
+
...message,
|
|
623
|
+
status: getAutoStatus(false, false, false, true, undefined),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
608
628
|
return converted;
|
|
609
629
|
}
|
|
610
630
|
|
|
@@ -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),
|