@assistant-ui/react-ag-ui 0.0.57 → 0.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +3 -4
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +29 -43
- 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 +326 -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 +42 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +1 -0
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +9 -9
- package/src/runtime/AgUiThreadRuntimeCore.ts +40 -60
- 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 +510 -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 +70 -12
- package/src/useAgUiRuntime.approval.test.tsx +34 -24
- package/src/useAgUiRuntime.toolDeferral.test.tsx +186 -0
- package/src/useAgUiRuntime.ts +1 -0
|
@@ -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;
|
|
@@ -50,6 +96,7 @@ type ToolCallState = {
|
|
|
50
96
|
mcpAppServerId?: string;
|
|
51
97
|
modelContent?: ToolModelContentPart[];
|
|
52
98
|
snapshotResultApplied: boolean;
|
|
99
|
+
subagentRunId?: string;
|
|
53
100
|
};
|
|
54
101
|
|
|
55
102
|
export const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
|
|
@@ -91,7 +138,7 @@ export class RunAggregator {
|
|
|
91
138
|
private status: ChatModelRunResult["status"] | undefined;
|
|
92
139
|
private interrupts: AgUiInterrupt[] | undefined;
|
|
93
140
|
private readonly textParts = new Map<string, { buffer: string }>();
|
|
94
|
-
private
|
|
141
|
+
private readonly activeTextMessageIdByScope = new Map<string, string>();
|
|
95
142
|
private readonly reasoningParts = new Map<string, string>(); // key → buffer
|
|
96
143
|
private readonly reasoningSignatures = new Map<string, string>();
|
|
97
144
|
private readonly reasoningSignatureIds = new Map<string, string>();
|
|
@@ -109,18 +156,23 @@ export class RunAggregator {
|
|
|
109
156
|
private readonly loggedDroppedOpaqueIds = new Set<string>();
|
|
110
157
|
private readonly reasoningMessageIds = new Map<string, string>();
|
|
111
158
|
private readonly anonymousReasoningKeys = new Set<string>();
|
|
112
|
-
private
|
|
159
|
+
private readonly activeReasoningKeyByScope = new Map<string, string>();
|
|
160
|
+
private readonly subagentRuns = new Map<string, SubagentRunState>();
|
|
161
|
+
// The nesting graph only moves when a run is announced or a tool call is
|
|
162
|
+
// created, which is rare next to emit(), so it is derived once per change
|
|
163
|
+
// rather than per streamed delta.
|
|
164
|
+
private nestingMemo:
|
|
165
|
+
| {
|
|
166
|
+
subagentsByParentToolCallId: Map<string, string[]>;
|
|
167
|
+
reachable: Set<string>;
|
|
168
|
+
}
|
|
169
|
+
| undefined;
|
|
113
170
|
private reasoningPartCounter = 0;
|
|
114
171
|
private readonly toolCalls = new Map<string, ToolCallState>();
|
|
115
172
|
private readonly a2uiBuckets = new Map<string, A2uiState>();
|
|
116
173
|
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
|
-
)[] = [];
|
|
174
|
+
private readonly lastResolvedToolCallIdByScope = new Map<string, string>();
|
|
175
|
+
private readonly partOrder: PartOrderEntry[] = [];
|
|
124
176
|
private textPartCounter = 0;
|
|
125
177
|
private serverMessageIdReported = false;
|
|
126
178
|
private reportedServerMessageId: string | undefined;
|
|
@@ -166,14 +218,18 @@ export class RunAggregator {
|
|
|
166
218
|
}
|
|
167
219
|
|
|
168
220
|
this.interrupts = undefined;
|
|
221
|
+
// A call nested inside a subagent message is answerable the same way
|
|
222
|
+
// a root call is: getPendingToolCalls and addToolResult walk
|
|
223
|
+
// ToolCallMessagePart.messages, so any unresolved call keeps the run
|
|
224
|
+
// resumable.
|
|
169
225
|
const hasUnresolvedToolCalls = Array.from(this.toolCalls.values()).some(
|
|
170
226
|
(tc) => tc.result === undefined,
|
|
171
227
|
);
|
|
172
228
|
|
|
173
|
-
this.status =
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
229
|
+
this.status = hasUnresolvedToolCalls
|
|
230
|
+
? { type: "requires-action", reason: "tool-calls" }
|
|
231
|
+
: { type: "complete", reason: "unknown" };
|
|
232
|
+
this.closeOpenSubagentRuns(this.status);
|
|
177
233
|
this.emit();
|
|
178
234
|
break;
|
|
179
235
|
}
|
|
@@ -183,48 +239,61 @@ export class RunAggregator {
|
|
|
183
239
|
reason: "error",
|
|
184
240
|
...(event.message !== undefined ? { error: event.message } : {}),
|
|
185
241
|
};
|
|
242
|
+
this.closeOpenSubagentRuns(this.status);
|
|
186
243
|
this.emit();
|
|
187
244
|
break;
|
|
188
245
|
}
|
|
189
246
|
case "RUN_CANCELLED": {
|
|
190
247
|
this.status = { type: "incomplete", reason: "cancelled" };
|
|
248
|
+
this.closeOpenSubagentRuns(this.status);
|
|
191
249
|
this.emit();
|
|
192
250
|
break;
|
|
193
251
|
}
|
|
194
252
|
|
|
195
253
|
case "TEXT_MESSAGE_START": {
|
|
196
|
-
this.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
254
|
+
const scope = this.scopeOf(event);
|
|
255
|
+
if (scope === ROOT_SCOPE) {
|
|
256
|
+
this.beginDistinctTextMessage(event.messageId);
|
|
257
|
+
this.reportServerMessageId(event.messageId);
|
|
258
|
+
if (event.messageId) this.lastTextMessageId = event.messageId;
|
|
259
|
+
}
|
|
260
|
+
this.startTextMessage(scope, event.messageId);
|
|
200
261
|
this.emit();
|
|
201
262
|
break;
|
|
202
263
|
}
|
|
203
264
|
case "TEXT_MESSAGE_CONTENT":
|
|
204
265
|
case "TEXT_MESSAGE_CHUNK": {
|
|
205
266
|
const incomingId = "messageId" in event ? event.messageId : undefined;
|
|
206
|
-
this.
|
|
207
|
-
|
|
208
|
-
|
|
267
|
+
const scope = this.scopeOf(event);
|
|
268
|
+
if (scope === ROOT_SCOPE) {
|
|
269
|
+
this.beginDistinctTextMessage(incomingId);
|
|
270
|
+
this.reportServerMessageId(incomingId);
|
|
271
|
+
if (incomingId) this.lastTextMessageId = incomingId;
|
|
272
|
+
}
|
|
209
273
|
if (!event.delta) break;
|
|
210
274
|
this.recordFirstToken();
|
|
211
|
-
const id = this.resolveTextMessageId(incomingId);
|
|
275
|
+
const id = this.resolveTextMessageId(scope, incomingId);
|
|
212
276
|
this.appendText(id, event.delta);
|
|
213
277
|
this.totalChunks++;
|
|
214
278
|
this.emit();
|
|
215
279
|
break;
|
|
216
280
|
}
|
|
217
281
|
case "TEXT_MESSAGE_END": {
|
|
218
|
-
this.
|
|
219
|
-
if (
|
|
220
|
-
|
|
282
|
+
const scope = this.scopeOf(event);
|
|
283
|
+
if (scope === ROOT_SCOPE) this.reportServerMessageId(event.messageId);
|
|
284
|
+
if (
|
|
285
|
+
event.messageId &&
|
|
286
|
+
this.activeTextMessageIdByScope.get(scope) ===
|
|
287
|
+
this.partKey(scope, event.messageId)
|
|
288
|
+
) {
|
|
289
|
+
this.activeTextMessageIdByScope.delete(scope);
|
|
221
290
|
}
|
|
222
291
|
this.emit();
|
|
223
292
|
break;
|
|
224
293
|
}
|
|
225
294
|
|
|
226
295
|
case "CUSTOM": {
|
|
227
|
-
this.
|
|
296
|
+
this.activeTextMessageIdByScope.delete(ROOT_SCOPE);
|
|
228
297
|
this.partOrder.push({
|
|
229
298
|
kind: "data",
|
|
230
299
|
name: event.name,
|
|
@@ -239,18 +308,21 @@ export class RunAggregator {
|
|
|
239
308
|
case "REASONING_START":
|
|
240
309
|
case "REASONING_MESSAGE_START":
|
|
241
310
|
this.handleReasoningStart(
|
|
311
|
+
this.scopeOf("subagentRunId" in event ? event : {}),
|
|
242
312
|
"messageId" in event ? event.messageId : undefined,
|
|
243
313
|
event.type === "REASONING_MESSAGE_START",
|
|
244
314
|
);
|
|
245
315
|
break;
|
|
246
316
|
case "REASONING_ENCRYPTED_VALUE":
|
|
247
317
|
if (event.subtype === "message") {
|
|
318
|
+
const scope = this.scopeOf(event);
|
|
248
319
|
// entityId names any message, not necessarily a reasoning one, so an
|
|
249
320
|
// unmatched id may only claim a block that has no id to contradict it.
|
|
250
|
-
const active = this.
|
|
321
|
+
const active = this.activeReasoningKeyByScope.get(scope);
|
|
322
|
+
const entityKey = this.partKey(scope, event.entityId);
|
|
251
323
|
const key = this.showThinking
|
|
252
|
-
? this.reasoningParts.has(
|
|
253
|
-
?
|
|
324
|
+
? this.reasoningParts.has(entityKey)
|
|
325
|
+
? entityKey
|
|
254
326
|
: active !== undefined && this.anonymousReasoningKeys.has(active)
|
|
255
327
|
? active
|
|
256
328
|
: undefined
|
|
@@ -261,6 +333,9 @@ export class RunAggregator {
|
|
|
261
333
|
this.emit();
|
|
262
334
|
} else if (
|
|
263
335
|
!this.showThinking &&
|
|
336
|
+
// Hidden-reasoning bookkeeping stays root-only for this task — a
|
|
337
|
+
// subagent's opaque/hidden reasoning signatures are not preserved.
|
|
338
|
+
scope === ROOT_SCOPE &&
|
|
264
339
|
event.entityId.trim().length > 0 &&
|
|
265
340
|
event.encryptedValue.trim().length > 0 &&
|
|
266
341
|
(this.hiddenReasoningIds.has(event.entityId) ||
|
|
@@ -278,12 +353,13 @@ export class RunAggregator {
|
|
|
278
353
|
}
|
|
279
354
|
break;
|
|
280
355
|
case "THINKING_TEXT_MESSAGE_CONTENT":
|
|
281
|
-
this.handleReasoningContent(event.delta);
|
|
356
|
+
this.handleReasoningContent(ROOT_SCOPE, event.delta);
|
|
282
357
|
this.totalChunks++;
|
|
283
358
|
this.recordFirstToken();
|
|
284
359
|
break;
|
|
285
360
|
case "REASONING_MESSAGE_CONTENT":
|
|
286
361
|
this.handleReasoningContent(
|
|
362
|
+
this.scopeOf(event),
|
|
287
363
|
event.delta,
|
|
288
364
|
"messageId" in event ? event.messageId : undefined,
|
|
289
365
|
true,
|
|
@@ -293,14 +369,20 @@ export class RunAggregator {
|
|
|
293
369
|
break;
|
|
294
370
|
case "THINKING_TEXT_MESSAGE_END":
|
|
295
371
|
case "THINKING_END":
|
|
372
|
+
this.handleReasoningEnd(ROOT_SCOPE);
|
|
373
|
+
break;
|
|
296
374
|
case "REASONING_MESSAGE_END":
|
|
297
375
|
case "REASONING_END":
|
|
298
|
-
this.handleReasoningEnd();
|
|
376
|
+
this.handleReasoningEnd(this.scopeOf(event));
|
|
299
377
|
break;
|
|
300
378
|
|
|
301
379
|
case "TOOL_CALL_START": {
|
|
302
|
-
this.
|
|
380
|
+
const scope = this.scopeOf(event);
|
|
381
|
+
if (scope === ROOT_SCOPE) {
|
|
382
|
+
this.reportServerMessageId(event.parentMessageId);
|
|
383
|
+
}
|
|
303
384
|
this.startToolCall(
|
|
385
|
+
scope,
|
|
304
386
|
event.toolCallId,
|
|
305
387
|
event.toolCallName,
|
|
306
388
|
event.parentMessageId,
|
|
@@ -310,7 +392,10 @@ export class RunAggregator {
|
|
|
310
392
|
}
|
|
311
393
|
case "TOOL_CALL_ARGS":
|
|
312
394
|
case "TOOL_CALL_CHUNK": {
|
|
313
|
-
if (
|
|
395
|
+
if (
|
|
396
|
+
event.type === "TOOL_CALL_CHUNK" &&
|
|
397
|
+
this.scopeOf(event) === ROOT_SCOPE
|
|
398
|
+
) {
|
|
314
399
|
this.reportServerMessageId(event.parentMessageId);
|
|
315
400
|
}
|
|
316
401
|
if (!event.delta) break;
|
|
@@ -324,6 +409,7 @@ export class RunAggregator {
|
|
|
324
409
|
}
|
|
325
410
|
case "TOOL_CALL_RESULT": {
|
|
326
411
|
this.finishToolCall(
|
|
412
|
+
this.scopeOf(event),
|
|
327
413
|
event.toolCallId,
|
|
328
414
|
event.content ?? "",
|
|
329
415
|
typeof event.mcpResult?.isError === "boolean"
|
|
@@ -343,12 +429,15 @@ export class RunAggregator {
|
|
|
343
429
|
break;
|
|
344
430
|
}
|
|
345
431
|
if (event.activityType !== MCP_APPS_ACTIVITY_TYPE) break;
|
|
432
|
+
const activityScope = this.scopeOf(event);
|
|
346
433
|
const toolCallId = event.content.toolCallId;
|
|
434
|
+
const fallbackId =
|
|
435
|
+
this.lastResolvedToolCallIdByScope.get(activityScope);
|
|
347
436
|
const entry =
|
|
348
437
|
typeof toolCallId === "string"
|
|
349
438
|
? this.toolCalls.get(toolCallId)
|
|
350
|
-
:
|
|
351
|
-
? this.toolCalls.get(
|
|
439
|
+
: fallbackId
|
|
440
|
+
? this.toolCalls.get(fallbackId)
|
|
352
441
|
: undefined;
|
|
353
442
|
const resourceUri = event.content.resourceUri;
|
|
354
443
|
if (
|
|
@@ -391,6 +480,70 @@ export class RunAggregator {
|
|
|
391
480
|
break;
|
|
392
481
|
}
|
|
393
482
|
|
|
483
|
+
case "SUBAGENT_STARTED": {
|
|
484
|
+
// A suspended subagent is re-announced with the same id on resume, so
|
|
485
|
+
// a repeat is a continuation: it may refresh the descriptive fields
|
|
486
|
+
// and reopen the status, but re-parenting an established run would
|
|
487
|
+
// move its already-rendered output to a different tool call.
|
|
488
|
+
const existing = this.subagentRuns.get(event.subagentRunId);
|
|
489
|
+
if (existing) {
|
|
490
|
+
existing.name = event.name;
|
|
491
|
+
if (event.description !== undefined)
|
|
492
|
+
existing.description = event.description;
|
|
493
|
+
existing.status = { type: "running" };
|
|
494
|
+
this.emit();
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
this.nestingMemo = undefined;
|
|
498
|
+
this.subagentRuns.set(event.subagentRunId, {
|
|
499
|
+
subagentRunId: event.subagentRunId,
|
|
500
|
+
name: event.name,
|
|
501
|
+
...(event.description !== undefined
|
|
502
|
+
? { description: event.description }
|
|
503
|
+
: {}),
|
|
504
|
+
...(event.parentSubagentRunId !== undefined
|
|
505
|
+
? { parentSubagentRunId: event.parentSubagentRunId }
|
|
506
|
+
: {}),
|
|
507
|
+
...(event.parentToolCallId !== undefined
|
|
508
|
+
? { parentToolCallId: event.parentToolCallId }
|
|
509
|
+
: {}),
|
|
510
|
+
...(event.parentMessageId !== undefined
|
|
511
|
+
? { parentMessageId: event.parentMessageId }
|
|
512
|
+
: {}),
|
|
513
|
+
createdAt: new Date(),
|
|
514
|
+
status: { type: "running" },
|
|
515
|
+
});
|
|
516
|
+
this.emit();
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case "SUBAGENT_FINISHED": {
|
|
520
|
+
const run = this.subagentRuns.get(event.subagentRunId);
|
|
521
|
+
if (run) {
|
|
522
|
+
run.status =
|
|
523
|
+
event.outcome?.type === "suspended"
|
|
524
|
+
? { type: "requires-action", reason: "interrupt" }
|
|
525
|
+
: { type: "complete", reason: "unknown" };
|
|
526
|
+
if (event.result !== undefined) run.result = event.result;
|
|
527
|
+
if (event.outcome?.type === "suspended" && event.outcome.interruptIds)
|
|
528
|
+
run.interruptIds = event.outcome.interruptIds;
|
|
529
|
+
}
|
|
530
|
+
this.emit();
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
case "SUBAGENT_ERROR": {
|
|
534
|
+
const run = this.subagentRuns.get(event.subagentRunId);
|
|
535
|
+
if (run) {
|
|
536
|
+
run.status = {
|
|
537
|
+
type: "incomplete",
|
|
538
|
+
reason: "error",
|
|
539
|
+
...(event.message !== undefined ? { error: event.message } : {}),
|
|
540
|
+
};
|
|
541
|
+
if (event.code !== undefined) run.errorCode = event.code;
|
|
542
|
+
}
|
|
543
|
+
this.emit();
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
|
|
394
547
|
default: {
|
|
395
548
|
this.logger.debug?.("[agui] aggregator ignored event", event);
|
|
396
549
|
}
|
|
@@ -448,12 +601,14 @@ export class RunAggregator {
|
|
|
448
601
|
if (!this.toolCalls.has(toolCallId)) {
|
|
449
602
|
this.partOrder.push({ kind: "tool-call", toolCallId });
|
|
450
603
|
}
|
|
604
|
+
this.nestingMemo = undefined;
|
|
451
605
|
this.toolCalls.set(toolCallId, entry);
|
|
452
606
|
this.a2uiToolCallIds.add(toolCallId);
|
|
453
607
|
}
|
|
454
608
|
|
|
455
609
|
for (const toolCallId of this.a2uiToolCallIds) {
|
|
456
610
|
if (activeToolCallIds.has(toolCallId)) continue;
|
|
611
|
+
this.nestingMemo = undefined;
|
|
457
612
|
this.toolCalls.delete(toolCallId);
|
|
458
613
|
const partIndex = this.partOrder.findIndex(
|
|
459
614
|
(part) => part.kind === "tool-call" && part.toolCallId === toolCallId,
|
|
@@ -492,18 +647,39 @@ export class RunAggregator {
|
|
|
492
647
|
this.hiddenActiveReasoning = "none";
|
|
493
648
|
this.loggedDroppedOpaqueIds.clear();
|
|
494
649
|
this.anonymousReasoningKeys.clear();
|
|
495
|
-
this.
|
|
650
|
+
this.activeReasoningKeyByScope.clear();
|
|
651
|
+
this.nestingMemo = undefined;
|
|
652
|
+
this.subagentRuns.clear();
|
|
496
653
|
this.reasoningPartCounter = 0;
|
|
497
654
|
this.toolCalls.clear();
|
|
498
655
|
this.a2uiBuckets.clear();
|
|
499
656
|
this.a2uiToolCallIds.clear();
|
|
500
|
-
this.
|
|
657
|
+
this.lastResolvedToolCallIdByScope.clear();
|
|
501
658
|
this.partOrder.length = 0;
|
|
502
659
|
this.textPartCounter = 0;
|
|
503
|
-
this.
|
|
660
|
+
this.activeTextMessageIdByScope.clear();
|
|
504
661
|
this.reportedServerMessageId = undefined;
|
|
505
662
|
}
|
|
506
663
|
|
|
664
|
+
// A subagent only ever reports its own terminal, so a run that ends while
|
|
665
|
+
// one is still streaming would leave that nested message spinning forever.
|
|
666
|
+
private closeOpenSubagentRuns(status: MessageStatus): void {
|
|
667
|
+
for (const run of this.subagentRuns.values()) {
|
|
668
|
+
if (run.status.type === "running") run.status = status;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private scopeOf(event: { subagentRunId?: string }): string {
|
|
673
|
+
return event.subagentRunId ?? ROOT_SCOPE;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Nothing in the AG-UI schema makes a messageId unique across subagent runs,
|
|
677
|
+
// so the accumulators key on the scope too. Root keys stay bare, which is
|
|
678
|
+
// what the wire-id collision checks against reported message ids compare on.
|
|
679
|
+
private partKey(scope: string, id: string): string {
|
|
680
|
+
return scope === ROOT_SCOPE ? id : `${scope}\u0000${id}`;
|
|
681
|
+
}
|
|
682
|
+
|
|
507
683
|
private beginDistinctTextMessage(messageId: string | undefined): void {
|
|
508
684
|
if (
|
|
509
685
|
!messageId ||
|
|
@@ -522,63 +698,71 @@ export class RunAggregator {
|
|
|
522
698
|
return `text-${this.textPartCounter}`;
|
|
523
699
|
}
|
|
524
700
|
|
|
525
|
-
private startTextMessage(messageId?: string): string {
|
|
701
|
+
private startTextMessage(scope: string, messageId?: string): string {
|
|
526
702
|
const id = messageId ?? this.generateTextKey();
|
|
527
|
-
this.ensureTextPart(id);
|
|
528
|
-
this.
|
|
529
|
-
return
|
|
703
|
+
const key = this.ensureTextPart(scope, id);
|
|
704
|
+
this.activeTextMessageIdByScope.set(scope, key);
|
|
705
|
+
return key;
|
|
530
706
|
}
|
|
531
707
|
|
|
532
|
-
private resolveTextMessageId(messageId?: string): string {
|
|
708
|
+
private resolveTextMessageId(scope: string, messageId?: string): string {
|
|
533
709
|
if (messageId) {
|
|
534
|
-
this.ensureTextPart(messageId);
|
|
535
|
-
this.
|
|
536
|
-
return
|
|
710
|
+
const key = this.ensureTextPart(scope, messageId);
|
|
711
|
+
this.activeTextMessageIdByScope.set(scope, key);
|
|
712
|
+
return key;
|
|
537
713
|
}
|
|
538
714
|
|
|
539
|
-
|
|
540
|
-
|
|
715
|
+
const active = this.activeTextMessageIdByScope.get(scope);
|
|
716
|
+
if (active) {
|
|
717
|
+
return active;
|
|
541
718
|
}
|
|
542
719
|
|
|
543
|
-
const
|
|
544
|
-
this.
|
|
545
|
-
|
|
546
|
-
return generated;
|
|
720
|
+
const key = this.ensureTextPart(scope, this.generateTextKey());
|
|
721
|
+
this.activeTextMessageIdByScope.set(scope, key);
|
|
722
|
+
return key;
|
|
547
723
|
}
|
|
548
724
|
|
|
549
|
-
private ensureTextPart(id: string):
|
|
550
|
-
|
|
551
|
-
|
|
725
|
+
private ensureTextPart(scope: string, id: string): string {
|
|
726
|
+
const key = this.partKey(scope, id);
|
|
727
|
+
if (!this.textParts.has(key)) {
|
|
728
|
+
this.textParts.set(key, { buffer: "" });
|
|
552
729
|
if (
|
|
553
|
-
!this.partOrder.some((part) => part.kind === "text" && part.key ===
|
|
730
|
+
!this.partOrder.some((part) => part.kind === "text" && part.key === key)
|
|
554
731
|
) {
|
|
555
|
-
this.partOrder.push(
|
|
732
|
+
this.partOrder.push(
|
|
733
|
+
scope === ROOT_SCOPE
|
|
734
|
+
? { kind: "text", key }
|
|
735
|
+
: { kind: "text", key, subagentRunId: scope },
|
|
736
|
+
);
|
|
556
737
|
}
|
|
557
738
|
}
|
|
739
|
+
return key;
|
|
558
740
|
}
|
|
559
741
|
|
|
560
742
|
private appendText(id: string, delta: string): void {
|
|
561
|
-
this.ensureTextPart(id);
|
|
562
743
|
const entry = this.textParts.get(id);
|
|
563
744
|
if (!entry) return;
|
|
564
745
|
entry.buffer += delta;
|
|
565
746
|
}
|
|
566
747
|
|
|
567
748
|
private startToolCall(
|
|
749
|
+
scope: string,
|
|
568
750
|
id: string | undefined,
|
|
569
751
|
name?: string,
|
|
570
752
|
parentMessageId?: string,
|
|
571
753
|
) {
|
|
572
754
|
if (!id) return;
|
|
573
755
|
// 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
|
-
|
|
756
|
+
// after it should be a new part, not appended to the pre-tool text —
|
|
757
|
+
// scoped, so a subagent's tool call doesn't break the root run's text
|
|
758
|
+
// and vice versa.
|
|
759
|
+
this.activeTextMessageIdByScope.delete(scope);
|
|
576
760
|
if (
|
|
577
761
|
!this.partOrder.some(
|
|
578
762
|
(part) => part.kind === "tool-call" && part.toolCallId === id,
|
|
579
763
|
)
|
|
580
764
|
) {
|
|
581
|
-
this.insertToolPart(id, parentMessageId);
|
|
765
|
+
this.insertToolPart(scope, id, parentMessageId);
|
|
582
766
|
}
|
|
583
767
|
const state: ToolCallState = {
|
|
584
768
|
toolCallId: id,
|
|
@@ -592,16 +776,21 @@ export class RunAggregator {
|
|
|
592
776
|
if (parentMessageId) {
|
|
593
777
|
state.parentMessageId = parentMessageId;
|
|
594
778
|
}
|
|
779
|
+
if (scope !== ROOT_SCOPE) {
|
|
780
|
+
state.subagentRunId = scope;
|
|
781
|
+
}
|
|
782
|
+
this.nestingMemo = undefined;
|
|
595
783
|
this.toolCalls.set(id, state);
|
|
596
784
|
}
|
|
597
785
|
|
|
598
786
|
// The message and tool-call channels are unordered on the wire, so anchor a
|
|
599
787
|
// tool call under its parentMessageId text part instead of appending it.
|
|
600
|
-
private insertToolPart(id: string, parentMessageId?: string) {
|
|
788
|
+
private insertToolPart(scope: string, id: string, parentMessageId?: string) {
|
|
601
789
|
const entry = { kind: "tool-call", toolCallId: id } as const;
|
|
602
790
|
if (parentMessageId) {
|
|
791
|
+
const parentKey = this.partKey(scope, parentMessageId);
|
|
603
792
|
const parentIndex = this.partOrder.findIndex(
|
|
604
|
-
(part) => part.kind === "text" && part.key ===
|
|
793
|
+
(part) => part.kind === "text" && part.key === parentKey,
|
|
605
794
|
);
|
|
606
795
|
if (parentIndex !== -1) {
|
|
607
796
|
let insertAt = parentIndex + 1;
|
|
@@ -641,6 +830,7 @@ export class RunAggregator {
|
|
|
641
830
|
}
|
|
642
831
|
|
|
643
832
|
private finishToolCall(
|
|
833
|
+
scope: string,
|
|
644
834
|
id: string,
|
|
645
835
|
content: string,
|
|
646
836
|
isError?: boolean,
|
|
@@ -658,7 +848,9 @@ export class RunAggregator {
|
|
|
658
848
|
result: undefined,
|
|
659
849
|
isError: undefined,
|
|
660
850
|
snapshotResultApplied: false,
|
|
851
|
+
...(scope !== ROOT_SCOPE ? { subagentRunId: scope } : {}),
|
|
661
852
|
};
|
|
853
|
+
this.nestingMemo = undefined;
|
|
662
854
|
this.toolCalls.set(id, entry);
|
|
663
855
|
}
|
|
664
856
|
if (
|
|
@@ -691,61 +883,199 @@ export class RunAggregator {
|
|
|
691
883
|
if (toolMessageId) {
|
|
692
884
|
entry.toolMessageId = toolMessageId;
|
|
693
885
|
}
|
|
694
|
-
this.
|
|
886
|
+
this.lastResolvedToolCallIdByScope.set(scope, id);
|
|
695
887
|
}
|
|
696
888
|
|
|
697
|
-
private
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
889
|
+
private nesting(): {
|
|
890
|
+
subagentsByParentToolCallId: Map<string, string[]>;
|
|
891
|
+
reachable: Set<string>;
|
|
892
|
+
} {
|
|
893
|
+
if (!this.nestingMemo) {
|
|
894
|
+
const subagentsByParentToolCallId = this.subagentsByParentToolCallId();
|
|
895
|
+
this.nestingMemo = {
|
|
896
|
+
subagentsByParentToolCallId,
|
|
897
|
+
reachable: this.reachableSubagentRunIds(subagentsByParentToolCallId),
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
return this.nestingMemo;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private subagentsByParentToolCallId(): Map<string, string[]> {
|
|
904
|
+
const map = new Map<string, string[]>();
|
|
905
|
+
for (const run of this.subagentRuns.values()) {
|
|
906
|
+
if (!run.parentToolCallId) continue;
|
|
907
|
+
const list = map.get(run.parentToolCallId) ?? [];
|
|
908
|
+
list.push(run.subagentRunId);
|
|
909
|
+
map.set(run.parentToolCallId, list);
|
|
910
|
+
}
|
|
911
|
+
return map;
|
|
912
|
+
}
|
|
707
913
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
914
|
+
// A subagent nests under its spawning tool call only when that call is itself
|
|
915
|
+
// reachable from the root scope. A run with no parentToolCallId, one naming a
|
|
916
|
+
// call this run never saw, or one in a cycle has nowhere to hang; the visited
|
|
917
|
+
// set also keeps a malformed chain to one visit per run instead of branching
|
|
918
|
+
// at every level.
|
|
919
|
+
private reachableSubagentRunIds(
|
|
920
|
+
subagentsByParentToolCallId: Map<string, string[]>,
|
|
921
|
+
): Set<string> {
|
|
922
|
+
const reachable = new Set<string>();
|
|
923
|
+
const walk = (scope: string, depth: number): void => {
|
|
924
|
+
if (depth > MAX_SUBAGENT_DEPTH) return;
|
|
925
|
+
for (const entry of this.toolCalls.values()) {
|
|
926
|
+
if ((entry.subagentRunId ?? ROOT_SCOPE) !== scope) continue;
|
|
927
|
+
for (const id of subagentsByParentToolCallId.get(entry.toolCallId) ??
|
|
928
|
+
[]) {
|
|
929
|
+
if (reachable.has(id)) continue;
|
|
930
|
+
reachable.add(id);
|
|
931
|
+
walk(id, depth + 1);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
720
934
|
};
|
|
935
|
+
walk(ROOT_SCOPE, 0);
|
|
936
|
+
return reachable;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Nested messages rather than a discovery hook (the shape react-langchain
|
|
940
|
+
// uses): ToolCallMessagePart.messages is the contract core consumers already
|
|
941
|
+
// walk, and SUBAGENT_STARTED.parentToolCallId names the spawning call
|
|
942
|
+
// outright, so the join needs no namespace scheme of its own.
|
|
943
|
+
private materializeSubagentMessage(
|
|
944
|
+
subagentRunId: string,
|
|
945
|
+
depth: number,
|
|
946
|
+
ctx: BuildContext,
|
|
947
|
+
): ThreadMessage | undefined {
|
|
948
|
+
const run = this.subagentRuns.get(subagentRunId);
|
|
949
|
+
if (!run || depth > MAX_SUBAGENT_DEPTH) return undefined;
|
|
950
|
+
const content = this.buildParts(subagentRunId, depth + 1, ctx);
|
|
951
|
+
return {
|
|
952
|
+
id: run.subagentRunId,
|
|
953
|
+
role: "assistant",
|
|
954
|
+
createdAt: run.createdAt,
|
|
955
|
+
status: run.status,
|
|
956
|
+
content,
|
|
957
|
+
metadata: {
|
|
958
|
+
unstable_state: null,
|
|
959
|
+
unstable_annotations: [],
|
|
960
|
+
unstable_data: [],
|
|
961
|
+
steps: [],
|
|
962
|
+
custom: {
|
|
963
|
+
[AG_UI_METADATA_NAMESPACE]: {
|
|
964
|
+
name: run.name,
|
|
965
|
+
...(run.description !== undefined
|
|
966
|
+
? { description: run.description }
|
|
967
|
+
: {}),
|
|
968
|
+
...(run.parentSubagentRunId !== undefined
|
|
969
|
+
? { parentSubagentRunId: run.parentSubagentRunId }
|
|
970
|
+
: {}),
|
|
971
|
+
...(run.result !== undefined ? { result: run.result } : {}),
|
|
972
|
+
...(run.interruptIds !== undefined
|
|
973
|
+
? { interruptIds: run.interruptIds }
|
|
974
|
+
: {}),
|
|
975
|
+
...(run.errorCode !== undefined
|
|
976
|
+
? { errorCode: run.errorCode }
|
|
977
|
+
: {}),
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
} as ThreadMessage;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
private buildParts(
|
|
985
|
+
scope: string,
|
|
986
|
+
depth: number,
|
|
987
|
+
ctx: BuildContext,
|
|
988
|
+
): ThreadAssistantMessagePart[] {
|
|
989
|
+
const snapshot: ThreadAssistantMessagePart[] = [];
|
|
990
|
+
const isRoot = scope === ROOT_SCOPE;
|
|
991
|
+
|
|
992
|
+
for (const { index, part } of ctx.partsByScope.get(scope) ?? []) {
|
|
993
|
+
const materialized = () => {
|
|
994
|
+
if (isRoot && ctx.root) ctx.root.lastMaterializedIndex = index;
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
if (part.kind === "tool-call") {
|
|
998
|
+
const entry = this.toolCalls.get(part.toolCallId);
|
|
999
|
+
if (!entry) continue;
|
|
1000
|
+
const approval = ctx.approvals.get(entry.toolCallId);
|
|
1001
|
+
const nestedMessages = (
|
|
1002
|
+
ctx.subagentsByParentToolCallId.get(entry.toolCallId) ?? []
|
|
1003
|
+
)
|
|
1004
|
+
.filter((id) => ctx.reachable.has(id))
|
|
1005
|
+
.map((id) => this.materializeSubagentMessage(id, depth, ctx))
|
|
1006
|
+
.filter((m): m is ThreadMessage => m !== undefined);
|
|
1007
|
+
const toolPart: ToolCallMessagePart = {
|
|
1008
|
+
type: "tool-call",
|
|
1009
|
+
toolCallId: entry.toolCallId,
|
|
1010
|
+
toolName: entry.toolCallName,
|
|
1011
|
+
args: (entry.parsedArgs ?? {}) as any,
|
|
1012
|
+
argsText: entry.argsText,
|
|
1013
|
+
...(approval ? { approval } : {}),
|
|
1014
|
+
...(entry.result !== undefined ? { result: entry.result } : {}),
|
|
1015
|
+
...(entry.modelContent !== undefined
|
|
1016
|
+
? { modelContent: entry.modelContent }
|
|
1017
|
+
: {}),
|
|
1018
|
+
...(entry.isError !== undefined ? { isError: entry.isError } : {}),
|
|
1019
|
+
...(entry.mcpAppResourceUri
|
|
1020
|
+
? {
|
|
1021
|
+
mcp: {
|
|
1022
|
+
app: {
|
|
1023
|
+
resourceUri: entry.mcpAppResourceUri,
|
|
1024
|
+
...(entry.mcpAppServerId
|
|
1025
|
+
? { serverId: entry.mcpAppServerId }
|
|
1026
|
+
: {}),
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
}
|
|
1030
|
+
: {}),
|
|
1031
|
+
...(entry.parentMessageId ? { parentId: entry.parentMessageId } : {}),
|
|
1032
|
+
...(entry.toolMessageId
|
|
1033
|
+
? { unstable_toolMessageId: entry.toolMessageId }
|
|
1034
|
+
: {}),
|
|
1035
|
+
...(nestedMessages.length > 0 ? { messages: nestedMessages } : {}),
|
|
1036
|
+
} as ToolCallMessagePart & { unstable_toolMessageId?: string };
|
|
1037
|
+
snapshot.push(toolPart);
|
|
1038
|
+
materialized();
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const rawScope =
|
|
1043
|
+
"subagentRunId" in part
|
|
1044
|
+
? (part.subagentRunId ?? ROOT_SCOPE)
|
|
1045
|
+
: ROOT_SCOPE;
|
|
721
1046
|
|
|
722
|
-
for (const [index, part] of this.partOrder.entries()) {
|
|
723
|
-
currentIndex = index;
|
|
724
1047
|
if (part.kind === "reasoning") {
|
|
725
1048
|
if (this.showThinking) {
|
|
726
1049
|
const buffer = this.reasoningParts.get(part.key) ?? "";
|
|
727
|
-
|
|
1050
|
+
const isActive =
|
|
1051
|
+
this.activeReasoningKeyByScope.get(rawScope) === part.key;
|
|
1052
|
+
if (buffer.length > 0 || isActive) {
|
|
728
1053
|
const encryptedValue = this.reasoningSignatures.get(part.key);
|
|
729
1054
|
const reasoningId = this.reasoningMessageIds.get(part.key);
|
|
730
1055
|
const meta = {
|
|
731
1056
|
...(reasoningId !== undefined ? { reasoningId } : {}),
|
|
732
1057
|
...(encryptedValue !== undefined ? { encryptedValue } : {}),
|
|
733
1058
|
};
|
|
734
|
-
|
|
1059
|
+
snapshot.push({
|
|
735
1060
|
type: "reasoning",
|
|
736
1061
|
text: buffer,
|
|
737
1062
|
...(Object.keys(meta).length > 0
|
|
738
1063
|
? { providerMetadata: { [AG_UI_METADATA_NAMESPACE]: meta } }
|
|
739
1064
|
: {}),
|
|
740
1065
|
} as const);
|
|
741
|
-
|
|
1066
|
+
materialized();
|
|
1067
|
+
} else if (isRoot && ctx.root) {
|
|
742
1068
|
// A retracted empty block still carries transport state: without
|
|
743
1069
|
// a part to live on, its signature rides the message metadata,
|
|
744
1070
|
// matching the shape a snapshot reload produces.
|
|
745
1071
|
const encryptedValue = this.reasoningSignatures.get(part.key);
|
|
746
1072
|
const id = this.reasoningSignatureIds.get(part.key);
|
|
747
1073
|
if (id?.trim() && encryptedValue?.trim()) {
|
|
748
|
-
opaqueCandidates.push({
|
|
1074
|
+
ctx.root.opaqueCandidates.push({
|
|
1075
|
+
id,
|
|
1076
|
+
encryptedValue,
|
|
1077
|
+
anchor: index + 1,
|
|
1078
|
+
});
|
|
749
1079
|
}
|
|
750
1080
|
}
|
|
751
1081
|
}
|
|
@@ -755,54 +1085,79 @@ export class RunAggregator {
|
|
|
755
1085
|
if (part.kind === "text") {
|
|
756
1086
|
const entry = this.textParts.get(part.key);
|
|
757
1087
|
if (entry && entry.buffer.trim().length > 0) {
|
|
758
|
-
|
|
1088
|
+
snapshot.push({ type: "text", text: entry.buffer } as const);
|
|
1089
|
+
materialized();
|
|
759
1090
|
}
|
|
760
1091
|
continue;
|
|
761
1092
|
}
|
|
762
1093
|
|
|
763
1094
|
if (part.kind === "data") {
|
|
764
|
-
|
|
1095
|
+
snapshot.push({
|
|
765
1096
|
type: "data",
|
|
766
1097
|
name: part.name,
|
|
767
1098
|
data: part.value,
|
|
768
1099
|
});
|
|
1100
|
+
materialized();
|
|
769
1101
|
continue;
|
|
770
1102
|
}
|
|
1103
|
+
}
|
|
771
1104
|
|
|
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
|
-
|
|
1105
|
+
return snapshot;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
private emit(): void {
|
|
1109
|
+
const { subagentsByParentToolCallId, reachable } = this.nesting();
|
|
1110
|
+
const root = {
|
|
1111
|
+
opaqueCandidates: Array.from(
|
|
1112
|
+
this.hiddenSignatures,
|
|
1113
|
+
([id, encryptedValue]) => ({
|
|
1114
|
+
id,
|
|
1115
|
+
encryptedValue,
|
|
1116
|
+
anchor: this.hiddenSignatureAnchors.get(id)!,
|
|
1117
|
+
}),
|
|
1118
|
+
) as (AgUiOpaqueReasoning & { anchor: number })[],
|
|
1119
|
+
lastMaterializedIndex: -1,
|
|
1120
|
+
};
|
|
1121
|
+
// A run that ended incomplete can no longer be resumed, so a gate left over
|
|
1122
|
+
// from an earlier interrupt outcome is unanswerable and must not stay
|
|
1123
|
+
// projected. The interrupts themselves are kept on the message, since the
|
|
1124
|
+
// bespoke hooks read that payload. Bound ids cover every call the run
|
|
1125
|
+
// produced, root or nested: the approval seams walk
|
|
1126
|
+
// ToolCallMessagePart.messages, so a gate naming a subagent-scoped call is
|
|
1127
|
+
// answerable and projects onto the nested part.
|
|
1128
|
+
const partsByScope = new Map<
|
|
1129
|
+
string,
|
|
1130
|
+
{ index: number; part: PartOrderEntry }[]
|
|
1131
|
+
>();
|
|
1132
|
+
for (const [index, part] of this.partOrder.entries()) {
|
|
1133
|
+
const rawScope =
|
|
1134
|
+
part.kind === "tool-call"
|
|
1135
|
+
? (this.toolCalls.get(part.toolCallId)?.subagentRunId ?? ROOT_SCOPE)
|
|
1136
|
+
: "subagentRunId" in part
|
|
1137
|
+
? (part.subagentRunId ?? ROOT_SCOPE)
|
|
1138
|
+
: ROOT_SCOPE;
|
|
1139
|
+
const bucketScope =
|
|
1140
|
+
rawScope === ROOT_SCOPE || reachable.has(rawScope)
|
|
1141
|
+
? rawScope
|
|
1142
|
+
: ROOT_SCOPE;
|
|
1143
|
+
const bucket = partsByScope.get(bucketScope);
|
|
1144
|
+
if (bucket) bucket.push({ index, part });
|
|
1145
|
+
else partsByScope.set(bucketScope, [{ index, part }]);
|
|
805
1146
|
}
|
|
1147
|
+
const ctx: BuildContext = {
|
|
1148
|
+
subagentsByParentToolCallId,
|
|
1149
|
+
reachable,
|
|
1150
|
+
partsByScope,
|
|
1151
|
+
approvals: projectAgUiToolApprovals(
|
|
1152
|
+
this.status?.type === "requires-action" ? this.interrupts : undefined,
|
|
1153
|
+
new Set(
|
|
1154
|
+
Array.from(this.toolCalls.values()).map((entry) => entry.toolCallId),
|
|
1155
|
+
),
|
|
1156
|
+
),
|
|
1157
|
+
root,
|
|
1158
|
+
};
|
|
1159
|
+
const snapshot = this.buildParts(ROOT_SCOPE, 0, ctx);
|
|
1160
|
+
const { opaqueCandidates, lastMaterializedIndex } = root;
|
|
806
1161
|
|
|
807
1162
|
// An anonymous claim promotes the signature's entityId to a wire message
|
|
808
1163
|
// id; when that id actually names a text message or the adopted assistant
|
|
@@ -906,8 +1261,15 @@ export class RunAggregator {
|
|
|
906
1261
|
};
|
|
907
1262
|
}
|
|
908
1263
|
|
|
909
|
-
private handleReasoningStart(
|
|
1264
|
+
private handleReasoningStart(
|
|
1265
|
+
scope: string,
|
|
1266
|
+
messageId?: string,
|
|
1267
|
+
isMessageId = false,
|
|
1268
|
+
): void {
|
|
910
1269
|
if (!this.showThinking) {
|
|
1270
|
+
// Hidden-reasoning bookkeeping stays root-only for this task — a
|
|
1271
|
+
// subagent's opaque/hidden reasoning signatures are not preserved.
|
|
1272
|
+
if (scope !== ROOT_SCOPE) return;
|
|
911
1273
|
const anchor = this.partOrder.length;
|
|
912
1274
|
if (messageId === undefined) {
|
|
913
1275
|
this.hiddenActiveReasoning = "anonymous";
|
|
@@ -920,9 +1282,12 @@ export class RunAggregator {
|
|
|
920
1282
|
return;
|
|
921
1283
|
}
|
|
922
1284
|
// 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 =
|
|
1285
|
+
// should be a new part, not appended to any pre-reasoning text — scoped.
|
|
1286
|
+
this.activeTextMessageIdByScope.delete(scope);
|
|
1287
|
+
const key = this.partKey(
|
|
1288
|
+
scope,
|
|
1289
|
+
messageId ?? `__auto-reasoning-${++this.reasoningPartCounter}`,
|
|
1290
|
+
);
|
|
926
1291
|
// Two different questions: which id may be replayed as a ReasoningMessage.id
|
|
927
1292
|
// (only the message-scoped aliases carry one), and which block an unmatched
|
|
928
1293
|
// signature may claim (any block opened without an id at all).
|
|
@@ -933,13 +1298,18 @@ export class RunAggregator {
|
|
|
933
1298
|
}
|
|
934
1299
|
if (!this.reasoningParts.has(key)) {
|
|
935
1300
|
this.reasoningParts.set(key, "");
|
|
936
|
-
this.partOrder.push(
|
|
1301
|
+
this.partOrder.push(
|
|
1302
|
+
scope === ROOT_SCOPE
|
|
1303
|
+
? { kind: "reasoning", key }
|
|
1304
|
+
: { kind: "reasoning", key, subagentRunId: scope },
|
|
1305
|
+
);
|
|
937
1306
|
}
|
|
938
|
-
this.
|
|
1307
|
+
this.activeReasoningKeyByScope.set(scope, key);
|
|
939
1308
|
this.emit();
|
|
940
1309
|
}
|
|
941
1310
|
|
|
942
1311
|
private handleReasoningContent(
|
|
1312
|
+
scope: string,
|
|
943
1313
|
delta: string,
|
|
944
1314
|
messageId?: string,
|
|
945
1315
|
isMessageId = false,
|
|
@@ -948,27 +1318,28 @@ export class RunAggregator {
|
|
|
948
1318
|
if (!this.showThinking) {
|
|
949
1319
|
// Content without a preceding START still names the block (the visible
|
|
950
1320
|
// path opens it lazily); register it so a signature can claim it.
|
|
1321
|
+
if (scope !== ROOT_SCOPE) return;
|
|
951
1322
|
if (this.hiddenActiveReasoning === "none") {
|
|
952
|
-
this.handleReasoningStart(messageId, isMessageId);
|
|
1323
|
+
this.handleReasoningStart(scope, messageId, isMessageId);
|
|
953
1324
|
}
|
|
954
1325
|
return;
|
|
955
1326
|
}
|
|
956
|
-
if (!this.
|
|
1327
|
+
if (!this.activeReasoningKeyByScope.has(scope)) {
|
|
957
1328
|
// Content arrived without a preceding START — create the slot lazily.
|
|
958
|
-
this.handleReasoningStart(messageId, isMessageId);
|
|
1329
|
+
this.handleReasoningStart(scope, messageId, isMessageId);
|
|
959
1330
|
}
|
|
960
|
-
const key = this.
|
|
1331
|
+
const key = this.activeReasoningKeyByScope.get(scope);
|
|
961
1332
|
if (!key) return;
|
|
962
1333
|
this.reasoningParts.set(key, (this.reasoningParts.get(key) ?? "") + delta);
|
|
963
1334
|
this.emit();
|
|
964
1335
|
}
|
|
965
1336
|
|
|
966
|
-
private handleReasoningEnd(): void {
|
|
1337
|
+
private handleReasoningEnd(scope: string): void {
|
|
967
1338
|
if (!this.showThinking) {
|
|
968
|
-
this.hiddenActiveReasoning = "none";
|
|
1339
|
+
if (scope === ROOT_SCOPE) this.hiddenActiveReasoning = "none";
|
|
969
1340
|
return;
|
|
970
1341
|
}
|
|
971
|
-
this.
|
|
1342
|
+
this.activeReasoningKeyByScope.delete(scope);
|
|
972
1343
|
this.emit();
|
|
973
1344
|
}
|
|
974
1345
|
}
|