@assistant-ui/react-ag-ui 0.0.56 → 0.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +5 -17
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +133 -234
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +54 -25
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +31 -4
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +419 -123
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +9 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +4 -1
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +3 -1
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
- package/dist/runtime/adapter/tool-approval.js +11 -12
- package/dist/runtime/adapter/tool-approval.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +176 -45
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +42 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +12 -9
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +10 -10
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +105 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +190 -319
- package/src/runtime/adapter/conversions.ts +128 -36
- package/src/runtime/adapter/run-aggregator.approval.test.ts +128 -0
- package/src/runtime/adapter/run-aggregator.ts +670 -148
- package/src/runtime/adapter/subscriber.ts +10 -1
- package/src/runtime/adapter/tool-approval.test.ts +75 -0
- package/src/runtime/adapter/tool-approval.ts +17 -12
- package/src/runtime/event-parser.ts +216 -36
- package/src/runtime/types.ts +70 -12
- package/src/useAgUiRuntime.approval.test.tsx +34 -24
- package/src/useAgUiRuntime.history.test.tsx +76 -0
- package/src/useAgUiRuntime.toolDeferral.test.tsx +186 -0
- package/src/useAgUiRuntime.ts +12 -9
|
@@ -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;
|
|
@@ -28,6 +74,9 @@ export type AgUiOpaqueReasoning = {
|
|
|
28
74
|
};
|
|
29
75
|
|
|
30
76
|
export type AgUiCustomMetadata = {
|
|
77
|
+
/** Wire role restored on export for messages the internal model cannot
|
|
78
|
+
* represent (a developer record rides as a system message). */
|
|
79
|
+
role?: "developer";
|
|
31
80
|
interrupts?: AgUiInterrupt[];
|
|
32
81
|
opaqueReasoning?: AgUiOpaqueReasoning[];
|
|
33
82
|
};
|
|
@@ -47,6 +96,7 @@ type ToolCallState = {
|
|
|
47
96
|
mcpAppServerId?: string;
|
|
48
97
|
modelContent?: ToolModelContentPart[];
|
|
49
98
|
snapshotResultApplied: boolean;
|
|
99
|
+
subagentRunId?: string;
|
|
50
100
|
};
|
|
51
101
|
|
|
52
102
|
export const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
|
|
@@ -63,6 +113,7 @@ export type RunAggregatorOptions = {
|
|
|
63
113
|
logger: Logger;
|
|
64
114
|
emit: Emit;
|
|
65
115
|
onServerMessageId?: (messageId: string) => void;
|
|
116
|
+
onTextMessageStart?: (messageId: string) => void;
|
|
66
117
|
};
|
|
67
118
|
|
|
68
119
|
/**
|
|
@@ -80,32 +131,52 @@ export class RunAggregator {
|
|
|
80
131
|
private readonly showThinking: boolean;
|
|
81
132
|
private readonly logger: Logger;
|
|
82
133
|
private readonly onServerMessageId: ((messageId: string) => void) | undefined;
|
|
134
|
+
private readonly onTextMessageStart:
|
|
135
|
+
| ((messageId: string) => void)
|
|
136
|
+
| undefined;
|
|
83
137
|
|
|
84
138
|
private status: ChatModelRunResult["status"] | undefined;
|
|
85
139
|
private interrupts: AgUiInterrupt[] | undefined;
|
|
86
|
-
private readonly textParts = new Map<
|
|
87
|
-
|
|
88
|
-
{ buffer: string; touched: boolean }
|
|
89
|
-
>();
|
|
90
|
-
private activeTextMessageId: string | undefined;
|
|
140
|
+
private readonly textParts = new Map<string, { buffer: string }>();
|
|
141
|
+
private readonly activeTextMessageIdByScope = new Map<string, string>();
|
|
91
142
|
private readonly reasoningParts = new Map<string, string>(); // key → buffer
|
|
92
143
|
private readonly reasoningSignatures = new Map<string, string>();
|
|
144
|
+
private readonly reasoningSignatureIds = new Map<string, string>();
|
|
145
|
+
// Signatures captured while thinking is hidden have no block to live on;
|
|
146
|
+
// they are transport state that must survive to the next run input. The
|
|
147
|
+
// claim rules mirror the visible path: an id-carrying block, or an open
|
|
148
|
+
// anonymous one.
|
|
149
|
+
private readonly hiddenSignatures = new Map<string, string>();
|
|
150
|
+
private readonly hiddenSignatureAnchors = new Map<string, number>();
|
|
151
|
+
private readonly hiddenBlockAnchors = new Map<string, number>();
|
|
152
|
+
private hiddenAnonymousAnchor: number | undefined;
|
|
153
|
+
private readonly hiddenReasoningIds = new Set<string>();
|
|
154
|
+
private hiddenActiveReasoning: "none" | "anonymous" | "identified" = "none";
|
|
155
|
+
private hasEmittedOpaqueReasoning = false;
|
|
156
|
+
private readonly loggedDroppedOpaqueIds = new Set<string>();
|
|
93
157
|
private readonly reasoningMessageIds = new Map<string, string>();
|
|
94
158
|
private readonly anonymousReasoningKeys = new Set<string>();
|
|
95
|
-
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;
|
|
96
170
|
private reasoningPartCounter = 0;
|
|
97
171
|
private readonly toolCalls = new Map<string, ToolCallState>();
|
|
98
172
|
private readonly a2uiBuckets = new Map<string, A2uiState>();
|
|
99
173
|
private readonly a2uiToolCallIds = new Set<string>();
|
|
100
|
-
private
|
|
101
|
-
private readonly partOrder:
|
|
102
|
-
| { kind: "text"; key: string }
|
|
103
|
-
| { kind: "reasoning"; key: string }
|
|
104
|
-
| { kind: "tool-call"; toolCallId: string }
|
|
105
|
-
| { kind: "data"; name: string; value: unknown }
|
|
106
|
-
)[] = [];
|
|
174
|
+
private readonly lastResolvedToolCallIdByScope = new Map<string, string>();
|
|
175
|
+
private readonly partOrder: PartOrderEntry[] = [];
|
|
107
176
|
private textPartCounter = 0;
|
|
108
177
|
private serverMessageIdReported = false;
|
|
178
|
+
private reportedServerMessageId: string | undefined;
|
|
179
|
+
private lastTextMessageId: string | undefined;
|
|
109
180
|
|
|
110
181
|
private streamStartTime: number | undefined;
|
|
111
182
|
private firstTokenTime: number | undefined;
|
|
@@ -116,6 +187,7 @@ export class RunAggregator {
|
|
|
116
187
|
this.showThinking = options.showThinking;
|
|
117
188
|
this.logger = options.logger;
|
|
118
189
|
this.onServerMessageId = options.onServerMessageId;
|
|
190
|
+
this.onTextMessageStart = options.onTextMessageStart;
|
|
119
191
|
}
|
|
120
192
|
|
|
121
193
|
hasToolCall(toolCallId: string): boolean {
|
|
@@ -125,22 +197,11 @@ export class RunAggregator {
|
|
|
125
197
|
handle(event: AgUiEvent): void {
|
|
126
198
|
switch (event.type) {
|
|
127
199
|
case "RUN_STARTED": {
|
|
128
|
-
this.
|
|
129
|
-
this.reasoningParts.clear();
|
|
130
|
-
this.reasoningSignatures.clear();
|
|
131
|
-
this.reasoningMessageIds.clear();
|
|
132
|
-
this.anonymousReasoningKeys.clear();
|
|
133
|
-
this.activeReasoningKey = undefined;
|
|
134
|
-
this.reasoningPartCounter = 0;
|
|
135
|
-
this.toolCalls.clear();
|
|
136
|
-
this.a2uiBuckets.clear();
|
|
137
|
-
this.a2uiToolCallIds.clear();
|
|
138
|
-
this.lastResolvedToolCallId = undefined;
|
|
139
|
-
this.partOrder.length = 0;
|
|
140
|
-
this.textPartCounter = 0;
|
|
141
|
-
this.activeTextMessageId = undefined;
|
|
200
|
+
this.resetMessageParts();
|
|
142
201
|
this.interrupts = undefined;
|
|
143
202
|
this.serverMessageIdReported = false;
|
|
203
|
+
this.lastTextMessageId = undefined;
|
|
204
|
+
this.reportedServerMessageId = undefined;
|
|
144
205
|
this.streamStartTime = Date.now();
|
|
145
206
|
this.firstTokenTime = undefined;
|
|
146
207
|
this.totalChunks = 0;
|
|
@@ -157,14 +218,18 @@ export class RunAggregator {
|
|
|
157
218
|
}
|
|
158
219
|
|
|
159
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.
|
|
160
225
|
const hasUnresolvedToolCalls = Array.from(this.toolCalls.values()).some(
|
|
161
226
|
(tc) => tc.result === undefined,
|
|
162
227
|
);
|
|
163
228
|
|
|
164
|
-
this.status =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
229
|
+
this.status = hasUnresolvedToolCalls
|
|
230
|
+
? { type: "requires-action", reason: "tool-calls" }
|
|
231
|
+
: { type: "complete", reason: "unknown" };
|
|
232
|
+
this.closeOpenSubagentRuns(this.status);
|
|
168
233
|
this.emit();
|
|
169
234
|
break;
|
|
170
235
|
}
|
|
@@ -174,47 +239,61 @@ export class RunAggregator {
|
|
|
174
239
|
reason: "error",
|
|
175
240
|
...(event.message !== undefined ? { error: event.message } : {}),
|
|
176
241
|
};
|
|
242
|
+
this.closeOpenSubagentRuns(this.status);
|
|
177
243
|
this.emit();
|
|
178
244
|
break;
|
|
179
245
|
}
|
|
180
246
|
case "RUN_CANCELLED": {
|
|
181
247
|
this.status = { type: "incomplete", reason: "cancelled" };
|
|
248
|
+
this.closeOpenSubagentRuns(this.status);
|
|
182
249
|
this.emit();
|
|
183
250
|
break;
|
|
184
251
|
}
|
|
185
252
|
|
|
186
253
|
case "TEXT_MESSAGE_START": {
|
|
187
|
-
this.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
this.
|
|
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;
|
|
191
259
|
}
|
|
260
|
+
this.startTextMessage(scope, event.messageId);
|
|
192
261
|
this.emit();
|
|
193
262
|
break;
|
|
194
263
|
}
|
|
195
264
|
case "TEXT_MESSAGE_CONTENT":
|
|
196
265
|
case "TEXT_MESSAGE_CHUNK": {
|
|
197
266
|
const incomingId = "messageId" in event ? event.messageId : undefined;
|
|
198
|
-
this.
|
|
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
|
+
}
|
|
199
273
|
if (!event.delta) break;
|
|
200
274
|
this.recordFirstToken();
|
|
201
|
-
const id = this.resolveTextMessageId(incomingId);
|
|
275
|
+
const id = this.resolveTextMessageId(scope, incomingId);
|
|
202
276
|
this.appendText(id, event.delta);
|
|
203
277
|
this.totalChunks++;
|
|
204
278
|
this.emit();
|
|
205
279
|
break;
|
|
206
280
|
}
|
|
207
281
|
case "TEXT_MESSAGE_END": {
|
|
208
|
-
this.
|
|
209
|
-
if (
|
|
210
|
-
|
|
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);
|
|
211
290
|
}
|
|
212
291
|
this.emit();
|
|
213
292
|
break;
|
|
214
293
|
}
|
|
215
294
|
|
|
216
295
|
case "CUSTOM": {
|
|
217
|
-
this.
|
|
296
|
+
this.activeTextMessageIdByScope.delete(ROOT_SCOPE);
|
|
218
297
|
this.partOrder.push({
|
|
219
298
|
kind: "data",
|
|
220
299
|
name: event.name,
|
|
@@ -229,33 +308,58 @@ export class RunAggregator {
|
|
|
229
308
|
case "REASONING_START":
|
|
230
309
|
case "REASONING_MESSAGE_START":
|
|
231
310
|
this.handleReasoningStart(
|
|
311
|
+
this.scopeOf("subagentRunId" in event ? event : {}),
|
|
232
312
|
"messageId" in event ? event.messageId : undefined,
|
|
233
313
|
event.type === "REASONING_MESSAGE_START",
|
|
234
314
|
);
|
|
235
315
|
break;
|
|
236
316
|
case "REASONING_ENCRYPTED_VALUE":
|
|
237
317
|
if (event.subtype === "message") {
|
|
318
|
+
const scope = this.scopeOf(event);
|
|
238
319
|
// entityId names any message, not necessarily a reasoning one, so an
|
|
239
320
|
// unmatched id may only claim a block that has no id to contradict it.
|
|
240
|
-
const active = this.
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
?
|
|
245
|
-
: undefined
|
|
321
|
+
const active = this.activeReasoningKeyByScope.get(scope);
|
|
322
|
+
const entityKey = this.partKey(scope, event.entityId);
|
|
323
|
+
const key = this.showThinking
|
|
324
|
+
? this.reasoningParts.has(entityKey)
|
|
325
|
+
? entityKey
|
|
326
|
+
: active !== undefined && this.anonymousReasoningKeys.has(active)
|
|
327
|
+
? active
|
|
328
|
+
: undefined
|
|
329
|
+
: undefined;
|
|
246
330
|
if (key !== undefined) {
|
|
247
331
|
this.reasoningSignatures.set(key, event.encryptedValue);
|
|
332
|
+
this.reasoningSignatureIds.set(key, event.entityId);
|
|
333
|
+
this.emit();
|
|
334
|
+
} else if (
|
|
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 &&
|
|
339
|
+
event.entityId.trim().length > 0 &&
|
|
340
|
+
event.encryptedValue.trim().length > 0 &&
|
|
341
|
+
(this.hiddenReasoningIds.has(event.entityId) ||
|
|
342
|
+
this.hiddenActiveReasoning === "anonymous")
|
|
343
|
+
) {
|
|
344
|
+
this.hiddenSignatures.set(event.entityId, event.encryptedValue);
|
|
345
|
+
this.hiddenSignatureAnchors.set(
|
|
346
|
+
event.entityId,
|
|
347
|
+
this.hiddenBlockAnchors.get(event.entityId) ??
|
|
348
|
+
this.hiddenAnonymousAnchor ??
|
|
349
|
+
this.partOrder.length,
|
|
350
|
+
);
|
|
248
351
|
this.emit();
|
|
249
352
|
}
|
|
250
353
|
}
|
|
251
354
|
break;
|
|
252
355
|
case "THINKING_TEXT_MESSAGE_CONTENT":
|
|
253
|
-
this.handleReasoningContent(event.delta);
|
|
356
|
+
this.handleReasoningContent(ROOT_SCOPE, event.delta);
|
|
254
357
|
this.totalChunks++;
|
|
255
358
|
this.recordFirstToken();
|
|
256
359
|
break;
|
|
257
360
|
case "REASONING_MESSAGE_CONTENT":
|
|
258
361
|
this.handleReasoningContent(
|
|
362
|
+
this.scopeOf(event),
|
|
259
363
|
event.delta,
|
|
260
364
|
"messageId" in event ? event.messageId : undefined,
|
|
261
365
|
true,
|
|
@@ -265,14 +369,20 @@ export class RunAggregator {
|
|
|
265
369
|
break;
|
|
266
370
|
case "THINKING_TEXT_MESSAGE_END":
|
|
267
371
|
case "THINKING_END":
|
|
372
|
+
this.handleReasoningEnd(ROOT_SCOPE);
|
|
373
|
+
break;
|
|
268
374
|
case "REASONING_MESSAGE_END":
|
|
269
375
|
case "REASONING_END":
|
|
270
|
-
this.handleReasoningEnd();
|
|
376
|
+
this.handleReasoningEnd(this.scopeOf(event));
|
|
271
377
|
break;
|
|
272
378
|
|
|
273
379
|
case "TOOL_CALL_START": {
|
|
274
|
-
this.
|
|
380
|
+
const scope = this.scopeOf(event);
|
|
381
|
+
if (scope === ROOT_SCOPE) {
|
|
382
|
+
this.reportServerMessageId(event.parentMessageId);
|
|
383
|
+
}
|
|
275
384
|
this.startToolCall(
|
|
385
|
+
scope,
|
|
276
386
|
event.toolCallId,
|
|
277
387
|
event.toolCallName,
|
|
278
388
|
event.parentMessageId,
|
|
@@ -282,7 +392,10 @@ export class RunAggregator {
|
|
|
282
392
|
}
|
|
283
393
|
case "TOOL_CALL_ARGS":
|
|
284
394
|
case "TOOL_CALL_CHUNK": {
|
|
285
|
-
if (
|
|
395
|
+
if (
|
|
396
|
+
event.type === "TOOL_CALL_CHUNK" &&
|
|
397
|
+
this.scopeOf(event) === ROOT_SCOPE
|
|
398
|
+
) {
|
|
286
399
|
this.reportServerMessageId(event.parentMessageId);
|
|
287
400
|
}
|
|
288
401
|
if (!event.delta) break;
|
|
@@ -296,6 +409,7 @@ export class RunAggregator {
|
|
|
296
409
|
}
|
|
297
410
|
case "TOOL_CALL_RESULT": {
|
|
298
411
|
this.finishToolCall(
|
|
412
|
+
this.scopeOf(event),
|
|
299
413
|
event.toolCallId,
|
|
300
414
|
event.content ?? "",
|
|
301
415
|
typeof event.mcpResult?.isError === "boolean"
|
|
@@ -315,12 +429,15 @@ export class RunAggregator {
|
|
|
315
429
|
break;
|
|
316
430
|
}
|
|
317
431
|
if (event.activityType !== MCP_APPS_ACTIVITY_TYPE) break;
|
|
432
|
+
const activityScope = this.scopeOf(event);
|
|
318
433
|
const toolCallId = event.content.toolCallId;
|
|
434
|
+
const fallbackId =
|
|
435
|
+
this.lastResolvedToolCallIdByScope.get(activityScope);
|
|
319
436
|
const entry =
|
|
320
437
|
typeof toolCallId === "string"
|
|
321
438
|
? this.toolCalls.get(toolCallId)
|
|
322
|
-
:
|
|
323
|
-
? this.toolCalls.get(
|
|
439
|
+
: fallbackId
|
|
440
|
+
? this.toolCalls.get(fallbackId)
|
|
324
441
|
: undefined;
|
|
325
442
|
const resourceUri = event.content.resourceUri;
|
|
326
443
|
if (
|
|
@@ -363,6 +480,70 @@ export class RunAggregator {
|
|
|
363
480
|
break;
|
|
364
481
|
}
|
|
365
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
|
+
|
|
366
547
|
default: {
|
|
367
548
|
this.logger.debug?.("[agui] aggregator ignored event", event);
|
|
368
549
|
}
|
|
@@ -420,12 +601,14 @@ export class RunAggregator {
|
|
|
420
601
|
if (!this.toolCalls.has(toolCallId)) {
|
|
421
602
|
this.partOrder.push({ kind: "tool-call", toolCallId });
|
|
422
603
|
}
|
|
604
|
+
this.nestingMemo = undefined;
|
|
423
605
|
this.toolCalls.set(toolCallId, entry);
|
|
424
606
|
this.a2uiToolCallIds.add(toolCallId);
|
|
425
607
|
}
|
|
426
608
|
|
|
427
609
|
for (const toolCallId of this.a2uiToolCallIds) {
|
|
428
610
|
if (activeToolCallIds.has(toolCallId)) continue;
|
|
611
|
+
this.nestingMemo = undefined;
|
|
429
612
|
this.toolCalls.delete(toolCallId);
|
|
430
613
|
const partIndex = this.partOrder.findIndex(
|
|
431
614
|
(part) => part.kind === "tool-call" && part.toolCallId === toolCallId,
|
|
@@ -436,8 +619,13 @@ export class RunAggregator {
|
|
|
436
619
|
}
|
|
437
620
|
|
|
438
621
|
private reportServerMessageId(messageId: string | undefined): void {
|
|
439
|
-
if (
|
|
622
|
+
if (!messageId) return;
|
|
623
|
+
if (this.lastTextMessageId === undefined) {
|
|
624
|
+
this.lastTextMessageId = messageId;
|
|
625
|
+
}
|
|
626
|
+
if (this.serverMessageIdReported) return;
|
|
440
627
|
this.serverMessageIdReported = true;
|
|
628
|
+
this.reportedServerMessageId = messageId;
|
|
441
629
|
this.onServerMessageId?.(messageId);
|
|
442
630
|
}
|
|
443
631
|
|
|
@@ -445,75 +633,136 @@ export class RunAggregator {
|
|
|
445
633
|
this.textParts.clear();
|
|
446
634
|
}
|
|
447
635
|
|
|
636
|
+
private resetMessageParts(): void {
|
|
637
|
+
this.clearTextParts();
|
|
638
|
+
this.reasoningParts.clear();
|
|
639
|
+
this.reasoningSignatures.clear();
|
|
640
|
+
this.reasoningSignatureIds.clear();
|
|
641
|
+
this.reasoningMessageIds.clear();
|
|
642
|
+
this.hiddenSignatures.clear();
|
|
643
|
+
this.hiddenSignatureAnchors.clear();
|
|
644
|
+
this.hiddenBlockAnchors.clear();
|
|
645
|
+
this.hiddenAnonymousAnchor = undefined;
|
|
646
|
+
this.hiddenReasoningIds.clear();
|
|
647
|
+
this.hiddenActiveReasoning = "none";
|
|
648
|
+
this.loggedDroppedOpaqueIds.clear();
|
|
649
|
+
this.anonymousReasoningKeys.clear();
|
|
650
|
+
this.activeReasoningKeyByScope.clear();
|
|
651
|
+
this.nestingMemo = undefined;
|
|
652
|
+
this.subagentRuns.clear();
|
|
653
|
+
this.reasoningPartCounter = 0;
|
|
654
|
+
this.toolCalls.clear();
|
|
655
|
+
this.a2uiBuckets.clear();
|
|
656
|
+
this.a2uiToolCallIds.clear();
|
|
657
|
+
this.lastResolvedToolCallIdByScope.clear();
|
|
658
|
+
this.partOrder.length = 0;
|
|
659
|
+
this.textPartCounter = 0;
|
|
660
|
+
this.activeTextMessageIdByScope.clear();
|
|
661
|
+
this.reportedServerMessageId = undefined;
|
|
662
|
+
}
|
|
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
|
+
|
|
683
|
+
private beginDistinctTextMessage(messageId: string | undefined): void {
|
|
684
|
+
if (
|
|
685
|
+
!messageId ||
|
|
686
|
+
this.lastTextMessageId === undefined ||
|
|
687
|
+
this.lastTextMessageId === messageId ||
|
|
688
|
+
!this.onTextMessageStart
|
|
689
|
+
) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
this.resetMessageParts();
|
|
693
|
+
this.onTextMessageStart(messageId);
|
|
694
|
+
}
|
|
695
|
+
|
|
448
696
|
private generateTextKey(): string {
|
|
449
697
|
this.textPartCounter += 1;
|
|
450
698
|
return `text-${this.textPartCounter}`;
|
|
451
699
|
}
|
|
452
700
|
|
|
453
|
-
private startTextMessage(messageId?: string): string {
|
|
701
|
+
private startTextMessage(scope: string, messageId?: string): string {
|
|
454
702
|
const id = messageId ?? this.generateTextKey();
|
|
455
|
-
this.ensureTextPart(id);
|
|
456
|
-
this.
|
|
457
|
-
return
|
|
703
|
+
const key = this.ensureTextPart(scope, id);
|
|
704
|
+
this.activeTextMessageIdByScope.set(scope, key);
|
|
705
|
+
return key;
|
|
458
706
|
}
|
|
459
707
|
|
|
460
|
-
private resolveTextMessageId(messageId?: string): string {
|
|
708
|
+
private resolveTextMessageId(scope: string, messageId?: string): string {
|
|
461
709
|
if (messageId) {
|
|
462
|
-
this.ensureTextPart(messageId);
|
|
463
|
-
this.
|
|
464
|
-
return
|
|
710
|
+
const key = this.ensureTextPart(scope, messageId);
|
|
711
|
+
this.activeTextMessageIdByScope.set(scope, key);
|
|
712
|
+
return key;
|
|
465
713
|
}
|
|
466
714
|
|
|
467
|
-
|
|
468
|
-
|
|
715
|
+
const active = this.activeTextMessageIdByScope.get(scope);
|
|
716
|
+
if (active) {
|
|
717
|
+
return active;
|
|
469
718
|
}
|
|
470
719
|
|
|
471
|
-
const
|
|
472
|
-
this.
|
|
473
|
-
|
|
474
|
-
return generated;
|
|
720
|
+
const key = this.ensureTextPart(scope, this.generateTextKey());
|
|
721
|
+
this.activeTextMessageIdByScope.set(scope, key);
|
|
722
|
+
return key;
|
|
475
723
|
}
|
|
476
724
|
|
|
477
|
-
private ensureTextPart(id: string):
|
|
478
|
-
|
|
479
|
-
|
|
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: "" });
|
|
480
729
|
if (
|
|
481
|
-
!this.partOrder.some((part) => part.kind === "text" && part.key ===
|
|
730
|
+
!this.partOrder.some((part) => part.kind === "text" && part.key === key)
|
|
482
731
|
) {
|
|
483
|
-
this.partOrder.push(
|
|
732
|
+
this.partOrder.push(
|
|
733
|
+
scope === ROOT_SCOPE
|
|
734
|
+
? { kind: "text", key }
|
|
735
|
+
: { kind: "text", key, subagentRunId: scope },
|
|
736
|
+
);
|
|
484
737
|
}
|
|
485
738
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
private markTextPartTouched(id: string): void {
|
|
489
|
-
const entry = this.textParts.get(id);
|
|
490
|
-
if (!entry) return;
|
|
491
|
-
entry.touched = true;
|
|
739
|
+
return key;
|
|
492
740
|
}
|
|
493
741
|
|
|
494
742
|
private appendText(id: string, delta: string): void {
|
|
495
|
-
this.ensureTextPart(id);
|
|
496
743
|
const entry = this.textParts.get(id);
|
|
497
744
|
if (!entry) return;
|
|
498
745
|
entry.buffer += delta;
|
|
499
|
-
entry.touched = true;
|
|
500
746
|
}
|
|
501
747
|
|
|
502
748
|
private startToolCall(
|
|
749
|
+
scope: string,
|
|
503
750
|
id: string | undefined,
|
|
504
751
|
name?: string,
|
|
505
752
|
parentMessageId?: string,
|
|
506
753
|
) {
|
|
507
754
|
if (!id) return;
|
|
508
755
|
// A new tool call acts as a boundary: any anonymous text that arrives
|
|
509
|
-
// after it should be a new part, not appended to the pre-tool text
|
|
510
|
-
|
|
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);
|
|
511
760
|
if (
|
|
512
761
|
!this.partOrder.some(
|
|
513
762
|
(part) => part.kind === "tool-call" && part.toolCallId === id,
|
|
514
763
|
)
|
|
515
764
|
) {
|
|
516
|
-
this.insertToolPart(id, parentMessageId);
|
|
765
|
+
this.insertToolPart(scope, id, parentMessageId);
|
|
517
766
|
}
|
|
518
767
|
const state: ToolCallState = {
|
|
519
768
|
toolCallId: id,
|
|
@@ -527,16 +776,21 @@ export class RunAggregator {
|
|
|
527
776
|
if (parentMessageId) {
|
|
528
777
|
state.parentMessageId = parentMessageId;
|
|
529
778
|
}
|
|
779
|
+
if (scope !== ROOT_SCOPE) {
|
|
780
|
+
state.subagentRunId = scope;
|
|
781
|
+
}
|
|
782
|
+
this.nestingMemo = undefined;
|
|
530
783
|
this.toolCalls.set(id, state);
|
|
531
784
|
}
|
|
532
785
|
|
|
533
786
|
// The message and tool-call channels are unordered on the wire, so anchor a
|
|
534
787
|
// tool call under its parentMessageId text part instead of appending it.
|
|
535
|
-
private insertToolPart(id: string, parentMessageId?: string) {
|
|
788
|
+
private insertToolPart(scope: string, id: string, parentMessageId?: string) {
|
|
536
789
|
const entry = { kind: "tool-call", toolCallId: id } as const;
|
|
537
790
|
if (parentMessageId) {
|
|
791
|
+
const parentKey = this.partKey(scope, parentMessageId);
|
|
538
792
|
const parentIndex = this.partOrder.findIndex(
|
|
539
|
-
(part) => part.kind === "text" && part.key ===
|
|
793
|
+
(part) => part.kind === "text" && part.key === parentKey,
|
|
540
794
|
);
|
|
541
795
|
if (parentIndex !== -1) {
|
|
542
796
|
let insertAt = parentIndex + 1;
|
|
@@ -576,6 +830,7 @@ export class RunAggregator {
|
|
|
576
830
|
}
|
|
577
831
|
|
|
578
832
|
private finishToolCall(
|
|
833
|
+
scope: string,
|
|
579
834
|
id: string,
|
|
580
835
|
content: string,
|
|
581
836
|
isError?: boolean,
|
|
@@ -593,7 +848,9 @@ export class RunAggregator {
|
|
|
593
848
|
result: undefined,
|
|
594
849
|
isError: undefined,
|
|
595
850
|
snapshotResultApplied: false,
|
|
851
|
+
...(scope !== ROOT_SCOPE ? { subagentRunId: scope } : {}),
|
|
596
852
|
};
|
|
853
|
+
this.nestingMemo = undefined;
|
|
597
854
|
this.toolCalls.set(id, entry);
|
|
598
855
|
}
|
|
599
856
|
if (
|
|
@@ -626,25 +883,173 @@ export class RunAggregator {
|
|
|
626
883
|
if (toolMessageId) {
|
|
627
884
|
entry.toolMessageId = toolMessageId;
|
|
628
885
|
}
|
|
629
|
-
this.
|
|
886
|
+
this.lastResolvedToolCallIdByScope.set(scope, id);
|
|
630
887
|
}
|
|
631
888
|
|
|
632
|
-
private
|
|
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
|
+
}
|
|
913
|
+
|
|
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
|
+
}
|
|
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[] {
|
|
633
989
|
const snapshot: ThreadAssistantMessagePart[] = [];
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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;
|
|
642
1046
|
|
|
643
|
-
for (const part of this.partOrder) {
|
|
644
1047
|
if (part.kind === "reasoning") {
|
|
645
1048
|
if (this.showThinking) {
|
|
646
1049
|
const buffer = this.reasoningParts.get(part.key) ?? "";
|
|
647
|
-
|
|
1050
|
+
const isActive =
|
|
1051
|
+
this.activeReasoningKeyByScope.get(rawScope) === part.key;
|
|
1052
|
+
if (buffer.length > 0 || isActive) {
|
|
648
1053
|
const encryptedValue = this.reasoningSignatures.get(part.key);
|
|
649
1054
|
const reasoningId = this.reasoningMessageIds.get(part.key);
|
|
650
1055
|
const meta = {
|
|
@@ -658,6 +1063,20 @@ export class RunAggregator {
|
|
|
658
1063
|
? { providerMetadata: { [AG_UI_METADATA_NAMESPACE]: meta } }
|
|
659
1064
|
: {}),
|
|
660
1065
|
} as const);
|
|
1066
|
+
materialized();
|
|
1067
|
+
} else if (isRoot && ctx.root) {
|
|
1068
|
+
// A retracted empty block still carries transport state: without
|
|
1069
|
+
// a part to live on, its signature rides the message metadata,
|
|
1070
|
+
// matching the shape a snapshot reload produces.
|
|
1071
|
+
const encryptedValue = this.reasoningSignatures.get(part.key);
|
|
1072
|
+
const id = this.reasoningSignatureIds.get(part.key);
|
|
1073
|
+
if (id?.trim() && encryptedValue?.trim()) {
|
|
1074
|
+
ctx.root.opaqueCandidates.push({
|
|
1075
|
+
id,
|
|
1076
|
+
encryptedValue,
|
|
1077
|
+
anchor: index + 1,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
661
1080
|
}
|
|
662
1081
|
}
|
|
663
1082
|
continue;
|
|
@@ -665,8 +1084,9 @@ export class RunAggregator {
|
|
|
665
1084
|
|
|
666
1085
|
if (part.kind === "text") {
|
|
667
1086
|
const entry = this.textParts.get(part.key);
|
|
668
|
-
if (entry
|
|
1087
|
+
if (entry && entry.buffer.trim().length > 0) {
|
|
669
1088
|
snapshot.push({ type: "text", text: entry.buffer } as const);
|
|
1089
|
+
materialized();
|
|
670
1090
|
}
|
|
671
1091
|
continue;
|
|
672
1092
|
}
|
|
@@ -677,52 +1097,116 @@ export class RunAggregator {
|
|
|
677
1097
|
name: part.name,
|
|
678
1098
|
data: part.value,
|
|
679
1099
|
});
|
|
1100
|
+
materialized();
|
|
680
1101
|
continue;
|
|
681
1102
|
}
|
|
1103
|
+
}
|
|
682
1104
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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 }]);
|
|
716
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;
|
|
717
1161
|
|
|
1162
|
+
// An anonymous claim promotes the signature's entityId to a wire message
|
|
1163
|
+
// id; when that id actually names a text message or the adopted assistant
|
|
1164
|
+
// message id (which can also arrive via TOOL_CALL_START.parentMessageId),
|
|
1165
|
+
// replaying it as its own record would put two wire records under one id,
|
|
1166
|
+
// so such entries are dropped instead — the same outcome the visible
|
|
1167
|
+
// path's claim guard produces for a signature that is "not for this
|
|
1168
|
+
// block". Entries with no materialized part after their anchor trail the
|
|
1169
|
+
// assistant record, matching the snapshot path's anchor/after
|
|
1170
|
+
// bookkeeping.
|
|
1171
|
+
const toolMessageIds = new Set<string>();
|
|
1172
|
+
for (const call of this.toolCalls.values()) {
|
|
1173
|
+
if (call.toolMessageId) toolMessageIds.add(call.toolMessageId);
|
|
1174
|
+
}
|
|
1175
|
+
opaqueCandidates.sort((a, b) => a.anchor - b.anchor);
|
|
1176
|
+
const publishableOpaqueReasoning = opaqueCandidates
|
|
1177
|
+
.filter((entry) => {
|
|
1178
|
+
const collides =
|
|
1179
|
+
this.textParts.has(entry.id) ||
|
|
1180
|
+
entry.id === this.reportedServerMessageId ||
|
|
1181
|
+
toolMessageIds.has(entry.id);
|
|
1182
|
+
if (collides && !this.loggedDroppedOpaqueIds.has(entry.id)) {
|
|
1183
|
+
this.loggedDroppedOpaqueIds.add(entry.id);
|
|
1184
|
+
this.logger.debug?.(
|
|
1185
|
+
"[agui] aggregator dropped opaque reasoning signature: id collides with a message id",
|
|
1186
|
+
entry.id,
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
return !collides;
|
|
1190
|
+
})
|
|
1191
|
+
.map(({ anchor, ...entry }) =>
|
|
1192
|
+
lastMaterializedIndex < anchor ? { ...entry, after: true } : entry,
|
|
1193
|
+
);
|
|
1194
|
+
if (publishableOpaqueReasoning.length > 0)
|
|
1195
|
+
this.hasEmittedOpaqueReasoning = true;
|
|
1196
|
+
// Once an opaque entry has been published this run, the key keeps being
|
|
1197
|
+
// emitted (empty when withdrawn) so the namespace merge can retract it.
|
|
1198
|
+
const includeOpaque = this.hasEmittedOpaqueReasoning;
|
|
718
1199
|
const timing = this.getTiming();
|
|
719
1200
|
const metadata = {
|
|
720
1201
|
...(timing ? { timing } : {}),
|
|
721
|
-
...(this.interrupts
|
|
1202
|
+
...(this.interrupts || includeOpaque
|
|
722
1203
|
? {
|
|
723
1204
|
custom: {
|
|
724
1205
|
[AG_UI_METADATA_NAMESPACE]: {
|
|
725
|
-
interrupts: this.interrupts,
|
|
1206
|
+
...(this.interrupts ? { interrupts: this.interrupts } : {}),
|
|
1207
|
+
...(includeOpaque
|
|
1208
|
+
? { opaqueReasoning: publishableOpaqueReasoning }
|
|
1209
|
+
: {}),
|
|
726
1210
|
} satisfies AgUiCustomMetadata,
|
|
727
1211
|
},
|
|
728
1212
|
}
|
|
@@ -777,12 +1261,33 @@ export class RunAggregator {
|
|
|
777
1261
|
};
|
|
778
1262
|
}
|
|
779
1263
|
|
|
780
|
-
private handleReasoningStart(
|
|
781
|
-
|
|
1264
|
+
private handleReasoningStart(
|
|
1265
|
+
scope: string,
|
|
1266
|
+
messageId?: string,
|
|
1267
|
+
isMessageId = false,
|
|
1268
|
+
): void {
|
|
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;
|
|
1273
|
+
const anchor = this.partOrder.length;
|
|
1274
|
+
if (messageId === undefined) {
|
|
1275
|
+
this.hiddenActiveReasoning = "anonymous";
|
|
1276
|
+
this.hiddenAnonymousAnchor = anchor;
|
|
1277
|
+
} else {
|
|
1278
|
+
this.hiddenReasoningIds.add(messageId);
|
|
1279
|
+
this.hiddenBlockAnchors.set(messageId, anchor);
|
|
1280
|
+
this.hiddenActiveReasoning = "identified";
|
|
1281
|
+
}
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
782
1284
|
// A reasoning block acts as a boundary: anonymous text arriving after it
|
|
783
|
-
// should be a new part, not appended to any pre-reasoning text.
|
|
784
|
-
this.
|
|
785
|
-
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
|
+
);
|
|
786
1291
|
// Two different questions: which id may be replayed as a ReasoningMessage.id
|
|
787
1292
|
// (only the message-scoped aliases carry one), and which block an unmatched
|
|
788
1293
|
// signature may claim (any block opened without an id at all).
|
|
@@ -793,31 +1298,48 @@ export class RunAggregator {
|
|
|
793
1298
|
}
|
|
794
1299
|
if (!this.reasoningParts.has(key)) {
|
|
795
1300
|
this.reasoningParts.set(key, "");
|
|
796
|
-
this.partOrder.push(
|
|
1301
|
+
this.partOrder.push(
|
|
1302
|
+
scope === ROOT_SCOPE
|
|
1303
|
+
? { kind: "reasoning", key }
|
|
1304
|
+
: { kind: "reasoning", key, subagentRunId: scope },
|
|
1305
|
+
);
|
|
797
1306
|
}
|
|
798
|
-
this.
|
|
1307
|
+
this.activeReasoningKeyByScope.set(scope, key);
|
|
799
1308
|
this.emit();
|
|
800
1309
|
}
|
|
801
1310
|
|
|
802
1311
|
private handleReasoningContent(
|
|
1312
|
+
scope: string,
|
|
803
1313
|
delta: string,
|
|
804
1314
|
messageId?: string,
|
|
805
1315
|
isMessageId = false,
|
|
806
1316
|
): void {
|
|
807
|
-
if (!
|
|
808
|
-
if (!this.
|
|
1317
|
+
if (!delta) return;
|
|
1318
|
+
if (!this.showThinking) {
|
|
1319
|
+
// Content without a preceding START still names the block (the visible
|
|
1320
|
+
// path opens it lazily); register it so a signature can claim it.
|
|
1321
|
+
if (scope !== ROOT_SCOPE) return;
|
|
1322
|
+
if (this.hiddenActiveReasoning === "none") {
|
|
1323
|
+
this.handleReasoningStart(scope, messageId, isMessageId);
|
|
1324
|
+
}
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (!this.activeReasoningKeyByScope.has(scope)) {
|
|
809
1328
|
// Content arrived without a preceding START — create the slot lazily.
|
|
810
|
-
this.handleReasoningStart(messageId, isMessageId);
|
|
1329
|
+
this.handleReasoningStart(scope, messageId, isMessageId);
|
|
811
1330
|
}
|
|
812
|
-
const key = this.
|
|
1331
|
+
const key = this.activeReasoningKeyByScope.get(scope);
|
|
813
1332
|
if (!key) return;
|
|
814
1333
|
this.reasoningParts.set(key, (this.reasoningParts.get(key) ?? "") + delta);
|
|
815
1334
|
this.emit();
|
|
816
1335
|
}
|
|
817
1336
|
|
|
818
|
-
private handleReasoningEnd(): void {
|
|
819
|
-
if (!this.showThinking)
|
|
820
|
-
|
|
1337
|
+
private handleReasoningEnd(scope: string): void {
|
|
1338
|
+
if (!this.showThinking) {
|
|
1339
|
+
if (scope === ROOT_SCOPE) this.hiddenActiveReasoning = "none";
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
this.activeReasoningKeyByScope.delete(scope);
|
|
821
1343
|
this.emit();
|
|
822
1344
|
}
|
|
823
1345
|
}
|