@assistant-ui/react-ag-ui 0.0.57 → 0.0.59
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/index.d.ts +2 -2
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +19 -8
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +118 -86
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +34 -11
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +13 -3
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +385 -119
- 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 +67 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +15 -5
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +9 -9
- package/src/index.ts +1 -0
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +18 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +204 -120
- package/src/runtime/adapter/conversions.ts +87 -17
- package/src/runtime/adapter/run-aggregator.approval.test.ts +128 -0
- package/src/runtime/adapter/run-aggregator.ts +595 -139
- 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 +96 -12
- package/src/useAgUiRuntime.approval.test.tsx +34 -24
- package/src/useAgUiRuntime.queue.test.tsx +54 -0
- package/src/useAgUiRuntime.thread-switch.test.tsx +199 -0
- package/src/useAgUiRuntime.toolDeferral.test.tsx +186 -0
- package/src/useAgUiRuntime.ts +15 -3
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import {
|
|
4
4
|
isMcpAppUri,
|
|
5
5
|
type ChatModelRunResult,
|
|
6
|
+
type MessageStatus,
|
|
6
7
|
type MessageTiming,
|
|
7
8
|
type ThreadAssistantMessagePart,
|
|
9
|
+
type ThreadMessage,
|
|
8
10
|
type ToolCallMessagePart,
|
|
9
11
|
type ToolModelContentPart,
|
|
10
12
|
} from "@assistant-ui/core";
|
|
@@ -21,6 +23,50 @@ import type { Logger } from "../logger";
|
|
|
21
23
|
|
|
22
24
|
export const AG_UI_METADATA_NAMESPACE = "agui";
|
|
23
25
|
|
|
26
|
+
const ROOT_SCOPE = "";
|
|
27
|
+
|
|
28
|
+
// Defensive guard against a malformed or cyclic parentToolCallId /
|
|
29
|
+
// parentSubagentRunId chain, not a realistic nesting depth.
|
|
30
|
+
const MAX_SUBAGENT_DEPTH = 16;
|
|
31
|
+
|
|
32
|
+
type SubagentRunState = {
|
|
33
|
+
subagentRunId: string;
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
parentSubagentRunId?: string;
|
|
37
|
+
parentToolCallId?: string;
|
|
38
|
+
parentMessageId?: string;
|
|
39
|
+
createdAt: Date;
|
|
40
|
+
status: MessageStatus;
|
|
41
|
+
result?: unknown;
|
|
42
|
+
interruptIds?: string[];
|
|
43
|
+
errorCode?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type ToolApproval = NonNullable<ToolCallMessagePart["approval"]>;
|
|
47
|
+
|
|
48
|
+
type PartOrderEntry =
|
|
49
|
+
| { kind: "text"; key: string; subagentRunId?: string }
|
|
50
|
+
| { kind: "reasoning"; key: string; subagentRunId?: string }
|
|
51
|
+
| { kind: "tool-call"; toolCallId: string }
|
|
52
|
+
| { kind: "data"; name: string; value: unknown };
|
|
53
|
+
|
|
54
|
+
type BuildContext = {
|
|
55
|
+
subagentsByParentToolCallId: Map<string, string[]>;
|
|
56
|
+
reachable: Set<string>;
|
|
57
|
+
approvals: ReadonlyMap<string, ToolApproval>;
|
|
58
|
+
// partOrder bucketed by the scope each part renders in, so one emit walks
|
|
59
|
+
// every part once however many subagents the run spawned.
|
|
60
|
+
partsByScope: Map<string, { index: number; part: PartOrderEntry }[]>;
|
|
61
|
+
// Present only for the root walk: the pass that builds the snapshot is also
|
|
62
|
+
// the one that decides where a retracted reasoning signature anchors, so the
|
|
63
|
+
// two can never disagree about what materialized.
|
|
64
|
+
root?: {
|
|
65
|
+
opaqueCandidates: (AgUiOpaqueReasoning & { anchor: number })[];
|
|
66
|
+
lastMaterializedIndex: number;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
24
70
|
export type AgUiOpaqueReasoning = {
|
|
25
71
|
id: string;
|
|
26
72
|
encryptedValue: string;
|
|
@@ -41,6 +87,7 @@ type ToolCallState = {
|
|
|
41
87
|
toolCallId: string;
|
|
42
88
|
toolCallName: string;
|
|
43
89
|
argsText: string;
|
|
90
|
+
argsTextScanner?: JSONContainerScanner | undefined;
|
|
44
91
|
parsedArgs: Record<string, unknown> | undefined;
|
|
45
92
|
result: unknown;
|
|
46
93
|
isError: boolean | undefined;
|
|
@@ -50,6 +97,81 @@ type ToolCallState = {
|
|
|
50
97
|
mcpAppServerId?: string;
|
|
51
98
|
modelContent?: ToolModelContentPart[];
|
|
52
99
|
snapshotResultApplied: boolean;
|
|
100
|
+
subagentRunId?: string;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type JSONContainerScanner = {
|
|
104
|
+
state: "pending" | "open" | "complete" | "invalid";
|
|
105
|
+
stack: ("{" | "[")[];
|
|
106
|
+
inString: boolean;
|
|
107
|
+
escaped: boolean;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const createJSONContainerScanner = (): JSONContainerScanner => ({
|
|
111
|
+
state: "pending",
|
|
112
|
+
stack: [],
|
|
113
|
+
inString: false,
|
|
114
|
+
escaped: false,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const isJSONWhitespace = (char: string): boolean =>
|
|
118
|
+
char === " " || char === "\t" || char === "\n" || char === "\r";
|
|
119
|
+
|
|
120
|
+
const scanJSONContainerDelta = (
|
|
121
|
+
scanner: JSONContainerScanner,
|
|
122
|
+
delta: string,
|
|
123
|
+
): boolean => {
|
|
124
|
+
let completed = false;
|
|
125
|
+
|
|
126
|
+
for (const char of delta) {
|
|
127
|
+
if (scanner.state === "invalid") continue;
|
|
128
|
+
if (scanner.state === "complete") {
|
|
129
|
+
if (isJSONWhitespace(char)) continue;
|
|
130
|
+
scanner.state = "invalid";
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (scanner.state === "pending") {
|
|
134
|
+
if (isJSONWhitespace(char)) continue;
|
|
135
|
+
if (char !== "{" && char !== "[") {
|
|
136
|
+
scanner.state = "invalid";
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
scanner.state = "open";
|
|
140
|
+
scanner.stack.push(char);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (scanner.inString) {
|
|
144
|
+
if (scanner.escaped) {
|
|
145
|
+
scanner.escaped = false;
|
|
146
|
+
} else if (char === "\\") {
|
|
147
|
+
scanner.escaped = true;
|
|
148
|
+
} else if (char === '"') {
|
|
149
|
+
scanner.inString = false;
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (char === '"') {
|
|
154
|
+
scanner.inString = true;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (char === "{" || char === "[") {
|
|
158
|
+
scanner.stack.push(char);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (char === "}" || char === "]") {
|
|
162
|
+
const expected = char === "}" ? "{" : "[";
|
|
163
|
+
if (scanner.stack.pop() !== expected) {
|
|
164
|
+
scanner.state = "invalid";
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (scanner.stack.length === 0) {
|
|
168
|
+
scanner.state = "complete";
|
|
169
|
+
completed = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return completed && scanner.state === "complete";
|
|
53
175
|
};
|
|
54
176
|
|
|
55
177
|
export const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
|
|
@@ -91,7 +213,7 @@ export class RunAggregator {
|
|
|
91
213
|
private status: ChatModelRunResult["status"] | undefined;
|
|
92
214
|
private interrupts: AgUiInterrupt[] | undefined;
|
|
93
215
|
private readonly textParts = new Map<string, { buffer: string }>();
|
|
94
|
-
private
|
|
216
|
+
private readonly activeTextMessageIdByScope = new Map<string, string>();
|
|
95
217
|
private readonly reasoningParts = new Map<string, string>(); // key → buffer
|
|
96
218
|
private readonly reasoningSignatures = new Map<string, string>();
|
|
97
219
|
private readonly reasoningSignatureIds = new Map<string, string>();
|
|
@@ -109,18 +231,23 @@ export class RunAggregator {
|
|
|
109
231
|
private readonly loggedDroppedOpaqueIds = new Set<string>();
|
|
110
232
|
private readonly reasoningMessageIds = new Map<string, string>();
|
|
111
233
|
private readonly anonymousReasoningKeys = new Set<string>();
|
|
112
|
-
private
|
|
234
|
+
private readonly activeReasoningKeyByScope = new Map<string, string>();
|
|
235
|
+
private readonly subagentRuns = new Map<string, SubagentRunState>();
|
|
236
|
+
// The nesting graph only moves when a run is announced or a tool call is
|
|
237
|
+
// created, which is rare next to emit(), so it is derived once per change
|
|
238
|
+
// rather than per streamed delta.
|
|
239
|
+
private nestingMemo:
|
|
240
|
+
| {
|
|
241
|
+
subagentsByParentToolCallId: Map<string, string[]>;
|
|
242
|
+
reachable: Set<string>;
|
|
243
|
+
}
|
|
244
|
+
| undefined;
|
|
113
245
|
private reasoningPartCounter = 0;
|
|
114
246
|
private readonly toolCalls = new Map<string, ToolCallState>();
|
|
115
247
|
private readonly a2uiBuckets = new Map<string, A2uiState>();
|
|
116
248
|
private readonly a2uiToolCallIds = new Set<string>();
|
|
117
|
-
private
|
|
118
|
-
private readonly partOrder:
|
|
119
|
-
| { kind: "text"; key: string }
|
|
120
|
-
| { kind: "reasoning"; key: string }
|
|
121
|
-
| { kind: "tool-call"; toolCallId: string }
|
|
122
|
-
| { kind: "data"; name: string; value: unknown }
|
|
123
|
-
)[] = [];
|
|
249
|
+
private readonly lastResolvedToolCallIdByScope = new Map<string, string>();
|
|
250
|
+
private readonly partOrder: PartOrderEntry[] = [];
|
|
124
251
|
private textPartCounter = 0;
|
|
125
252
|
private serverMessageIdReported = false;
|
|
126
253
|
private reportedServerMessageId: string | undefined;
|
|
@@ -166,14 +293,18 @@ export class RunAggregator {
|
|
|
166
293
|
}
|
|
167
294
|
|
|
168
295
|
this.interrupts = undefined;
|
|
296
|
+
// A call nested inside a subagent message is answerable the same way
|
|
297
|
+
// a root call is: getPendingToolCalls and addToolResult walk
|
|
298
|
+
// ToolCallMessagePart.messages, so any unresolved call keeps the run
|
|
299
|
+
// resumable.
|
|
169
300
|
const hasUnresolvedToolCalls = Array.from(this.toolCalls.values()).some(
|
|
170
301
|
(tc) => tc.result === undefined,
|
|
171
302
|
);
|
|
172
303
|
|
|
173
|
-
this.status =
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
304
|
+
this.status = hasUnresolvedToolCalls
|
|
305
|
+
? { type: "requires-action", reason: "tool-calls" }
|
|
306
|
+
: { type: "complete", reason: "unknown" };
|
|
307
|
+
this.closeOpenSubagentRuns(this.status);
|
|
177
308
|
this.emit();
|
|
178
309
|
break;
|
|
179
310
|
}
|
|
@@ -183,48 +314,61 @@ export class RunAggregator {
|
|
|
183
314
|
reason: "error",
|
|
184
315
|
...(event.message !== undefined ? { error: event.message } : {}),
|
|
185
316
|
};
|
|
317
|
+
this.closeOpenSubagentRuns(this.status);
|
|
186
318
|
this.emit();
|
|
187
319
|
break;
|
|
188
320
|
}
|
|
189
321
|
case "RUN_CANCELLED": {
|
|
190
322
|
this.status = { type: "incomplete", reason: "cancelled" };
|
|
323
|
+
this.closeOpenSubagentRuns(this.status);
|
|
191
324
|
this.emit();
|
|
192
325
|
break;
|
|
193
326
|
}
|
|
194
327
|
|
|
195
328
|
case "TEXT_MESSAGE_START": {
|
|
196
|
-
this.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
329
|
+
const scope = this.scopeOf(event);
|
|
330
|
+
if (scope === ROOT_SCOPE) {
|
|
331
|
+
this.beginDistinctTextMessage(event.messageId);
|
|
332
|
+
this.reportServerMessageId(event.messageId);
|
|
333
|
+
if (event.messageId) this.lastTextMessageId = event.messageId;
|
|
334
|
+
}
|
|
335
|
+
this.startTextMessage(scope, event.messageId);
|
|
200
336
|
this.emit();
|
|
201
337
|
break;
|
|
202
338
|
}
|
|
203
339
|
case "TEXT_MESSAGE_CONTENT":
|
|
204
340
|
case "TEXT_MESSAGE_CHUNK": {
|
|
205
341
|
const incomingId = "messageId" in event ? event.messageId : undefined;
|
|
206
|
-
this.
|
|
207
|
-
|
|
208
|
-
|
|
342
|
+
const scope = this.scopeOf(event);
|
|
343
|
+
if (scope === ROOT_SCOPE) {
|
|
344
|
+
this.beginDistinctTextMessage(incomingId);
|
|
345
|
+
this.reportServerMessageId(incomingId);
|
|
346
|
+
if (incomingId) this.lastTextMessageId = incomingId;
|
|
347
|
+
}
|
|
209
348
|
if (!event.delta) break;
|
|
210
349
|
this.recordFirstToken();
|
|
211
|
-
const id = this.resolveTextMessageId(incomingId);
|
|
350
|
+
const id = this.resolveTextMessageId(scope, incomingId);
|
|
212
351
|
this.appendText(id, event.delta);
|
|
213
352
|
this.totalChunks++;
|
|
214
353
|
this.emit();
|
|
215
354
|
break;
|
|
216
355
|
}
|
|
217
356
|
case "TEXT_MESSAGE_END": {
|
|
218
|
-
this.
|
|
219
|
-
if (
|
|
220
|
-
|
|
357
|
+
const scope = this.scopeOf(event);
|
|
358
|
+
if (scope === ROOT_SCOPE) this.reportServerMessageId(event.messageId);
|
|
359
|
+
if (
|
|
360
|
+
event.messageId &&
|
|
361
|
+
this.activeTextMessageIdByScope.get(scope) ===
|
|
362
|
+
this.partKey(scope, event.messageId)
|
|
363
|
+
) {
|
|
364
|
+
this.activeTextMessageIdByScope.delete(scope);
|
|
221
365
|
}
|
|
222
366
|
this.emit();
|
|
223
367
|
break;
|
|
224
368
|
}
|
|
225
369
|
|
|
226
370
|
case "CUSTOM": {
|
|
227
|
-
this.
|
|
371
|
+
this.activeTextMessageIdByScope.delete(ROOT_SCOPE);
|
|
228
372
|
this.partOrder.push({
|
|
229
373
|
kind: "data",
|
|
230
374
|
name: event.name,
|
|
@@ -239,18 +383,21 @@ export class RunAggregator {
|
|
|
239
383
|
case "REASONING_START":
|
|
240
384
|
case "REASONING_MESSAGE_START":
|
|
241
385
|
this.handleReasoningStart(
|
|
386
|
+
this.scopeOf("subagentRunId" in event ? event : {}),
|
|
242
387
|
"messageId" in event ? event.messageId : undefined,
|
|
243
388
|
event.type === "REASONING_MESSAGE_START",
|
|
244
389
|
);
|
|
245
390
|
break;
|
|
246
391
|
case "REASONING_ENCRYPTED_VALUE":
|
|
247
392
|
if (event.subtype === "message") {
|
|
393
|
+
const scope = this.scopeOf(event);
|
|
248
394
|
// entityId names any message, not necessarily a reasoning one, so an
|
|
249
395
|
// unmatched id may only claim a block that has no id to contradict it.
|
|
250
|
-
const active = this.
|
|
396
|
+
const active = this.activeReasoningKeyByScope.get(scope);
|
|
397
|
+
const entityKey = this.partKey(scope, event.entityId);
|
|
251
398
|
const key = this.showThinking
|
|
252
|
-
? this.reasoningParts.has(
|
|
253
|
-
?
|
|
399
|
+
? this.reasoningParts.has(entityKey)
|
|
400
|
+
? entityKey
|
|
254
401
|
: active !== undefined && this.anonymousReasoningKeys.has(active)
|
|
255
402
|
? active
|
|
256
403
|
: undefined
|
|
@@ -261,6 +408,9 @@ export class RunAggregator {
|
|
|
261
408
|
this.emit();
|
|
262
409
|
} else if (
|
|
263
410
|
!this.showThinking &&
|
|
411
|
+
// Hidden-reasoning bookkeeping stays root-only for this task — a
|
|
412
|
+
// subagent's opaque/hidden reasoning signatures are not preserved.
|
|
413
|
+
scope === ROOT_SCOPE &&
|
|
264
414
|
event.entityId.trim().length > 0 &&
|
|
265
415
|
event.encryptedValue.trim().length > 0 &&
|
|
266
416
|
(this.hiddenReasoningIds.has(event.entityId) ||
|
|
@@ -278,12 +428,13 @@ export class RunAggregator {
|
|
|
278
428
|
}
|
|
279
429
|
break;
|
|
280
430
|
case "THINKING_TEXT_MESSAGE_CONTENT":
|
|
281
|
-
this.handleReasoningContent(event.delta);
|
|
431
|
+
this.handleReasoningContent(ROOT_SCOPE, event.delta);
|
|
282
432
|
this.totalChunks++;
|
|
283
433
|
this.recordFirstToken();
|
|
284
434
|
break;
|
|
285
435
|
case "REASONING_MESSAGE_CONTENT":
|
|
286
436
|
this.handleReasoningContent(
|
|
437
|
+
this.scopeOf(event),
|
|
287
438
|
event.delta,
|
|
288
439
|
"messageId" in event ? event.messageId : undefined,
|
|
289
440
|
true,
|
|
@@ -293,14 +444,20 @@ export class RunAggregator {
|
|
|
293
444
|
break;
|
|
294
445
|
case "THINKING_TEXT_MESSAGE_END":
|
|
295
446
|
case "THINKING_END":
|
|
447
|
+
this.handleReasoningEnd(ROOT_SCOPE);
|
|
448
|
+
break;
|
|
296
449
|
case "REASONING_MESSAGE_END":
|
|
297
450
|
case "REASONING_END":
|
|
298
|
-
this.handleReasoningEnd();
|
|
451
|
+
this.handleReasoningEnd(this.scopeOf(event));
|
|
299
452
|
break;
|
|
300
453
|
|
|
301
454
|
case "TOOL_CALL_START": {
|
|
302
|
-
this.
|
|
455
|
+
const scope = this.scopeOf(event);
|
|
456
|
+
if (scope === ROOT_SCOPE) {
|
|
457
|
+
this.reportServerMessageId(event.parentMessageId);
|
|
458
|
+
}
|
|
303
459
|
this.startToolCall(
|
|
460
|
+
scope,
|
|
304
461
|
event.toolCallId,
|
|
305
462
|
event.toolCallName,
|
|
306
463
|
event.parentMessageId,
|
|
@@ -310,7 +467,10 @@ export class RunAggregator {
|
|
|
310
467
|
}
|
|
311
468
|
case "TOOL_CALL_ARGS":
|
|
312
469
|
case "TOOL_CALL_CHUNK": {
|
|
313
|
-
if (
|
|
470
|
+
if (
|
|
471
|
+
event.type === "TOOL_CALL_CHUNK" &&
|
|
472
|
+
this.scopeOf(event) === ROOT_SCOPE
|
|
473
|
+
) {
|
|
314
474
|
this.reportServerMessageId(event.parentMessageId);
|
|
315
475
|
}
|
|
316
476
|
if (!event.delta) break;
|
|
@@ -324,6 +484,7 @@ export class RunAggregator {
|
|
|
324
484
|
}
|
|
325
485
|
case "TOOL_CALL_RESULT": {
|
|
326
486
|
this.finishToolCall(
|
|
487
|
+
this.scopeOf(event),
|
|
327
488
|
event.toolCallId,
|
|
328
489
|
event.content ?? "",
|
|
329
490
|
typeof event.mcpResult?.isError === "boolean"
|
|
@@ -343,12 +504,15 @@ export class RunAggregator {
|
|
|
343
504
|
break;
|
|
344
505
|
}
|
|
345
506
|
if (event.activityType !== MCP_APPS_ACTIVITY_TYPE) break;
|
|
507
|
+
const activityScope = this.scopeOf(event);
|
|
346
508
|
const toolCallId = event.content.toolCallId;
|
|
509
|
+
const fallbackId =
|
|
510
|
+
this.lastResolvedToolCallIdByScope.get(activityScope);
|
|
347
511
|
const entry =
|
|
348
512
|
typeof toolCallId === "string"
|
|
349
513
|
? this.toolCalls.get(toolCallId)
|
|
350
|
-
:
|
|
351
|
-
? this.toolCalls.get(
|
|
514
|
+
: fallbackId
|
|
515
|
+
? this.toolCalls.get(fallbackId)
|
|
352
516
|
: undefined;
|
|
353
517
|
const resourceUri = event.content.resourceUri;
|
|
354
518
|
if (
|
|
@@ -391,6 +555,70 @@ export class RunAggregator {
|
|
|
391
555
|
break;
|
|
392
556
|
}
|
|
393
557
|
|
|
558
|
+
case "SUBAGENT_STARTED": {
|
|
559
|
+
// A suspended subagent is re-announced with the same id on resume, so
|
|
560
|
+
// a repeat is a continuation: it may refresh the descriptive fields
|
|
561
|
+
// and reopen the status, but re-parenting an established run would
|
|
562
|
+
// move its already-rendered output to a different tool call.
|
|
563
|
+
const existing = this.subagentRuns.get(event.subagentRunId);
|
|
564
|
+
if (existing) {
|
|
565
|
+
existing.name = event.name;
|
|
566
|
+
if (event.description !== undefined)
|
|
567
|
+
existing.description = event.description;
|
|
568
|
+
existing.status = { type: "running" };
|
|
569
|
+
this.emit();
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
this.nestingMemo = undefined;
|
|
573
|
+
this.subagentRuns.set(event.subagentRunId, {
|
|
574
|
+
subagentRunId: event.subagentRunId,
|
|
575
|
+
name: event.name,
|
|
576
|
+
...(event.description !== undefined
|
|
577
|
+
? { description: event.description }
|
|
578
|
+
: {}),
|
|
579
|
+
...(event.parentSubagentRunId !== undefined
|
|
580
|
+
? { parentSubagentRunId: event.parentSubagentRunId }
|
|
581
|
+
: {}),
|
|
582
|
+
...(event.parentToolCallId !== undefined
|
|
583
|
+
? { parentToolCallId: event.parentToolCallId }
|
|
584
|
+
: {}),
|
|
585
|
+
...(event.parentMessageId !== undefined
|
|
586
|
+
? { parentMessageId: event.parentMessageId }
|
|
587
|
+
: {}),
|
|
588
|
+
createdAt: new Date(),
|
|
589
|
+
status: { type: "running" },
|
|
590
|
+
});
|
|
591
|
+
this.emit();
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
case "SUBAGENT_FINISHED": {
|
|
595
|
+
const run = this.subagentRuns.get(event.subagentRunId);
|
|
596
|
+
if (run) {
|
|
597
|
+
run.status =
|
|
598
|
+
event.outcome?.type === "suspended"
|
|
599
|
+
? { type: "requires-action", reason: "interrupt" }
|
|
600
|
+
: { type: "complete", reason: "unknown" };
|
|
601
|
+
if (event.result !== undefined) run.result = event.result;
|
|
602
|
+
if (event.outcome?.type === "suspended" && event.outcome.interruptIds)
|
|
603
|
+
run.interruptIds = event.outcome.interruptIds;
|
|
604
|
+
}
|
|
605
|
+
this.emit();
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
case "SUBAGENT_ERROR": {
|
|
609
|
+
const run = this.subagentRuns.get(event.subagentRunId);
|
|
610
|
+
if (run) {
|
|
611
|
+
run.status = {
|
|
612
|
+
type: "incomplete",
|
|
613
|
+
reason: "error",
|
|
614
|
+
...(event.message !== undefined ? { error: event.message } : {}),
|
|
615
|
+
};
|
|
616
|
+
if (event.code !== undefined) run.errorCode = event.code;
|
|
617
|
+
}
|
|
618
|
+
this.emit();
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
|
|
394
622
|
default: {
|
|
395
623
|
this.logger.debug?.("[agui] aggregator ignored event", event);
|
|
396
624
|
}
|
|
@@ -448,12 +676,14 @@ export class RunAggregator {
|
|
|
448
676
|
if (!this.toolCalls.has(toolCallId)) {
|
|
449
677
|
this.partOrder.push({ kind: "tool-call", toolCallId });
|
|
450
678
|
}
|
|
679
|
+
this.nestingMemo = undefined;
|
|
451
680
|
this.toolCalls.set(toolCallId, entry);
|
|
452
681
|
this.a2uiToolCallIds.add(toolCallId);
|
|
453
682
|
}
|
|
454
683
|
|
|
455
684
|
for (const toolCallId of this.a2uiToolCallIds) {
|
|
456
685
|
if (activeToolCallIds.has(toolCallId)) continue;
|
|
686
|
+
this.nestingMemo = undefined;
|
|
457
687
|
this.toolCalls.delete(toolCallId);
|
|
458
688
|
const partIndex = this.partOrder.findIndex(
|
|
459
689
|
(part) => part.kind === "tool-call" && part.toolCallId === toolCallId,
|
|
@@ -492,18 +722,39 @@ export class RunAggregator {
|
|
|
492
722
|
this.hiddenActiveReasoning = "none";
|
|
493
723
|
this.loggedDroppedOpaqueIds.clear();
|
|
494
724
|
this.anonymousReasoningKeys.clear();
|
|
495
|
-
this.
|
|
725
|
+
this.activeReasoningKeyByScope.clear();
|
|
726
|
+
this.nestingMemo = undefined;
|
|
727
|
+
this.subagentRuns.clear();
|
|
496
728
|
this.reasoningPartCounter = 0;
|
|
497
729
|
this.toolCalls.clear();
|
|
498
730
|
this.a2uiBuckets.clear();
|
|
499
731
|
this.a2uiToolCallIds.clear();
|
|
500
|
-
this.
|
|
732
|
+
this.lastResolvedToolCallIdByScope.clear();
|
|
501
733
|
this.partOrder.length = 0;
|
|
502
734
|
this.textPartCounter = 0;
|
|
503
|
-
this.
|
|
735
|
+
this.activeTextMessageIdByScope.clear();
|
|
504
736
|
this.reportedServerMessageId = undefined;
|
|
505
737
|
}
|
|
506
738
|
|
|
739
|
+
// A subagent only ever reports its own terminal, so a run that ends while
|
|
740
|
+
// one is still streaming would leave that nested message spinning forever.
|
|
741
|
+
private closeOpenSubagentRuns(status: MessageStatus): void {
|
|
742
|
+
for (const run of this.subagentRuns.values()) {
|
|
743
|
+
if (run.status.type === "running") run.status = status;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private scopeOf(event: { subagentRunId?: string }): string {
|
|
748
|
+
return event.subagentRunId ?? ROOT_SCOPE;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Nothing in the AG-UI schema makes a messageId unique across subagent runs,
|
|
752
|
+
// so the accumulators key on the scope too. Root keys stay bare, which is
|
|
753
|
+
// what the wire-id collision checks against reported message ids compare on.
|
|
754
|
+
private partKey(scope: string, id: string): string {
|
|
755
|
+
return scope === ROOT_SCOPE ? id : `${scope}\u0000${id}`;
|
|
756
|
+
}
|
|
757
|
+
|
|
507
758
|
private beginDistinctTextMessage(messageId: string | undefined): void {
|
|
508
759
|
if (
|
|
509
760
|
!messageId ||
|
|
@@ -522,63 +773,71 @@ export class RunAggregator {
|
|
|
522
773
|
return `text-${this.textPartCounter}`;
|
|
523
774
|
}
|
|
524
775
|
|
|
525
|
-
private startTextMessage(messageId?: string): string {
|
|
776
|
+
private startTextMessage(scope: string, messageId?: string): string {
|
|
526
777
|
const id = messageId ?? this.generateTextKey();
|
|
527
|
-
this.ensureTextPart(id);
|
|
528
|
-
this.
|
|
529
|
-
return
|
|
778
|
+
const key = this.ensureTextPart(scope, id);
|
|
779
|
+
this.activeTextMessageIdByScope.set(scope, key);
|
|
780
|
+
return key;
|
|
530
781
|
}
|
|
531
782
|
|
|
532
|
-
private resolveTextMessageId(messageId?: string): string {
|
|
783
|
+
private resolveTextMessageId(scope: string, messageId?: string): string {
|
|
533
784
|
if (messageId) {
|
|
534
|
-
this.ensureTextPart(messageId);
|
|
535
|
-
this.
|
|
536
|
-
return
|
|
785
|
+
const key = this.ensureTextPart(scope, messageId);
|
|
786
|
+
this.activeTextMessageIdByScope.set(scope, key);
|
|
787
|
+
return key;
|
|
537
788
|
}
|
|
538
789
|
|
|
539
|
-
|
|
540
|
-
|
|
790
|
+
const active = this.activeTextMessageIdByScope.get(scope);
|
|
791
|
+
if (active) {
|
|
792
|
+
return active;
|
|
541
793
|
}
|
|
542
794
|
|
|
543
|
-
const
|
|
544
|
-
this.
|
|
545
|
-
|
|
546
|
-
return generated;
|
|
795
|
+
const key = this.ensureTextPart(scope, this.generateTextKey());
|
|
796
|
+
this.activeTextMessageIdByScope.set(scope, key);
|
|
797
|
+
return key;
|
|
547
798
|
}
|
|
548
799
|
|
|
549
|
-
private ensureTextPart(id: string):
|
|
550
|
-
|
|
551
|
-
|
|
800
|
+
private ensureTextPart(scope: string, id: string): string {
|
|
801
|
+
const key = this.partKey(scope, id);
|
|
802
|
+
if (!this.textParts.has(key)) {
|
|
803
|
+
this.textParts.set(key, { buffer: "" });
|
|
552
804
|
if (
|
|
553
|
-
!this.partOrder.some((part) => part.kind === "text" && part.key ===
|
|
805
|
+
!this.partOrder.some((part) => part.kind === "text" && part.key === key)
|
|
554
806
|
) {
|
|
555
|
-
this.partOrder.push(
|
|
807
|
+
this.partOrder.push(
|
|
808
|
+
scope === ROOT_SCOPE
|
|
809
|
+
? { kind: "text", key }
|
|
810
|
+
: { kind: "text", key, subagentRunId: scope },
|
|
811
|
+
);
|
|
556
812
|
}
|
|
557
813
|
}
|
|
814
|
+
return key;
|
|
558
815
|
}
|
|
559
816
|
|
|
560
817
|
private appendText(id: string, delta: string): void {
|
|
561
|
-
this.ensureTextPart(id);
|
|
562
818
|
const entry = this.textParts.get(id);
|
|
563
819
|
if (!entry) return;
|
|
564
820
|
entry.buffer += delta;
|
|
565
821
|
}
|
|
566
822
|
|
|
567
823
|
private startToolCall(
|
|
824
|
+
scope: string,
|
|
568
825
|
id: string | undefined,
|
|
569
826
|
name?: string,
|
|
570
827
|
parentMessageId?: string,
|
|
571
828
|
) {
|
|
572
829
|
if (!id) return;
|
|
573
830
|
// A new tool call acts as a boundary: any anonymous text that arrives
|
|
574
|
-
// after it should be a new part, not appended to the pre-tool text
|
|
575
|
-
|
|
831
|
+
// after it should be a new part, not appended to the pre-tool text —
|
|
832
|
+
// scoped, so a subagent's tool call doesn't break the root run's text
|
|
833
|
+
// and vice versa.
|
|
834
|
+
this.activeTextMessageIdByScope.delete(scope);
|
|
576
835
|
if (
|
|
577
836
|
!this.partOrder.some(
|
|
578
837
|
(part) => part.kind === "tool-call" && part.toolCallId === id,
|
|
579
838
|
)
|
|
580
839
|
) {
|
|
581
|
-
this.insertToolPart(id, parentMessageId);
|
|
840
|
+
this.insertToolPart(scope, id, parentMessageId);
|
|
582
841
|
}
|
|
583
842
|
const state: ToolCallState = {
|
|
584
843
|
toolCallId: id,
|
|
@@ -592,16 +851,21 @@ export class RunAggregator {
|
|
|
592
851
|
if (parentMessageId) {
|
|
593
852
|
state.parentMessageId = parentMessageId;
|
|
594
853
|
}
|
|
854
|
+
if (scope !== ROOT_SCOPE) {
|
|
855
|
+
state.subagentRunId = scope;
|
|
856
|
+
}
|
|
857
|
+
this.nestingMemo = undefined;
|
|
595
858
|
this.toolCalls.set(id, state);
|
|
596
859
|
}
|
|
597
860
|
|
|
598
861
|
// The message and tool-call channels are unordered on the wire, so anchor a
|
|
599
862
|
// tool call under its parentMessageId text part instead of appending it.
|
|
600
|
-
private insertToolPart(id: string, parentMessageId?: string) {
|
|
863
|
+
private insertToolPart(scope: string, id: string, parentMessageId?: string) {
|
|
601
864
|
const entry = { kind: "tool-call", toolCallId: id } as const;
|
|
602
865
|
if (parentMessageId) {
|
|
866
|
+
const parentKey = this.partKey(scope, parentMessageId);
|
|
603
867
|
const parentIndex = this.partOrder.findIndex(
|
|
604
|
-
(part) => part.kind === "text" && part.key ===
|
|
868
|
+
(part) => part.kind === "text" && part.key === parentKey,
|
|
605
869
|
);
|
|
606
870
|
if (parentIndex !== -1) {
|
|
607
871
|
let insertAt = parentIndex + 1;
|
|
@@ -627,7 +891,17 @@ export class RunAggregator {
|
|
|
627
891
|
private appendToolArgs(id: string | undefined, delta: string) {
|
|
628
892
|
const entry = id ? this.toolCalls.get(id) : undefined;
|
|
629
893
|
if (!entry) return;
|
|
894
|
+
if (!entry.argsTextScanner) {
|
|
895
|
+
entry.argsTextScanner = createJSONContainerScanner();
|
|
896
|
+
}
|
|
630
897
|
entry.argsText += delta;
|
|
898
|
+
const shouldParse = scanJSONContainerDelta(entry.argsTextScanner, delta);
|
|
899
|
+
if (!shouldParse) {
|
|
900
|
+
if (entry.argsTextScanner.state === "invalid") {
|
|
901
|
+
entry.parsedArgs = undefined;
|
|
902
|
+
}
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
631
905
|
try {
|
|
632
906
|
const parsed = JSON.parse(entry.argsText);
|
|
633
907
|
if (parsed && typeof parsed === "object") {
|
|
@@ -641,6 +915,7 @@ export class RunAggregator {
|
|
|
641
915
|
}
|
|
642
916
|
|
|
643
917
|
private finishToolCall(
|
|
918
|
+
scope: string,
|
|
644
919
|
id: string,
|
|
645
920
|
content: string,
|
|
646
921
|
isError?: boolean,
|
|
@@ -658,7 +933,9 @@ export class RunAggregator {
|
|
|
658
933
|
result: undefined,
|
|
659
934
|
isError: undefined,
|
|
660
935
|
snapshotResultApplied: false,
|
|
936
|
+
...(scope !== ROOT_SCOPE ? { subagentRunId: scope } : {}),
|
|
661
937
|
};
|
|
938
|
+
this.nestingMemo = undefined;
|
|
662
939
|
this.toolCalls.set(id, entry);
|
|
663
940
|
}
|
|
664
941
|
if (
|
|
@@ -691,61 +968,199 @@ export class RunAggregator {
|
|
|
691
968
|
if (toolMessageId) {
|
|
692
969
|
entry.toolMessageId = toolMessageId;
|
|
693
970
|
}
|
|
694
|
-
this.
|
|
971
|
+
this.lastResolvedToolCallIdByScope.set(scope, id);
|
|
695
972
|
}
|
|
696
973
|
|
|
697
|
-
private
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
974
|
+
private nesting(): {
|
|
975
|
+
subagentsByParentToolCallId: Map<string, string[]>;
|
|
976
|
+
reachable: Set<string>;
|
|
977
|
+
} {
|
|
978
|
+
if (!this.nestingMemo) {
|
|
979
|
+
const subagentsByParentToolCallId = this.subagentsByParentToolCallId();
|
|
980
|
+
this.nestingMemo = {
|
|
981
|
+
subagentsByParentToolCallId,
|
|
982
|
+
reachable: this.reachableSubagentRunIds(subagentsByParentToolCallId),
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
return this.nestingMemo;
|
|
986
|
+
}
|
|
707
987
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
988
|
+
private subagentsByParentToolCallId(): Map<string, string[]> {
|
|
989
|
+
const map = new Map<string, string[]>();
|
|
990
|
+
for (const run of this.subagentRuns.values()) {
|
|
991
|
+
if (!run.parentToolCallId) continue;
|
|
992
|
+
const list = map.get(run.parentToolCallId) ?? [];
|
|
993
|
+
list.push(run.subagentRunId);
|
|
994
|
+
map.set(run.parentToolCallId, list);
|
|
995
|
+
}
|
|
996
|
+
return map;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// A subagent nests under its spawning tool call only when that call is itself
|
|
1000
|
+
// reachable from the root scope. A run with no parentToolCallId, one naming a
|
|
1001
|
+
// call this run never saw, or one in a cycle has nowhere to hang; the visited
|
|
1002
|
+
// set also keeps a malformed chain to one visit per run instead of branching
|
|
1003
|
+
// at every level.
|
|
1004
|
+
private reachableSubagentRunIds(
|
|
1005
|
+
subagentsByParentToolCallId: Map<string, string[]>,
|
|
1006
|
+
): Set<string> {
|
|
1007
|
+
const reachable = new Set<string>();
|
|
1008
|
+
const walk = (scope: string, depth: number): void => {
|
|
1009
|
+
if (depth > MAX_SUBAGENT_DEPTH) return;
|
|
1010
|
+
for (const entry of this.toolCalls.values()) {
|
|
1011
|
+
if ((entry.subagentRunId ?? ROOT_SCOPE) !== scope) continue;
|
|
1012
|
+
for (const id of subagentsByParentToolCallId.get(entry.toolCallId) ??
|
|
1013
|
+
[]) {
|
|
1014
|
+
if (reachable.has(id)) continue;
|
|
1015
|
+
reachable.add(id);
|
|
1016
|
+
walk(id, depth + 1);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
720
1019
|
};
|
|
1020
|
+
walk(ROOT_SCOPE, 0);
|
|
1021
|
+
return reachable;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Nested messages rather than a discovery hook (the shape react-langchain
|
|
1025
|
+
// uses): ToolCallMessagePart.messages is the contract core consumers already
|
|
1026
|
+
// walk, and SUBAGENT_STARTED.parentToolCallId names the spawning call
|
|
1027
|
+
// outright, so the join needs no namespace scheme of its own.
|
|
1028
|
+
private materializeSubagentMessage(
|
|
1029
|
+
subagentRunId: string,
|
|
1030
|
+
depth: number,
|
|
1031
|
+
ctx: BuildContext,
|
|
1032
|
+
): ThreadMessage | undefined {
|
|
1033
|
+
const run = this.subagentRuns.get(subagentRunId);
|
|
1034
|
+
if (!run || depth > MAX_SUBAGENT_DEPTH) return undefined;
|
|
1035
|
+
const content = this.buildParts(subagentRunId, depth + 1, ctx);
|
|
1036
|
+
return {
|
|
1037
|
+
id: run.subagentRunId,
|
|
1038
|
+
role: "assistant",
|
|
1039
|
+
createdAt: run.createdAt,
|
|
1040
|
+
status: run.status,
|
|
1041
|
+
content,
|
|
1042
|
+
metadata: {
|
|
1043
|
+
unstable_state: null,
|
|
1044
|
+
unstable_annotations: [],
|
|
1045
|
+
unstable_data: [],
|
|
1046
|
+
steps: [],
|
|
1047
|
+
custom: {
|
|
1048
|
+
[AG_UI_METADATA_NAMESPACE]: {
|
|
1049
|
+
name: run.name,
|
|
1050
|
+
...(run.description !== undefined
|
|
1051
|
+
? { description: run.description }
|
|
1052
|
+
: {}),
|
|
1053
|
+
...(run.parentSubagentRunId !== undefined
|
|
1054
|
+
? { parentSubagentRunId: run.parentSubagentRunId }
|
|
1055
|
+
: {}),
|
|
1056
|
+
...(run.result !== undefined ? { result: run.result } : {}),
|
|
1057
|
+
...(run.interruptIds !== undefined
|
|
1058
|
+
? { interruptIds: run.interruptIds }
|
|
1059
|
+
: {}),
|
|
1060
|
+
...(run.errorCode !== undefined
|
|
1061
|
+
? { errorCode: run.errorCode }
|
|
1062
|
+
: {}),
|
|
1063
|
+
},
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1066
|
+
} as ThreadMessage;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
private buildParts(
|
|
1070
|
+
scope: string,
|
|
1071
|
+
depth: number,
|
|
1072
|
+
ctx: BuildContext,
|
|
1073
|
+
): ThreadAssistantMessagePart[] {
|
|
1074
|
+
const snapshot: ThreadAssistantMessagePart[] = [];
|
|
1075
|
+
const isRoot = scope === ROOT_SCOPE;
|
|
1076
|
+
|
|
1077
|
+
for (const { index, part } of ctx.partsByScope.get(scope) ?? []) {
|
|
1078
|
+
const materialized = () => {
|
|
1079
|
+
if (isRoot && ctx.root) ctx.root.lastMaterializedIndex = index;
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
if (part.kind === "tool-call") {
|
|
1083
|
+
const entry = this.toolCalls.get(part.toolCallId);
|
|
1084
|
+
if (!entry) continue;
|
|
1085
|
+
const approval = ctx.approvals.get(entry.toolCallId);
|
|
1086
|
+
const nestedMessages = (
|
|
1087
|
+
ctx.subagentsByParentToolCallId.get(entry.toolCallId) ?? []
|
|
1088
|
+
)
|
|
1089
|
+
.filter((id) => ctx.reachable.has(id))
|
|
1090
|
+
.map((id) => this.materializeSubagentMessage(id, depth, ctx))
|
|
1091
|
+
.filter((m): m is ThreadMessage => m !== undefined);
|
|
1092
|
+
const toolPart: ToolCallMessagePart = {
|
|
1093
|
+
type: "tool-call",
|
|
1094
|
+
toolCallId: entry.toolCallId,
|
|
1095
|
+
toolName: entry.toolCallName,
|
|
1096
|
+
args: (entry.parsedArgs ?? {}) as any,
|
|
1097
|
+
argsText: entry.argsText,
|
|
1098
|
+
...(approval ? { approval } : {}),
|
|
1099
|
+
...(entry.result !== undefined ? { result: entry.result } : {}),
|
|
1100
|
+
...(entry.modelContent !== undefined
|
|
1101
|
+
? { modelContent: entry.modelContent }
|
|
1102
|
+
: {}),
|
|
1103
|
+
...(entry.isError !== undefined ? { isError: entry.isError } : {}),
|
|
1104
|
+
...(entry.mcpAppResourceUri
|
|
1105
|
+
? {
|
|
1106
|
+
mcp: {
|
|
1107
|
+
app: {
|
|
1108
|
+
resourceUri: entry.mcpAppResourceUri,
|
|
1109
|
+
...(entry.mcpAppServerId
|
|
1110
|
+
? { serverId: entry.mcpAppServerId }
|
|
1111
|
+
: {}),
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
}
|
|
1115
|
+
: {}),
|
|
1116
|
+
...(entry.parentMessageId ? { parentId: entry.parentMessageId } : {}),
|
|
1117
|
+
...(entry.toolMessageId
|
|
1118
|
+
? { unstable_toolMessageId: entry.toolMessageId }
|
|
1119
|
+
: {}),
|
|
1120
|
+
...(nestedMessages.length > 0 ? { messages: nestedMessages } : {}),
|
|
1121
|
+
} as ToolCallMessagePart & { unstable_toolMessageId?: string };
|
|
1122
|
+
snapshot.push(toolPart);
|
|
1123
|
+
materialized();
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const rawScope =
|
|
1128
|
+
"subagentRunId" in part
|
|
1129
|
+
? (part.subagentRunId ?? ROOT_SCOPE)
|
|
1130
|
+
: ROOT_SCOPE;
|
|
721
1131
|
|
|
722
|
-
for (const [index, part] of this.partOrder.entries()) {
|
|
723
|
-
currentIndex = index;
|
|
724
1132
|
if (part.kind === "reasoning") {
|
|
725
1133
|
if (this.showThinking) {
|
|
726
1134
|
const buffer = this.reasoningParts.get(part.key) ?? "";
|
|
727
|
-
|
|
1135
|
+
const isActive =
|
|
1136
|
+
this.activeReasoningKeyByScope.get(rawScope) === part.key;
|
|
1137
|
+
if (buffer.length > 0 || isActive) {
|
|
728
1138
|
const encryptedValue = this.reasoningSignatures.get(part.key);
|
|
729
1139
|
const reasoningId = this.reasoningMessageIds.get(part.key);
|
|
730
1140
|
const meta = {
|
|
731
1141
|
...(reasoningId !== undefined ? { reasoningId } : {}),
|
|
732
1142
|
...(encryptedValue !== undefined ? { encryptedValue } : {}),
|
|
733
1143
|
};
|
|
734
|
-
|
|
1144
|
+
snapshot.push({
|
|
735
1145
|
type: "reasoning",
|
|
736
1146
|
text: buffer,
|
|
737
1147
|
...(Object.keys(meta).length > 0
|
|
738
1148
|
? { providerMetadata: { [AG_UI_METADATA_NAMESPACE]: meta } }
|
|
739
1149
|
: {}),
|
|
740
1150
|
} as const);
|
|
741
|
-
|
|
1151
|
+
materialized();
|
|
1152
|
+
} else if (isRoot && ctx.root) {
|
|
742
1153
|
// A retracted empty block still carries transport state: without
|
|
743
1154
|
// a part to live on, its signature rides the message metadata,
|
|
744
1155
|
// matching the shape a snapshot reload produces.
|
|
745
1156
|
const encryptedValue = this.reasoningSignatures.get(part.key);
|
|
746
1157
|
const id = this.reasoningSignatureIds.get(part.key);
|
|
747
1158
|
if (id?.trim() && encryptedValue?.trim()) {
|
|
748
|
-
opaqueCandidates.push({
|
|
1159
|
+
ctx.root.opaqueCandidates.push({
|
|
1160
|
+
id,
|
|
1161
|
+
encryptedValue,
|
|
1162
|
+
anchor: index + 1,
|
|
1163
|
+
});
|
|
749
1164
|
}
|
|
750
1165
|
}
|
|
751
1166
|
}
|
|
@@ -755,54 +1170,79 @@ export class RunAggregator {
|
|
|
755
1170
|
if (part.kind === "text") {
|
|
756
1171
|
const entry = this.textParts.get(part.key);
|
|
757
1172
|
if (entry && entry.buffer.trim().length > 0) {
|
|
758
|
-
|
|
1173
|
+
snapshot.push({ type: "text", text: entry.buffer } as const);
|
|
1174
|
+
materialized();
|
|
759
1175
|
}
|
|
760
1176
|
continue;
|
|
761
1177
|
}
|
|
762
1178
|
|
|
763
1179
|
if (part.kind === "data") {
|
|
764
|
-
|
|
1180
|
+
snapshot.push({
|
|
765
1181
|
type: "data",
|
|
766
1182
|
name: part.name,
|
|
767
1183
|
data: part.value,
|
|
768
1184
|
});
|
|
1185
|
+
materialized();
|
|
769
1186
|
continue;
|
|
770
1187
|
}
|
|
1188
|
+
}
|
|
771
1189
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1190
|
+
return snapshot;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
private emit(): void {
|
|
1194
|
+
const { subagentsByParentToolCallId, reachable } = this.nesting();
|
|
1195
|
+
const root = {
|
|
1196
|
+
opaqueCandidates: Array.from(
|
|
1197
|
+
this.hiddenSignatures,
|
|
1198
|
+
([id, encryptedValue]) => ({
|
|
1199
|
+
id,
|
|
1200
|
+
encryptedValue,
|
|
1201
|
+
anchor: this.hiddenSignatureAnchors.get(id)!,
|
|
1202
|
+
}),
|
|
1203
|
+
) as (AgUiOpaqueReasoning & { anchor: number })[],
|
|
1204
|
+
lastMaterializedIndex: -1,
|
|
1205
|
+
};
|
|
1206
|
+
// A run that ended incomplete can no longer be resumed, so a gate left over
|
|
1207
|
+
// from an earlier interrupt outcome is unanswerable and must not stay
|
|
1208
|
+
// projected. The interrupts themselves are kept on the message, since the
|
|
1209
|
+
// bespoke hooks read that payload. Bound ids cover every call the run
|
|
1210
|
+
// produced, root or nested: the approval seams walk
|
|
1211
|
+
// ToolCallMessagePart.messages, so a gate naming a subagent-scoped call is
|
|
1212
|
+
// answerable and projects onto the nested part.
|
|
1213
|
+
const partsByScope = new Map<
|
|
1214
|
+
string,
|
|
1215
|
+
{ index: number; part: PartOrderEntry }[]
|
|
1216
|
+
>();
|
|
1217
|
+
for (const [index, part] of this.partOrder.entries()) {
|
|
1218
|
+
const rawScope =
|
|
1219
|
+
part.kind === "tool-call"
|
|
1220
|
+
? (this.toolCalls.get(part.toolCallId)?.subagentRunId ?? ROOT_SCOPE)
|
|
1221
|
+
: "subagentRunId" in part
|
|
1222
|
+
? (part.subagentRunId ?? ROOT_SCOPE)
|
|
1223
|
+
: ROOT_SCOPE;
|
|
1224
|
+
const bucketScope =
|
|
1225
|
+
rawScope === ROOT_SCOPE || reachable.has(rawScope)
|
|
1226
|
+
? rawScope
|
|
1227
|
+
: ROOT_SCOPE;
|
|
1228
|
+
const bucket = partsByScope.get(bucketScope);
|
|
1229
|
+
if (bucket) bucket.push({ index, part });
|
|
1230
|
+
else partsByScope.set(bucketScope, [{ index, part }]);
|
|
805
1231
|
}
|
|
1232
|
+
const ctx: BuildContext = {
|
|
1233
|
+
subagentsByParentToolCallId,
|
|
1234
|
+
reachable,
|
|
1235
|
+
partsByScope,
|
|
1236
|
+
approvals: projectAgUiToolApprovals(
|
|
1237
|
+
this.status?.type === "requires-action" ? this.interrupts : undefined,
|
|
1238
|
+
new Set(
|
|
1239
|
+
Array.from(this.toolCalls.values()).map((entry) => entry.toolCallId),
|
|
1240
|
+
),
|
|
1241
|
+
),
|
|
1242
|
+
root,
|
|
1243
|
+
};
|
|
1244
|
+
const snapshot = this.buildParts(ROOT_SCOPE, 0, ctx);
|
|
1245
|
+
const { opaqueCandidates, lastMaterializedIndex } = root;
|
|
806
1246
|
|
|
807
1247
|
// An anonymous claim promotes the signature's entityId to a wire message
|
|
808
1248
|
// id; when that id actually names a text message or the adopted assistant
|
|
@@ -906,8 +1346,15 @@ export class RunAggregator {
|
|
|
906
1346
|
};
|
|
907
1347
|
}
|
|
908
1348
|
|
|
909
|
-
private handleReasoningStart(
|
|
1349
|
+
private handleReasoningStart(
|
|
1350
|
+
scope: string,
|
|
1351
|
+
messageId?: string,
|
|
1352
|
+
isMessageId = false,
|
|
1353
|
+
): void {
|
|
910
1354
|
if (!this.showThinking) {
|
|
1355
|
+
// Hidden-reasoning bookkeeping stays root-only for this task — a
|
|
1356
|
+
// subagent's opaque/hidden reasoning signatures are not preserved.
|
|
1357
|
+
if (scope !== ROOT_SCOPE) return;
|
|
911
1358
|
const anchor = this.partOrder.length;
|
|
912
1359
|
if (messageId === undefined) {
|
|
913
1360
|
this.hiddenActiveReasoning = "anonymous";
|
|
@@ -920,9 +1367,12 @@ export class RunAggregator {
|
|
|
920
1367
|
return;
|
|
921
1368
|
}
|
|
922
1369
|
// A reasoning block acts as a boundary: anonymous text arriving after it
|
|
923
|
-
// should be a new part, not appended to any pre-reasoning text.
|
|
924
|
-
this.
|
|
925
|
-
const key =
|
|
1370
|
+
// should be a new part, not appended to any pre-reasoning text — scoped.
|
|
1371
|
+
this.activeTextMessageIdByScope.delete(scope);
|
|
1372
|
+
const key = this.partKey(
|
|
1373
|
+
scope,
|
|
1374
|
+
messageId ?? `__auto-reasoning-${++this.reasoningPartCounter}`,
|
|
1375
|
+
);
|
|
926
1376
|
// Two different questions: which id may be replayed as a ReasoningMessage.id
|
|
927
1377
|
// (only the message-scoped aliases carry one), and which block an unmatched
|
|
928
1378
|
// signature may claim (any block opened without an id at all).
|
|
@@ -933,13 +1383,18 @@ export class RunAggregator {
|
|
|
933
1383
|
}
|
|
934
1384
|
if (!this.reasoningParts.has(key)) {
|
|
935
1385
|
this.reasoningParts.set(key, "");
|
|
936
|
-
this.partOrder.push(
|
|
1386
|
+
this.partOrder.push(
|
|
1387
|
+
scope === ROOT_SCOPE
|
|
1388
|
+
? { kind: "reasoning", key }
|
|
1389
|
+
: { kind: "reasoning", key, subagentRunId: scope },
|
|
1390
|
+
);
|
|
937
1391
|
}
|
|
938
|
-
this.
|
|
1392
|
+
this.activeReasoningKeyByScope.set(scope, key);
|
|
939
1393
|
this.emit();
|
|
940
1394
|
}
|
|
941
1395
|
|
|
942
1396
|
private handleReasoningContent(
|
|
1397
|
+
scope: string,
|
|
943
1398
|
delta: string,
|
|
944
1399
|
messageId?: string,
|
|
945
1400
|
isMessageId = false,
|
|
@@ -948,27 +1403,28 @@ export class RunAggregator {
|
|
|
948
1403
|
if (!this.showThinking) {
|
|
949
1404
|
// Content without a preceding START still names the block (the visible
|
|
950
1405
|
// path opens it lazily); register it so a signature can claim it.
|
|
1406
|
+
if (scope !== ROOT_SCOPE) return;
|
|
951
1407
|
if (this.hiddenActiveReasoning === "none") {
|
|
952
|
-
this.handleReasoningStart(messageId, isMessageId);
|
|
1408
|
+
this.handleReasoningStart(scope, messageId, isMessageId);
|
|
953
1409
|
}
|
|
954
1410
|
return;
|
|
955
1411
|
}
|
|
956
|
-
if (!this.
|
|
1412
|
+
if (!this.activeReasoningKeyByScope.has(scope)) {
|
|
957
1413
|
// Content arrived without a preceding START — create the slot lazily.
|
|
958
|
-
this.handleReasoningStart(messageId, isMessageId);
|
|
1414
|
+
this.handleReasoningStart(scope, messageId, isMessageId);
|
|
959
1415
|
}
|
|
960
|
-
const key = this.
|
|
1416
|
+
const key = this.activeReasoningKeyByScope.get(scope);
|
|
961
1417
|
if (!key) return;
|
|
962
1418
|
this.reasoningParts.set(key, (this.reasoningParts.get(key) ?? "") + delta);
|
|
963
1419
|
this.emit();
|
|
964
1420
|
}
|
|
965
1421
|
|
|
966
|
-
private handleReasoningEnd(): void {
|
|
1422
|
+
private handleReasoningEnd(scope: string): void {
|
|
967
1423
|
if (!this.showThinking) {
|
|
968
|
-
this.hiddenActiveReasoning = "none";
|
|
1424
|
+
if (scope === ROOT_SCOPE) this.hiddenActiveReasoning = "none";
|
|
969
1425
|
return;
|
|
970
1426
|
}
|
|
971
|
-
this.
|
|
1427
|
+
this.activeReasoningKeyByScope.delete(scope);
|
|
972
1428
|
this.emit();
|
|
973
1429
|
}
|
|
974
1430
|
}
|