@assistant-ui/react-ag-ui 0.0.54 → 0.0.56
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/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +23 -2
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +169 -35
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +13 -3
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +58 -26
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +5 -1
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +26 -1
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +3 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +9 -0
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +56 -0
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -0
- package/dist/runtime/adapter/tool-approval.js +244 -0
- package/dist/runtime/adapter/tool-approval.js.map +1 -0
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +2 -1
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/interrupt-internals.d.ts +8 -0
- package/dist/runtime/interrupt-internals.d.ts.map +1 -0
- package/dist/runtime/interrupt-internals.js +25 -0
- package/dist/runtime/interrupt-internals.js.map +1 -0
- package/dist/useAgUiRuntime.js +3 -0
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +8 -2
- package/src/runtime/AgUiThreadRuntimeCore.ts +314 -41
- package/src/runtime/adapter/conversions.content.test.ts +61 -3
- package/src/runtime/adapter/conversions.ts +83 -39
- package/src/runtime/adapter/run-aggregator.approval.test.ts +103 -0
- package/src/runtime/adapter/run-aggregator.ts +37 -1
- package/src/runtime/adapter/subscriber.ts +14 -0
- package/src/runtime/adapter/tool-approval.test.ts +681 -0
- package/src/runtime/adapter/tool-approval.ts +366 -0
- package/src/runtime/event-parser.ts +10 -1
- package/src/runtime/interrupt-internals.ts +35 -0
- package/src/useAgUiRuntime.approval.test.tsx +885 -0
- package/src/useAgUiRuntime.ts +4 -0
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type A2uiSurfaceState,
|
|
28
28
|
} from "@assistant-ui/react-generative-ui/a2ui";
|
|
29
29
|
import type { AgUiInterrupt } from "../types";
|
|
30
|
+
import { projectAgUiToolApprovals } from "./tool-approval";
|
|
30
31
|
import {
|
|
31
32
|
parseMcpToolCallResult,
|
|
32
33
|
readMcpAppResourceUri,
|
|
@@ -41,7 +42,7 @@ type AttachmentLike = {
|
|
|
41
42
|
};
|
|
42
43
|
|
|
43
44
|
type ThreadMessageLike = {
|
|
44
|
-
id
|
|
45
|
+
id?: string;
|
|
45
46
|
role: string;
|
|
46
47
|
content: unknown;
|
|
47
48
|
metadata?: unknown;
|
|
@@ -51,6 +52,8 @@ type ThreadMessageLike = {
|
|
|
51
52
|
attachments?: readonly AttachmentLike[];
|
|
52
53
|
};
|
|
53
54
|
|
|
55
|
+
type NormalizedThreadMessageLike = ThreadMessageLike & { id: string };
|
|
56
|
+
|
|
54
57
|
type AgUiToolCall = {
|
|
55
58
|
id: string;
|
|
56
59
|
type: "function";
|
|
@@ -60,11 +63,23 @@ type AgUiToolCall = {
|
|
|
60
63
|
export type AgUiMessage =
|
|
61
64
|
| {
|
|
62
65
|
id: string;
|
|
63
|
-
role:
|
|
66
|
+
role: "user";
|
|
64
67
|
content: string | InputContent[];
|
|
65
68
|
name?: string;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
id: string;
|
|
72
|
+
role: "assistant";
|
|
73
|
+
content: string;
|
|
74
|
+
name?: string;
|
|
66
75
|
toolCalls?: AgUiToolCall[];
|
|
67
76
|
}
|
|
77
|
+
| {
|
|
78
|
+
id: string;
|
|
79
|
+
role: "system" | "developer";
|
|
80
|
+
content: string;
|
|
81
|
+
name?: string;
|
|
82
|
+
}
|
|
68
83
|
| {
|
|
69
84
|
id: string;
|
|
70
85
|
role: "reasoning";
|
|
@@ -606,13 +621,27 @@ function toAssistantSnapshotMessage(
|
|
|
606
621
|
rawMessage: Record<string, unknown>,
|
|
607
622
|
): CoreThreadMessageLike {
|
|
608
623
|
const text = extractText(rawMessage.content);
|
|
609
|
-
const
|
|
624
|
+
const interrupts = readPersistedInterrupts(rawMessage.metadata);
|
|
625
|
+
const restoredToolCalls = extractAssistantToolCalls(rawMessage);
|
|
626
|
+
const approvals = projectAgUiToolApprovals(
|
|
627
|
+
interrupts,
|
|
628
|
+
new Set(
|
|
629
|
+
restoredToolCalls
|
|
630
|
+
.map((part) => part.toolCallId)
|
|
631
|
+
.filter((id): id is string => !!id),
|
|
632
|
+
),
|
|
633
|
+
);
|
|
634
|
+
const toolCallParts = restoredToolCalls.map((part) => {
|
|
635
|
+
const approval = part.toolCallId
|
|
636
|
+
? approvals.get(part.toolCallId)
|
|
637
|
+
: undefined;
|
|
638
|
+
return approval ? { ...part, approval } : part;
|
|
639
|
+
});
|
|
610
640
|
const assistantContent = [
|
|
611
641
|
...(text.length > 0 ? [{ type: "text" as const, text }] : []),
|
|
612
642
|
...toolCallParts,
|
|
613
643
|
];
|
|
614
644
|
const messageName = getString(rawMessage, "name");
|
|
615
|
-
const interrupts = readPersistedInterrupts(rawMessage.metadata);
|
|
616
645
|
return {
|
|
617
646
|
id: getString(rawMessage, "id") ?? generateId(),
|
|
618
647
|
role: "assistant",
|
|
@@ -960,7 +989,7 @@ export function fromAgUiMessages(
|
|
|
960
989
|
}
|
|
961
990
|
|
|
962
991
|
function convertAssistantMessage(
|
|
963
|
-
message:
|
|
992
|
+
message: NormalizedThreadMessageLike,
|
|
964
993
|
converted: AgUiMessage[],
|
|
965
994
|
): void {
|
|
966
995
|
const content = extractText(message.content);
|
|
@@ -1012,18 +1041,15 @@ function convertAssistantMessage(
|
|
|
1012
1041
|
return;
|
|
1013
1042
|
}
|
|
1014
1043
|
|
|
1015
|
-
|
|
1044
|
+
converted.push({
|
|
1016
1045
|
id: message.id,
|
|
1017
1046
|
role: "assistant",
|
|
1018
1047
|
content,
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
assistantMessage.toolCalls = toolCalls.map((entry) => entry.call);
|
|
1025
|
-
}
|
|
1026
|
-
converted.push(assistantMessage);
|
|
1048
|
+
...(message.name ? { name: message.name } : {}),
|
|
1049
|
+
...(toolCalls.length > 0
|
|
1050
|
+
? { toolCalls: toolCalls.map((entry) => entry.call) }
|
|
1051
|
+
: {}),
|
|
1052
|
+
});
|
|
1027
1053
|
|
|
1028
1054
|
for (const { id: toolCallId, part } of toolCalls) {
|
|
1029
1055
|
if (part.result === undefined) continue;
|
|
@@ -1035,36 +1061,30 @@ function convertAssistantMessage(
|
|
|
1035
1061
|
? part.result
|
|
1036
1062
|
: JSON.stringify(part.result);
|
|
1037
1063
|
|
|
1038
|
-
|
|
1064
|
+
converted.push({
|
|
1039
1065
|
id: part.unstable_toolMessageId ?? `${toolCallId}:tool`,
|
|
1040
1066
|
role: "tool",
|
|
1041
1067
|
content: resultContent,
|
|
1042
1068
|
toolCallId,
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
toolMessage.error = resultContent;
|
|
1046
|
-
}
|
|
1047
|
-
converted.push(toolMessage);
|
|
1069
|
+
...(part.isError ? { error: resultContent } : {}),
|
|
1070
|
+
});
|
|
1048
1071
|
}
|
|
1049
1072
|
}
|
|
1050
1073
|
|
|
1051
1074
|
function convertToolMessage(
|
|
1052
|
-
message:
|
|
1075
|
+
message: NormalizedThreadMessageLike,
|
|
1053
1076
|
converted: AgUiMessage[],
|
|
1054
1077
|
): void {
|
|
1055
1078
|
const content = extractText(message.content);
|
|
1056
1079
|
const toolCallId = message.toolCallId ?? generateId();
|
|
1057
1080
|
|
|
1058
|
-
|
|
1081
|
+
converted.push({
|
|
1059
1082
|
id: message.id,
|
|
1060
1083
|
role: "tool",
|
|
1061
1084
|
content,
|
|
1062
1085
|
toolCallId,
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
toolMessage.error = message.error;
|
|
1066
|
-
}
|
|
1067
|
-
converted.push(toolMessage);
|
|
1086
|
+
...(typeof message.error === "string" ? { error: message.error } : {}),
|
|
1087
|
+
});
|
|
1068
1088
|
}
|
|
1069
1089
|
|
|
1070
1090
|
export function toAgUiMessages(
|
|
@@ -1072,7 +1092,11 @@ export function toAgUiMessages(
|
|
|
1072
1092
|
): AgUiMessage[] {
|
|
1073
1093
|
const converted: AgUiMessage[] = [];
|
|
1074
1094
|
|
|
1075
|
-
for (const
|
|
1095
|
+
for (const rawMessage of messages) {
|
|
1096
|
+
const message: NormalizedThreadMessageLike = {
|
|
1097
|
+
...rawMessage,
|
|
1098
|
+
id: rawMessage.id ?? generateId(),
|
|
1099
|
+
};
|
|
1076
1100
|
const opaqueReasoning = readOpaqueReasoning(message.metadata);
|
|
1077
1101
|
const toOpaqueRecord = (entry: AgUiOpaqueReasoning): AgUiMessage => ({
|
|
1078
1102
|
id: entry.id,
|
|
@@ -1101,18 +1125,38 @@ export function toAgUiMessages(
|
|
|
1101
1125
|
continue;
|
|
1102
1126
|
}
|
|
1103
1127
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
message
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1128
|
+
if (message.role === "user") {
|
|
1129
|
+
converted.push({
|
|
1130
|
+
id: message.id,
|
|
1131
|
+
role: "user",
|
|
1132
|
+
content: buildUserContent(message),
|
|
1133
|
+
...(message.name ? { name: message.name } : {}),
|
|
1134
|
+
});
|
|
1135
|
+
flushTrailingOpaqueReasoning();
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (message.role === "system" || message.role === "developer") {
|
|
1140
|
+
converted.push({
|
|
1141
|
+
id: message.id,
|
|
1142
|
+
role: message.role,
|
|
1143
|
+
content: extractText(message.content),
|
|
1144
|
+
...(message.name ? { name: message.name } : {}),
|
|
1145
|
+
});
|
|
1146
|
+
flushTrailingOpaqueReasoning();
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (message.role === "reasoning") {
|
|
1151
|
+
converted.push({
|
|
1152
|
+
id: message.id,
|
|
1153
|
+
role: "reasoning",
|
|
1154
|
+
content: extractText(message.content),
|
|
1155
|
+
});
|
|
1156
|
+
flushTrailingOpaqueReasoning();
|
|
1157
|
+
continue;
|
|
1114
1158
|
}
|
|
1115
|
-
|
|
1159
|
+
|
|
1116
1160
|
flushTrailingOpaqueReasoning();
|
|
1117
1161
|
}
|
|
1118
1162
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import type { ChatModelRunResult } from "@assistant-ui/core";
|
|
3
|
+
import { RunAggregator } from "./run-aggregator";
|
|
4
|
+
import type { AgUiInterrupt } from "../types";
|
|
5
|
+
|
|
6
|
+
const GATE: AgUiInterrupt = {
|
|
7
|
+
id: "int-1",
|
|
8
|
+
reason: "tool_call",
|
|
9
|
+
toolCallId: "tc-1",
|
|
10
|
+
message: "Delete /tmp/a?",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const noopLogger = {
|
|
14
|
+
debug: vi.fn(),
|
|
15
|
+
info: vi.fn(),
|
|
16
|
+
warn: vi.fn(),
|
|
17
|
+
error: vi.fn(),
|
|
18
|
+
} as any;
|
|
19
|
+
|
|
20
|
+
const gatedAggregator = () => {
|
|
21
|
+
const emitted: ChatModelRunResult[] = [];
|
|
22
|
+
const aggregator = new RunAggregator({
|
|
23
|
+
showThinking: false,
|
|
24
|
+
logger: noopLogger,
|
|
25
|
+
emit: (result) => {
|
|
26
|
+
emitted.push(result);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
aggregator.handle({ type: "RUN_STARTED", runId: "run-1" });
|
|
31
|
+
aggregator.handle({
|
|
32
|
+
type: "TOOL_CALL_START",
|
|
33
|
+
toolCallId: "tc-1",
|
|
34
|
+
toolCallName: "delete_file",
|
|
35
|
+
});
|
|
36
|
+
aggregator.handle({
|
|
37
|
+
type: "TOOL_CALL_ARGS",
|
|
38
|
+
toolCallId: "tc-1",
|
|
39
|
+
delta: '{"path":"/tmp/a"}',
|
|
40
|
+
});
|
|
41
|
+
aggregator.handle({ type: "TOOL_CALL_END", toolCallId: "tc-1" });
|
|
42
|
+
aggregator.handle({
|
|
43
|
+
type: "RUN_FINISHED",
|
|
44
|
+
runId: "run-1",
|
|
45
|
+
outcome: { type: "interrupt", interrupts: [GATE] },
|
|
46
|
+
} as any);
|
|
47
|
+
|
|
48
|
+
return { aggregator, emitted, last: () => emitted[emitted.length - 1]! };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const approvalOf = (result: ChatModelRunResult) => {
|
|
52
|
+
const part = (result.content ?? []).find((p) => p.type === "tool-call");
|
|
53
|
+
return (part as { approval?: unknown } | undefined)?.approval;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe("RunAggregator tool approval projection", () => {
|
|
57
|
+
it("projects the gate while the run awaits the interrupt", () => {
|
|
58
|
+
const { last } = gatedAggregator();
|
|
59
|
+
|
|
60
|
+
expect(last().status).toMatchObject({
|
|
61
|
+
type: "requires-action",
|
|
62
|
+
reason: "interrupt",
|
|
63
|
+
});
|
|
64
|
+
expect(approvalOf(last())).toMatchObject({ id: "int-1" });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The gate cannot be clicked once the run is unresumable, but the interrupts
|
|
69
|
+
* stay on the message: the bespoke hooks read that payload, and dropping it
|
|
70
|
+
* would take a shipped surface away with the projection.
|
|
71
|
+
*/
|
|
72
|
+
it("drops the gate when the run then errors, keeping the interrupts", () => {
|
|
73
|
+
const { aggregator, last } = gatedAggregator();
|
|
74
|
+
|
|
75
|
+
aggregator.handle({ type: "RUN_ERROR", message: "stream dropped" } as any);
|
|
76
|
+
|
|
77
|
+
expect(last().status).toMatchObject({
|
|
78
|
+
type: "incomplete",
|
|
79
|
+
reason: "error",
|
|
80
|
+
});
|
|
81
|
+
expect(approvalOf(last())).toBeUndefined();
|
|
82
|
+
expect(
|
|
83
|
+
(last().metadata?.custom as Record<string, any> | undefined)?.agui
|
|
84
|
+
?.interrupts,
|
|
85
|
+
).toEqual([GATE]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("drops the gate when the run is then cancelled, keeping the interrupts", () => {
|
|
89
|
+
const { aggregator, last } = gatedAggregator();
|
|
90
|
+
|
|
91
|
+
aggregator.handle({ type: "RUN_CANCELLED" } as any);
|
|
92
|
+
|
|
93
|
+
expect(last().status).toMatchObject({
|
|
94
|
+
type: "incomplete",
|
|
95
|
+
reason: "cancelled",
|
|
96
|
+
});
|
|
97
|
+
expect(approvalOf(last())).toBeUndefined();
|
|
98
|
+
expect(
|
|
99
|
+
(last().metadata?.custom as Record<string, any> | undefined)?.agui
|
|
100
|
+
?.interrupts,
|
|
101
|
+
).toEqual([GATE]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type A2uiSurfaceState,
|
|
16
16
|
} from "@assistant-ui/react-generative-ui/a2ui";
|
|
17
17
|
import { readMcpAppResourceUri } from "../mcp-tool-result";
|
|
18
|
+
import { projectAgUiToolApprovals } from "./tool-approval";
|
|
18
19
|
import type { AgUiEvent, AgUiInterrupt } from "../types";
|
|
19
20
|
import type { Logger } from "../logger";
|
|
20
21
|
|
|
@@ -68,7 +69,11 @@ export type RunAggregatorOptions = {
|
|
|
68
69
|
* Collects AG-UI events into assistant-ui run snapshots that can be yielded from a ChatModelAdapter.
|
|
69
70
|
*
|
|
70
71
|
* The aggregator keeps a single assistant message worth of parts. Each incoming event updates the parts and
|
|
71
|
-
* emits a fresh snapshot through the provided `emit` callback.
|
|
72
|
+
* emits a fresh snapshot through the provided `emit` callback. `CUSTOM` events
|
|
73
|
+
* become canonical data parts; integration plumbing names such as
|
|
74
|
+
* `on_interrupt`, `PredictState`, `Exit`, `hook_error`, `state_update_error`,
|
|
75
|
+
* `system:*`, and `MultiAgentHandoff` are forwarded the same way, so apps only
|
|
76
|
+
* need data renderers for names they own.
|
|
72
77
|
*/
|
|
73
78
|
export class RunAggregator {
|
|
74
79
|
private readonly emitUpdate: Emit;
|
|
@@ -97,6 +102,7 @@ export class RunAggregator {
|
|
|
97
102
|
| { kind: "text"; key: string }
|
|
98
103
|
| { kind: "reasoning"; key: string }
|
|
99
104
|
| { kind: "tool-call"; toolCallId: string }
|
|
105
|
+
| { kind: "data"; name: string; value: unknown }
|
|
100
106
|
)[] = [];
|
|
101
107
|
private textPartCounter = 0;
|
|
102
108
|
private serverMessageIdReported = false;
|
|
@@ -207,6 +213,17 @@ export class RunAggregator {
|
|
|
207
213
|
break;
|
|
208
214
|
}
|
|
209
215
|
|
|
216
|
+
case "CUSTOM": {
|
|
217
|
+
this.activeTextMessageId = undefined;
|
|
218
|
+
this.partOrder.push({
|
|
219
|
+
kind: "data",
|
|
220
|
+
name: event.name,
|
|
221
|
+
value: event.value,
|
|
222
|
+
});
|
|
223
|
+
this.emit();
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
|
|
210
227
|
case "THINKING_START":
|
|
211
228
|
case "THINKING_TEXT_MESSAGE_START":
|
|
212
229
|
case "REASONING_START":
|
|
@@ -614,6 +631,14 @@ export class RunAggregator {
|
|
|
614
631
|
|
|
615
632
|
private emit(): void {
|
|
616
633
|
const snapshot: ThreadAssistantMessagePart[] = [];
|
|
634
|
+
// A run that ended incomplete can no longer be resumed, so a gate left over
|
|
635
|
+
// from an earlier interrupt outcome is unanswerable and must not stay
|
|
636
|
+
// projected. The interrupts themselves are kept on the message, since the
|
|
637
|
+
// bespoke hooks read that payload.
|
|
638
|
+
const approvals = projectAgUiToolApprovals(
|
|
639
|
+
this.status?.type === "requires-action" ? this.interrupts : undefined,
|
|
640
|
+
new Set(this.toolCalls.keys()),
|
|
641
|
+
);
|
|
617
642
|
|
|
618
643
|
for (const part of this.partOrder) {
|
|
619
644
|
if (part.kind === "reasoning") {
|
|
@@ -646,14 +671,25 @@ export class RunAggregator {
|
|
|
646
671
|
continue;
|
|
647
672
|
}
|
|
648
673
|
|
|
674
|
+
if (part.kind === "data") {
|
|
675
|
+
snapshot.push({
|
|
676
|
+
type: "data",
|
|
677
|
+
name: part.name,
|
|
678
|
+
data: part.value,
|
|
679
|
+
});
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
|
|
649
683
|
const entry = this.toolCalls.get(part.toolCallId);
|
|
650
684
|
if (!entry) continue;
|
|
685
|
+
const approval = approvals.get(entry.toolCallId);
|
|
651
686
|
const toolPart: ToolCallMessagePart = {
|
|
652
687
|
type: "tool-call",
|
|
653
688
|
toolCallId: entry.toolCallId,
|
|
654
689
|
toolName: entry.toolCallName,
|
|
655
690
|
args: (entry.parsedArgs ?? {}) as any,
|
|
656
691
|
argsText: entry.argsText,
|
|
692
|
+
...(approval ? { approval } : {}),
|
|
657
693
|
...(entry.result !== undefined ? { result: entry.result } : {}),
|
|
658
694
|
...(entry.modelContent !== undefined
|
|
659
695
|
? { modelContent: entry.modelContent }
|
|
@@ -35,6 +35,7 @@ type Subscriber = {
|
|
|
35
35
|
onCustomEvent?: (payload: { event: unknown }) => void;
|
|
36
36
|
onRawEvent?: (payload: { event: unknown }) => void;
|
|
37
37
|
onRunFinishedEvent?: (payload: { event: unknown }) => void;
|
|
38
|
+
onRunErrorEvent?: (payload: { event: unknown }) => void;
|
|
38
39
|
onRunFinalized?: () => void;
|
|
39
40
|
onRunFailed?: (payload: { error: Error }) => void;
|
|
40
41
|
};
|
|
@@ -146,11 +147,24 @@ export const createAgUiSubscriber = (
|
|
|
146
147
|
onCustomEvent: ({ event }) => dispatchIfValid(event, "CUSTOM"),
|
|
147
148
|
onRawEvent: ({ event }) => dispatchIfValid(event, "RAW"),
|
|
148
149
|
onRunFinishedEvent: ({ event }) => {
|
|
150
|
+
if (runFinishedDispatched) return;
|
|
149
151
|
const parsed = ensureEvent(event, "RUN_FINISHED", logger);
|
|
150
152
|
if (!parsed) return;
|
|
151
153
|
runFinishedDispatched = true;
|
|
152
154
|
dispatch(parsed);
|
|
153
155
|
},
|
|
156
|
+
onRunErrorEvent: ({ event }) => {
|
|
157
|
+
const parsed = ensureEvent(event, "RUN_ERROR", logger);
|
|
158
|
+
if (!parsed || parsed.type !== "RUN_ERROR") return;
|
|
159
|
+
runFinishedDispatched = true;
|
|
160
|
+
dispatch(parsed);
|
|
161
|
+
|
|
162
|
+
const error = Object.assign(
|
|
163
|
+
new Error(parsed.message ?? "Run failed"),
|
|
164
|
+
parsed.code === undefined ? {} : { code: parsed.code },
|
|
165
|
+
);
|
|
166
|
+
onRunFailed?.(error);
|
|
167
|
+
},
|
|
154
168
|
onRunFinalized: () => {
|
|
155
169
|
if (runFinishedDispatched) return;
|
|
156
170
|
dispatch({ type: "RUN_FINISHED", runId });
|