@assistant-ui/react-ag-ui 0.0.64 → 0.0.66
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 +19 -29
- 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 +100 -23
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +16 -8
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +30 -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 +80 -33
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +7 -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.nested-tool-calls.test.ts +107 -0
- package/src/runtime/adapter/conversions.ts +37 -64
- package/src/runtime/adapter/run-aggregator.reasoning.test.ts +143 -0
- package/src/runtime/adapter/run-aggregator.ts +163 -40
- package/src/runtime/event-parser.ts +18 -4
- package/src/runtime/types.ts +43 -11
- package/src/useAgUiRuntime.external-store.test.tsx +51 -0
- package/src/useAgUiRuntime.queue.test.tsx +63 -2
- package/src/useAgUiRuntime.thread-switch.test.tsx +587 -3
- package/src/useAgUiRuntime.ts +106 -44
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
isMcpAppUri,
|
|
5
5
|
type ChatModelRunResult,
|
|
6
|
+
type MessagePartTiming,
|
|
6
7
|
type MessageStatus,
|
|
7
8
|
type MessageTiming,
|
|
8
9
|
type ThreadAssistantMessagePart,
|
|
@@ -12,19 +13,24 @@ import {
|
|
|
12
13
|
} from "@assistant-ui/core";
|
|
13
14
|
import {
|
|
14
15
|
applyA2uiOperations,
|
|
15
|
-
|
|
16
|
-
surfaceToOperations,
|
|
16
|
+
surfaceToPresentToolCall,
|
|
17
17
|
type A2uiState,
|
|
18
18
|
type A2uiSurfaceState,
|
|
19
|
-
} from "@assistant-ui/
|
|
19
|
+
} from "@assistant-ui/generative-ui/a2ui";
|
|
20
20
|
import jsonpatch, { type Operation } from "fast-json-patch";
|
|
21
21
|
import { readMcpAppResourceUri } from "../mcp-tool-result";
|
|
22
22
|
import { projectAgUiToolApprovals } from "./tool-approval";
|
|
23
|
-
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";
|
|
24
32
|
import type { Logger } from "../logger";
|
|
25
33
|
|
|
26
|
-
export const AG_UI_METADATA_NAMESPACE = "agui";
|
|
27
|
-
|
|
28
34
|
const ROOT_SCOPE = "";
|
|
29
35
|
|
|
30
36
|
// Defensive guard against a malformed or cyclic parentToolCallId /
|
|
@@ -69,20 +75,6 @@ type BuildContext = {
|
|
|
69
75
|
};
|
|
70
76
|
};
|
|
71
77
|
|
|
72
|
-
export type AgUiOpaqueReasoning = {
|
|
73
|
-
id: string;
|
|
74
|
-
encryptedValue: string;
|
|
75
|
-
after?: boolean;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export type AgUiCustomMetadata = {
|
|
79
|
-
/** Wire role restored on export for messages the internal model cannot
|
|
80
|
-
* represent (a developer record rides as a system message). */
|
|
81
|
-
role?: "developer";
|
|
82
|
-
interrupts?: AgUiInterrupt[];
|
|
83
|
-
opaqueReasoning?: AgUiOpaqueReasoning[];
|
|
84
|
-
};
|
|
85
|
-
|
|
86
78
|
type Emit = (update: ChatModelRunResult) => void;
|
|
87
79
|
|
|
88
80
|
type ToolCallState = {
|
|
@@ -177,10 +169,6 @@ const scanJSONContainerDelta = (
|
|
|
177
169
|
return completed && scanner.state === "complete";
|
|
178
170
|
};
|
|
179
171
|
|
|
180
|
-
export const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
|
|
181
|
-
|
|
182
|
-
export const A2UI_SURFACE_ACTIVITY_TYPE = "a2ui-surface";
|
|
183
|
-
|
|
184
172
|
export const isPlainObject = (
|
|
185
173
|
value: unknown,
|
|
186
174
|
): value is Record<string, unknown> =>
|
|
@@ -217,7 +205,10 @@ export class RunAggregator {
|
|
|
217
205
|
private interrupts: AgUiInterrupt[] | undefined;
|
|
218
206
|
private readonly textParts = new Map<string, { buffer: string }>();
|
|
219
207
|
private readonly activeTextMessageIdByScope = new Map<string, string>();
|
|
220
|
-
private readonly reasoningParts = new Map<
|
|
208
|
+
private readonly reasoningParts = new Map<
|
|
209
|
+
string,
|
|
210
|
+
{ buffer: string; timing?: MessagePartTiming }
|
|
211
|
+
>();
|
|
221
212
|
private readonly reasoningSignatures = new Map<string, string>();
|
|
222
213
|
private readonly reasoningSignatureIds = new Map<string, string>();
|
|
223
214
|
// Signatures captured while thinking is hidden have no block to live on;
|
|
@@ -255,6 +246,7 @@ export class RunAggregator {
|
|
|
255
246
|
>();
|
|
256
247
|
private readonly lastResolvedToolCallIdByScope = new Map<string, string>();
|
|
257
248
|
private readonly partOrder: PartOrderEntry[] = [];
|
|
249
|
+
private lastReasoningBeforeToolCall: PartOrderEntry | undefined;
|
|
258
250
|
private textPartCounter = 0;
|
|
259
251
|
private serverMessageIdReported = false;
|
|
260
252
|
private reportedServerMessageId: string | undefined;
|
|
@@ -396,6 +388,7 @@ export class RunAggregator {
|
|
|
396
388
|
this.scopeOf("subagentRunId" in event ? event : {}),
|
|
397
389
|
"messageId" in event ? event.messageId : undefined,
|
|
398
390
|
event.type === "REASONING_MESSAGE_START",
|
|
391
|
+
event.timestamp,
|
|
399
392
|
);
|
|
400
393
|
break;
|
|
401
394
|
case "REASONING_ENCRYPTED_VALUE":
|
|
@@ -454,11 +447,11 @@ export class RunAggregator {
|
|
|
454
447
|
break;
|
|
455
448
|
case "THINKING_TEXT_MESSAGE_END":
|
|
456
449
|
case "THINKING_END":
|
|
457
|
-
this.handleReasoningEnd(ROOT_SCOPE);
|
|
450
|
+
this.handleReasoningEnd(ROOT_SCOPE, event.timestamp);
|
|
458
451
|
break;
|
|
459
452
|
case "REASONING_MESSAGE_END":
|
|
460
453
|
case "REASONING_END":
|
|
461
|
-
this.handleReasoningEnd(this.scopeOf(event));
|
|
454
|
+
this.handleReasoningEnd(this.scopeOf(event), event.timestamp);
|
|
462
455
|
break;
|
|
463
456
|
|
|
464
457
|
case "TOOL_CALL_START": {
|
|
@@ -743,23 +736,27 @@ export class RunAggregator {
|
|
|
743
736
|
|
|
744
737
|
const activeToolCallIds = new Set<string>();
|
|
745
738
|
for (const [surfaceId, surface] of surfaces) {
|
|
746
|
-
const
|
|
747
|
-
|
|
739
|
+
const { toolCall, warnings } = surfaceToPresentToolCall(
|
|
740
|
+
surfaceId,
|
|
741
|
+
surface,
|
|
742
|
+
);
|
|
748
743
|
for (const warning of warnings) {
|
|
749
744
|
this.logger.debug("[agui] a2ui surface conversion warning", warning);
|
|
750
745
|
}
|
|
751
|
-
if (!
|
|
746
|
+
if (!toolCall) continue;
|
|
747
|
+
|
|
748
|
+
const { toolCallId } = toolCall;
|
|
752
749
|
|
|
753
750
|
activeToolCallIds.add(toolCallId);
|
|
754
751
|
|
|
755
752
|
const entry: ToolCallState = {
|
|
756
|
-
toolCallId,
|
|
757
|
-
toolCallName:
|
|
758
|
-
argsText:
|
|
759
|
-
parsedArgs:
|
|
760
|
-
result:
|
|
753
|
+
toolCallId: toolCall.toolCallId,
|
|
754
|
+
toolCallName: toolCall.toolName,
|
|
755
|
+
argsText: toolCall.argsText,
|
|
756
|
+
parsedArgs: toolCall.args,
|
|
757
|
+
result: toolCall.result,
|
|
761
758
|
isError: undefined,
|
|
762
|
-
artifact:
|
|
759
|
+
artifact: toolCall.artifact,
|
|
763
760
|
snapshotResultApplied: false,
|
|
764
761
|
};
|
|
765
762
|
if (!this.toolCalls.has(toolCallId)) {
|
|
@@ -821,6 +818,7 @@ export class RunAggregator {
|
|
|
821
818
|
this.activityParts.clear();
|
|
822
819
|
this.lastResolvedToolCallIdByScope.clear();
|
|
823
820
|
this.partOrder.length = 0;
|
|
821
|
+
this.lastReasoningBeforeToolCall = undefined;
|
|
824
822
|
this.textPartCounter = 0;
|
|
825
823
|
this.activeTextMessageIdByScope.clear();
|
|
826
824
|
this.reportedServerMessageId = undefined;
|
|
@@ -854,8 +852,103 @@ export class RunAggregator {
|
|
|
854
852
|
) {
|
|
855
853
|
return;
|
|
856
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;
|
|
857
860
|
this.resetMessageParts();
|
|
858
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;
|
|
859
952
|
}
|
|
860
953
|
|
|
861
954
|
private generateTextKey(): string {
|
|
@@ -927,6 +1020,13 @@ export class RunAggregator {
|
|
|
927
1020
|
(part) => part.kind === "tool-call" && part.toolCallId === id,
|
|
928
1021
|
)
|
|
929
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
|
+
}
|
|
930
1030
|
this.insertToolPart(scope, id, parentMessageId);
|
|
931
1031
|
}
|
|
932
1032
|
const state: ToolCallState = {
|
|
@@ -1222,7 +1322,8 @@ export class RunAggregator {
|
|
|
1222
1322
|
|
|
1223
1323
|
if (part.kind === "reasoning") {
|
|
1224
1324
|
if (this.showThinking) {
|
|
1225
|
-
const
|
|
1325
|
+
const entry = this.reasoningParts.get(part.key);
|
|
1326
|
+
const buffer = entry?.buffer ?? "";
|
|
1226
1327
|
const isActive =
|
|
1227
1328
|
this.activeReasoningKeyByScope.get(rawScope) === part.key;
|
|
1228
1329
|
if (buffer.length > 0 || isActive) {
|
|
@@ -1235,6 +1336,7 @@ export class RunAggregator {
|
|
|
1235
1336
|
snapshot.push({
|
|
1236
1337
|
type: "reasoning",
|
|
1237
1338
|
text: buffer,
|
|
1339
|
+
...(entry?.timing ? { timing: entry.timing } : {}),
|
|
1238
1340
|
...(Object.keys(meta).length > 0
|
|
1239
1341
|
? { providerMetadata: { [AG_UI_METADATA_NAMESPACE]: meta } }
|
|
1240
1342
|
: {}),
|
|
@@ -1441,6 +1543,7 @@ export class RunAggregator {
|
|
|
1441
1543
|
scope: string,
|
|
1442
1544
|
messageId?: string,
|
|
1443
1545
|
isMessageId = false,
|
|
1546
|
+
timestamp?: number,
|
|
1444
1547
|
): void {
|
|
1445
1548
|
if (!this.showThinking) {
|
|
1446
1549
|
// Hidden-reasoning bookkeeping stays root-only for this task — a
|
|
@@ -1473,13 +1576,23 @@ export class RunAggregator {
|
|
|
1473
1576
|
this.reasoningMessageIds.set(key, messageId);
|
|
1474
1577
|
}
|
|
1475
1578
|
if (!this.reasoningParts.has(key)) {
|
|
1476
|
-
this.reasoningParts.set(key, "");
|
|
1579
|
+
this.reasoningParts.set(key, { buffer: "" });
|
|
1477
1580
|
this.partOrder.push(
|
|
1478
1581
|
scope === ROOT_SCOPE
|
|
1479
1582
|
? { kind: "reasoning", key }
|
|
1480
1583
|
: { kind: "reasoning", key, subagentRunId: scope },
|
|
1481
1584
|
);
|
|
1482
1585
|
}
|
|
1586
|
+
const entry = this.reasoningParts.get(key);
|
|
1587
|
+
if (entry?.timing) {
|
|
1588
|
+
entry.timing = { startedAt: entry.timing.startedAt };
|
|
1589
|
+
} else if (
|
|
1590
|
+
entry &&
|
|
1591
|
+
typeof timestamp === "number" &&
|
|
1592
|
+
Number.isFinite(timestamp)
|
|
1593
|
+
) {
|
|
1594
|
+
entry.timing = { startedAt: timestamp };
|
|
1595
|
+
}
|
|
1483
1596
|
this.activeReasoningKeyByScope.set(scope, key);
|
|
1484
1597
|
this.emit();
|
|
1485
1598
|
}
|
|
@@ -1506,15 +1619,25 @@ export class RunAggregator {
|
|
|
1506
1619
|
}
|
|
1507
1620
|
const key = this.activeReasoningKeyByScope.get(scope);
|
|
1508
1621
|
if (!key) return;
|
|
1509
|
-
|
|
1622
|
+
const entry = this.reasoningParts.get(key);
|
|
1623
|
+
if (entry) entry.buffer += delta;
|
|
1510
1624
|
this.emit();
|
|
1511
1625
|
}
|
|
1512
1626
|
|
|
1513
|
-
private handleReasoningEnd(scope: string): void {
|
|
1627
|
+
private handleReasoningEnd(scope: string, timestamp?: number): void {
|
|
1514
1628
|
if (!this.showThinking) {
|
|
1515
1629
|
if (scope === ROOT_SCOPE) this.hiddenActiveReasoning = "none";
|
|
1516
1630
|
return;
|
|
1517
1631
|
}
|
|
1632
|
+
const key = this.activeReasoningKeyByScope.get(scope);
|
|
1633
|
+
const entry = key ? this.reasoningParts.get(key) : undefined;
|
|
1634
|
+
if (
|
|
1635
|
+
entry?.timing &&
|
|
1636
|
+
typeof timestamp === "number" &&
|
|
1637
|
+
Number.isFinite(timestamp)
|
|
1638
|
+
) {
|
|
1639
|
+
entry.timing = { ...entry.timing, completedAt: timestamp };
|
|
1640
|
+
}
|
|
1518
1641
|
this.activeReasoningKeyByScope.delete(scope);
|
|
1519
1642
|
this.emit();
|
|
1520
1643
|
}
|
|
@@ -146,6 +146,10 @@ export const parseAgUiEvent = (
|
|
|
146
146
|
|
|
147
147
|
const getString = (key: string) =>
|
|
148
148
|
isString(payload[key]) ? (payload[key] as string) : undefined;
|
|
149
|
+
const timestamp =
|
|
150
|
+
typeof payload.timestamp === "number" && Number.isFinite(payload.timestamp)
|
|
151
|
+
? payload.timestamp
|
|
152
|
+
: undefined;
|
|
149
153
|
|
|
150
154
|
switch (typeValue) {
|
|
151
155
|
case "RUN_STARTED": {
|
|
@@ -219,24 +223,31 @@ export const parseAgUiEvent = (
|
|
|
219
223
|
case "THINKING_START":
|
|
220
224
|
return withOptional(
|
|
221
225
|
{ type: "THINKING_START" as const },
|
|
222
|
-
{ title: getString("title") },
|
|
226
|
+
{ title: getString("title"), timestamp },
|
|
223
227
|
);
|
|
224
228
|
case "THINKING_TEXT_MESSAGE_START":
|
|
225
|
-
return
|
|
229
|
+
return withOptional(
|
|
230
|
+
{ type: "THINKING_TEXT_MESSAGE_START" as const },
|
|
231
|
+
{ timestamp },
|
|
232
|
+
);
|
|
226
233
|
case "THINKING_TEXT_MESSAGE_CONTENT": {
|
|
227
234
|
const delta = getString("delta") ?? "";
|
|
228
235
|
return { type: "THINKING_TEXT_MESSAGE_CONTENT", delta };
|
|
229
236
|
}
|
|
230
237
|
case "THINKING_TEXT_MESSAGE_END":
|
|
231
|
-
return
|
|
238
|
+
return withOptional(
|
|
239
|
+
{ type: "THINKING_TEXT_MESSAGE_END" as const },
|
|
240
|
+
{ timestamp },
|
|
241
|
+
);
|
|
232
242
|
case "THINKING_END":
|
|
233
|
-
return { type: "THINKING_END" };
|
|
243
|
+
return withOptional({ type: "THINKING_END" as const }, { timestamp });
|
|
234
244
|
case "REASONING_START":
|
|
235
245
|
return withOptional(
|
|
236
246
|
{ type: "REASONING_START" as const },
|
|
237
247
|
{
|
|
238
248
|
messageId: getString("messageId"),
|
|
239
249
|
subagentRunId: getString("subagentRunId"),
|
|
250
|
+
timestamp,
|
|
240
251
|
},
|
|
241
252
|
);
|
|
242
253
|
case "REASONING_MESSAGE_START":
|
|
@@ -245,6 +256,7 @@ export const parseAgUiEvent = (
|
|
|
245
256
|
{
|
|
246
257
|
messageId: getString("messageId"),
|
|
247
258
|
subagentRunId: getString("subagentRunId"),
|
|
259
|
+
timestamp,
|
|
248
260
|
},
|
|
249
261
|
);
|
|
250
262
|
case "REASONING_MESSAGE_CONTENT": {
|
|
@@ -263,6 +275,7 @@ export const parseAgUiEvent = (
|
|
|
263
275
|
{
|
|
264
276
|
messageId: getString("messageId"),
|
|
265
277
|
subagentRunId: getString("subagentRunId"),
|
|
278
|
+
timestamp,
|
|
266
279
|
},
|
|
267
280
|
);
|
|
268
281
|
case "REASONING_ENCRYPTED_VALUE": {
|
|
@@ -292,6 +305,7 @@ export const parseAgUiEvent = (
|
|
|
292
305
|
{
|
|
293
306
|
messageId: getString("messageId"),
|
|
294
307
|
subagentRunId: getString("subagentRunId"),
|
|
308
|
+
timestamp,
|
|
295
309
|
},
|
|
296
310
|
);
|
|
297
311
|
case "TOOL_CALL_START": {
|
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;
|
|
@@ -180,16 +200,22 @@ export type AgUiEvent =
|
|
|
180
200
|
delta: string;
|
|
181
201
|
subagentRunId?: string;
|
|
182
202
|
}
|
|
183
|
-
| { type: "THINKING_START"; title?: string }
|
|
184
|
-
| { type: "THINKING_TEXT_MESSAGE_START" }
|
|
203
|
+
| { type: "THINKING_START"; title?: string; timestamp?: number }
|
|
204
|
+
| { type: "THINKING_TEXT_MESSAGE_START"; timestamp?: number }
|
|
185
205
|
| { type: "THINKING_TEXT_MESSAGE_CONTENT"; delta: string }
|
|
186
|
-
| { type: "THINKING_TEXT_MESSAGE_END" }
|
|
187
|
-
| { type: "THINKING_END" }
|
|
188
|
-
| {
|
|
206
|
+
| { type: "THINKING_TEXT_MESSAGE_END"; timestamp?: number }
|
|
207
|
+
| { type: "THINKING_END"; timestamp?: number }
|
|
208
|
+
| {
|
|
209
|
+
type: "REASONING_START";
|
|
210
|
+
messageId?: string;
|
|
211
|
+
subagentRunId?: string;
|
|
212
|
+
timestamp?: number;
|
|
213
|
+
}
|
|
189
214
|
| {
|
|
190
215
|
type: "REASONING_MESSAGE_START";
|
|
191
216
|
messageId?: string;
|
|
192
217
|
subagentRunId?: string;
|
|
218
|
+
timestamp?: number;
|
|
193
219
|
}
|
|
194
220
|
| {
|
|
195
221
|
type: "REASONING_MESSAGE_CONTENT";
|
|
@@ -201,6 +227,7 @@ export type AgUiEvent =
|
|
|
201
227
|
type: "REASONING_MESSAGE_END";
|
|
202
228
|
messageId?: string;
|
|
203
229
|
subagentRunId?: string;
|
|
230
|
+
timestamp?: number;
|
|
204
231
|
}
|
|
205
232
|
| {
|
|
206
233
|
type: "REASONING_ENCRYPTED_VALUE";
|
|
@@ -209,7 +236,12 @@ export type AgUiEvent =
|
|
|
209
236
|
encryptedValue: string;
|
|
210
237
|
subagentRunId?: string;
|
|
211
238
|
}
|
|
212
|
-
| {
|
|
239
|
+
| {
|
|
240
|
+
type: "REASONING_END";
|
|
241
|
+
messageId?: string;
|
|
242
|
+
subagentRunId?: string;
|
|
243
|
+
timestamp?: number;
|
|
244
|
+
}
|
|
213
245
|
| {
|
|
214
246
|
type: "TOOL_CALL_START";
|
|
215
247
|
toolCallId: string;
|
|
@@ -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
|
+
});
|
|
@@ -96,11 +96,67 @@ describe("useAgUiRuntime unstable_enableMessageQueue", () => {
|
|
|
96
96
|
);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
it("holds queued sends while sending is disabled", async () => {
|
|
100
|
+
const { agent, runAgent, release } = gatedAgent();
|
|
101
|
+
|
|
102
|
+
const { result, rerender } = renderHook(
|
|
103
|
+
({ isSendDisabled }) =>
|
|
104
|
+
useAgUiRuntime({
|
|
105
|
+
agent,
|
|
106
|
+
unstable_enableMessageQueue: true,
|
|
107
|
+
isSendDisabled,
|
|
108
|
+
}),
|
|
109
|
+
{ initialProps: { isSendDisabled: false } },
|
|
110
|
+
);
|
|
111
|
+
mount(result.current);
|
|
112
|
+
|
|
113
|
+
await act(async () => {
|
|
114
|
+
await result.current.thread.append({
|
|
115
|
+
role: "user",
|
|
116
|
+
content: [{ type: "text", text: "first" }],
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
await waitFor(() => expect(runAgent).toHaveBeenCalledTimes(1));
|
|
120
|
+
|
|
121
|
+
await act(async () => {
|
|
122
|
+
await result.current.thread.append({
|
|
123
|
+
role: "user",
|
|
124
|
+
content: [{ type: "text", text: "second" }],
|
|
125
|
+
parentId: result.current.thread.getState().messages.at(-1)?.id ?? null,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
await waitFor(() =>
|
|
129
|
+
expect(screen.getByTestId("queued").textContent).toBe("second"),
|
|
130
|
+
);
|
|
131
|
+
rerender({ isSendDisabled: true });
|
|
132
|
+
|
|
133
|
+
await act(async () => {
|
|
134
|
+
release();
|
|
135
|
+
});
|
|
136
|
+
await waitFor(() =>
|
|
137
|
+
expect(result.current.thread.getState().isRunning).toBe(false),
|
|
138
|
+
);
|
|
139
|
+
expect(runAgent).toHaveBeenCalledTimes(1);
|
|
140
|
+
expect(screen.getByTestId("queued").textContent).toBe("second");
|
|
141
|
+
|
|
142
|
+
rerender({ isSendDisabled: false });
|
|
143
|
+
await waitFor(() => expect(runAgent).toHaveBeenCalledTimes(2));
|
|
144
|
+
await waitFor(() =>
|
|
145
|
+
expect(screen.getByTestId("queued").textContent).toBe(""),
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
99
149
|
it("preserves queued sends when the active run is cancelled", async () => {
|
|
100
150
|
const { agent, runAgent, release } = gatedAgent();
|
|
101
151
|
|
|
102
|
-
const { result } = renderHook(
|
|
103
|
-
|
|
152
|
+
const { result, rerender } = renderHook(
|
|
153
|
+
({ isSendDisabled }) =>
|
|
154
|
+
useAgUiRuntime({
|
|
155
|
+
agent,
|
|
156
|
+
unstable_enableMessageQueue: true,
|
|
157
|
+
isSendDisabled,
|
|
158
|
+
}),
|
|
159
|
+
{ initialProps: { isSendDisabled: false } },
|
|
104
160
|
);
|
|
105
161
|
mount(result.current);
|
|
106
162
|
|
|
@@ -135,6 +191,11 @@ describe("useAgUiRuntime unstable_enableMessageQueue", () => {
|
|
|
135
191
|
expect(screen.getByTestId("queued").textContent).toBe("second"),
|
|
136
192
|
);
|
|
137
193
|
|
|
194
|
+
rerender({ isSendDisabled: true });
|
|
195
|
+
rerender({ isSendDisabled: false });
|
|
196
|
+
expect(runAgent).toHaveBeenCalledTimes(1);
|
|
197
|
+
expect(screen.getByTestId("queued").textContent).toBe("second");
|
|
198
|
+
|
|
138
199
|
await act(async () => {
|
|
139
200
|
await result.current.thread.append({
|
|
140
201
|
role: "user",
|