@assistant-ui/react-ag-ui 0.0.71 → 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 (52) hide show
  1. package/README.md +1 -1
  2. package/dist/runtime/{AgUiThreadRuntimeCore.d.ts → AgUiThreadController.d.ts} +6 -12
  3. package/dist/runtime/AgUiThreadController.d.ts.map +1 -0
  4. package/dist/runtime/{AgUiThreadRuntimeCore.js → AgUiThreadController.js} +246 -159
  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 +3 -5
  11. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  12. package/dist/runtime/adapter/run-aggregator.js +47 -61
  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 +11 -35
  30. package/dist/runtime/types.d.ts.map +1 -1
  31. package/dist/runtime/types.js.map +1 -1
  32. package/dist/useAgUiRuntime.js +2 -2
  33. package/dist/useAgUiRuntime.js.map +1 -1
  34. package/package.json +12 -11
  35. package/src/runtime/{AgUiThreadRuntimeCore.interrupt-cancel.test.ts → AgUiThreadController.interrupt-cancel.test.ts} +4 -4
  36. package/src/runtime/{AgUiThreadRuntimeCore.test.ts → AgUiThreadController.test.ts} +35 -6
  37. package/src/runtime/{AgUiThreadRuntimeCore.ts → AgUiThreadController.ts} +356 -171
  38. package/src/runtime/adapter/conversions.content.test.ts +37 -5
  39. package/src/runtime/adapter/conversions.tools.test.ts +2 -1
  40. package/src/runtime/adapter/conversions.ts +235 -58
  41. package/src/runtime/adapter/run-aggregator.ts +79 -114
  42. package/src/runtime/adapter/subscriber.ts +1 -44
  43. package/src/runtime/agUiThreadState.test.ts +205 -0
  44. package/src/runtime/agUiThreadState.ts +130 -0
  45. package/src/runtime/event-parser.ts +40 -58
  46. package/src/runtime/mcp-tool-result.ts +16 -2
  47. package/src/runtime/types.ts +16 -28
  48. package/src/useAgUiRuntime.fast-refresh.test.tsx +3 -3
  49. package/src/useAgUiRuntime.thread-switch.test.tsx +4 -4
  50. package/src/useAgUiRuntime.ts +3 -3
  51. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +0 -1
  52. 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 hiddenActiveReasoning: "none" | "anonymous" | "identified" = "none";
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 hasUnresolvedToolCalls = Array.from(this.toolCalls.values()).some(
304
- (tc) => tc.result === undefined,
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
- case "TEXT_MESSAGE_CHUNK": {
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("subagentRunId" in event ? event : {}),
399
- "messageId" in event ? event.messageId : undefined,
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 = this.showThinking
412
- ? this.reasoningParts.has(entityKey)
421
+ const key =
422
+ this.showThinking && this.reasoningParts.has(entityKey)
413
423
  ? entityKey
414
- : active !== undefined && this.anonymousReasoningKeys.has(active)
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
- (this.hiddenReasoningIds.has(event.entityId) ||
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
- "messageId" in event ? event.messageId : undefined,
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.hiddenActiveReasoning = "none";
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 reasoningPartCounter = this.reasoningPartCounter;
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 hiddenAnonymous =
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
- hiddenAnonymous,
927
- hiddenActiveReasoning: activeCarried
928
- ? this.hiddenActiveReasoning
929
- : ("none" as const),
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
- if (carried.hiddenAnonymous) this.hiddenAnonymousAnchor = 0;
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: content }];
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: content }];
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 || includeOpaque
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?: string,
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
- if (messageId === undefined) {
1565
- this.hiddenActiveReasoning = "anonymous";
1566
- this.hiddenAnonymousAnchor = anchor;
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
- scope,
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?: string,
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.hiddenActiveReasoning === "none") {
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.hiddenActiveReasoning = "none";
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
- 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 }) =>