@assistant-ui/react-ag-ui 0.0.56 → 0.0.58
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/README.md +16 -0
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +5 -17
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +133 -234
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +54 -25
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +31 -4
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +419 -123
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +9 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +4 -1
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +3 -1
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
- package/dist/runtime/adapter/tool-approval.js +11 -12
- package/dist/runtime/adapter/tool-approval.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +176 -45
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +42 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +12 -9
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +10 -10
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +105 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +190 -319
- package/src/runtime/adapter/conversions.ts +128 -36
- package/src/runtime/adapter/run-aggregator.approval.test.ts +128 -0
- package/src/runtime/adapter/run-aggregator.ts +670 -148
- package/src/runtime/adapter/subscriber.ts +10 -1
- package/src/runtime/adapter/tool-approval.test.ts +75 -0
- package/src/runtime/adapter/tool-approval.ts +17 -12
- package/src/runtime/event-parser.ts +216 -36
- package/src/runtime/types.ts +70 -12
- package/src/useAgUiRuntime.approval.test.tsx +34 -24
- package/src/useAgUiRuntime.history.test.tsx +76 -0
- package/src/useAgUiRuntime.toolDeferral.test.tsx +186 -0
- package/src/useAgUiRuntime.ts +12 -9
|
@@ -34,6 +34,9 @@ type Subscriber = {
|
|
|
34
34
|
onMessagesSnapshotEvent?: (payload: { event: unknown }) => void;
|
|
35
35
|
onCustomEvent?: (payload: { event: unknown }) => void;
|
|
36
36
|
onRawEvent?: (payload: { event: unknown }) => void;
|
|
37
|
+
onSubagentStartedEvent?: (payload: { event: unknown }) => void;
|
|
38
|
+
onSubagentFinishedEvent?: (payload: { event: unknown }) => void;
|
|
39
|
+
onSubagentErrorEvent?: (payload: { event: unknown }) => void;
|
|
37
40
|
onRunFinishedEvent?: (payload: { event: unknown }) => void;
|
|
38
41
|
onRunErrorEvent?: (payload: { event: unknown }) => void;
|
|
39
42
|
onRunFinalized?: () => void;
|
|
@@ -97,7 +100,7 @@ export const createAgUiSubscriber = (
|
|
|
97
100
|
// Typed handlers will receive this via the discriminated callbacks; avoid duplicates.
|
|
98
101
|
return;
|
|
99
102
|
}
|
|
100
|
-
const parsed = parseAgUiEvent(event);
|
|
103
|
+
const parsed = parseAgUiEvent(event, logger ? { logger } : undefined);
|
|
101
104
|
if (parsed) dispatch(parsed);
|
|
102
105
|
},
|
|
103
106
|
onTextMessageStartEvent: ({ event }) =>
|
|
@@ -146,6 +149,12 @@ export const createAgUiSubscriber = (
|
|
|
146
149
|
dispatchIfValid(event, "MESSAGES_SNAPSHOT"),
|
|
147
150
|
onCustomEvent: ({ event }) => dispatchIfValid(event, "CUSTOM"),
|
|
148
151
|
onRawEvent: ({ event }) => dispatchIfValid(event, "RAW"),
|
|
152
|
+
onSubagentStartedEvent: ({ event }) =>
|
|
153
|
+
dispatchIfValid(event, "SUBAGENT_STARTED"),
|
|
154
|
+
onSubagentFinishedEvent: ({ event }) =>
|
|
155
|
+
dispatchIfValid(event, "SUBAGENT_FINISHED"),
|
|
156
|
+
onSubagentErrorEvent: ({ event }) =>
|
|
157
|
+
dispatchIfValid(event, "SUBAGENT_ERROR"),
|
|
149
158
|
onRunFinishedEvent: ({ event }) => {
|
|
150
159
|
if (runFinishedDispatched) return;
|
|
151
160
|
const parsed = ensureEvent(event, "RUN_FINISHED", logger);
|
|
@@ -36,6 +36,16 @@ describe("projectAgUiToolApprovals", () => {
|
|
|
36
36
|
]);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
it("carries the gate's message as the approval prompt", () => {
|
|
40
|
+
expect([
|
|
41
|
+
...projectAgUiToolApprovals([
|
|
42
|
+
{ ...gate("int-1", "tc-1"), message: "Delete the release branch?" },
|
|
43
|
+
]),
|
|
44
|
+
]).toEqual([
|
|
45
|
+
["tc-1", { id: "int-1", prompt: "Delete the release branch?" }],
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
39
49
|
it("gates a batch whose every tool call is rendered", () => {
|
|
40
50
|
expect([
|
|
41
51
|
...projectAgUiToolApprovals(
|
|
@@ -679,3 +689,68 @@ describe("restored snapshots", () => {
|
|
|
679
689
|
}
|
|
680
690
|
});
|
|
681
691
|
});
|
|
692
|
+
|
|
693
|
+
describe("nested tool-call approval seams", () => {
|
|
694
|
+
const nestedContent = (
|
|
695
|
+
approval?: Record<string, unknown>,
|
|
696
|
+
): ThreadAssistantMessagePart[] => [
|
|
697
|
+
{
|
|
698
|
+
...(toolCall("tc-spawn") as any),
|
|
699
|
+
messages: [
|
|
700
|
+
{
|
|
701
|
+
id: "sub-1",
|
|
702
|
+
role: "assistant",
|
|
703
|
+
createdAt: new Date(),
|
|
704
|
+
status: { type: "requires-action", reason: "interrupt" },
|
|
705
|
+
content: [toolCall("tc-nested", approval ?? { id: "int-nested" })],
|
|
706
|
+
metadata: {
|
|
707
|
+
unstable_state: null,
|
|
708
|
+
unstable_annotations: [],
|
|
709
|
+
unstable_data: [],
|
|
710
|
+
steps: [],
|
|
711
|
+
custom: {},
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
} as ThreadAssistantMessagePart,
|
|
716
|
+
];
|
|
717
|
+
|
|
718
|
+
it("records a decision on a nested gate", () => {
|
|
719
|
+
const next = withToolApprovalDecision(nestedContent(), {
|
|
720
|
+
approvalId: "int-nested",
|
|
721
|
+
approved: true,
|
|
722
|
+
});
|
|
723
|
+
const nested = (next[0] as any).messages[0].content[0];
|
|
724
|
+
expect(nested.approval).toEqual({ id: "int-nested", approved: true });
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("reads a nested decision into the resume array", () => {
|
|
728
|
+
const decided = nestedContent({ id: "int-nested", approved: false });
|
|
729
|
+
const resume = buildToolApprovalResume(decided, [
|
|
730
|
+
{
|
|
731
|
+
id: "int-nested",
|
|
732
|
+
reason: "tool_call",
|
|
733
|
+
toolCallId: "tc-nested",
|
|
734
|
+
message: "Delete?",
|
|
735
|
+
},
|
|
736
|
+
]);
|
|
737
|
+
expect(resume).toEqual([
|
|
738
|
+
expect.objectContaining({
|
|
739
|
+
interruptId: "int-nested",
|
|
740
|
+
status: "resolved",
|
|
741
|
+
payload: { approved: false },
|
|
742
|
+
}),
|
|
743
|
+
]);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it("settles a nested gate that a cancellation resumes", () => {
|
|
747
|
+
const next = withSettledToolApprovals(nestedContent(), [
|
|
748
|
+
{ interruptId: "int-nested", status: "cancelled" },
|
|
749
|
+
]);
|
|
750
|
+
const nested = (next[0] as any).messages[0].content[0];
|
|
751
|
+
expect(nested.approval).toEqual({
|
|
752
|
+
id: "int-nested",
|
|
753
|
+
resolution: "cancelled",
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
});
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
iterateToolCallParts,
|
|
3
|
+
mapToolCallPartsDeep,
|
|
4
|
+
} from "@assistant-ui/core/internal";
|
|
1
5
|
import { buildResumeArray } from "@ag-ui/client";
|
|
2
6
|
import type {
|
|
3
7
|
RespondToToolApprovalOptions,
|
|
@@ -193,7 +197,9 @@ const isPending = (approval: ToolCallMessagePart["approval"]) =>
|
|
|
193
197
|
* render can never be decided through this seam, so the batch it belongs to
|
|
194
198
|
* could never be completed either: the decisions taken on its visible siblings
|
|
195
199
|
* would sit on the message while the run waited for a response that no click
|
|
196
|
-
* can produce. `boundToolCallIds`
|
|
200
|
+
* can produce. `boundToolCallIds` spans every call the run produced — root or
|
|
201
|
+
* nested on ToolCallMessagePart.messages — so only a gate naming a call the
|
|
202
|
+
* message renders nowhere leaves the batch bespoke.
|
|
197
203
|
*/
|
|
198
204
|
export const projectAgUiToolApprovals = (
|
|
199
205
|
interrupts: readonly AgUiInterrupt[] | undefined,
|
|
@@ -212,7 +218,13 @@ export const projectAgUiToolApprovals = (
|
|
|
212
218
|
const toolCallIds = interrupts.map((interrupt) => interrupt.toolCallId);
|
|
213
219
|
if (new Set(toolCallIds).size !== toolCallIds.length) return EMPTY_APPROVALS;
|
|
214
220
|
return new Map(
|
|
215
|
-
interrupts.map((interrupt) => [
|
|
221
|
+
interrupts.map((interrupt) => [
|
|
222
|
+
interrupt.toolCallId,
|
|
223
|
+
{
|
|
224
|
+
id: interrupt.id,
|
|
225
|
+
...(interrupt.message !== undefined && { prompt: interrupt.message }),
|
|
226
|
+
},
|
|
227
|
+
]),
|
|
216
228
|
);
|
|
217
229
|
};
|
|
218
230
|
|
|
@@ -228,12 +240,9 @@ export const withToolApprovalDecision = (
|
|
|
228
240
|
content: readonly ThreadAssistantMessagePart[],
|
|
229
241
|
{ approvalId, approved, reason }: RespondToToolApprovalOptions,
|
|
230
242
|
): readonly ThreadAssistantMessagePart[] => {
|
|
231
|
-
|
|
232
|
-
const next = content.map((part) => {
|
|
233
|
-
if (part.type !== "tool-call") return part;
|
|
243
|
+
const { content: next, changed } = mapToolCallPartsDeep(content, (part) => {
|
|
234
244
|
const approval = part.approval;
|
|
235
245
|
if (approval?.id !== approvalId || !isPending(approval)) return part;
|
|
236
|
-
changed = true;
|
|
237
246
|
return {
|
|
238
247
|
...part,
|
|
239
248
|
approval: {
|
|
@@ -263,8 +272,7 @@ export const buildToolApprovalResume = (
|
|
|
263
272
|
// instead of recording a response, and `constructor` or `toString` would
|
|
264
273
|
// report themselves as answered while undecided.
|
|
265
274
|
const responses = new Map<string, { status: "resolved"; payload: unknown }>();
|
|
266
|
-
for (const part of content) {
|
|
267
|
-
if (part.type !== "tool-call") continue;
|
|
275
|
+
for (const part of iterateToolCallParts(content)) {
|
|
268
276
|
const approval = part.approval;
|
|
269
277
|
if (!approval || approval.approved === undefined) continue;
|
|
270
278
|
if (!open.has(approval.id)) continue;
|
|
@@ -350,16 +358,13 @@ export const withSettledToolApprovals = (
|
|
|
350
358
|
): readonly ThreadAssistantMessagePart[] => {
|
|
351
359
|
if (resume.length === 0) return content;
|
|
352
360
|
const byId = new Map(resume.map((entry) => [entry.interruptId, entry]));
|
|
353
|
-
|
|
354
|
-
const next = content.map((part) => {
|
|
355
|
-
if (part.type !== "tool-call") return part;
|
|
361
|
+
const { content: next, changed } = mapToolCallPartsDeep(content, (part) => {
|
|
356
362
|
const approval = part.approval;
|
|
357
363
|
if (!approval) return part;
|
|
358
364
|
const entry = byId.get(approval.id);
|
|
359
365
|
if (!entry) return part;
|
|
360
366
|
const settled = settle(approval, entry);
|
|
361
367
|
if (!settled) return part;
|
|
362
|
-
changed = true;
|
|
363
368
|
return { ...part, approval: settled };
|
|
364
369
|
});
|
|
365
370
|
return changed ? next : content;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AgUiEvent,
|
|
3
|
+
AgUiInterrupt,
|
|
4
|
+
AgUiRunFinishedOutcome,
|
|
5
|
+
AgUiSubagentFinishedOutcome,
|
|
6
|
+
} from "./types";
|
|
2
7
|
import type { Logger } from "./logger";
|
|
3
8
|
import { parseMcpToolCallResult } from "./mcp-tool-result";
|
|
4
9
|
import { withRawResponseSchema } from "./interrupt-internals";
|
|
@@ -26,6 +31,16 @@ const withOptional = <T extends object>(
|
|
|
26
31
|
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
27
32
|
!!value && typeof value === "object" && !Array.isArray(value);
|
|
28
33
|
|
|
34
|
+
const logRejectedEvent = (
|
|
35
|
+
logger: Logger | undefined,
|
|
36
|
+
type: unknown,
|
|
37
|
+
payload: unknown,
|
|
38
|
+
reason: string,
|
|
39
|
+
) => {
|
|
40
|
+
const typeLabel = isString(type) ? type : "unknown";
|
|
41
|
+
logger?.debug?.(`[agui] ${typeLabel} ${reason}`, payload);
|
|
42
|
+
};
|
|
43
|
+
|
|
29
44
|
const parseInterrupt = (raw: unknown): AgUiInterrupt | null => {
|
|
30
45
|
if (!isPlainObject(raw)) return null;
|
|
31
46
|
const id = raw.id;
|
|
@@ -51,30 +66,61 @@ const parseInterrupt = (raw: unknown): AgUiInterrupt | null => {
|
|
|
51
66
|
|
|
52
67
|
const parseRunFinishedOutcome = (
|
|
53
68
|
raw: unknown,
|
|
69
|
+
runId: string,
|
|
54
70
|
logger: Logger | undefined,
|
|
55
71
|
): AgUiRunFinishedOutcome | undefined => {
|
|
56
|
-
if (
|
|
72
|
+
if (raw == null) return undefined;
|
|
73
|
+
if (!isPlainObject(raw)) {
|
|
74
|
+
logRejectedEvent(
|
|
75
|
+
logger,
|
|
76
|
+
"RUN_FINISHED",
|
|
77
|
+
{ runId, outcome: raw },
|
|
78
|
+
"has invalid outcome",
|
|
79
|
+
);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
57
82
|
if (raw.type === "success") return { type: "success" };
|
|
58
83
|
if (raw.type === "interrupt") {
|
|
59
84
|
if (!Array.isArray(raw.interrupts)) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
logRejectedEvent(
|
|
86
|
+
logger,
|
|
87
|
+
"RUN_FINISHED",
|
|
88
|
+
{ runId, outcome: raw },
|
|
89
|
+
"has an interrupt outcome missing interrupts array",
|
|
63
90
|
);
|
|
64
91
|
return undefined;
|
|
65
92
|
}
|
|
66
93
|
const parsed = raw.interrupts
|
|
67
|
-
.map((entry) =>
|
|
94
|
+
.map((entry) => {
|
|
95
|
+
const interrupt = parseInterrupt(entry);
|
|
96
|
+
if (interrupt === null) {
|
|
97
|
+
logRejectedEvent(
|
|
98
|
+
logger,
|
|
99
|
+
"RUN_FINISHED",
|
|
100
|
+
{ runId, interrupt: entry },
|
|
101
|
+
"has malformed interrupt",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return interrupt;
|
|
105
|
+
})
|
|
68
106
|
.filter((entry): entry is AgUiInterrupt => entry !== null);
|
|
69
107
|
if (parsed.length === 0) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
108
|
+
logRejectedEvent(
|
|
109
|
+
logger,
|
|
110
|
+
"RUN_FINISHED",
|
|
111
|
+
{ runId, interrupts: raw.interrupts },
|
|
112
|
+
"has an interrupt outcome with no valid interrupts",
|
|
73
113
|
);
|
|
74
114
|
return undefined;
|
|
75
115
|
}
|
|
76
116
|
return { type: "interrupt", interrupts: parsed };
|
|
77
117
|
}
|
|
118
|
+
logRejectedEvent(
|
|
119
|
+
logger,
|
|
120
|
+
"RUN_FINISHED",
|
|
121
|
+
{ runId, outcome: raw },
|
|
122
|
+
"has unsupported outcome type",
|
|
123
|
+
);
|
|
78
124
|
return undefined;
|
|
79
125
|
};
|
|
80
126
|
|
|
@@ -82,10 +128,21 @@ export const parseAgUiEvent = (
|
|
|
82
128
|
event: unknown,
|
|
83
129
|
options?: ParseAgUiEventOptions,
|
|
84
130
|
): AgUiEvent | null => {
|
|
85
|
-
if (!event || typeof event !== "object")
|
|
131
|
+
if (!event || typeof event !== "object") {
|
|
132
|
+
logRejectedEvent(options?.logger, undefined, event, "is not an object");
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
86
135
|
const payload = event as Record<string, unknown>;
|
|
87
136
|
const typeValue = payload.type;
|
|
88
|
-
if (!isString(typeValue))
|
|
137
|
+
if (!isString(typeValue)) {
|
|
138
|
+
logRejectedEvent(options?.logger, typeValue, payload, "has no string type");
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const reject = (reason: string): null => {
|
|
143
|
+
logRejectedEvent(options?.logger, typeValue, payload, reason);
|
|
144
|
+
return null;
|
|
145
|
+
};
|
|
89
146
|
|
|
90
147
|
const getString = (key: string) =>
|
|
91
148
|
isString(payload[key]) ? (payload[key] as string) : undefined;
|
|
@@ -93,15 +150,19 @@ export const parseAgUiEvent = (
|
|
|
93
150
|
switch (typeValue) {
|
|
94
151
|
case "RUN_STARTED": {
|
|
95
152
|
const runId = getString("runId");
|
|
96
|
-
return runId ? { type: "RUN_STARTED", runId } :
|
|
153
|
+
return runId ? { type: "RUN_STARTED", runId } : reject("missing runId");
|
|
97
154
|
}
|
|
98
155
|
case "RUN_FINISHED": {
|
|
99
156
|
const runId = getString("runId");
|
|
100
|
-
if (!runId) return
|
|
157
|
+
if (!runId) return reject("missing runId");
|
|
101
158
|
return withOptional(
|
|
102
159
|
{ type: "RUN_FINISHED" as const, runId },
|
|
103
160
|
{
|
|
104
|
-
outcome: parseRunFinishedOutcome(
|
|
161
|
+
outcome: parseRunFinishedOutcome(
|
|
162
|
+
payload.outcome,
|
|
163
|
+
runId,
|
|
164
|
+
options?.logger,
|
|
165
|
+
),
|
|
105
166
|
},
|
|
106
167
|
);
|
|
107
168
|
}
|
|
@@ -121,26 +182,38 @@ export const parseAgUiEvent = (
|
|
|
121
182
|
case "TEXT_MESSAGE_START":
|
|
122
183
|
return withOptional(
|
|
123
184
|
{ type: "TEXT_MESSAGE_START" as const },
|
|
124
|
-
{
|
|
185
|
+
{
|
|
186
|
+
messageId: getString("messageId"),
|
|
187
|
+
subagentRunId: getString("subagentRunId"),
|
|
188
|
+
},
|
|
125
189
|
);
|
|
126
190
|
case "TEXT_MESSAGE_CONTENT": {
|
|
127
191
|
const delta = getString("delta");
|
|
128
|
-
if (!isNonEmptyString(delta)) return
|
|
192
|
+
if (!isNonEmptyString(delta)) return reject("missing non-empty delta");
|
|
129
193
|
return withOptional(
|
|
130
194
|
{ type: "TEXT_MESSAGE_CONTENT" as const, delta },
|
|
131
|
-
{
|
|
195
|
+
{
|
|
196
|
+
messageId: getString("messageId"),
|
|
197
|
+
subagentRunId: getString("subagentRunId"),
|
|
198
|
+
},
|
|
132
199
|
);
|
|
133
200
|
}
|
|
134
201
|
case "TEXT_MESSAGE_END":
|
|
135
202
|
return withOptional(
|
|
136
203
|
{ type: "TEXT_MESSAGE_END" as const },
|
|
137
|
-
{
|
|
204
|
+
{
|
|
205
|
+
messageId: getString("messageId"),
|
|
206
|
+
subagentRunId: getString("subagentRunId"),
|
|
207
|
+
},
|
|
138
208
|
);
|
|
139
209
|
case "TEXT_MESSAGE_CHUNK": {
|
|
140
210
|
const delta = getString("delta") ?? "";
|
|
141
211
|
return withOptional(
|
|
142
212
|
{ type: "TEXT_MESSAGE_CHUNK" as const, delta },
|
|
143
|
-
{
|
|
213
|
+
{
|
|
214
|
+
messageId: getString("messageId"),
|
|
215
|
+
subagentRunId: getString("subagentRunId"),
|
|
216
|
+
},
|
|
144
217
|
);
|
|
145
218
|
}
|
|
146
219
|
case "THINKING_START":
|
|
@@ -161,63 +234,94 @@ export const parseAgUiEvent = (
|
|
|
161
234
|
case "REASONING_START":
|
|
162
235
|
return withOptional(
|
|
163
236
|
{ type: "REASONING_START" as const },
|
|
164
|
-
{
|
|
237
|
+
{
|
|
238
|
+
messageId: getString("messageId"),
|
|
239
|
+
subagentRunId: getString("subagentRunId"),
|
|
240
|
+
},
|
|
165
241
|
);
|
|
166
242
|
case "REASONING_MESSAGE_START":
|
|
167
243
|
return withOptional(
|
|
168
244
|
{ type: "REASONING_MESSAGE_START" as const },
|
|
169
|
-
{
|
|
245
|
+
{
|
|
246
|
+
messageId: getString("messageId"),
|
|
247
|
+
subagentRunId: getString("subagentRunId"),
|
|
248
|
+
},
|
|
170
249
|
);
|
|
171
250
|
case "REASONING_MESSAGE_CONTENT": {
|
|
172
251
|
const delta = getString("delta") ?? "";
|
|
173
252
|
return withOptional(
|
|
174
253
|
{ type: "REASONING_MESSAGE_CONTENT" as const, delta },
|
|
175
|
-
{
|
|
254
|
+
{
|
|
255
|
+
messageId: getString("messageId"),
|
|
256
|
+
subagentRunId: getString("subagentRunId"),
|
|
257
|
+
},
|
|
176
258
|
);
|
|
177
259
|
}
|
|
178
260
|
case "REASONING_MESSAGE_END":
|
|
179
261
|
return withOptional(
|
|
180
262
|
{ type: "REASONING_MESSAGE_END" as const },
|
|
181
|
-
{
|
|
263
|
+
{
|
|
264
|
+
messageId: getString("messageId"),
|
|
265
|
+
subagentRunId: getString("subagentRunId"),
|
|
266
|
+
},
|
|
182
267
|
);
|
|
183
268
|
case "REASONING_ENCRYPTED_VALUE": {
|
|
184
269
|
const entityId = getString("entityId");
|
|
185
270
|
const encryptedValue = getString("encryptedValue");
|
|
186
271
|
const subtype = getString("subtype");
|
|
187
|
-
if (!entityId || !encryptedValue)
|
|
188
|
-
|
|
272
|
+
if (!entityId || !encryptedValue) {
|
|
273
|
+
return reject("missing entityId or encryptedValue");
|
|
274
|
+
}
|
|
275
|
+
if (subtype !== "message" && subtype !== "tool-call") {
|
|
276
|
+
return reject("has an invalid subtype");
|
|
277
|
+
}
|
|
278
|
+
// Spread rather than withOptional: routing the narrowed `subtype` through
|
|
279
|
+
// a generic helper widens it back to string.
|
|
280
|
+
const subagentRunId = getString("subagentRunId");
|
|
189
281
|
return {
|
|
190
282
|
type: "REASONING_ENCRYPTED_VALUE" as const,
|
|
191
283
|
subtype,
|
|
192
284
|
entityId,
|
|
193
285
|
encryptedValue,
|
|
286
|
+
...(subagentRunId !== undefined ? { subagentRunId } : {}),
|
|
194
287
|
};
|
|
195
288
|
}
|
|
196
289
|
case "REASONING_END":
|
|
197
290
|
return withOptional(
|
|
198
291
|
{ type: "REASONING_END" as const },
|
|
199
|
-
{
|
|
292
|
+
{
|
|
293
|
+
messageId: getString("messageId"),
|
|
294
|
+
subagentRunId: getString("subagentRunId"),
|
|
295
|
+
},
|
|
200
296
|
);
|
|
201
297
|
case "TOOL_CALL_START": {
|
|
202
298
|
const toolCallId = getString("toolCallId");
|
|
203
|
-
if (!toolCallId) return
|
|
299
|
+
if (!toolCallId) return reject("missing toolCallId");
|
|
204
300
|
return withOptional(
|
|
205
301
|
{ type: "TOOL_CALL_START" as const, toolCallId },
|
|
206
302
|
{
|
|
207
303
|
toolCallName: getString("toolCallName"),
|
|
208
304
|
parentMessageId: getString("parentMessageId"),
|
|
305
|
+
subagentRunId: getString("subagentRunId"),
|
|
209
306
|
},
|
|
210
307
|
);
|
|
211
308
|
}
|
|
212
309
|
case "TOOL_CALL_ARGS": {
|
|
213
310
|
const toolCallId = getString("toolCallId");
|
|
214
|
-
if (!toolCallId) return
|
|
311
|
+
if (!toolCallId) return reject("missing toolCallId");
|
|
215
312
|
const delta = getString("delta") ?? "";
|
|
216
|
-
return
|
|
313
|
+
return withOptional(
|
|
314
|
+
{ type: "TOOL_CALL_ARGS" as const, toolCallId, delta },
|
|
315
|
+
{ subagentRunId: getString("subagentRunId") },
|
|
316
|
+
);
|
|
217
317
|
}
|
|
218
318
|
case "TOOL_CALL_END": {
|
|
219
319
|
const toolCallId = getString("toolCallId");
|
|
220
|
-
|
|
320
|
+
if (!toolCallId) return reject("missing toolCallId");
|
|
321
|
+
return withOptional(
|
|
322
|
+
{ type: "TOOL_CALL_END" as const, toolCallId },
|
|
323
|
+
{ subagentRunId: getString("subagentRunId") },
|
|
324
|
+
);
|
|
221
325
|
}
|
|
222
326
|
case "TOOL_CALL_CHUNK":
|
|
223
327
|
return withOptional(
|
|
@@ -227,11 +331,12 @@ export const parseAgUiEvent = (
|
|
|
227
331
|
toolCallName: getString("toolCallName"),
|
|
228
332
|
parentMessageId: getString("parentMessageId"),
|
|
229
333
|
delta: getString("delta"),
|
|
334
|
+
subagentRunId: getString("subagentRunId"),
|
|
230
335
|
},
|
|
231
336
|
);
|
|
232
337
|
case "TOOL_CALL_RESULT": {
|
|
233
338
|
const toolCallId = getString("toolCallId");
|
|
234
|
-
if (!toolCallId) return
|
|
339
|
+
if (!toolCallId) return reject("missing toolCallId");
|
|
235
340
|
const content = getString("content") ?? "";
|
|
236
341
|
return withOptional(
|
|
237
342
|
{
|
|
@@ -243,10 +348,14 @@ export const parseAgUiEvent = (
|
|
|
243
348
|
messageId: getString("messageId"),
|
|
244
349
|
role: payload.role === "tool" ? "tool" : undefined,
|
|
245
350
|
mcpResult: parseMcpToolCallResult(payload, content),
|
|
351
|
+
subagentRunId: getString("subagentRunId"),
|
|
246
352
|
},
|
|
247
353
|
);
|
|
248
354
|
}
|
|
249
355
|
case "STATE_SNAPSHOT":
|
|
356
|
+
if (payload.snapshot === undefined) {
|
|
357
|
+
return reject("missing snapshot");
|
|
358
|
+
}
|
|
250
359
|
return { type: "STATE_SNAPSHOT", snapshot: payload.snapshot };
|
|
251
360
|
case "STATE_DELTA":
|
|
252
361
|
return {
|
|
@@ -254,15 +363,18 @@ export const parseAgUiEvent = (
|
|
|
254
363
|
delta: Array.isArray(payload.delta) ? (payload.delta as any[]) : [],
|
|
255
364
|
};
|
|
256
365
|
case "MESSAGES_SNAPSHOT":
|
|
366
|
+
if (!Array.isArray(payload.messages)) {
|
|
367
|
+
return reject("missing messages array");
|
|
368
|
+
}
|
|
257
369
|
return {
|
|
258
370
|
type: "MESSAGES_SNAPSHOT",
|
|
259
|
-
messages:
|
|
260
|
-
? (payload.messages as any[])
|
|
261
|
-
: [],
|
|
371
|
+
messages: payload.messages as any[],
|
|
262
372
|
};
|
|
263
373
|
case "ACTIVITY_SNAPSHOT": {
|
|
264
374
|
const activityType = getString("activityType");
|
|
265
|
-
if (!activityType || !isPlainObject(payload.content))
|
|
375
|
+
if (!activityType || !isPlainObject(payload.content)) {
|
|
376
|
+
return reject("missing activityType or object content");
|
|
377
|
+
}
|
|
266
378
|
return withOptional(
|
|
267
379
|
{
|
|
268
380
|
type: "ACTIVITY_SNAPSHOT" as const,
|
|
@@ -273,6 +385,7 @@ export const parseAgUiEvent = (
|
|
|
273
385
|
messageId: getString("messageId"),
|
|
274
386
|
replace:
|
|
275
387
|
typeof payload.replace === "boolean" ? payload.replace : undefined,
|
|
388
|
+
subagentRunId: getString("subagentRunId"),
|
|
276
389
|
},
|
|
277
390
|
);
|
|
278
391
|
}
|
|
@@ -283,9 +396,76 @@ export const parseAgUiEvent = (
|
|
|
283
396
|
);
|
|
284
397
|
case "CUSTOM": {
|
|
285
398
|
const name = getString("name");
|
|
286
|
-
if (!name) return
|
|
399
|
+
if (!name) return reject("missing name");
|
|
287
400
|
return { type: "CUSTOM", name, value: payload.value };
|
|
288
401
|
}
|
|
402
|
+
case "SUBAGENT_STARTED": {
|
|
403
|
+
const subagentRunId = getString("subagentRunId");
|
|
404
|
+
const name = getString("name");
|
|
405
|
+
if (!subagentRunId || !name) {
|
|
406
|
+
return reject("missing subagentRunId or name");
|
|
407
|
+
}
|
|
408
|
+
return withOptional(
|
|
409
|
+
{ type: "SUBAGENT_STARTED" as const, subagentRunId, name },
|
|
410
|
+
{
|
|
411
|
+
description: getString("description"),
|
|
412
|
+
parentSubagentRunId: getString("parentSubagentRunId"),
|
|
413
|
+
parentToolCallId: getString("parentToolCallId"),
|
|
414
|
+
parentMessageId: getString("parentMessageId"),
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
case "SUBAGENT_FINISHED": {
|
|
419
|
+
const subagentRunId = getString("subagentRunId");
|
|
420
|
+
if (!subagentRunId) return reject("missing subagentRunId");
|
|
421
|
+
const rawOutcome = payload.outcome;
|
|
422
|
+
let outcome: AgUiSubagentFinishedOutcome | undefined;
|
|
423
|
+
if (rawOutcome == null) {
|
|
424
|
+
outcome = undefined;
|
|
425
|
+
} else if (!isPlainObject(rawOutcome)) {
|
|
426
|
+
logRejectedEvent(
|
|
427
|
+
options?.logger,
|
|
428
|
+
"SUBAGENT_FINISHED",
|
|
429
|
+
{ subagentRunId, outcome: rawOutcome },
|
|
430
|
+
"has invalid outcome",
|
|
431
|
+
);
|
|
432
|
+
outcome = undefined;
|
|
433
|
+
} else if (rawOutcome.type === "success") {
|
|
434
|
+
outcome = { type: "success" as const };
|
|
435
|
+
} else if (rawOutcome.type === "suspended") {
|
|
436
|
+
outcome = withOptional(
|
|
437
|
+
{ type: "suspended" as const },
|
|
438
|
+
{
|
|
439
|
+
interruptIds: Array.isArray(rawOutcome.interruptIds)
|
|
440
|
+
? (rawOutcome.interruptIds as unknown[]).filter(isString)
|
|
441
|
+
: undefined,
|
|
442
|
+
},
|
|
443
|
+
);
|
|
444
|
+
} else {
|
|
445
|
+
logRejectedEvent(
|
|
446
|
+
options?.logger,
|
|
447
|
+
"SUBAGENT_FINISHED",
|
|
448
|
+
{ subagentRunId, outcome: rawOutcome },
|
|
449
|
+
"has unsupported outcome type",
|
|
450
|
+
);
|
|
451
|
+
outcome = undefined;
|
|
452
|
+
}
|
|
453
|
+
return withOptional(
|
|
454
|
+
{ type: "SUBAGENT_FINISHED" as const, subagentRunId },
|
|
455
|
+
{ result: payload.result, outcome },
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
case "SUBAGENT_ERROR": {
|
|
459
|
+
const subagentRunId = getString("subagentRunId");
|
|
460
|
+
const message = getString("message");
|
|
461
|
+
if (!subagentRunId || !message) {
|
|
462
|
+
return reject("missing subagentRunId or message");
|
|
463
|
+
}
|
|
464
|
+
return withOptional(
|
|
465
|
+
{ type: "SUBAGENT_ERROR" as const, subagentRunId, message },
|
|
466
|
+
{ code: getString("code") },
|
|
467
|
+
);
|
|
468
|
+
}
|
|
289
469
|
default:
|
|
290
470
|
return withOptional(
|
|
291
471
|
{ type: "RAW" as const, event: payload },
|