@assistant-ui/react-ag-ui 0.0.71 → 0.1.1
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 +1 -1
- package/dist/agUiExtras.js +1 -2
- package/dist/hooks.js +1 -2
- package/dist/runtime/{AgUiThreadRuntimeCore.d.ts → AgUiThreadController.d.ts} +6 -12
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -0
- package/dist/runtime/{AgUiThreadRuntimeCore.js → AgUiThreadController.js} +567 -194
- package/dist/runtime/AgUiThreadController.js.map +1 -0
- package/dist/runtime/adapter/conversions.d.ts +4 -4
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +120 -35
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/replay-content.d.ts +9 -0
- package/dist/runtime/adapter/replay-content.d.ts.map +1 -0
- package/dist/runtime/adapter/replay-content.js +185 -0
- package/dist/runtime/adapter/replay-content.js.map +1 -0
- package/dist/runtime/adapter/run-aggregator.d.ts +3 -5
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +48 -63
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +0 -24
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +2 -24
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +2 -0
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
- package/dist/runtime/adapter/tool-approval.js +23 -6
- package/dist/runtime/adapter/tool-approval.js.map +1 -1
- package/dist/runtime/agUiThreadState.d.ts +60 -0
- package/dist/runtime/agUiThreadState.d.ts.map +1 -0
- package/dist/runtime/agUiThreadState.js +94 -0
- package/dist/runtime/agUiThreadState.js.map +1 -0
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +46 -50
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/interrupt-internals.js +1 -2
- package/dist/runtime/logger.js +1 -2
- package/dist/runtime/mcp-tool-result.d.ts +3 -1
- package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
- package/dist/runtime/mcp-tool-result.js +7 -5
- package/dist/runtime/mcp-tool-result.js.map +1 -1
- package/dist/runtime/types.d.ts +11 -35
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js +1 -2
- package/dist/runtime/types.js.map +1 -1
- package/dist/useAgUiRuntime.js +3 -4
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +12 -11
- package/src/runtime/AgUiThreadController.deferred-results.test.ts +548 -0
- package/src/runtime/{AgUiThreadRuntimeCore.interrupt-cancel.test.ts → AgUiThreadController.interrupt-cancel.test.ts} +4 -4
- package/src/runtime/AgUiThreadController.replay.test.ts +3932 -0
- package/src/runtime/{AgUiThreadRuntimeCore.test.ts → AgUiThreadController.test.ts} +114 -6
- package/src/runtime/{AgUiThreadRuntimeCore.ts → AgUiThreadController.ts} +1037 -236
- package/src/runtime/adapter/conversions.content.test.ts +149 -5
- package/src/runtime/adapter/conversions.tools.test.ts +2 -1
- package/src/runtime/adapter/conversions.ts +249 -62
- package/src/runtime/adapter/replay-content.test.ts +235 -0
- package/src/runtime/adapter/replay-content.ts +306 -0
- package/src/runtime/adapter/run-aggregator.ts +79 -114
- package/src/runtime/adapter/subscriber.ts +1 -44
- package/src/runtime/adapter/tool-approval.ts +41 -3
- package/src/runtime/agUiThreadState.test.ts +224 -0
- package/src/runtime/agUiThreadState.ts +139 -0
- package/src/runtime/event-parser.ts +40 -58
- package/src/runtime/mcp-tool-result.ts +16 -2
- package/src/runtime/types.ts +16 -28
- package/src/useAgUiRuntime.fast-refresh.test.tsx +3 -3
- package/src/useAgUiRuntime.thread-switch.test.tsx +4 -4
- package/src/useAgUiRuntime.ts +3 -3
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +0 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +0 -1
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type ToolModelContentPart,
|
|
13
13
|
} from "@assistant-ui/core";
|
|
14
14
|
import type { JoinStrategy } from "@assistant-ui/core/react";
|
|
15
|
+
import type { ContentPart } from "@ag-ui/client";
|
|
15
16
|
import {
|
|
16
17
|
applyA2uiOperations,
|
|
17
18
|
surfaceToPresentToolCall,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
type A2uiSurfaceState,
|
|
20
21
|
} from "@assistant-ui/generative-ui/a2ui";
|
|
21
22
|
import jsonpatch, { type Operation } from "fast-json-patch";
|
|
22
|
-
import { readMcpAppResourceUri } from "../mcp-tool-result";
|
|
23
|
+
import { readMcpAppResourceUri, toolContentText } from "../mcp-tool-result";
|
|
23
24
|
import { projectAgUiToolApprovals } from "./tool-approval";
|
|
24
25
|
import {
|
|
25
26
|
AG_UI_METADATA_NAMESPACE,
|
|
@@ -181,7 +182,7 @@ export type RunAggregatorOptions = {
|
|
|
181
182
|
logger: Logger;
|
|
182
183
|
emit: Emit;
|
|
183
184
|
onServerMessageId?: (messageId: string) => void;
|
|
184
|
-
onTextMessageStart?: (messageId: string) => void;
|
|
185
|
+
onTextMessageStart?: (messageId: string, openToolCallIds: string[]) => void;
|
|
185
186
|
};
|
|
186
187
|
|
|
187
188
|
/**
|
|
@@ -201,11 +202,12 @@ export class RunAggregator {
|
|
|
201
202
|
private readonly logger: Logger;
|
|
202
203
|
private readonly onServerMessageId: ((messageId: string) => void) | undefined;
|
|
203
204
|
private readonly onTextMessageStart:
|
|
204
|
-
| ((messageId: string) => void)
|
|
205
|
+
| ((messageId: string, openToolCallIds: string[]) => void)
|
|
205
206
|
| undefined;
|
|
206
207
|
|
|
207
208
|
private status: ChatModelRunResult["status"] | undefined;
|
|
208
209
|
private interrupts: AgUiInterrupt[] | undefined;
|
|
210
|
+
private pendingToolCallIds: string[] | undefined;
|
|
209
211
|
private readonly textParts = new Map<string, { buffer: string }>();
|
|
210
212
|
private readonly activeTextMessageIdByScope = new Map<string, string>();
|
|
211
213
|
private readonly reasoningParts = new Map<
|
|
@@ -214,20 +216,14 @@ export class RunAggregator {
|
|
|
214
216
|
>();
|
|
215
217
|
private readonly reasoningSignatures = new Map<string, string>();
|
|
216
218
|
private readonly reasoningSignatureIds = new Map<string, string>();
|
|
217
|
-
// Signatures captured while thinking is hidden have no block to live on;
|
|
218
|
-
// they are transport state that must survive to the next run input. The
|
|
219
|
-
// claim rules mirror the visible path: an id-carrying block, or an open
|
|
220
|
-
// anonymous one.
|
|
221
219
|
private readonly hiddenSignatures = new Map<string, string>();
|
|
222
220
|
private readonly hiddenSignatureAnchors = new Map<string, number>();
|
|
223
221
|
private readonly hiddenBlockAnchors = new Map<string, number>();
|
|
224
|
-
private hiddenAnonymousAnchor: number | undefined;
|
|
225
222
|
private readonly hiddenReasoningIds = new Set<string>();
|
|
226
|
-
private
|
|
223
|
+
private hiddenActiveReasoningId: string | undefined;
|
|
227
224
|
private hasEmittedOpaqueReasoning = false;
|
|
228
225
|
private readonly loggedDroppedOpaqueIds = new Set<string>();
|
|
229
226
|
private readonly reasoningMessageIds = new Map<string, string>();
|
|
230
|
-
private readonly anonymousReasoningKeys = new Set<string>();
|
|
231
227
|
private readonly activeReasoningKeyByScope = new Map<string, string>();
|
|
232
228
|
private readonly subagentRuns = new Map<string, SubagentRunState>();
|
|
233
229
|
// The nesting graph only moves when a run is announced or a tool call is
|
|
@@ -239,7 +235,6 @@ export class RunAggregator {
|
|
|
239
235
|
reachable: Set<string>;
|
|
240
236
|
}
|
|
241
237
|
| undefined;
|
|
242
|
-
private reasoningPartCounter = 0;
|
|
243
238
|
private readonly toolCalls = new Map<string, ToolCallState>();
|
|
244
239
|
private readonly a2uiBuckets = new Map<string, A2uiState>();
|
|
245
240
|
private readonly a2uiToolCallIds = new Set<string>();
|
|
@@ -288,6 +283,13 @@ export class RunAggregator {
|
|
|
288
283
|
break;
|
|
289
284
|
}
|
|
290
285
|
case "RUN_FINISHED": {
|
|
286
|
+
if (event.outcome?.type === "cancelled") {
|
|
287
|
+
this.interrupts = undefined;
|
|
288
|
+
this.status = { type: "incomplete", reason: "cancelled" };
|
|
289
|
+
this.closeOpenSubagentRuns(this.status);
|
|
290
|
+
this.emit();
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
291
293
|
if (event.outcome?.type === "interrupt") {
|
|
292
294
|
this.interrupts = event.outcome.interrupts;
|
|
293
295
|
this.status = { type: "requires-action", reason: "interrupt" };
|
|
@@ -300,13 +302,27 @@ export class RunAggregator {
|
|
|
300
302
|
// a root call is: getPendingToolCalls and addToolResult walk
|
|
301
303
|
// ToolCallMessagePart.messages, so any unresolved call keeps the run
|
|
302
304
|
// resumable.
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
305
|
+
const pendingToolCallIds =
|
|
306
|
+
event.outcome?.type === "success"
|
|
307
|
+
? event.outcome.pendingToolCallIds
|
|
308
|
+
: undefined;
|
|
309
|
+
const hasUnresolvedToolCalls =
|
|
310
|
+
pendingToolCallIds && pendingToolCallIds.length > 0
|
|
311
|
+
? pendingToolCallIds.some((id) => {
|
|
312
|
+
const call = this.toolCalls.get(id);
|
|
313
|
+
return call !== undefined && call.result === undefined;
|
|
314
|
+
})
|
|
315
|
+
: Array.from(this.toolCalls.values()).some(
|
|
316
|
+
(tc) => tc.result === undefined,
|
|
317
|
+
);
|
|
306
318
|
|
|
307
319
|
this.status = hasUnresolvedToolCalls
|
|
308
320
|
? { type: "requires-action", reason: "tool-calls" }
|
|
309
321
|
: { type: "complete", reason: "unknown" };
|
|
322
|
+
this.pendingToolCallIds =
|
|
323
|
+
hasUnresolvedToolCalls && pendingToolCallIds?.length
|
|
324
|
+
? pendingToolCallIds
|
|
325
|
+
: undefined;
|
|
310
326
|
this.closeOpenSubagentRuns(this.status);
|
|
311
327
|
this.emit();
|
|
312
328
|
break;
|
|
@@ -348,9 +364,8 @@ export class RunAggregator {
|
|
|
348
364
|
this.emit();
|
|
349
365
|
break;
|
|
350
366
|
}
|
|
351
|
-
case "TEXT_MESSAGE_CONTENT":
|
|
352
|
-
|
|
353
|
-
const incomingId = "messageId" in event ? event.messageId : undefined;
|
|
367
|
+
case "TEXT_MESSAGE_CONTENT": {
|
|
368
|
+
const incomingId = event.messageId;
|
|
354
369
|
const scope = this.scopeOf(event);
|
|
355
370
|
if (scope === ROOT_SCOPE) {
|
|
356
371
|
this.beginDistinctTextMessage(incomingId);
|
|
@@ -390,13 +405,11 @@ export class RunAggregator {
|
|
|
390
405
|
break;
|
|
391
406
|
}
|
|
392
407
|
|
|
393
|
-
case "THINKING_START":
|
|
394
|
-
case "THINKING_TEXT_MESSAGE_START":
|
|
395
408
|
case "REASONING_START":
|
|
396
409
|
case "REASONING_MESSAGE_START":
|
|
397
410
|
this.handleReasoningStart(
|
|
398
|
-
this.scopeOf(
|
|
399
|
-
|
|
411
|
+
this.scopeOf(event),
|
|
412
|
+
event.messageId,
|
|
400
413
|
event.type === "REASONING_MESSAGE_START",
|
|
401
414
|
event.timestamp,
|
|
402
415
|
);
|
|
@@ -404,61 +417,42 @@ export class RunAggregator {
|
|
|
404
417
|
case "REASONING_ENCRYPTED_VALUE":
|
|
405
418
|
if (event.subtype === "message") {
|
|
406
419
|
const scope = this.scopeOf(event);
|
|
407
|
-
// entityId names any message, not necessarily a reasoning one, so an
|
|
408
|
-
// unmatched id may only claim a block that has no id to contradict it.
|
|
409
|
-
const active = this.activeReasoningKeyByScope.get(scope);
|
|
410
420
|
const entityKey = this.partKey(scope, event.entityId);
|
|
411
|
-
const key =
|
|
412
|
-
|
|
421
|
+
const key =
|
|
422
|
+
this.showThinking && this.reasoningParts.has(entityKey)
|
|
413
423
|
? entityKey
|
|
414
|
-
:
|
|
415
|
-
? active
|
|
416
|
-
: undefined
|
|
417
|
-
: undefined;
|
|
424
|
+
: undefined;
|
|
418
425
|
if (key !== undefined) {
|
|
419
426
|
this.reasoningSignatures.set(key, event.encryptedValue);
|
|
420
427
|
this.reasoningSignatureIds.set(key, event.entityId);
|
|
421
428
|
this.emit();
|
|
422
429
|
} else if (
|
|
423
430
|
!this.showThinking &&
|
|
424
|
-
// Hidden-reasoning bookkeeping stays root-only for this task — a
|
|
425
|
-
// subagent's opaque/hidden reasoning signatures are not preserved.
|
|
426
431
|
scope === ROOT_SCOPE &&
|
|
427
432
|
event.entityId.trim().length > 0 &&
|
|
428
433
|
event.encryptedValue.trim().length > 0 &&
|
|
429
|
-
|
|
430
|
-
this.hiddenActiveReasoning === "anonymous")
|
|
434
|
+
this.hiddenReasoningIds.has(event.entityId)
|
|
431
435
|
) {
|
|
432
436
|
this.hiddenSignatures.set(event.entityId, event.encryptedValue);
|
|
433
437
|
this.hiddenSignatureAnchors.set(
|
|
434
438
|
event.entityId,
|
|
435
439
|
this.hiddenBlockAnchors.get(event.entityId) ??
|
|
436
|
-
this.hiddenAnonymousAnchor ??
|
|
437
440
|
this.partOrder.length,
|
|
438
441
|
);
|
|
439
442
|
this.emit();
|
|
440
443
|
}
|
|
441
444
|
}
|
|
442
445
|
break;
|
|
443
|
-
case "THINKING_TEXT_MESSAGE_CONTENT":
|
|
444
|
-
this.handleReasoningContent(ROOT_SCOPE, event.delta);
|
|
445
|
-
this.totalChunks++;
|
|
446
|
-
this.recordFirstToken();
|
|
447
|
-
break;
|
|
448
446
|
case "REASONING_MESSAGE_CONTENT":
|
|
449
447
|
this.handleReasoningContent(
|
|
450
448
|
this.scopeOf(event),
|
|
451
449
|
event.delta,
|
|
452
|
-
|
|
450
|
+
event.messageId,
|
|
453
451
|
true,
|
|
454
452
|
);
|
|
455
453
|
this.totalChunks++;
|
|
456
454
|
this.recordFirstToken();
|
|
457
455
|
break;
|
|
458
|
-
case "THINKING_TEXT_MESSAGE_END":
|
|
459
|
-
case "THINKING_END":
|
|
460
|
-
this.handleReasoningEnd(ROOT_SCOPE, event.timestamp);
|
|
461
|
-
break;
|
|
462
456
|
case "REASONING_MESSAGE_END":
|
|
463
457
|
case "REASONING_END":
|
|
464
458
|
this.handleReasoningEnd(this.scopeOf(event), event.timestamp);
|
|
@@ -478,14 +472,7 @@ export class RunAggregator {
|
|
|
478
472
|
this.emit();
|
|
479
473
|
break;
|
|
480
474
|
}
|
|
481
|
-
case "TOOL_CALL_ARGS":
|
|
482
|
-
case "TOOL_CALL_CHUNK": {
|
|
483
|
-
if (
|
|
484
|
-
event.type === "TOOL_CALL_CHUNK" &&
|
|
485
|
-
this.scopeOf(event) === ROOT_SCOPE
|
|
486
|
-
) {
|
|
487
|
-
this.reportServerMessageId(event.parentMessageId);
|
|
488
|
-
}
|
|
475
|
+
case "TOOL_CALL_ARGS": {
|
|
489
476
|
if (!event.delta) break;
|
|
490
477
|
this.appendToolArgs(event.toolCallId, event.delta);
|
|
491
478
|
this.emit();
|
|
@@ -813,15 +800,12 @@ export class RunAggregator {
|
|
|
813
800
|
this.hiddenSignatures.clear();
|
|
814
801
|
this.hiddenSignatureAnchors.clear();
|
|
815
802
|
this.hiddenBlockAnchors.clear();
|
|
816
|
-
this.hiddenAnonymousAnchor = undefined;
|
|
817
803
|
this.hiddenReasoningIds.clear();
|
|
818
|
-
this.
|
|
804
|
+
this.hiddenActiveReasoningId = undefined;
|
|
819
805
|
this.loggedDroppedOpaqueIds.clear();
|
|
820
|
-
this.anonymousReasoningKeys.clear();
|
|
821
806
|
this.activeReasoningKeyByScope.clear();
|
|
822
807
|
this.nestingMemo = undefined;
|
|
823
808
|
this.subagentRuns.clear();
|
|
824
|
-
this.reasoningPartCounter = 0;
|
|
825
809
|
this.toolCalls.clear();
|
|
826
810
|
this.a2uiBuckets.clear();
|
|
827
811
|
this.a2uiToolCallIds.clear();
|
|
@@ -867,10 +851,11 @@ export class RunAggregator {
|
|
|
867
851
|
if (carried.parts.length > 0 || carried.hiddenSignatures.length > 0) {
|
|
868
852
|
this.emit();
|
|
869
853
|
}
|
|
870
|
-
const
|
|
854
|
+
const openToolCallIds = Array.from(this.toolCalls.values())
|
|
855
|
+
.filter((call) => call.result === undefined)
|
|
856
|
+
.map((call) => call.toolCallId);
|
|
871
857
|
this.resetMessageParts();
|
|
872
|
-
this.onTextMessageStart(messageId);
|
|
873
|
-
this.reasoningPartCounter = reasoningPartCounter;
|
|
858
|
+
this.onTextMessageStart(messageId, openToolCallIds);
|
|
874
859
|
this.restoreReasoning(carried);
|
|
875
860
|
}
|
|
876
861
|
|
|
@@ -881,7 +866,6 @@ export class RunAggregator {
|
|
|
881
866
|
signature: string | undefined;
|
|
882
867
|
signatureId: string | undefined;
|
|
883
868
|
reasoningId: string | undefined;
|
|
884
|
-
anonymous: boolean;
|
|
885
869
|
}[] = [];
|
|
886
870
|
let last = this.partOrder.at(-1);
|
|
887
871
|
while (
|
|
@@ -896,7 +880,6 @@ export class RunAggregator {
|
|
|
896
880
|
signature: this.reasoningSignatures.get(last.key),
|
|
897
881
|
signatureId: this.reasoningSignatureIds.get(last.key),
|
|
898
882
|
reasoningId: this.reasoningMessageIds.get(last.key),
|
|
899
|
-
anonymous: this.anonymousReasoningKeys.has(last.key),
|
|
900
883
|
});
|
|
901
884
|
last = this.partOrder.at(-1);
|
|
902
885
|
}
|
|
@@ -911,22 +894,17 @@ export class RunAggregator {
|
|
|
911
894
|
const hiddenBlockIds = Array.from(this.hiddenBlockAnchors)
|
|
912
895
|
.filter(([, anchor]) => anchor >= cut)
|
|
913
896
|
.map(([id]) => id);
|
|
914
|
-
const
|
|
915
|
-
this.hiddenAnonymousAnchor !== undefined &&
|
|
916
|
-
this.hiddenAnonymousAnchor >= cut;
|
|
917
|
-
const activeCarried =
|
|
918
|
-
(this.hiddenActiveReasoning === "identified" &&
|
|
919
|
-
hiddenBlockIds.length > 0) ||
|
|
920
|
-
(this.hiddenActiveReasoning === "anonymous" && hiddenAnonymous);
|
|
897
|
+
const hiddenActiveReasoningId = this.hiddenActiveReasoningId;
|
|
921
898
|
return {
|
|
922
899
|
parts,
|
|
923
900
|
activeKey: this.activeReasoningKeyByScope.get(ROOT_SCOPE),
|
|
924
901
|
hiddenSignatures,
|
|
925
902
|
hiddenBlockIds,
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
903
|
+
hiddenActiveReasoningId:
|
|
904
|
+
hiddenActiveReasoningId !== undefined &&
|
|
905
|
+
hiddenBlockIds.includes(hiddenActiveReasoningId)
|
|
906
|
+
? hiddenActiveReasoningId
|
|
907
|
+
: undefined,
|
|
930
908
|
};
|
|
931
909
|
}
|
|
932
910
|
|
|
@@ -942,7 +920,6 @@ export class RunAggregator {
|
|
|
942
920
|
this.reasoningSignatureIds.set(part.key, part.signatureId);
|
|
943
921
|
if (part.reasoningId !== undefined)
|
|
944
922
|
this.reasoningMessageIds.set(part.key, part.reasoningId);
|
|
945
|
-
if (part.anonymous) this.anonymousReasoningKeys.add(part.key);
|
|
946
923
|
}
|
|
947
924
|
if (
|
|
948
925
|
carried.activeKey !== undefined &&
|
|
@@ -958,8 +935,7 @@ export class RunAggregator {
|
|
|
958
935
|
this.hiddenReasoningIds.add(id);
|
|
959
936
|
this.hiddenBlockAnchors.set(id, 0);
|
|
960
937
|
}
|
|
961
|
-
|
|
962
|
-
this.hiddenActiveReasoning = carried.hiddenActiveReasoning;
|
|
938
|
+
this.hiddenActiveReasoningId = carried.hiddenActiveReasoningId;
|
|
963
939
|
}
|
|
964
940
|
|
|
965
941
|
private generateTextKey(): string {
|
|
@@ -1118,12 +1094,13 @@ export class RunAggregator {
|
|
|
1118
1094
|
private finishToolCall(
|
|
1119
1095
|
scope: string,
|
|
1120
1096
|
id: string,
|
|
1121
|
-
content: string,
|
|
1097
|
+
content: string | ContentPart[],
|
|
1122
1098
|
isError?: boolean,
|
|
1123
1099
|
toolMessageId?: string,
|
|
1124
1100
|
mcpResult?: Record<string, unknown>,
|
|
1125
1101
|
) {
|
|
1126
1102
|
if (!id) return;
|
|
1103
|
+
const modelText = toolContentText(content);
|
|
1127
1104
|
let entry = this.toolCalls.get(id);
|
|
1128
1105
|
if (!entry) {
|
|
1129
1106
|
entry = {
|
|
@@ -1148,7 +1125,7 @@ export class RunAggregator {
|
|
|
1148
1125
|
}
|
|
1149
1126
|
if (mcpResult !== undefined && !entry.snapshotResultApplied) {
|
|
1150
1127
|
entry.result = mcpResult;
|
|
1151
|
-
entry.modelContent = [{ type: "text", text:
|
|
1128
|
+
entry.modelContent = [{ type: "text", text: modelText }];
|
|
1152
1129
|
entry.snapshotResultApplied = true;
|
|
1153
1130
|
entry.isError = isError;
|
|
1154
1131
|
if (entry.mcpAppResourceUri === undefined) {
|
|
@@ -1157,13 +1134,16 @@ export class RunAggregator {
|
|
|
1157
1134
|
}
|
|
1158
1135
|
} else if (entry.snapshotResultApplied) {
|
|
1159
1136
|
if (entry.modelContent === undefined) {
|
|
1160
|
-
entry.modelContent = [{ type: "text", text:
|
|
1137
|
+
entry.modelContent = [{ type: "text", text: modelText }];
|
|
1161
1138
|
}
|
|
1162
1139
|
if (entry.isError === undefined) {
|
|
1163
1140
|
entry.isError = isError;
|
|
1164
1141
|
}
|
|
1165
1142
|
} else {
|
|
1166
|
-
entry.result = tryParseJSON(content);
|
|
1143
|
+
entry.result = Array.isArray(content) ? content : tryParseJSON(content);
|
|
1144
|
+
if (Array.isArray(content)) {
|
|
1145
|
+
delete entry.modelContent;
|
|
1146
|
+
}
|
|
1167
1147
|
entry.isError = isError;
|
|
1168
1148
|
}
|
|
1169
1149
|
if (toolMessageId) {
|
|
@@ -1448,15 +1428,6 @@ export class RunAggregator {
|
|
|
1448
1428
|
const snapshot = this.buildParts(ROOT_SCOPE, 0, ctx);
|
|
1449
1429
|
const { opaqueCandidates, lastMaterializedIndex } = root;
|
|
1450
1430
|
|
|
1451
|
-
// An anonymous claim promotes the signature's entityId to a wire message
|
|
1452
|
-
// id; when that id actually names a text message or the adopted assistant
|
|
1453
|
-
// message id (which can also arrive via TOOL_CALL_START.parentMessageId),
|
|
1454
|
-
// replaying it as its own record would put two wire records under one id,
|
|
1455
|
-
// so such entries are dropped instead — the same outcome the visible
|
|
1456
|
-
// path's claim guard produces for a signature that is "not for this
|
|
1457
|
-
// block". Entries with no materialized part after their anchor trail the
|
|
1458
|
-
// assistant record, matching the snapshot path's anchor/after
|
|
1459
|
-
// bookkeeping.
|
|
1460
1431
|
const toolMessageIds = new Set<string>();
|
|
1461
1432
|
for (const call of this.toolCalls.values()) {
|
|
1462
1433
|
if (call.toolMessageId) toolMessageIds.add(call.toolMessageId);
|
|
@@ -1488,11 +1459,20 @@ export class RunAggregator {
|
|
|
1488
1459
|
const timing = this.getTiming();
|
|
1489
1460
|
const metadata = {
|
|
1490
1461
|
...(timing ? { timing } : {}),
|
|
1491
|
-
...(this.interrupts ||
|
|
1462
|
+
...(this.interrupts ||
|
|
1463
|
+
(this.status?.type === "requires-action" &&
|
|
1464
|
+
this.status.reason === "tool-calls" &&
|
|
1465
|
+
this.pendingToolCallIds) ||
|
|
1466
|
+
includeOpaque
|
|
1492
1467
|
? {
|
|
1493
1468
|
custom: {
|
|
1494
1469
|
[AG_UI_METADATA_NAMESPACE]: {
|
|
1495
1470
|
...(this.interrupts ? { interrupts: this.interrupts } : {}),
|
|
1471
|
+
...(this.status?.type === "requires-action" &&
|
|
1472
|
+
this.status.reason === "tool-calls" &&
|
|
1473
|
+
this.pendingToolCallIds
|
|
1474
|
+
? { pendingToolCallIds: this.pendingToolCallIds }
|
|
1475
|
+
: {}),
|
|
1496
1476
|
...(includeOpaque
|
|
1497
1477
|
? { opaqueReasoning: publishableOpaqueReasoning }
|
|
1498
1478
|
: {}),
|
|
@@ -1552,38 +1532,23 @@ export class RunAggregator {
|
|
|
1552
1532
|
|
|
1553
1533
|
private handleReasoningStart(
|
|
1554
1534
|
scope: string,
|
|
1555
|
-
messageId
|
|
1535
|
+
messageId: string,
|
|
1556
1536
|
isMessageId = false,
|
|
1557
1537
|
timestamp?: number,
|
|
1558
1538
|
): void {
|
|
1559
1539
|
if (!this.showThinking) {
|
|
1560
|
-
// Hidden-reasoning bookkeeping stays root-only for this task — a
|
|
1561
|
-
// subagent's opaque/hidden reasoning signatures are not preserved.
|
|
1562
1540
|
if (scope !== ROOT_SCOPE) return;
|
|
1563
1541
|
const anchor = this.partOrder.length;
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
} else {
|
|
1568
|
-
this.hiddenReasoningIds.add(messageId);
|
|
1569
|
-
this.hiddenBlockAnchors.set(messageId, anchor);
|
|
1570
|
-
this.hiddenActiveReasoning = "identified";
|
|
1571
|
-
}
|
|
1542
|
+
this.hiddenReasoningIds.add(messageId);
|
|
1543
|
+
this.hiddenBlockAnchors.set(messageId, anchor);
|
|
1544
|
+
this.hiddenActiveReasoningId = messageId;
|
|
1572
1545
|
return;
|
|
1573
1546
|
}
|
|
1574
1547
|
// A reasoning block acts as a boundary: anonymous text arriving after it
|
|
1575
1548
|
// should be a new part, not appended to any pre-reasoning text — scoped.
|
|
1576
1549
|
this.activeTextMessageIdByScope.delete(scope);
|
|
1577
|
-
const key = this.partKey(
|
|
1578
|
-
|
|
1579
|
-
messageId ?? `__auto-reasoning-${++this.reasoningPartCounter}`,
|
|
1580
|
-
);
|
|
1581
|
-
// Two different questions: which id may be replayed as a ReasoningMessage.id
|
|
1582
|
-
// (only the message-scoped aliases carry one), and which block an unmatched
|
|
1583
|
-
// signature may claim (any block opened without an id at all).
|
|
1584
|
-
if (messageId === undefined) {
|
|
1585
|
-
this.anonymousReasoningKeys.add(key);
|
|
1586
|
-
} else if (isMessageId) {
|
|
1550
|
+
const key = this.partKey(scope, messageId);
|
|
1551
|
+
if (isMessageId) {
|
|
1587
1552
|
this.reasoningMessageIds.set(key, messageId);
|
|
1588
1553
|
}
|
|
1589
1554
|
if (!this.reasoningParts.has(key)) {
|
|
@@ -1611,7 +1576,7 @@ export class RunAggregator {
|
|
|
1611
1576
|
private handleReasoningContent(
|
|
1612
1577
|
scope: string,
|
|
1613
1578
|
delta: string,
|
|
1614
|
-
messageId
|
|
1579
|
+
messageId: string,
|
|
1615
1580
|
isMessageId = false,
|
|
1616
1581
|
): void {
|
|
1617
1582
|
if (!delta) return;
|
|
@@ -1619,7 +1584,7 @@ export class RunAggregator {
|
|
|
1619
1584
|
// Content without a preceding START still names the block (the visible
|
|
1620
1585
|
// path opens it lazily); register it so a signature can claim it.
|
|
1621
1586
|
if (scope !== ROOT_SCOPE) return;
|
|
1622
|
-
if (this.
|
|
1587
|
+
if (this.hiddenActiveReasoningId === undefined) {
|
|
1623
1588
|
this.handleReasoningStart(scope, messageId, isMessageId);
|
|
1624
1589
|
}
|
|
1625
1590
|
return;
|
|
@@ -1637,7 +1602,7 @@ export class RunAggregator {
|
|
|
1637
1602
|
|
|
1638
1603
|
private handleReasoningEnd(scope: string, timestamp?: number): void {
|
|
1639
1604
|
if (!this.showThinking) {
|
|
1640
|
-
if (scope === ROOT_SCOPE) this.
|
|
1605
|
+
if (scope === ROOT_SCOPE) this.hiddenActiveReasoningId = undefined;
|
|
1641
1606
|
return;
|
|
1642
1607
|
}
|
|
1643
1608
|
const key = this.activeReasoningKeyByScope.get(scope);
|
|
@@ -7,16 +7,9 @@ import type { Logger } from "../logger";
|
|
|
7
7
|
type Dispatch = (event: AgUiEvent) => void;
|
|
8
8
|
|
|
9
9
|
type Subscriber = {
|
|
10
|
-
onEvent?: (payload: { event: unknown }) => void;
|
|
11
10
|
onTextMessageStartEvent?: (payload: { event: unknown }) => void;
|
|
12
11
|
onTextMessageContentEvent?: (payload: { event: unknown }) => void;
|
|
13
12
|
onTextMessageEndEvent?: (payload: { event: unknown }) => void;
|
|
14
|
-
onTextMessageChunkEvent?: (payload: { event: unknown }) => void;
|
|
15
|
-
onThinkingStartEvent?: (payload: { event: unknown }) => void;
|
|
16
|
-
onThinkingEndEvent?: (payload: { event: unknown }) => void;
|
|
17
|
-
onThinkingTextMessageStartEvent?: (payload: { event: unknown }) => void;
|
|
18
|
-
onThinkingTextMessageContentEvent?: (payload: { event: unknown }) => void;
|
|
19
|
-
onThinkingTextMessageEndEvent?: (payload: { event: unknown }) => void;
|
|
20
13
|
onReasoningStartEvent?: (payload: { event: unknown }) => void;
|
|
21
14
|
onReasoningEndEvent?: (payload: { event: unknown }) => void;
|
|
22
15
|
onReasoningMessageStartEvent?: (payload: { event: unknown }) => void;
|
|
@@ -26,7 +19,6 @@ type Subscriber = {
|
|
|
26
19
|
onToolCallStartEvent?: (payload: { event: unknown }) => void;
|
|
27
20
|
onToolCallArgsEvent?: (payload: { event: unknown }) => void;
|
|
28
21
|
onToolCallEndEvent?: (payload: { event: unknown }) => void;
|
|
29
|
-
onToolCallChunkEvent?: (payload: { event: unknown }) => void;
|
|
30
22
|
onToolCallResultEvent?: (payload: { event: unknown }) => void;
|
|
31
23
|
onActivitySnapshotEvent?: (payload: { event: unknown }) => void;
|
|
32
24
|
onActivityDeltaEvent?: (payload: { event: unknown }) => void;
|
|
@@ -62,17 +54,7 @@ const ensureEvent = (
|
|
|
62
54
|
logger?: Logger,
|
|
63
55
|
): AgUiEvent | null => {
|
|
64
56
|
const parseOptions = logger ? { logger } : undefined;
|
|
65
|
-
|
|
66
|
-
if (raw && typeof raw === "object") {
|
|
67
|
-
const payload = raw as Record<string, unknown>;
|
|
68
|
-
if (typeof payload.type === "string") {
|
|
69
|
-
parsed = parseAgUiEvent(payload, parseOptions);
|
|
70
|
-
} else {
|
|
71
|
-
parsed = parseAgUiEvent({ type, ...payload }, parseOptions);
|
|
72
|
-
}
|
|
73
|
-
} else {
|
|
74
|
-
parsed = parseAgUiEvent({ type }, parseOptions);
|
|
75
|
-
}
|
|
57
|
+
const parsed = parseAgUiEvent(raw, parseOptions);
|
|
76
58
|
return parsed && parsed.type === type ? parsed : null;
|
|
77
59
|
};
|
|
78
60
|
|
|
@@ -94,35 +76,12 @@ export const createAgUiSubscriber = (
|
|
|
94
76
|
dispatch(event);
|
|
95
77
|
};
|
|
96
78
|
return {
|
|
97
|
-
onEvent: ({ event }) => {
|
|
98
|
-
const typeCandidate =
|
|
99
|
-
event && typeof event === "object"
|
|
100
|
-
? (event as Record<string, unknown>).type
|
|
101
|
-
: undefined;
|
|
102
|
-
if (typeof typeCandidate === "string") {
|
|
103
|
-
// Typed handlers will receive this via the discriminated callbacks; avoid duplicates.
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const parsed = parseAgUiEvent(event, logger ? { logger } : undefined);
|
|
107
|
-
if (parsed) dispatch(parsed);
|
|
108
|
-
},
|
|
109
79
|
onTextMessageStartEvent: ({ event }) =>
|
|
110
80
|
dispatchIfValid(event, "TEXT_MESSAGE_START"),
|
|
111
81
|
onTextMessageContentEvent: ({ event }) =>
|
|
112
82
|
dispatchIfValid(event, "TEXT_MESSAGE_CONTENT"),
|
|
113
83
|
onTextMessageEndEvent: ({ event }) =>
|
|
114
84
|
dispatchIfValid(event, "TEXT_MESSAGE_END"),
|
|
115
|
-
onTextMessageChunkEvent: ({ event }) =>
|
|
116
|
-
dispatchIfValid(event, "TEXT_MESSAGE_CHUNK"),
|
|
117
|
-
onThinkingStartEvent: ({ event }) =>
|
|
118
|
-
dispatchIfValid(event, "THINKING_START"),
|
|
119
|
-
onThinkingEndEvent: ({ event }) => dispatchIfValid(event, "THINKING_END"),
|
|
120
|
-
onThinkingTextMessageStartEvent: ({ event }) =>
|
|
121
|
-
dispatchIfValid(event, "THINKING_TEXT_MESSAGE_START"),
|
|
122
|
-
onThinkingTextMessageContentEvent: ({ event }) =>
|
|
123
|
-
dispatchIfValid(event, "THINKING_TEXT_MESSAGE_CONTENT"),
|
|
124
|
-
onThinkingTextMessageEndEvent: ({ event }) =>
|
|
125
|
-
dispatchIfValid(event, "THINKING_TEXT_MESSAGE_END"),
|
|
126
85
|
onReasoningStartEvent: ({ event }) =>
|
|
127
86
|
dispatchIfValid(event, "REASONING_START"),
|
|
128
87
|
onReasoningEndEvent: ({ event }) => dispatchIfValid(event, "REASONING_END"),
|
|
@@ -139,8 +98,6 @@ export const createAgUiSubscriber = (
|
|
|
139
98
|
onToolCallArgsEvent: ({ event }) =>
|
|
140
99
|
dispatchIfValid(event, "TOOL_CALL_ARGS"),
|
|
141
100
|
onToolCallEndEvent: ({ event }) => dispatchIfValid(event, "TOOL_CALL_END"),
|
|
142
|
-
onToolCallChunkEvent: ({ event }) =>
|
|
143
|
-
dispatchIfValid(event, "TOOL_CALL_CHUNK"),
|
|
144
101
|
onToolCallResultEvent: ({ event }) =>
|
|
145
102
|
dispatchIfValid(event, "TOOL_CALL_RESULT"),
|
|
146
103
|
onActivitySnapshotEvent: ({ event }) =>
|
|
@@ -228,6 +228,40 @@ export const projectAgUiToolApprovals = (
|
|
|
228
228
|
);
|
|
229
229
|
};
|
|
230
230
|
|
|
231
|
+
/** A recorded decision authorizes one exact call and gate, not a reused id. */
|
|
232
|
+
export const preserveToolApprovalDecision = (
|
|
233
|
+
previous: ToolCallMessagePart | undefined,
|
|
234
|
+
next: ToolCallMessagePart,
|
|
235
|
+
): ToolApproval | undefined => {
|
|
236
|
+
const prior = previous?.approval;
|
|
237
|
+
let approval = next.approval;
|
|
238
|
+
if (!approval && prior) {
|
|
239
|
+
const {
|
|
240
|
+
approved: _approved,
|
|
241
|
+
reason: _reason,
|
|
242
|
+
resolution: _resolution,
|
|
243
|
+
...pending
|
|
244
|
+
} = prior;
|
|
245
|
+
approval = pending;
|
|
246
|
+
}
|
|
247
|
+
if (!approval) return undefined;
|
|
248
|
+
// AG-UI may omit argument events for a no-argument call; both forms mean {}.
|
|
249
|
+
if (
|
|
250
|
+
!previous ||
|
|
251
|
+
prior?.id !== approval.id ||
|
|
252
|
+
previous.toolCallId !== next.toolCallId ||
|
|
253
|
+
previous.toolName !== next.toolName ||
|
|
254
|
+
(previous.argsText || "{}") !== (next.argsText || "{}")
|
|
255
|
+
)
|
|
256
|
+
return approval;
|
|
257
|
+
return {
|
|
258
|
+
...approval,
|
|
259
|
+
...(prior.approved !== undefined ? { approved: prior.approved } : {}),
|
|
260
|
+
...(prior.reason !== undefined ? { reason: prior.reason } : {}),
|
|
261
|
+
...(prior.resolution !== undefined ? { resolution: prior.resolution } : {}),
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
|
|
231
265
|
/**
|
|
232
266
|
* Records a decision on the gated tool call. The decision is held on the part
|
|
233
267
|
* until every gate in the batch is answered, because AG-UI resumes a run with
|
|
@@ -266,16 +300,20 @@ export const buildToolApprovalResume = (
|
|
|
266
300
|
content: readonly ThreadAssistantMessagePart[],
|
|
267
301
|
interrupts: readonly AgUiInterrupt[],
|
|
268
302
|
): AgUiResumeEntry[] | null => {
|
|
269
|
-
const
|
|
303
|
+
const calls = Array.from(iterateToolCallParts(content));
|
|
304
|
+
const gates = projectAgUiToolApprovals(
|
|
305
|
+
interrupts,
|
|
306
|
+
new Set(calls.map((part) => part.toolCallId)),
|
|
307
|
+
);
|
|
270
308
|
// Decisions accumulate in a `Map` rather than in an object literal, because an
|
|
271
309
|
// interrupt id of `__proto__` would otherwise write to the prototype chain
|
|
272
310
|
// instead of recording a response, and `constructor` or `toString` would
|
|
273
311
|
// report themselves as answered while undecided.
|
|
274
312
|
const responses = new Map<string, { status: "resolved"; payload: unknown }>();
|
|
275
|
-
for (const part of
|
|
313
|
+
for (const part of calls) {
|
|
276
314
|
const approval = part.approval;
|
|
277
315
|
if (!approval || approval.approved === undefined) continue;
|
|
278
|
-
if (
|
|
316
|
+
if (gates.get(part.toolCallId)?.id !== approval.id) continue;
|
|
279
317
|
responses.set(approval.id, {
|
|
280
318
|
status: "resolved",
|
|
281
319
|
payload: {
|