@assistant-ui/react-ag-ui 0.0.70 → 0.1.0

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.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/dist/runtime/{AgUiThreadRuntimeCore.d.ts → AgUiThreadController.d.ts} +8 -12
  3. package/dist/runtime/AgUiThreadController.d.ts.map +1 -0
  4. package/dist/runtime/{AgUiThreadRuntimeCore.js → AgUiThreadController.js} +302 -162
  5. package/dist/runtime/AgUiThreadController.js.map +1 -0
  6. package/dist/runtime/adapter/conversions.d.ts +4 -4
  7. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  8. package/dist/runtime/adapter/conversions.js +111 -33
  9. package/dist/runtime/adapter/conversions.js.map +1 -1
  10. package/dist/runtime/adapter/run-aggregator.d.ts +6 -5
  11. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  12. package/dist/runtime/adapter/run-aggregator.js +50 -62
  13. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  14. package/dist/runtime/adapter/subscriber.d.ts +0 -24
  15. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  16. package/dist/runtime/adapter/subscriber.js +1 -22
  17. package/dist/runtime/adapter/subscriber.js.map +1 -1
  18. package/dist/runtime/agUiThreadState.d.ts +60 -0
  19. package/dist/runtime/agUiThreadState.d.ts.map +1 -0
  20. package/dist/runtime/agUiThreadState.js +95 -0
  21. package/dist/runtime/agUiThreadState.js.map +1 -0
  22. package/dist/runtime/event-parser.d.ts.map +1 -1
  23. package/dist/runtime/event-parser.js +45 -48
  24. package/dist/runtime/event-parser.js.map +1 -1
  25. package/dist/runtime/mcp-tool-result.d.ts +3 -1
  26. package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
  27. package/dist/runtime/mcp-tool-result.js +6 -3
  28. package/dist/runtime/mcp-tool-result.js.map +1 -1
  29. package/dist/runtime/types.d.ts +13 -35
  30. package/dist/runtime/types.d.ts.map +1 -1
  31. package/dist/runtime/types.js.map +1 -1
  32. package/dist/useAgUiRuntime.d.ts.map +1 -1
  33. package/dist/useAgUiRuntime.js +4 -2
  34. package/dist/useAgUiRuntime.js.map +1 -1
  35. package/package.json +13 -12
  36. package/src/runtime/{AgUiThreadRuntimeCore.interrupt-cancel.test.ts → AgUiThreadController.interrupt-cancel.test.ts} +4 -4
  37. package/src/runtime/{AgUiThreadRuntimeCore.test.ts → AgUiThreadController.test.ts} +35 -6
  38. package/src/runtime/{AgUiThreadRuntimeCore.ts → AgUiThreadController.ts} +471 -169
  39. package/src/runtime/adapter/conversions.content.test.ts +37 -5
  40. package/src/runtime/adapter/conversions.tools.test.ts +2 -1
  41. package/src/runtime/adapter/conversions.ts +235 -58
  42. package/src/runtime/adapter/run-aggregator.ts +84 -114
  43. package/src/runtime/adapter/subscriber.ts +1 -44
  44. package/src/runtime/agUiThreadState.test.ts +205 -0
  45. package/src/runtime/agUiThreadState.ts +130 -0
  46. package/src/runtime/event-parser.ts +40 -58
  47. package/src/runtime/mcp-tool-result.ts +16 -2
  48. package/src/runtime/types.ts +18 -28
  49. package/src/useAgUiRuntime.external-store.test.tsx +39 -0
  50. package/src/useAgUiRuntime.fast-refresh.test.tsx +3 -3
  51. package/src/useAgUiRuntime.thread-switch.test.tsx +4 -4
  52. package/src/useAgUiRuntime.ts +5 -3
  53. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +0 -1
  54. package/dist/runtime/AgUiThreadRuntimeCore.js.map +0 -1
@@ -11,6 +11,8 @@ import {
11
11
  type ToolCallMessagePart,
12
12
  type ToolModelContentPart,
13
13
  } from "@assistant-ui/core";
14
+ import type { JoinStrategy } from "@assistant-ui/core/react";
15
+ import type { ContentPart } from "@ag-ui/client";
14
16
  import {
15
17
  applyA2uiOperations,
16
18
  surfaceToPresentToolCall,
@@ -18,7 +20,7 @@ import {
18
20
  type A2uiSurfaceState,
19
21
  } from "@assistant-ui/generative-ui/a2ui";
20
22
  import jsonpatch, { type Operation } from "fast-json-patch";
21
- import { readMcpAppResourceUri } from "../mcp-tool-result";
23
+ import { readMcpAppResourceUri, toolContentText } from "../mcp-tool-result";
22
24
  import { projectAgUiToolApprovals } from "./tool-approval";
23
25
  import {
24
26
  AG_UI_METADATA_NAMESPACE,
@@ -176,10 +178,11 @@ export const isPlainObject = (
176
178
 
177
179
  export type RunAggregatorOptions = {
178
180
  showThinking: boolean;
181
+ joinStrategy?: JoinStrategy | undefined;
179
182
  logger: Logger;
180
183
  emit: Emit;
181
184
  onServerMessageId?: (messageId: string) => void;
182
- onTextMessageStart?: (messageId: string) => void;
185
+ onTextMessageStart?: (messageId: string, openToolCallIds: string[]) => void;
183
186
  };
184
187
 
185
188
  /**
@@ -195,14 +198,16 @@ export type RunAggregatorOptions = {
195
198
  export class RunAggregator {
196
199
  private readonly emitUpdate: Emit;
197
200
  private readonly showThinking: boolean;
201
+ private readonly joinStrategy: JoinStrategy | undefined;
198
202
  private readonly logger: Logger;
199
203
  private readonly onServerMessageId: ((messageId: string) => void) | undefined;
200
204
  private readonly onTextMessageStart:
201
- | ((messageId: string) => void)
205
+ | ((messageId: string, openToolCallIds: string[]) => void)
202
206
  | undefined;
203
207
 
204
208
  private status: ChatModelRunResult["status"] | undefined;
205
209
  private interrupts: AgUiInterrupt[] | undefined;
210
+ private pendingToolCallIds: string[] | undefined;
206
211
  private readonly textParts = new Map<string, { buffer: string }>();
207
212
  private readonly activeTextMessageIdByScope = new Map<string, string>();
208
213
  private readonly reasoningParts = new Map<
@@ -211,20 +216,14 @@ export class RunAggregator {
211
216
  >();
212
217
  private readonly reasoningSignatures = new Map<string, string>();
213
218
  private readonly reasoningSignatureIds = new Map<string, string>();
214
- // Signatures captured while thinking is hidden have no block to live on;
215
- // they are transport state that must survive to the next run input. The
216
- // claim rules mirror the visible path: an id-carrying block, or an open
217
- // anonymous one.
218
219
  private readonly hiddenSignatures = new Map<string, string>();
219
220
  private readonly hiddenSignatureAnchors = new Map<string, number>();
220
221
  private readonly hiddenBlockAnchors = new Map<string, number>();
221
- private hiddenAnonymousAnchor: number | undefined;
222
222
  private readonly hiddenReasoningIds = new Set<string>();
223
- private hiddenActiveReasoning: "none" | "anonymous" | "identified" = "none";
223
+ private hiddenActiveReasoningId: string | undefined;
224
224
  private hasEmittedOpaqueReasoning = false;
225
225
  private readonly loggedDroppedOpaqueIds = new Set<string>();
226
226
  private readonly reasoningMessageIds = new Map<string, string>();
227
- private readonly anonymousReasoningKeys = new Set<string>();
228
227
  private readonly activeReasoningKeyByScope = new Map<string, string>();
229
228
  private readonly subagentRuns = new Map<string, SubagentRunState>();
230
229
  // The nesting graph only moves when a run is announced or a tool call is
@@ -236,7 +235,6 @@ export class RunAggregator {
236
235
  reachable: Set<string>;
237
236
  }
238
237
  | undefined;
239
- private reasoningPartCounter = 0;
240
238
  private readonly toolCalls = new Map<string, ToolCallState>();
241
239
  private readonly a2uiBuckets = new Map<string, A2uiState>();
242
240
  private readonly a2uiToolCallIds = new Set<string>();
@@ -259,6 +257,7 @@ export class RunAggregator {
259
257
  constructor(options: RunAggregatorOptions) {
260
258
  this.emitUpdate = options.emit;
261
259
  this.showThinking = options.showThinking;
260
+ this.joinStrategy = options.joinStrategy;
262
261
  this.logger = options.logger;
263
262
  this.onServerMessageId = options.onServerMessageId;
264
263
  this.onTextMessageStart = options.onTextMessageStart;
@@ -284,6 +283,13 @@ export class RunAggregator {
284
283
  break;
285
284
  }
286
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
+ }
287
293
  if (event.outcome?.type === "interrupt") {
288
294
  this.interrupts = event.outcome.interrupts;
289
295
  this.status = { type: "requires-action", reason: "interrupt" };
@@ -296,13 +302,27 @@ export class RunAggregator {
296
302
  // a root call is: getPendingToolCalls and addToolResult walk
297
303
  // ToolCallMessagePart.messages, so any unresolved call keeps the run
298
304
  // resumable.
299
- const hasUnresolvedToolCalls = Array.from(this.toolCalls.values()).some(
300
- (tc) => tc.result === undefined,
301
- );
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
+ );
302
318
 
303
319
  this.status = hasUnresolvedToolCalls
304
320
  ? { type: "requires-action", reason: "tool-calls" }
305
321
  : { type: "complete", reason: "unknown" };
322
+ this.pendingToolCallIds =
323
+ hasUnresolvedToolCalls && pendingToolCallIds?.length
324
+ ? pendingToolCallIds
325
+ : undefined;
306
326
  this.closeOpenSubagentRuns(this.status);
307
327
  this.emit();
308
328
  break;
@@ -344,9 +364,8 @@ export class RunAggregator {
344
364
  this.emit();
345
365
  break;
346
366
  }
347
- case "TEXT_MESSAGE_CONTENT":
348
- case "TEXT_MESSAGE_CHUNK": {
349
- const incomingId = "messageId" in event ? event.messageId : undefined;
367
+ case "TEXT_MESSAGE_CONTENT": {
368
+ const incomingId = event.messageId;
350
369
  const scope = this.scopeOf(event);
351
370
  if (scope === ROOT_SCOPE) {
352
371
  this.beginDistinctTextMessage(incomingId);
@@ -386,13 +405,11 @@ export class RunAggregator {
386
405
  break;
387
406
  }
388
407
 
389
- case "THINKING_START":
390
- case "THINKING_TEXT_MESSAGE_START":
391
408
  case "REASONING_START":
392
409
  case "REASONING_MESSAGE_START":
393
410
  this.handleReasoningStart(
394
- this.scopeOf("subagentRunId" in event ? event : {}),
395
- "messageId" in event ? event.messageId : undefined,
411
+ this.scopeOf(event),
412
+ event.messageId,
396
413
  event.type === "REASONING_MESSAGE_START",
397
414
  event.timestamp,
398
415
  );
@@ -400,61 +417,42 @@ export class RunAggregator {
400
417
  case "REASONING_ENCRYPTED_VALUE":
401
418
  if (event.subtype === "message") {
402
419
  const scope = this.scopeOf(event);
403
- // entityId names any message, not necessarily a reasoning one, so an
404
- // unmatched id may only claim a block that has no id to contradict it.
405
- const active = this.activeReasoningKeyByScope.get(scope);
406
420
  const entityKey = this.partKey(scope, event.entityId);
407
- const key = this.showThinking
408
- ? this.reasoningParts.has(entityKey)
421
+ const key =
422
+ this.showThinking && this.reasoningParts.has(entityKey)
409
423
  ? entityKey
410
- : active !== undefined && this.anonymousReasoningKeys.has(active)
411
- ? active
412
- : undefined
413
- : undefined;
424
+ : undefined;
414
425
  if (key !== undefined) {
415
426
  this.reasoningSignatures.set(key, event.encryptedValue);
416
427
  this.reasoningSignatureIds.set(key, event.entityId);
417
428
  this.emit();
418
429
  } else if (
419
430
  !this.showThinking &&
420
- // Hidden-reasoning bookkeeping stays root-only for this task — a
421
- // subagent's opaque/hidden reasoning signatures are not preserved.
422
431
  scope === ROOT_SCOPE &&
423
432
  event.entityId.trim().length > 0 &&
424
433
  event.encryptedValue.trim().length > 0 &&
425
- (this.hiddenReasoningIds.has(event.entityId) ||
426
- this.hiddenActiveReasoning === "anonymous")
434
+ this.hiddenReasoningIds.has(event.entityId)
427
435
  ) {
428
436
  this.hiddenSignatures.set(event.entityId, event.encryptedValue);
429
437
  this.hiddenSignatureAnchors.set(
430
438
  event.entityId,
431
439
  this.hiddenBlockAnchors.get(event.entityId) ??
432
- this.hiddenAnonymousAnchor ??
433
440
  this.partOrder.length,
434
441
  );
435
442
  this.emit();
436
443
  }
437
444
  }
438
445
  break;
439
- case "THINKING_TEXT_MESSAGE_CONTENT":
440
- this.handleReasoningContent(ROOT_SCOPE, event.delta);
441
- this.totalChunks++;
442
- this.recordFirstToken();
443
- break;
444
446
  case "REASONING_MESSAGE_CONTENT":
445
447
  this.handleReasoningContent(
446
448
  this.scopeOf(event),
447
449
  event.delta,
448
- "messageId" in event ? event.messageId : undefined,
450
+ event.messageId,
449
451
  true,
450
452
  );
451
453
  this.totalChunks++;
452
454
  this.recordFirstToken();
453
455
  break;
454
- case "THINKING_TEXT_MESSAGE_END":
455
- case "THINKING_END":
456
- this.handleReasoningEnd(ROOT_SCOPE, event.timestamp);
457
- break;
458
456
  case "REASONING_MESSAGE_END":
459
457
  case "REASONING_END":
460
458
  this.handleReasoningEnd(this.scopeOf(event), event.timestamp);
@@ -474,14 +472,7 @@ export class RunAggregator {
474
472
  this.emit();
475
473
  break;
476
474
  }
477
- case "TOOL_CALL_ARGS":
478
- case "TOOL_CALL_CHUNK": {
479
- if (
480
- event.type === "TOOL_CALL_CHUNK" &&
481
- this.scopeOf(event) === ROOT_SCOPE
482
- ) {
483
- this.reportServerMessageId(event.parentMessageId);
484
- }
475
+ case "TOOL_CALL_ARGS": {
485
476
  if (!event.delta) break;
486
477
  this.appendToolArgs(event.toolCallId, event.delta);
487
478
  this.emit();
@@ -809,15 +800,12 @@ export class RunAggregator {
809
800
  this.hiddenSignatures.clear();
810
801
  this.hiddenSignatureAnchors.clear();
811
802
  this.hiddenBlockAnchors.clear();
812
- this.hiddenAnonymousAnchor = undefined;
813
803
  this.hiddenReasoningIds.clear();
814
- this.hiddenActiveReasoning = "none";
804
+ this.hiddenActiveReasoningId = undefined;
815
805
  this.loggedDroppedOpaqueIds.clear();
816
- this.anonymousReasoningKeys.clear();
817
806
  this.activeReasoningKeyByScope.clear();
818
807
  this.nestingMemo = undefined;
819
808
  this.subagentRuns.clear();
820
- this.reasoningPartCounter = 0;
821
809
  this.toolCalls.clear();
822
810
  this.a2uiBuckets.clear();
823
811
  this.a2uiToolCallIds.clear();
@@ -851,6 +839,7 @@ export class RunAggregator {
851
839
 
852
840
  private beginDistinctTextMessage(messageId: string | undefined): void {
853
841
  if (
842
+ this.joinStrategy === "concat-content" ||
854
843
  !messageId ||
855
844
  this.lastTextMessageId === undefined ||
856
845
  this.lastTextMessageId === messageId ||
@@ -862,10 +851,11 @@ export class RunAggregator {
862
851
  if (carried.parts.length > 0 || carried.hiddenSignatures.length > 0) {
863
852
  this.emit();
864
853
  }
865
- const reasoningPartCounter = this.reasoningPartCounter;
854
+ const openToolCallIds = Array.from(this.toolCalls.values())
855
+ .filter((call) => call.result === undefined)
856
+ .map((call) => call.toolCallId);
866
857
  this.resetMessageParts();
867
- this.onTextMessageStart(messageId);
868
- this.reasoningPartCounter = reasoningPartCounter;
858
+ this.onTextMessageStart(messageId, openToolCallIds);
869
859
  this.restoreReasoning(carried);
870
860
  }
871
861
 
@@ -876,7 +866,6 @@ export class RunAggregator {
876
866
  signature: string | undefined;
877
867
  signatureId: string | undefined;
878
868
  reasoningId: string | undefined;
879
- anonymous: boolean;
880
869
  }[] = [];
881
870
  let last = this.partOrder.at(-1);
882
871
  while (
@@ -891,7 +880,6 @@ export class RunAggregator {
891
880
  signature: this.reasoningSignatures.get(last.key),
892
881
  signatureId: this.reasoningSignatureIds.get(last.key),
893
882
  reasoningId: this.reasoningMessageIds.get(last.key),
894
- anonymous: this.anonymousReasoningKeys.has(last.key),
895
883
  });
896
884
  last = this.partOrder.at(-1);
897
885
  }
@@ -906,22 +894,17 @@ export class RunAggregator {
906
894
  const hiddenBlockIds = Array.from(this.hiddenBlockAnchors)
907
895
  .filter(([, anchor]) => anchor >= cut)
908
896
  .map(([id]) => id);
909
- const hiddenAnonymous =
910
- this.hiddenAnonymousAnchor !== undefined &&
911
- this.hiddenAnonymousAnchor >= cut;
912
- const activeCarried =
913
- (this.hiddenActiveReasoning === "identified" &&
914
- hiddenBlockIds.length > 0) ||
915
- (this.hiddenActiveReasoning === "anonymous" && hiddenAnonymous);
897
+ const hiddenActiveReasoningId = this.hiddenActiveReasoningId;
916
898
  return {
917
899
  parts,
918
900
  activeKey: this.activeReasoningKeyByScope.get(ROOT_SCOPE),
919
901
  hiddenSignatures,
920
902
  hiddenBlockIds,
921
- hiddenAnonymous,
922
- hiddenActiveReasoning: activeCarried
923
- ? this.hiddenActiveReasoning
924
- : ("none" as const),
903
+ hiddenActiveReasoningId:
904
+ hiddenActiveReasoningId !== undefined &&
905
+ hiddenBlockIds.includes(hiddenActiveReasoningId)
906
+ ? hiddenActiveReasoningId
907
+ : undefined,
925
908
  };
926
909
  }
927
910
 
@@ -937,7 +920,6 @@ export class RunAggregator {
937
920
  this.reasoningSignatureIds.set(part.key, part.signatureId);
938
921
  if (part.reasoningId !== undefined)
939
922
  this.reasoningMessageIds.set(part.key, part.reasoningId);
940
- if (part.anonymous) this.anonymousReasoningKeys.add(part.key);
941
923
  }
942
924
  if (
943
925
  carried.activeKey !== undefined &&
@@ -953,8 +935,7 @@ export class RunAggregator {
953
935
  this.hiddenReasoningIds.add(id);
954
936
  this.hiddenBlockAnchors.set(id, 0);
955
937
  }
956
- if (carried.hiddenAnonymous) this.hiddenAnonymousAnchor = 0;
957
- this.hiddenActiveReasoning = carried.hiddenActiveReasoning;
938
+ this.hiddenActiveReasoningId = carried.hiddenActiveReasoningId;
958
939
  }
959
940
 
960
941
  private generateTextKey(): string {
@@ -1113,12 +1094,13 @@ export class RunAggregator {
1113
1094
  private finishToolCall(
1114
1095
  scope: string,
1115
1096
  id: string,
1116
- content: string,
1097
+ content: string | ContentPart[],
1117
1098
  isError?: boolean,
1118
1099
  toolMessageId?: string,
1119
1100
  mcpResult?: Record<string, unknown>,
1120
1101
  ) {
1121
1102
  if (!id) return;
1103
+ const modelText = toolContentText(content);
1122
1104
  let entry = this.toolCalls.get(id);
1123
1105
  if (!entry) {
1124
1106
  entry = {
@@ -1143,7 +1125,7 @@ export class RunAggregator {
1143
1125
  }
1144
1126
  if (mcpResult !== undefined && !entry.snapshotResultApplied) {
1145
1127
  entry.result = mcpResult;
1146
- entry.modelContent = [{ type: "text", text: content }];
1128
+ entry.modelContent = [{ type: "text", text: modelText }];
1147
1129
  entry.snapshotResultApplied = true;
1148
1130
  entry.isError = isError;
1149
1131
  if (entry.mcpAppResourceUri === undefined) {
@@ -1152,13 +1134,16 @@ export class RunAggregator {
1152
1134
  }
1153
1135
  } else if (entry.snapshotResultApplied) {
1154
1136
  if (entry.modelContent === undefined) {
1155
- entry.modelContent = [{ type: "text", text: content }];
1137
+ entry.modelContent = [{ type: "text", text: modelText }];
1156
1138
  }
1157
1139
  if (entry.isError === undefined) {
1158
1140
  entry.isError = isError;
1159
1141
  }
1160
1142
  } else {
1161
- entry.result = tryParseJSON(content);
1143
+ entry.result = Array.isArray(content) ? content : tryParseJSON(content);
1144
+ if (Array.isArray(content)) {
1145
+ delete entry.modelContent;
1146
+ }
1162
1147
  entry.isError = isError;
1163
1148
  }
1164
1149
  if (toolMessageId) {
@@ -1443,15 +1428,6 @@ export class RunAggregator {
1443
1428
  const snapshot = this.buildParts(ROOT_SCOPE, 0, ctx);
1444
1429
  const { opaqueCandidates, lastMaterializedIndex } = root;
1445
1430
 
1446
- // An anonymous claim promotes the signature's entityId to a wire message
1447
- // id; when that id actually names a text message or the adopted assistant
1448
- // message id (which can also arrive via TOOL_CALL_START.parentMessageId),
1449
- // replaying it as its own record would put two wire records under one id,
1450
- // so such entries are dropped instead — the same outcome the visible
1451
- // path's claim guard produces for a signature that is "not for this
1452
- // block". Entries with no materialized part after their anchor trail the
1453
- // assistant record, matching the snapshot path's anchor/after
1454
- // bookkeeping.
1455
1431
  const toolMessageIds = new Set<string>();
1456
1432
  for (const call of this.toolCalls.values()) {
1457
1433
  if (call.toolMessageId) toolMessageIds.add(call.toolMessageId);
@@ -1483,11 +1459,20 @@ export class RunAggregator {
1483
1459
  const timing = this.getTiming();
1484
1460
  const metadata = {
1485
1461
  ...(timing ? { timing } : {}),
1486
- ...(this.interrupts || includeOpaque
1462
+ ...(this.interrupts ||
1463
+ (this.status?.type === "requires-action" &&
1464
+ this.status.reason === "tool-calls" &&
1465
+ this.pendingToolCallIds) ||
1466
+ includeOpaque
1487
1467
  ? {
1488
1468
  custom: {
1489
1469
  [AG_UI_METADATA_NAMESPACE]: {
1490
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
+ : {}),
1491
1476
  ...(includeOpaque
1492
1477
  ? { opaqueReasoning: publishableOpaqueReasoning }
1493
1478
  : {}),
@@ -1547,38 +1532,23 @@ export class RunAggregator {
1547
1532
 
1548
1533
  private handleReasoningStart(
1549
1534
  scope: string,
1550
- messageId?: string,
1535
+ messageId: string,
1551
1536
  isMessageId = false,
1552
1537
  timestamp?: number,
1553
1538
  ): void {
1554
1539
  if (!this.showThinking) {
1555
- // Hidden-reasoning bookkeeping stays root-only for this task — a
1556
- // subagent's opaque/hidden reasoning signatures are not preserved.
1557
1540
  if (scope !== ROOT_SCOPE) return;
1558
1541
  const anchor = this.partOrder.length;
1559
- if (messageId === undefined) {
1560
- this.hiddenActiveReasoning = "anonymous";
1561
- this.hiddenAnonymousAnchor = anchor;
1562
- } else {
1563
- this.hiddenReasoningIds.add(messageId);
1564
- this.hiddenBlockAnchors.set(messageId, anchor);
1565
- this.hiddenActiveReasoning = "identified";
1566
- }
1542
+ this.hiddenReasoningIds.add(messageId);
1543
+ this.hiddenBlockAnchors.set(messageId, anchor);
1544
+ this.hiddenActiveReasoningId = messageId;
1567
1545
  return;
1568
1546
  }
1569
1547
  // A reasoning block acts as a boundary: anonymous text arriving after it
1570
1548
  // should be a new part, not appended to any pre-reasoning text — scoped.
1571
1549
  this.activeTextMessageIdByScope.delete(scope);
1572
- const key = this.partKey(
1573
- scope,
1574
- messageId ?? `__auto-reasoning-${++this.reasoningPartCounter}`,
1575
- );
1576
- // Two different questions: which id may be replayed as a ReasoningMessage.id
1577
- // (only the message-scoped aliases carry one), and which block an unmatched
1578
- // signature may claim (any block opened without an id at all).
1579
- if (messageId === undefined) {
1580
- this.anonymousReasoningKeys.add(key);
1581
- } else if (isMessageId) {
1550
+ const key = this.partKey(scope, messageId);
1551
+ if (isMessageId) {
1582
1552
  this.reasoningMessageIds.set(key, messageId);
1583
1553
  }
1584
1554
  if (!this.reasoningParts.has(key)) {
@@ -1606,7 +1576,7 @@ export class RunAggregator {
1606
1576
  private handleReasoningContent(
1607
1577
  scope: string,
1608
1578
  delta: string,
1609
- messageId?: string,
1579
+ messageId: string,
1610
1580
  isMessageId = false,
1611
1581
  ): void {
1612
1582
  if (!delta) return;
@@ -1614,7 +1584,7 @@ export class RunAggregator {
1614
1584
  // Content without a preceding START still names the block (the visible
1615
1585
  // path opens it lazily); register it so a signature can claim it.
1616
1586
  if (scope !== ROOT_SCOPE) return;
1617
- if (this.hiddenActiveReasoning === "none") {
1587
+ if (this.hiddenActiveReasoningId === undefined) {
1618
1588
  this.handleReasoningStart(scope, messageId, isMessageId);
1619
1589
  }
1620
1590
  return;
@@ -1632,7 +1602,7 @@ export class RunAggregator {
1632
1602
 
1633
1603
  private handleReasoningEnd(scope: string, timestamp?: number): void {
1634
1604
  if (!this.showThinking) {
1635
- if (scope === ROOT_SCOPE) this.hiddenActiveReasoning = "none";
1605
+ if (scope === ROOT_SCOPE) this.hiddenActiveReasoningId = undefined;
1636
1606
  return;
1637
1607
  }
1638
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
- let parsed: AgUiEvent | null;
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 }) =>