@assistant-ui/react-ag-ui 0.0.65 → 0.0.67
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 +19 -3
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +193 -39
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +11 -12
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +4 -16
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +80 -15
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/types.d.ts +22 -5
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js +8 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +76 -33
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +6 -6
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +380 -6
- package/src/runtime/AgUiThreadRuntimeCore.ts +314 -70
- package/src/runtime/adapter/conversions.content.test.ts +41 -0
- package/src/runtime/adapter/conversions.ts +16 -19
- package/src/runtime/adapter/run-aggregator.reasoning.test.ts +143 -0
- package/src/runtime/adapter/run-aggregator.ts +128 -33
- package/src/runtime/types.ts +25 -5
- package/src/useAgUiRuntime.external-store.test.tsx +51 -0
- package/src/useAgUiRuntime.thread-switch.test.tsx +587 -3
- package/src/useAgUiRuntime.ts +98 -44
|
@@ -40,6 +40,47 @@ const appendMessage = (): AppendMessage => ({
|
|
|
40
40
|
metadata: { custom: {} },
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
describe("toAgUiMessages data URLs", () => {
|
|
44
|
+
it.each(["application/pdf", ""])(
|
|
45
|
+
"unwraps a media-less data URL while retaining the adapter's %j MIME fallback",
|
|
46
|
+
(mimeType) => {
|
|
47
|
+
expect(
|
|
48
|
+
contentOf(
|
|
49
|
+
userMessage([
|
|
50
|
+
{ type: "file", data: "data:;base64,SGVsbG8=", mimeType },
|
|
51
|
+
]),
|
|
52
|
+
),
|
|
53
|
+
).toEqual([
|
|
54
|
+
{
|
|
55
|
+
type: "document",
|
|
56
|
+
source: {
|
|
57
|
+
type: "data",
|
|
58
|
+
value: "SGVsbG8=",
|
|
59
|
+
mimeType: mimeType || "application/octet-stream",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
it("sniffs a media-less image data URL", () => {
|
|
67
|
+
expect(
|
|
68
|
+
contentOf(
|
|
69
|
+
userMessage([{ type: "image", image: "data:;base64,iVBORw0KGgo=" }]),
|
|
70
|
+
),
|
|
71
|
+
).toEqual([
|
|
72
|
+
{
|
|
73
|
+
type: "image",
|
|
74
|
+
source: {
|
|
75
|
+
type: "data",
|
|
76
|
+
value: "iVBORw0KGgo=",
|
|
77
|
+
mimeType: "image/png",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
43
84
|
describe("toAgUiMessages content metadata", () => {
|
|
44
85
|
it("emits AgUiMessage values assignable to AG-UI Message", () => {
|
|
45
86
|
expectTypeOf<AgUiMessage>().toExtend<AgUiWireMessage>();
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getAutoStatus,
|
|
12
12
|
parseDataUrl,
|
|
13
13
|
resolveFilePartSource,
|
|
14
|
+
resolveImageMediaType,
|
|
14
15
|
walkToolCallTree,
|
|
15
16
|
} from "@assistant-ui/core/internal";
|
|
16
17
|
import { type Tool, toToolsJSONSchema } from "assistant-stream";
|
|
@@ -21,14 +22,13 @@ import {
|
|
|
21
22
|
MCP_APPS_ACTIVITY_TYPE,
|
|
22
23
|
type AgUiCustomMetadata,
|
|
23
24
|
type AgUiOpaqueReasoning,
|
|
24
|
-
} from "
|
|
25
|
+
} from "../types";
|
|
25
26
|
import {
|
|
26
27
|
applyA2uiOperations,
|
|
27
|
-
|
|
28
|
-
surfaceToOperations,
|
|
28
|
+
surfaceToPresentToolCall,
|
|
29
29
|
type A2uiState,
|
|
30
30
|
type A2uiSurfaceState,
|
|
31
|
-
} from "@assistant-ui/
|
|
31
|
+
} from "@assistant-ui/generative-ui/a2ui";
|
|
32
32
|
import type { AgUiInterrupt } from "../types";
|
|
33
33
|
import { projectAgUiToolApprovals } from "./tool-approval";
|
|
34
34
|
import {
|
|
@@ -105,6 +105,7 @@ type CoreToolCallPart = Extract<
|
|
|
105
105
|
type ToolCallPart = Omit<CoreToolCallPart, "result" | "isError"> & {
|
|
106
106
|
result?: unknown;
|
|
107
107
|
isError?: boolean | undefined;
|
|
108
|
+
/** @deprecated Experimental since 2026-03-08. Not scheduled for removal; the API may change in any release. */
|
|
108
109
|
unstable_toolMessageId?: string;
|
|
109
110
|
};
|
|
110
111
|
|
|
@@ -276,9 +277,16 @@ function toInputContent(
|
|
|
276
277
|
const image = getString(part, "image");
|
|
277
278
|
if (image === undefined) return null;
|
|
278
279
|
const metadata = buildInputMetadata(part, getString(part, "filename"));
|
|
280
|
+
const source = buildInputSource(image, fallbackMimeType);
|
|
279
281
|
return {
|
|
280
282
|
type: "image",
|
|
281
|
-
source:
|
|
283
|
+
source:
|
|
284
|
+
source.type === "data"
|
|
285
|
+
? {
|
|
286
|
+
...source,
|
|
287
|
+
mimeType: resolveImageMediaType(image, fallbackMimeType),
|
|
288
|
+
}
|
|
289
|
+
: source,
|
|
282
290
|
...(metadata && { metadata }),
|
|
283
291
|
};
|
|
284
292
|
}
|
|
@@ -502,7 +510,6 @@ function toToolCallPart(value: unknown): ToolCallPart | null {
|
|
|
502
510
|
: isObject(value.args) && !Array.isArray(value.args)
|
|
503
511
|
? (value.args as ReadonlyJSONObject)
|
|
504
512
|
: undefined;
|
|
505
|
-
|
|
506
513
|
const part: ToolCallPart = {
|
|
507
514
|
type: "tool-call",
|
|
508
515
|
...(toolCallId !== undefined ? { toolCallId } : {}),
|
|
@@ -693,27 +700,17 @@ function toUserOrSystemSnapshotMessage(
|
|
|
693
700
|
};
|
|
694
701
|
}
|
|
695
702
|
|
|
696
|
-
// Rebuilds the a2ui:<surfaceId> "present" tool-call parts for one owning
|
|
697
|
-
// assistant message from a bucket of rebuilt surface state, mirroring the
|
|
698
|
-
// live RunAggregator.synthesizeA2uiToolCalls shape. Non-a2ui parts (text,
|
|
699
|
-
// other tool calls) are preserved; existing a2ui parts are replaced wholesale
|
|
700
|
-
// so create/update/delete within the bucket all converge on the rebuilt set.
|
|
701
703
|
function attachA2uiSurfaces(
|
|
702
704
|
message: CoreThreadMessageLike,
|
|
703
705
|
state: A2uiState,
|
|
704
706
|
): CoreThreadMessageLike {
|
|
705
707
|
const a2uiParts: ToolCallPart[] = [];
|
|
706
708
|
for (const [surfaceId, surface] of state) {
|
|
707
|
-
const {
|
|
708
|
-
if (!
|
|
709
|
+
const { toolCall } = surfaceToPresentToolCall(surfaceId, surface);
|
|
710
|
+
if (!toolCall) continue;
|
|
709
711
|
a2uiParts.push({
|
|
710
712
|
type: "tool-call",
|
|
711
|
-
|
|
712
|
-
toolName: "present",
|
|
713
|
-
args: spec as unknown as ReadonlyJSONObject,
|
|
714
|
-
argsText: JSON.stringify(spec),
|
|
715
|
-
result: {},
|
|
716
|
-
artifact: { a2ui: surfaceToOperations(surface) },
|
|
713
|
+
...toolCall,
|
|
717
714
|
});
|
|
718
715
|
}
|
|
719
716
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { ChatModelRunResult } from "@assistant-ui/core";
|
|
3
|
+
import { RunAggregator } from "./run-aggregator";
|
|
4
|
+
import type { AgUiCustomMetadata } from "../types";
|
|
5
|
+
import { toAgUiMessages } from "./conversions";
|
|
6
|
+
|
|
7
|
+
describe.each([true, false])(
|
|
8
|
+
"reasoning ownership (showThinking=%s)",
|
|
9
|
+
(showThinking) => {
|
|
10
|
+
it.each([
|
|
11
|
+
{ parentText: "", nextReasoning: false },
|
|
12
|
+
{ parentText: "", nextReasoning: true },
|
|
13
|
+
{ parentText: "Looking it up.", nextReasoning: false },
|
|
14
|
+
{ parentText: "Looking it up.", nextReasoning: true },
|
|
15
|
+
])(
|
|
16
|
+
"keeps pre-tool reasoning on its parent ($parentText, nextReasoning=$nextReasoning)",
|
|
17
|
+
({ parentText, nextReasoning }) => {
|
|
18
|
+
let latest: ChatModelRunResult = {};
|
|
19
|
+
let previous: ChatModelRunResult | undefined;
|
|
20
|
+
const aggregator = new RunAggregator({
|
|
21
|
+
showThinking,
|
|
22
|
+
logger: { debug: () => {}, error: () => {} },
|
|
23
|
+
emit: (update) => {
|
|
24
|
+
latest = update;
|
|
25
|
+
},
|
|
26
|
+
onTextMessageStart: () => {
|
|
27
|
+
previous = latest;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const reason = (messageId: string, delta: string) => {
|
|
31
|
+
aggregator.handle({ type: "REASONING_MESSAGE_START", messageId });
|
|
32
|
+
aggregator.handle({
|
|
33
|
+
type: "REASONING_MESSAGE_CONTENT",
|
|
34
|
+
messageId,
|
|
35
|
+
delta,
|
|
36
|
+
});
|
|
37
|
+
aggregator.handle({ type: "REASONING_MESSAGE_END", messageId });
|
|
38
|
+
aggregator.handle({
|
|
39
|
+
type: "REASONING_ENCRYPTED_VALUE",
|
|
40
|
+
subtype: "message",
|
|
41
|
+
entityId: messageId,
|
|
42
|
+
encryptedValue: `sig-${messageId}`,
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
const reasoningOf = (result: ChatModelRunResult | undefined) =>
|
|
46
|
+
result?.content?.flatMap((part) =>
|
|
47
|
+
part.type === "reasoning" ? [part.text] : [],
|
|
48
|
+
);
|
|
49
|
+
const opaqueOf = (result: ChatModelRunResult | undefined) =>
|
|
50
|
+
(result?.metadata?.custom?.agui as AgUiCustomMetadata | undefined)
|
|
51
|
+
?.opaqueReasoning ?? [];
|
|
52
|
+
|
|
53
|
+
aggregator.handle({ type: "RUN_STARTED", runId: "run-1" });
|
|
54
|
+
aggregator.handle({
|
|
55
|
+
type: "TEXT_MESSAGE_START",
|
|
56
|
+
messageId: "assistant-1",
|
|
57
|
+
});
|
|
58
|
+
if (parentText)
|
|
59
|
+
aggregator.handle({
|
|
60
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
61
|
+
messageId: "assistant-1",
|
|
62
|
+
delta: parentText,
|
|
63
|
+
});
|
|
64
|
+
reason("r-1", "plan the call");
|
|
65
|
+
aggregator.handle({
|
|
66
|
+
type: "TOOL_CALL_START",
|
|
67
|
+
toolCallId: "call-1",
|
|
68
|
+
toolCallName: "lookup",
|
|
69
|
+
parentMessageId: "assistant-1",
|
|
70
|
+
});
|
|
71
|
+
aggregator.handle({ type: "TOOL_CALL_END", toolCallId: "call-1" });
|
|
72
|
+
aggregator.handle({
|
|
73
|
+
type: "TOOL_CALL_RESULT",
|
|
74
|
+
toolCallId: "call-1",
|
|
75
|
+
messageId: "tool-1",
|
|
76
|
+
content: "ok",
|
|
77
|
+
});
|
|
78
|
+
if (nextReasoning) reason("r-2", "write the answer");
|
|
79
|
+
aggregator.handle({
|
|
80
|
+
type: "TEXT_MESSAGE_START",
|
|
81
|
+
messageId: "assistant-2",
|
|
82
|
+
});
|
|
83
|
+
aggregator.handle({
|
|
84
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
85
|
+
messageId: "assistant-2",
|
|
86
|
+
delta: "Done.",
|
|
87
|
+
});
|
|
88
|
+
aggregator.handle({ type: "RUN_FINISHED", runId: "run-1" });
|
|
89
|
+
|
|
90
|
+
expect(
|
|
91
|
+
previous?.content?.filter((part) => part.type === "tool-call"),
|
|
92
|
+
).toHaveLength(1);
|
|
93
|
+
expect(
|
|
94
|
+
latest.content?.filter((part) => part.type === "tool-call"),
|
|
95
|
+
).toHaveLength(0);
|
|
96
|
+
if (showThinking) {
|
|
97
|
+
expect(reasoningOf(previous)).toEqual(["plan the call"]);
|
|
98
|
+
expect(reasoningOf(latest)).toEqual(
|
|
99
|
+
nextReasoning ? ["write the answer"] : [],
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
expect(opaqueOf(previous)).toEqual([
|
|
103
|
+
{ id: "r-1", encryptedValue: "sig-r-1" },
|
|
104
|
+
]);
|
|
105
|
+
expect(opaqueOf(latest)).toEqual(
|
|
106
|
+
nextReasoning ? [{ id: "r-2", encryptedValue: "sig-r-2" }] : [],
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const exported = toAgUiMessages([
|
|
110
|
+
{
|
|
111
|
+
id: "assistant-1",
|
|
112
|
+
role: "assistant",
|
|
113
|
+
content: previous?.content ?? [],
|
|
114
|
+
metadata: previous?.metadata,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "assistant-2",
|
|
118
|
+
role: "assistant",
|
|
119
|
+
content: latest.content ?? [],
|
|
120
|
+
metadata: latest.metadata,
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
expect(exported.map((message) => message.id)).toEqual([
|
|
124
|
+
"r-1",
|
|
125
|
+
"assistant-1",
|
|
126
|
+
"tool-1",
|
|
127
|
+
...(nextReasoning ? ["r-2"] : []),
|
|
128
|
+
"assistant-2",
|
|
129
|
+
]);
|
|
130
|
+
expect(
|
|
131
|
+
exported.flatMap((message) =>
|
|
132
|
+
message.role === "reasoning"
|
|
133
|
+
? [[message.id, message.encryptedValue]]
|
|
134
|
+
: [],
|
|
135
|
+
),
|
|
136
|
+
).toEqual([
|
|
137
|
+
["r-1", "sig-r-1"],
|
|
138
|
+
...(nextReasoning ? [["r-2", "sig-r-2"]] : []),
|
|
139
|
+
]);
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
);
|
|
@@ -13,19 +13,24 @@ import {
|
|
|
13
13
|
} from "@assistant-ui/core";
|
|
14
14
|
import {
|
|
15
15
|
applyA2uiOperations,
|
|
16
|
-
|
|
17
|
-
surfaceToOperations,
|
|
16
|
+
surfaceToPresentToolCall,
|
|
18
17
|
type A2uiState,
|
|
19
18
|
type A2uiSurfaceState,
|
|
20
|
-
} from "@assistant-ui/
|
|
19
|
+
} from "@assistant-ui/generative-ui/a2ui";
|
|
21
20
|
import jsonpatch, { type Operation } from "fast-json-patch";
|
|
22
21
|
import { readMcpAppResourceUri } from "../mcp-tool-result";
|
|
23
22
|
import { projectAgUiToolApprovals } from "./tool-approval";
|
|
24
|
-
import
|
|
23
|
+
import {
|
|
24
|
+
AG_UI_METADATA_NAMESPACE,
|
|
25
|
+
A2UI_SURFACE_ACTIVITY_TYPE,
|
|
26
|
+
MCP_APPS_ACTIVITY_TYPE,
|
|
27
|
+
type AgUiCustomMetadata,
|
|
28
|
+
type AgUiEvent,
|
|
29
|
+
type AgUiInterrupt,
|
|
30
|
+
type AgUiOpaqueReasoning,
|
|
31
|
+
} from "../types";
|
|
25
32
|
import type { Logger } from "../logger";
|
|
26
33
|
|
|
27
|
-
export const AG_UI_METADATA_NAMESPACE = "agui";
|
|
28
|
-
|
|
29
34
|
const ROOT_SCOPE = "";
|
|
30
35
|
|
|
31
36
|
// Defensive guard against a malformed or cyclic parentToolCallId /
|
|
@@ -70,20 +75,6 @@ type BuildContext = {
|
|
|
70
75
|
};
|
|
71
76
|
};
|
|
72
77
|
|
|
73
|
-
export type AgUiOpaqueReasoning = {
|
|
74
|
-
id: string;
|
|
75
|
-
encryptedValue: string;
|
|
76
|
-
after?: boolean;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export type AgUiCustomMetadata = {
|
|
80
|
-
/** Wire role restored on export for messages the internal model cannot
|
|
81
|
-
* represent (a developer record rides as a system message). */
|
|
82
|
-
role?: "developer";
|
|
83
|
-
interrupts?: AgUiInterrupt[];
|
|
84
|
-
opaqueReasoning?: AgUiOpaqueReasoning[];
|
|
85
|
-
};
|
|
86
|
-
|
|
87
78
|
type Emit = (update: ChatModelRunResult) => void;
|
|
88
79
|
|
|
89
80
|
type ToolCallState = {
|
|
@@ -178,10 +169,6 @@ const scanJSONContainerDelta = (
|
|
|
178
169
|
return completed && scanner.state === "complete";
|
|
179
170
|
};
|
|
180
171
|
|
|
181
|
-
export const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
|
|
182
|
-
|
|
183
|
-
export const A2UI_SURFACE_ACTIVITY_TYPE = "a2ui-surface";
|
|
184
|
-
|
|
185
172
|
export const isPlainObject = (
|
|
186
173
|
value: unknown,
|
|
187
174
|
): value is Record<string, unknown> =>
|
|
@@ -259,6 +246,7 @@ export class RunAggregator {
|
|
|
259
246
|
>();
|
|
260
247
|
private readonly lastResolvedToolCallIdByScope = new Map<string, string>();
|
|
261
248
|
private readonly partOrder: PartOrderEntry[] = [];
|
|
249
|
+
private lastReasoningBeforeToolCall: PartOrderEntry | undefined;
|
|
262
250
|
private textPartCounter = 0;
|
|
263
251
|
private serverMessageIdReported = false;
|
|
264
252
|
private reportedServerMessageId: string | undefined;
|
|
@@ -748,23 +736,27 @@ export class RunAggregator {
|
|
|
748
736
|
|
|
749
737
|
const activeToolCallIds = new Set<string>();
|
|
750
738
|
for (const [surfaceId, surface] of surfaces) {
|
|
751
|
-
const
|
|
752
|
-
|
|
739
|
+
const { toolCall, warnings } = surfaceToPresentToolCall(
|
|
740
|
+
surfaceId,
|
|
741
|
+
surface,
|
|
742
|
+
);
|
|
753
743
|
for (const warning of warnings) {
|
|
754
744
|
this.logger.debug("[agui] a2ui surface conversion warning", warning);
|
|
755
745
|
}
|
|
756
|
-
if (!
|
|
746
|
+
if (!toolCall) continue;
|
|
747
|
+
|
|
748
|
+
const { toolCallId } = toolCall;
|
|
757
749
|
|
|
758
750
|
activeToolCallIds.add(toolCallId);
|
|
759
751
|
|
|
760
752
|
const entry: ToolCallState = {
|
|
761
|
-
toolCallId,
|
|
762
|
-
toolCallName:
|
|
763
|
-
argsText:
|
|
764
|
-
parsedArgs:
|
|
765
|
-
result:
|
|
753
|
+
toolCallId: toolCall.toolCallId,
|
|
754
|
+
toolCallName: toolCall.toolName,
|
|
755
|
+
argsText: toolCall.argsText,
|
|
756
|
+
parsedArgs: toolCall.args,
|
|
757
|
+
result: toolCall.result,
|
|
766
758
|
isError: undefined,
|
|
767
|
-
artifact:
|
|
759
|
+
artifact: toolCall.artifact,
|
|
768
760
|
snapshotResultApplied: false,
|
|
769
761
|
};
|
|
770
762
|
if (!this.toolCalls.has(toolCallId)) {
|
|
@@ -826,6 +818,7 @@ export class RunAggregator {
|
|
|
826
818
|
this.activityParts.clear();
|
|
827
819
|
this.lastResolvedToolCallIdByScope.clear();
|
|
828
820
|
this.partOrder.length = 0;
|
|
821
|
+
this.lastReasoningBeforeToolCall = undefined;
|
|
829
822
|
this.textPartCounter = 0;
|
|
830
823
|
this.activeTextMessageIdByScope.clear();
|
|
831
824
|
this.reportedServerMessageId = undefined;
|
|
@@ -859,8 +852,103 @@ export class RunAggregator {
|
|
|
859
852
|
) {
|
|
860
853
|
return;
|
|
861
854
|
}
|
|
855
|
+
const carried = this.takeTrailingReasoning();
|
|
856
|
+
if (carried.parts.length > 0 || carried.hiddenSignatures.length > 0) {
|
|
857
|
+
this.emit();
|
|
858
|
+
}
|
|
859
|
+
const reasoningPartCounter = this.reasoningPartCounter;
|
|
862
860
|
this.resetMessageParts();
|
|
863
861
|
this.onTextMessageStart(messageId);
|
|
862
|
+
this.reasoningPartCounter = reasoningPartCounter;
|
|
863
|
+
this.restoreReasoning(carried);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
private takeTrailingReasoning() {
|
|
867
|
+
const parts: {
|
|
868
|
+
key: string;
|
|
869
|
+
entry: { buffer: string; timing?: MessagePartTiming };
|
|
870
|
+
signature: string | undefined;
|
|
871
|
+
signatureId: string | undefined;
|
|
872
|
+
reasoningId: string | undefined;
|
|
873
|
+
anonymous: boolean;
|
|
874
|
+
}[] = [];
|
|
875
|
+
let last = this.partOrder.at(-1);
|
|
876
|
+
while (
|
|
877
|
+
last?.kind === "reasoning" &&
|
|
878
|
+
last.subagentRunId === undefined &&
|
|
879
|
+
last !== this.lastReasoningBeforeToolCall
|
|
880
|
+
) {
|
|
881
|
+
this.partOrder.pop();
|
|
882
|
+
parts.unshift({
|
|
883
|
+
key: last.key,
|
|
884
|
+
entry: this.reasoningParts.get(last.key) ?? { buffer: "" },
|
|
885
|
+
signature: this.reasoningSignatures.get(last.key),
|
|
886
|
+
signatureId: this.reasoningSignatureIds.get(last.key),
|
|
887
|
+
reasoningId: this.reasoningMessageIds.get(last.key),
|
|
888
|
+
anonymous: this.anonymousReasoningKeys.has(last.key),
|
|
889
|
+
});
|
|
890
|
+
last = this.partOrder.at(-1);
|
|
891
|
+
}
|
|
892
|
+
const cut = this.partOrder.length;
|
|
893
|
+
const hiddenSignatures = Array.from(this.hiddenSignatures).filter(
|
|
894
|
+
([id]) => this.hiddenSignatureAnchors.get(id)! >= cut,
|
|
895
|
+
);
|
|
896
|
+
for (const [id] of hiddenSignatures) {
|
|
897
|
+
this.hiddenSignatures.delete(id);
|
|
898
|
+
this.hiddenSignatureAnchors.delete(id);
|
|
899
|
+
}
|
|
900
|
+
const hiddenBlockIds = Array.from(this.hiddenBlockAnchors)
|
|
901
|
+
.filter(([, anchor]) => anchor >= cut)
|
|
902
|
+
.map(([id]) => id);
|
|
903
|
+
const hiddenAnonymous =
|
|
904
|
+
this.hiddenAnonymousAnchor !== undefined &&
|
|
905
|
+
this.hiddenAnonymousAnchor >= cut;
|
|
906
|
+
const activeCarried =
|
|
907
|
+
(this.hiddenActiveReasoning === "identified" &&
|
|
908
|
+
hiddenBlockIds.length > 0) ||
|
|
909
|
+
(this.hiddenActiveReasoning === "anonymous" && hiddenAnonymous);
|
|
910
|
+
return {
|
|
911
|
+
parts,
|
|
912
|
+
activeKey: this.activeReasoningKeyByScope.get(ROOT_SCOPE),
|
|
913
|
+
hiddenSignatures,
|
|
914
|
+
hiddenBlockIds,
|
|
915
|
+
hiddenAnonymous,
|
|
916
|
+
hiddenActiveReasoning: activeCarried
|
|
917
|
+
? this.hiddenActiveReasoning
|
|
918
|
+
: ("none" as const),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
private restoreReasoning(
|
|
923
|
+
carried: ReturnType<RunAggregator["takeTrailingReasoning"]>,
|
|
924
|
+
): void {
|
|
925
|
+
for (const part of carried.parts) {
|
|
926
|
+
this.partOrder.push({ kind: "reasoning", key: part.key });
|
|
927
|
+
this.reasoningParts.set(part.key, part.entry);
|
|
928
|
+
if (part.signature !== undefined)
|
|
929
|
+
this.reasoningSignatures.set(part.key, part.signature);
|
|
930
|
+
if (part.signatureId !== undefined)
|
|
931
|
+
this.reasoningSignatureIds.set(part.key, part.signatureId);
|
|
932
|
+
if (part.reasoningId !== undefined)
|
|
933
|
+
this.reasoningMessageIds.set(part.key, part.reasoningId);
|
|
934
|
+
if (part.anonymous) this.anonymousReasoningKeys.add(part.key);
|
|
935
|
+
}
|
|
936
|
+
if (
|
|
937
|
+
carried.activeKey !== undefined &&
|
|
938
|
+
this.reasoningParts.has(carried.activeKey)
|
|
939
|
+
) {
|
|
940
|
+
this.activeReasoningKeyByScope.set(ROOT_SCOPE, carried.activeKey);
|
|
941
|
+
}
|
|
942
|
+
for (const [id, encryptedValue] of carried.hiddenSignatures) {
|
|
943
|
+
this.hiddenSignatures.set(id, encryptedValue);
|
|
944
|
+
this.hiddenSignatureAnchors.set(id, 0);
|
|
945
|
+
}
|
|
946
|
+
for (const id of carried.hiddenBlockIds) {
|
|
947
|
+
this.hiddenReasoningIds.add(id);
|
|
948
|
+
this.hiddenBlockAnchors.set(id, 0);
|
|
949
|
+
}
|
|
950
|
+
if (carried.hiddenAnonymous) this.hiddenAnonymousAnchor = 0;
|
|
951
|
+
this.hiddenActiveReasoning = carried.hiddenActiveReasoning;
|
|
864
952
|
}
|
|
865
953
|
|
|
866
954
|
private generateTextKey(): string {
|
|
@@ -932,6 +1020,13 @@ export class RunAggregator {
|
|
|
932
1020
|
(part) => part.kind === "tool-call" && part.toolCallId === id,
|
|
933
1021
|
)
|
|
934
1022
|
) {
|
|
1023
|
+
// Tool insertion can precede existing reasoning in display order.
|
|
1024
|
+
if (scope === ROOT_SCOPE) {
|
|
1025
|
+
this.lastReasoningBeforeToolCall = this.partOrder.findLast(
|
|
1026
|
+
(part) =>
|
|
1027
|
+
part.kind === "reasoning" && part.subagentRunId === undefined,
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
935
1030
|
this.insertToolPart(scope, id, parentMessageId);
|
|
936
1031
|
}
|
|
937
1032
|
const state: ToolCallState = {
|
package/src/runtime/types.ts
CHANGED
|
@@ -15,11 +15,29 @@ import type { Logger } from "./logger";
|
|
|
15
15
|
import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
16
16
|
import type { McpToolCallResult } from "./mcp-tool-result";
|
|
17
17
|
|
|
18
|
+
export const AG_UI_METADATA_NAMESPACE = "agui";
|
|
19
|
+
export const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
|
|
20
|
+
export const A2UI_SURFACE_ACTIVITY_TYPE = "a2ui-surface";
|
|
21
|
+
|
|
22
|
+
export type AgUiOpaqueReasoning = {
|
|
23
|
+
id: string;
|
|
24
|
+
encryptedValue: string;
|
|
25
|
+
after?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type AgUiCustomMetadata = {
|
|
29
|
+
/** Wire role restored on export for messages the internal model cannot
|
|
30
|
+
* represent (a developer record rides as a system message). */
|
|
31
|
+
role?: "developer";
|
|
32
|
+
interrupts?: AgUiInterrupt[];
|
|
33
|
+
opaqueReasoning?: AgUiOpaqueReasoning[];
|
|
34
|
+
};
|
|
35
|
+
|
|
18
36
|
/**
|
|
19
|
-
* @experimental This API is still under active development and might change without notice.
|
|
20
|
-
*
|
|
21
37
|
* Same as ExternalStoreThreadListAdapter, except `onSwitchToThread` returns
|
|
22
38
|
* the messages (and optional state) to hydrate the thread with.
|
|
39
|
+
*
|
|
40
|
+
* @deprecated Experimental since 2026-06-13. Not scheduled for removal; the API may change in any release.
|
|
23
41
|
*/
|
|
24
42
|
type SwitchToThreadResult = {
|
|
25
43
|
messages: readonly ThreadMessage[];
|
|
@@ -28,6 +46,8 @@ type SwitchToThreadResult = {
|
|
|
28
46
|
* Set when the thread has a run in flight. The runtime resumes the run
|
|
29
47
|
* after hydrating, the same way `ThreadHistoryAdapter.load()` does when it
|
|
30
48
|
* returns `unstable_resume: true`.
|
|
49
|
+
*
|
|
50
|
+
* @deprecated Experimental since 2025-03-31. Not scheduled for removal; the API may change in any release.
|
|
31
51
|
*/
|
|
32
52
|
unstable_resume?: boolean;
|
|
33
53
|
};
|
|
@@ -50,9 +70,7 @@ export type UseAgUiRuntimeAdapters = {
|
|
|
50
70
|
voice?: RealtimeVoiceAdapter;
|
|
51
71
|
feedback?: FeedbackAdapter;
|
|
52
72
|
history?: ThreadHistoryAdapter;
|
|
53
|
-
/**
|
|
54
|
-
* @experimental This API is still under active development and might change without notice.
|
|
55
|
-
*/
|
|
73
|
+
/** @deprecated Experimental since 2026-01-28. Not scheduled for removal; the API may change in any release. */
|
|
56
74
|
threadList?: UseAgUiThreadListAdapter;
|
|
57
75
|
};
|
|
58
76
|
|
|
@@ -102,6 +120,8 @@ export type UseAgUiRuntimeOptions = ExternalStoreSharedOptions & {
|
|
|
102
120
|
* settles, exposing it on `composer.queue` for `ComposerPrimitive.Queue`.
|
|
103
121
|
* The runtime owns the queue lifecycle because flushing needs the agent's
|
|
104
122
|
* send path and the run's own busy and idle edges.
|
|
123
|
+
*
|
|
124
|
+
* @deprecated Experimental since 2026-06-04. Not scheduled for removal; the API may change in any release.
|
|
105
125
|
*/
|
|
106
126
|
unstable_enableMessageQueue?: boolean | undefined;
|
|
107
127
|
onError?: (e: Error) => void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, cleanup, renderHook } from "@testing-library/react";
|
|
4
|
+
import type { HttpAgent } from "@ag-ui/client";
|
|
5
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
6
|
+
import { useAgUiRuntime } from "./useAgUiRuntime";
|
|
7
|
+
|
|
8
|
+
afterEach(cleanup);
|
|
9
|
+
|
|
10
|
+
it("exposes the deprecated interrupt methods on the runtime facade", () => {
|
|
11
|
+
const agent = {
|
|
12
|
+
runAgent: vi.fn(),
|
|
13
|
+
abortRun: vi.fn(),
|
|
14
|
+
} as unknown as HttpAgent;
|
|
15
|
+
const { result } = renderHook(() => useAgUiRuntime({ agent }));
|
|
16
|
+
|
|
17
|
+
expect(Object.hasOwn(result.current, "unstable_getPendingInterrupts")).toBe(
|
|
18
|
+
true,
|
|
19
|
+
);
|
|
20
|
+
expect(
|
|
21
|
+
Object.hasOwn(result.current, "unstable_submitInterruptResponses"),
|
|
22
|
+
).toBe(true);
|
|
23
|
+
expect(result.current.unstable_getPendingInterrupts()).toEqual([]);
|
|
24
|
+
expect(result.current.unstable_submitInterruptResponses).toEqual(
|
|
25
|
+
expect.any(Function),
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("re-renders when the core publishes a state change", () => {
|
|
30
|
+
const agent = {
|
|
31
|
+
runAgent: vi.fn(),
|
|
32
|
+
abortRun: vi.fn(),
|
|
33
|
+
} as unknown as HttpAgent;
|
|
34
|
+
let renders = 0;
|
|
35
|
+
const { result } = renderHook(() => {
|
|
36
|
+
renders++;
|
|
37
|
+
return useAgUiRuntime({ agent });
|
|
38
|
+
});
|
|
39
|
+
const initialRenders = renders;
|
|
40
|
+
|
|
41
|
+
act(() => {
|
|
42
|
+
result.current.thread.append({
|
|
43
|
+
role: "user",
|
|
44
|
+
content: [{ type: "text", text: "hello" }],
|
|
45
|
+
startRun: false,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(renders).toBeGreaterThan(initialRenders);
|
|
50
|
+
expect(result.current.thread.getState().messages).toHaveLength(1);
|
|
51
|
+
});
|