@runtypelabs/persona 4.22.0 → 4.23.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 (95) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-B_JdVK9o.d.cts → types-9Vxm-PPz.d.cts} +136 -6
  4. package/dist/animations/{types-B_JdVK9o.d.ts → types-9Vxm-PPz.d.ts} +136 -6
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/animations-extra.d.cts +136 -6
  8. package/dist/animations-extra.d.ts +136 -6
  9. package/dist/approval-ui.d.cts +148 -14
  10. package/dist/approval-ui.d.ts +148 -14
  11. package/dist/artifacts-ui.d.cts +148 -14
  12. package/dist/artifacts-ui.d.ts +148 -14
  13. package/dist/codegen.cjs +1 -1
  14. package/dist/codegen.js +1 -1
  15. package/dist/context-mentions.d.cts +148 -14
  16. package/dist/context-mentions.d.ts +148 -14
  17. package/dist/event-stream-view-GL3ADRY2.js +1 -0
  18. package/dist/event-stream-view.cjs +1 -1
  19. package/dist/event-stream-view.d.cts +148 -14
  20. package/dist/event-stream-view.d.ts +148 -14
  21. package/dist/event-stream-view.js +1 -1
  22. package/dist/forms-ui.d.cts +161 -23
  23. package/dist/forms-ui.d.ts +161 -23
  24. package/dist/history-view.d.cts +136 -6
  25. package/dist/history-view.d.ts +136 -6
  26. package/dist/index.cjs +67 -67
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +172 -27
  29. package/dist/index.d.ts +172 -27
  30. package/dist/index.global.js +51 -51
  31. package/dist/index.global.js.map +1 -1
  32. package/dist/index.js +67 -67
  33. package/dist/index.js.map +1 -1
  34. package/dist/launcher.global.js.map +1 -1
  35. package/dist/session-reconnect.d.cts +148 -14
  36. package/dist/session-reconnect.d.ts +148 -14
  37. package/dist/smart-dom-reader.d.cts +148 -14
  38. package/dist/smart-dom-reader.d.ts +148 -14
  39. package/dist/testing.cjs +3 -3
  40. package/dist/testing.d.cts +3 -4
  41. package/dist/testing.d.ts +3 -4
  42. package/dist/testing.js +3 -3
  43. package/dist/theme-editor-preview.cjs +71 -71
  44. package/dist/theme-editor-preview.d.cts +154 -14
  45. package/dist/theme-editor-preview.d.ts +154 -14
  46. package/dist/theme-editor-preview.js +48 -48
  47. package/dist/theme-editor.d.cts +148 -14
  48. package/dist/theme-editor.d.ts +148 -14
  49. package/dist/voice-runtime.d.cts +6 -2
  50. package/dist/voice-runtime.d.ts +6 -2
  51. package/package.json +1 -1
  52. package/src/ask-user-question-tool.ts +1 -1
  53. package/src/client-history-rest.test.ts +2 -1
  54. package/src/client-visitor-history.test.ts +106 -4
  55. package/src/client.composer-options.test.ts +1 -1
  56. package/src/client.interrupt.test.ts +2 -2
  57. package/src/client.test.ts +477 -915
  58. package/src/client.ts +134 -494
  59. package/src/client.unified-tools.test.ts +76 -0
  60. package/src/components/event-stream-badge-theming.test.ts +3 -0
  61. package/src/components/event-stream-view.test.ts +76 -76
  62. package/src/components/event-stream-view.ts +5 -0
  63. package/src/components/tool-bubble.test.ts +12 -0
  64. package/src/components/tool-bubble.ts +13 -2
  65. package/src/event-stream-view-bundle.test.ts +2 -2
  66. package/src/generated/runtype-openapi-contract.ts +129 -1
  67. package/src/internal/runtype-history-provider.test.ts +2 -2
  68. package/src/launcher-global.ts +1 -1
  69. package/src/session-history.test.ts +1 -1
  70. package/src/session.quote.test.ts +1 -1
  71. package/src/session.resubmit-from.test.ts +1 -1
  72. package/src/session.test.ts +5 -5
  73. package/src/session.ts +95 -36
  74. package/src/suggest-replies-tool.ts +1 -1
  75. package/src/testing/mock-stream.test.ts +37 -16
  76. package/src/testing/mock-stream.ts +14 -9
  77. package/src/types.ts +19 -14
  78. package/src/ui.ask-user-question-plugin.test.ts +7 -7
  79. package/src/ui.composer-defer-one.test.ts +6 -34
  80. package/src/ui.composer-draft-persistence.test.ts +1 -1
  81. package/src/ui.composer-quote.test.ts +1 -1
  82. package/src/ui.message-history-actions.test.ts +1 -1
  83. package/src/ui.streaming-coalescing.test.ts +10 -13
  84. package/src/ui.ts +80 -32
  85. package/src/ui.typing-indicator.test.ts +216 -0
  86. package/src/ui.voice-custom-provider.test.ts +211 -0
  87. package/src/utils/event-stream-buffer.test.ts +13 -13
  88. package/src/utils/event-stream-capture.test.ts +20 -20
  89. package/src/utils/throughput-tracker.test.ts +100 -142
  90. package/src/utils/throughput-tracker.ts +18 -132
  91. package/src/utils/voice-support.ts +18 -1
  92. package/src/voice/voice-turn-tracker.ts +18 -0
  93. package/src/webmcp-bridge.ts +1 -1
  94. package/dist/event-stream-view-FKWDY3UB.js +0 -1
  95. package/src/utils/__fixtures__/unified-translator.oracle.ts +0 -717
package/src/client.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { RuntypeExecutionStreamEvent, RuntypeClientInitResponse } from "./generated/runtype-openapi-contract";
1
2
  import {
2
3
  AgentWidgetConfig,
3
4
  AgentWidgetMessage,
@@ -12,10 +13,8 @@ import {
12
13
  AgentWidgetSSEEventParser,
13
14
  AgentWidgetHeadersFunction,
14
15
  AgentWidgetSSEEventResult as _AgentWidgetSSEEventResult,
15
- AgentExecutionState,
16
16
  StopReasonKind,
17
17
  ClientSession,
18
- ClientInitResponse,
19
18
  ClientChatRequest,
20
19
  ClientToolDefinition,
21
20
  ClientFeedbackRequest,
@@ -138,15 +137,10 @@ export class HistoryClientError extends Error {
138
137
  const DISPLAY_PROJECTION_MESSAGE_CAP = 32768;
139
138
  const DISPLAY_PROJECTION_BATCH_CAP = 49152;
140
139
 
141
- /** Deprecated wire `flowId` is consumed here and never propagated onward. */
140
+ /** Normalize current history metadata without leaking wire-only aliases. */
142
141
  const normalizeHistorySummary = (raw: unknown): HistoryConversationSummary => {
143
142
  const row = (raw ?? {}) as Record<string, unknown>;
144
- const targetId =
145
- typeof row.targetId === "string"
146
- ? row.targetId
147
- : typeof row.flowId === "string"
148
- ? row.flowId
149
- : null;
143
+ const targetId = typeof row.targetId === "string" ? row.targetId : null;
150
144
  return {
151
145
  id: typeof row.id === "string" ? row.id : "",
152
146
  title: typeof row.title === "string" ? row.title : "",
@@ -798,7 +792,7 @@ export class AgentWidgetClient {
798
792
  : {}),
799
793
  };
800
794
 
801
- let response = await fetch(this.getClientApiUrl('init'), {
795
+ const response = await fetch(this.getClientApiUrl('init'), {
802
796
  method: 'POST',
803
797
  headers: {
804
798
  'Content-Type': 'application/json',
@@ -807,22 +801,6 @@ export class AgentWidgetClient {
807
801
  body: JSON.stringify(requestBody),
808
802
  });
809
803
 
810
- // Rolling/self-hosted compatibility: pre-recovery servers use a strict
811
- // init schema and answer 400 to the additive field. Retry once without it;
812
- // the absent response capability keeps Persona on ordinary streaming.
813
- if (response.status === 400 && durableRecoveryRequested) {
814
- const fallbackBody = { ...requestBody };
815
- delete fallbackBody.durableRecovery;
816
- response = await fetch(this.getClientApiUrl('init'), {
817
- method: 'POST',
818
- headers: {
819
- 'Content-Type': 'application/json',
820
- 'X-Persona-Version': VERSION,
821
- },
822
- body: JSON.stringify(fallbackBody),
823
- });
824
- }
825
-
826
804
  if (!response.ok) {
827
805
  const error = await response.json().catch(() => ({ error: 'Session initialization failed' }));
828
806
  if (historyCapable && response.status === 403 && error.error === 'visitor_history_disabled') {
@@ -861,7 +839,7 @@ export class AgentWidgetClient {
861
839
  throw new Error(error.error || 'Failed to initialize session');
862
840
  }
863
841
 
864
- const data: ClientInitResponse = await response.json();
842
+ const data: RuntypeClientInitResponse = await response.json();
865
843
 
866
844
  // First awaited op after parse: the server cannot re-issue a minted secret.
867
845
  if (data.visitor?.token) {
@@ -871,12 +849,10 @@ export class AgentWidgetClient {
871
849
  return {
872
850
  sessionId: data.sessionId,
873
851
  expiresAt: new Date(data.expiresAt),
874
- flow: data.flow,
852
+ // Preserve the published session facade while consuming the current wire type.
853
+ flow: data.flow as ClientSession["flow"],
875
854
  ...(data.conversationId ? { conversationId: data.conversationId } : {}),
876
- // New history code reads only the normalized field; `flow` stays untouched.
877
- ...(data.targetId ?? data.flow?.id
878
- ? { targetId: data.targetId ?? data.flow?.id }
879
- : {}),
855
+ ...(data.targetId ? { targetId: data.targetId } : {}),
880
856
  ...(data.conversationRevision
881
857
  ? { conversationRevision: data.conversationRevision }
882
858
  : {}),
@@ -885,7 +861,7 @@ export class AgentWidgetClient {
885
861
  config: {
886
862
  welcomeMessage: data.config.welcomeMessage,
887
863
  placeholder: data.config.placeholder,
888
- theme: data.config.theme,
864
+ theme: data.config.theme as ClientSession["config"]["theme"],
889
865
  },
890
866
  };
891
867
  }
@@ -1675,12 +1651,8 @@ export class AgentWidgetClient {
1675
1651
  ...(opts?.scope ? { scope: opts.scope } : {}),
1676
1652
  }
1677
1653
  );
1678
- const summarySource =
1679
- detail && typeof detail.conversation === 'object' && detail.conversation !== null
1680
- ? detail.conversation
1681
- : detail;
1682
1654
  return {
1683
- summary: normalizeHistorySummary(summarySource),
1655
+ summary: normalizeHistorySummary(detail),
1684
1656
  messages: Array.isArray(detail?.messages)
1685
1657
  ? (detail.messages as HistoryConversationDetail['messages'])
1686
1658
  : [],
@@ -2208,7 +2180,7 @@ export class AgentWidgetClient {
2208
2180
  * Returns the raw Response so the caller can pipe its SSE body through
2209
2181
  * `connectStream()`.
2210
2182
  *
2211
- * @param executionId - The paused execution id carried on `step_await`.
2183
+ * @param executionId - The paused execution id carried on `await`.
2212
2184
  * @param toolOutputs - Map keyed by per-call `toolCallId` (core#3878),
2213
2185
  * falling back to tool name for legacy servers → the tool's result value.
2214
2186
  */
@@ -2832,7 +2804,7 @@ export class AgentWidgetClient {
2832
2804
 
2833
2805
  let assistantMessage: AgentWidgetMessage | null = null;
2834
2806
  // Tracks the most recently touched assistant text message for the
2835
- // current agent turn so `agent_turn_complete.stopReason` can attach
2807
+ // current agent turn so `turn_complete.stopReason` can attach
2836
2808
  // to the final visible text segment even after `assistantMessage`
2837
2809
  // has been finalized at a tool-call boundary within the turn.
2838
2810
  let lastAssistantInTurn: AgentWidgetMessage | null = null;
@@ -2859,46 +2831,6 @@ export class AgentWidgetClient {
2859
2831
  const nestedBlockRaw = new Map<string, string>();
2860
2832
  const reasoningMessages = new Map<string, AgentWidgetMessage>();
2861
2833
  const toolMessages = new Map<string, AgentWidgetMessage>();
2862
- const reasoningContext = {
2863
- lastId: null as string | null,
2864
- byStep: new Map<string, string>()
2865
- };
2866
- const toolContext = {
2867
- lastId: null as string | null,
2868
- byCall: new Map<string, string>()
2869
- };
2870
-
2871
- const normalizeKey = (value: unknown): string | null => {
2872
- if (value === null || value === undefined) return null;
2873
- try {
2874
- return String(value);
2875
- } catch (error) {
2876
- return null;
2877
- }
2878
- };
2879
-
2880
- const getStepKey = (payload: Record<string, any>) =>
2881
- normalizeKey(
2882
- payload.stepId ??
2883
- payload.step_id ??
2884
- payload.step ??
2885
- payload.parentId ??
2886
- payload.flowStepId ??
2887
- payload.flow_step_id
2888
- );
2889
-
2890
- const getToolCallKey = (payload: Record<string, any>) =>
2891
- normalizeKey(
2892
- payload.callId ??
2893
- payload.call_id ??
2894
- payload.requestId ??
2895
- payload.request_id ??
2896
- payload.toolCallId ??
2897
- payload.tool_call_id ??
2898
- payload.stepId ??
2899
- payload.step_id
2900
- );
2901
-
2902
2834
  const baseAssistantId = assistantMessageId;
2903
2835
  let assistantIdConsumed = false;
2904
2836
 
@@ -2930,42 +2862,6 @@ export class AgentWidgetClient {
2930
2862
  return assistantMessage;
2931
2863
  };
2932
2864
 
2933
- const trackReasoningId = (stepKey: string | null, id: string) => {
2934
- reasoningContext.lastId = id;
2935
- if (stepKey) {
2936
- reasoningContext.byStep.set(stepKey, id);
2937
- }
2938
- };
2939
-
2940
- const resolveReasoningId = (
2941
- payload: Record<string, any>,
2942
- allowCreate: boolean
2943
- ): string | null => {
2944
- const rawId = payload.reasoningId ?? payload.id;
2945
- const stepKey = getStepKey(payload);
2946
- if (rawId) {
2947
- const resolved = String(rawId);
2948
- trackReasoningId(stepKey, resolved);
2949
- return resolved;
2950
- }
2951
- if (stepKey) {
2952
- const existing = reasoningContext.byStep.get(stepKey);
2953
- if (existing) {
2954
- reasoningContext.lastId = existing;
2955
- return existing;
2956
- }
2957
- }
2958
- if (reasoningContext.lastId && !allowCreate) {
2959
- return reasoningContext.lastId;
2960
- }
2961
- if (!allowCreate) {
2962
- return null;
2963
- }
2964
- const generated = `reason-${nextSequence()}`;
2965
- trackReasoningId(stepKey, generated);
2966
- return generated;
2967
- };
2968
-
2969
2865
  const ensureReasoningMessage = (reasoningId: string) => {
2970
2866
  const existing = reasoningMessages.get(reasoningId);
2971
2867
  if (existing) {
@@ -2992,19 +2888,12 @@ export class AgentWidgetClient {
2992
2888
  return message;
2993
2889
  };
2994
2890
 
2995
- const trackToolId = (callKey: string | null, id: string) => {
2996
- toolContext.lastId = id;
2997
- if (callKey) {
2998
- toolContext.byCall.set(callKey, id);
2999
- }
3000
- };
3001
-
3002
2891
  // Track tool call IDs for artifact emit tools so we can suppress their UI
3003
2892
  const artifactToolCallIds = new Set<string>();
3004
2893
  // Track artifact block messages (reference card or inline block) so we can
3005
2894
  // update them on artifact_complete
3006
2895
  const artifactCardMessages = new Map<string, AgentWidgetMessage>();
3007
- // Track artifact IDs that already have a reference card (from auto-creation or transcript_insert)
2896
+ // Track artifact IDs that already have a reference card
3008
2897
  const artifactIdsWithCards = new Set<string>();
3009
2898
  // Accumulate artifact markdown content (and component props) for embedding
3010
2899
  // in block props on complete. `props` accumulates across `artifact_update`
@@ -3026,35 +2915,6 @@ export class AgentWidgetClient {
3026
2915
  return normalized === "emit_artifact_markdown" || normalized === "emit_artifact_component";
3027
2916
  };
3028
2917
 
3029
- const resolveToolId = (
3030
- payload: Record<string, any>,
3031
- allowCreate: boolean
3032
- ): string | null => {
3033
- const rawId = payload.toolId ?? payload.id;
3034
- const callKey = getToolCallKey(payload);
3035
- if (rawId) {
3036
- const resolved = String(rawId);
3037
- trackToolId(callKey, resolved);
3038
- return resolved;
3039
- }
3040
- if (callKey) {
3041
- const existing = toolContext.byCall.get(callKey);
3042
- if (existing) {
3043
- toolContext.lastId = existing;
3044
- return existing;
3045
- }
3046
- }
3047
- if (toolContext.lastId && !allowCreate) {
3048
- return toolContext.lastId;
3049
- }
3050
- if (!allowCreate) {
3051
- return null;
3052
- }
3053
- const generated = `tool-${nextSequence()}`;
3054
- trackToolId(callKey, generated);
3055
- return generated;
3056
- };
3057
-
3058
2918
  const ensureToolMessage = (toolId: string) => {
3059
2919
  const existing = toolMessages.get(toolId);
3060
2920
  if (existing) {
@@ -3116,45 +2976,7 @@ export class AgentWidgetClient {
3116
2976
  const streamParsers = new Map<string, AgentWidgetStreamParser>();
3117
2977
  // Track accumulated raw content for structured formats (JSON, XML, etc.)
3118
2978
  const rawContentBuffers = new Map<string, string>();
3119
- // Rebuild incremental text by sequence so late arrivals can repair already-emitted
3120
- // content after the reorder buffer's gap-timeout flush.
3121
- const orderedChunkBuffers = new Map<string, Array<{ seq: number; text: string }>>();
3122
-
3123
- const insertOrderedChunk = (key: string, seq: number, text: string): string => {
3124
- let chunks = orderedChunkBuffers.get(key);
3125
- if (!chunks) {
3126
- chunks = [];
3127
- orderedChunkBuffers.set(key, chunks);
3128
- }
3129
-
3130
- let lo = 0;
3131
- let hi = chunks.length;
3132
- while (lo < hi) {
3133
- const mid = (lo + hi) >>> 1;
3134
- if (chunks[mid].seq < seq) {
3135
- lo = mid + 1;
3136
- } else {
3137
- hi = mid;
3138
- }
3139
- }
3140
-
3141
- if (chunks[lo]?.seq === seq) {
3142
- chunks[lo] = { seq, text };
3143
- } else {
3144
- chunks.splice(lo, 0, { seq, text });
3145
- }
3146
-
3147
- let accumulated = "";
3148
- for (let index = 0; index < chunks.length; index++) {
3149
- accumulated += chunks[index].text;
3150
- }
3151
- return accumulated;
3152
- };
3153
-
3154
- /**
3155
- * After text_end + didSplitByPartId, merge the authoritative final response into the
3156
- * sealed message when streaming left content short (e.g. async parser lag).
3157
- */
2979
+ /** Reconcile a sealed text block with its authoritative structured response. */
3158
2980
  const reconcileSealedAssistantWithFinalResponse = (
3159
2981
  msg: AgentWidgetMessage,
3160
2982
  finalContent: unknown
@@ -3427,11 +3249,11 @@ export class AgentWidgetClient {
3427
3249
  };
3428
3250
 
3429
3251
  // Ready queue of parsed wire frames awaiting a drain. The API streams the
3430
- // 33-event wire vocabulary; each frame is parsed in the SSE loop
3252
+ // unified wire vocabulary; each frame is parsed in the SSE loop
3431
3253
  // below and rendered directly by the handler (no translation bridge), then
3432
3254
  // pushed here. The wire stream is a single, in-order SSE connection, so
3433
3255
  // frames drain straight through with no reordering.
3434
- const seqReadyQueue: Array<{ payloadType: string; payload: any }> = [];
3256
+ const eventQueue: RuntypeExecutionStreamEvent[] = [];
3435
3257
  // Declared here so later closures can reference it; assigned after all
3436
3258
  // handler-scoped variables are initialised (before the SSE loop).
3437
3259
  let drainReadyQueue: () => void;
@@ -3440,7 +3262,7 @@ export class AgentWidgetClient {
3440
3262
  // synthetic message at media_complete, keyed by the block id.
3441
3263
  const mediaBuffers = new Map<
3442
3264
  string,
3443
- { mediaType?: string; role?: string; toolCallId?: unknown; parts: string[] }
3265
+ { mediaType?: string; toolCallId?: string; parts: string[] }
3444
3266
  >();
3445
3267
  // Tracks the last iteration surfaced as a per-iteration message boundary, so
3446
3268
  // `turn_start` advancing the iteration rotates the bubble in 'separate' mode.
@@ -3462,18 +3284,15 @@ export class AgentWidgetClient {
3462
3284
  // from here.
3463
3285
  let openTurnId: string | null = null;
3464
3286
  // Agent execution state tracking
3465
- let agentExecution: AgentExecutionState | null = null;
3466
- // Track assistant messages per agent iteration for 'separate' mode
3467
- const agentIterationMessages = new Map<number, AgentWidgetMessage>();
3287
+ let agentExecution: { executionId: string; agentId: string; agentName: string } | null = null;
3468
3288
  const iterationDisplay = this.config.iterationDisplay ?? 'separate';
3469
3289
 
3470
- // Drains the queued transduced events through the main event handler.
3290
+ // Drains the queued native events through the main event handler.
3471
3291
  // Also invoked after the SSE loop exits so any events queued at
3472
3292
  // end-of-stream are processed.
3473
3293
  drainReadyQueue = () => {
3474
- for (let i = 0; i < seqReadyQueue.length; i++) {
3475
- const payloadType = seqReadyQueue[i].payloadType;
3476
- const payload = seqReadyQueue[i].payload;
3294
+ for (let i = 0; i < eventQueue.length; i++) {
3295
+ const payload = eventQueue[i];
3477
3296
 
3478
3297
  // Recover the execution kind on continuation streams that omit
3479
3298
  // `execution_start` (e.g. a tool-driven `/resume`). Flow `step_*` frames
@@ -3490,7 +3309,7 @@ export class AgentWidgetClient {
3490
3309
  executionKind = "flow";
3491
3310
  }
3492
3311
 
3493
- if (payloadType === "reasoning_start") {
3312
+ if (payload.type === "reasoning_start") {
3494
3313
  // Nested flow-as-tool thinking (PR #4602): route to the parent tool's row.
3495
3314
  const rStartBlockId = typeof payload.id === "string" ? payload.id : null;
3496
3315
  const rStartParent =
@@ -3502,8 +3321,7 @@ export class AgentWidgetClient {
3502
3321
  ensureNestedBlockMessage(rStartBlockId, rStartParent, "reasoning");
3503
3322
  continue;
3504
3323
  }
3505
- const reasoningId =
3506
- resolveReasoningId(payload, true) ?? `reason-${nextSequence()}`;
3324
+ const reasoningId = payload.id;
3507
3325
  const reasoningMessage = ensureReasoningMessage(reasoningId);
3508
3326
  reasoningMessage.reasoning = reasoningMessage.reasoning ?? {
3509
3327
  id: reasoningId,
@@ -3512,7 +3330,7 @@ export class AgentWidgetClient {
3512
3330
  };
3513
3331
  reasoningMessage.reasoning.startedAt =
3514
3332
  reasoningMessage.reasoning.startedAt ??
3515
- resolveTimestamp(payload.startedAt ?? payload.timestamp);
3333
+ Date.now();
3516
3334
  reasoningMessage.reasoning.completedAt = undefined;
3517
3335
  reasoningMessage.reasoning.durationMs = undefined;
3518
3336
  if (payload.scope === "loop" || payload.scope === "turn") {
@@ -3521,7 +3339,7 @@ export class AgentWidgetClient {
3521
3339
  reasoningMessage.streaming = true;
3522
3340
  reasoningMessage.reasoning.status = "streaming";
3523
3341
  emitMessage(reasoningMessage);
3524
- } else if (payloadType === "reasoning_delta") {
3342
+ } else if (payload.type === "reasoning_delta") {
3525
3343
  // Nested flow-as-tool thinking: append to the parent-tool-row message.
3526
3344
  const rDeltaBlockId = typeof payload.id === "string" ? payload.id : null;
3527
3345
  if (
@@ -3530,18 +3348,14 @@ export class AgentWidgetClient {
3530
3348
  nestedBlockMessages.has(rDeltaBlockId)
3531
3349
  ) {
3532
3350
  const nested = nestedBlockMessages.get(rDeltaBlockId)!;
3533
- const nestedChunk =
3534
- payload.reasoningText ?? payload.text ?? payload.delta ?? "";
3535
- if (nestedChunk && payload.hidden !== true && nested.reasoning) {
3351
+ const nestedChunk = payload.delta;
3352
+ if (nestedChunk && nested.reasoning) {
3536
3353
  nested.reasoning.chunks.push(String(nestedChunk));
3537
3354
  emitMessage(nested);
3538
3355
  }
3539
3356
  continue;
3540
3357
  }
3541
- const reasoningId =
3542
- resolveReasoningId(payload, false) ??
3543
- resolveReasoningId(payload, true) ??
3544
- `reason-${nextSequence()}`;
3358
+ const reasoningId = payload.id;
3545
3359
  const reasoningMessage = ensureReasoningMessage(reasoningId);
3546
3360
  reasoningMessage.reasoning = reasoningMessage.reasoning ?? {
3547
3361
  id: reasoningId,
@@ -3550,38 +3364,12 @@ export class AgentWidgetClient {
3550
3364
  };
3551
3365
  reasoningMessage.reasoning.startedAt =
3552
3366
  reasoningMessage.reasoning.startedAt ??
3553
- resolveTimestamp(payload.startedAt ?? payload.timestamp);
3554
- const chunk =
3555
- payload.reasoningText ??
3556
- payload.text ??
3557
- payload.delta ??
3558
- "";
3559
- if (chunk && payload.hidden !== true) {
3560
- const reasonSeq = typeof payload.sequenceIndex === "number" ? payload.sequenceIndex : undefined;
3561
- if (reasonSeq !== undefined) {
3562
- // Rebuild chunks by seq so late arrivals after a gap-timeout flush
3563
- // are inserted at the correct position rather than appended.
3564
- const ordered = insertOrderedChunk(reasoningId, reasonSeq, String(chunk));
3565
- reasoningMessage.reasoning.chunks = [ordered];
3566
- } else {
3567
- reasoningMessage.reasoning.chunks.push(String(chunk));
3568
- }
3569
- }
3570
- reasoningMessage.reasoning.status = payload.done ? "complete" : "streaming";
3571
- if (payload.done) {
3572
- reasoningMessage.reasoning.completedAt = resolveTimestamp(
3573
- payload.completedAt ?? payload.timestamp
3574
- );
3575
- const start = reasoningMessage.reasoning.startedAt ?? Date.now();
3576
- reasoningMessage.reasoning.durationMs = Math.max(
3577
- 0,
3578
- (reasoningMessage.reasoning.completedAt ?? Date.now()) - start
3579
- );
3580
-
3581
- }
3582
- reasoningMessage.streaming = reasoningMessage.reasoning.status !== "complete";
3367
+ Date.now();
3368
+ if (payload.delta) reasoningMessage.reasoning.chunks.push(payload.delta);
3369
+ reasoningMessage.reasoning.status = "streaming";
3370
+ reasoningMessage.streaming = true;
3583
3371
  emitMessage(reasoningMessage);
3584
- } else if (payloadType === "reasoning_complete") {
3372
+ } else if (payload.type === "reasoning_complete") {
3585
3373
  // Nested flow-as-tool thinking close: seal the parent-tool-row message.
3586
3374
  const rCompleteBlockId = typeof payload.id === "string" ? payload.id : null;
3587
3375
  if (
@@ -3604,10 +3392,7 @@ export class AgentWidgetClient {
3604
3392
  nestedBlockMessages.delete(rCompleteBlockId);
3605
3393
  continue;
3606
3394
  }
3607
- const reasoningId =
3608
- resolveReasoningId(payload, false) ??
3609
- resolveReasoningId(payload, true) ??
3610
- `reason-${nextSequence()}`;
3395
+ const reasoningId = payload.id;
3611
3396
  // A close carrying text (or scope:"loop") is a cross-iteration
3612
3397
  // reflection fold (merged spec §4 E3): the API streams nothing for the
3613
3398
  // block, then delivers the whole reflection as `text` on the close.
@@ -3627,9 +3412,7 @@ export class AgentWidgetClient {
3627
3412
  reasoningMessage.reasoning.chunks.push(reflectionText);
3628
3413
  }
3629
3414
  reasoningMessage.reasoning.status = "complete";
3630
- reasoningMessage.reasoning.completedAt = resolveTimestamp(
3631
- payload.completedAt ?? payload.timestamp
3632
- );
3415
+ reasoningMessage.reasoning.completedAt = Date.now();
3633
3416
  const start = reasoningMessage.reasoning.startedAt ?? Date.now();
3634
3417
  reasoningMessage.reasoning.durationMs = Math.max(
3635
3418
  0,
@@ -3639,11 +3422,7 @@ export class AgentWidgetClient {
3639
3422
 
3640
3423
  emitMessage(reasoningMessage);
3641
3424
  }
3642
- const stepKey = getStepKey(payload);
3643
- if (stepKey) {
3644
- reasoningContext.byStep.delete(stepKey);
3645
- }
3646
- } else if (payloadType === "tool_start") {
3425
+ } else if (payload.type === "tool_start") {
3647
3426
  // Unified tool family (agent + flow). Seal any open assistant bubble so
3648
3427
  // text→tool→text interleaves chronologically (the API also emits a
3649
3428
  // text_complete here, so this is usually a no-op — kept for safety).
@@ -3657,17 +3436,13 @@ export class AgentWidgetClient {
3657
3436
  // own — can be stamped with the enclosing iteration even on tool-only
3658
3437
  // turns that never emit a `turn_start`.
3659
3438
  if (typeof payload.iteration === "number") lastIterationSeen = payload.iteration;
3660
- const toolId: string =
3661
- (typeof payload.toolCallId === "string" ? payload.toolCallId : undefined) ??
3662
- resolveToolId(payload, true) ??
3663
- `tool-${nextSequence()}`;
3664
- const toolName = payload.toolName ?? payload.name;
3439
+ const toolId = payload.toolCallId;
3440
+ const toolName = payload.toolName;
3665
3441
  // Suppress tool UI for artifact emit tools: artifacts are handled via artifact_* events
3666
3442
  if (isArtifactEmitToolName(toolName)) {
3667
3443
  artifactToolCallIds.add(toolId);
3668
3444
  continue;
3669
3445
  }
3670
- trackToolId(getToolCallKey(payload), toolId);
3671
3446
  const toolMessage = ensureToolMessage(toolId);
3672
3447
  const tool = toolMessage.toolCall ?? {
3673
3448
  id: toolId,
@@ -3675,14 +3450,15 @@ export class AgentWidgetClient {
3675
3450
  };
3676
3451
  tool.name = toolName ?? tool.name;
3677
3452
  tool.status = "running";
3453
+ tool.success = undefined;
3454
+ tool.error = undefined;
3455
+ tool.duration = undefined;
3678
3456
  if (payload.parameters !== undefined) {
3679
3457
  tool.args = payload.parameters;
3680
- } else if (payload.args !== undefined) {
3681
- tool.args = payload.args;
3682
3458
  }
3683
3459
  tool.startedAt =
3684
3460
  tool.startedAt ??
3685
- resolveTimestamp(payload.startedAt ?? payload.timestamp);
3461
+ resolveTimestamp(payload.startedAt);
3686
3462
  tool.completedAt = undefined;
3687
3463
  tool.durationMs = undefined;
3688
3464
  toolMessage.toolCall = tool;
@@ -3694,11 +3470,8 @@ export class AgentWidgetClient {
3694
3470
  };
3695
3471
  }
3696
3472
  emitMessage(toolMessage);
3697
- } else if (payloadType === "tool_output_delta") {
3698
- const toolId =
3699
- resolveToolId(payload, false) ??
3700
- resolveToolId(payload, true) ??
3701
- `tool-${nextSequence()}`;
3473
+ } else if (payload.type === "tool_output_delta") {
3474
+ const toolId = payload.toolCallId;
3702
3475
  if (artifactToolCallIds.has(toolId)) continue;
3703
3476
  const toolMessage = ensureToolMessage(toolId);
3704
3477
  const tool = toolMessage.toolCall ?? {
@@ -3707,9 +3480,8 @@ export class AgentWidgetClient {
3707
3480
  };
3708
3481
  tool.startedAt =
3709
3482
  tool.startedAt ??
3710
- resolveTimestamp(payload.startedAt ?? payload.timestamp);
3711
- const chunkText =
3712
- payload.text ?? payload.delta ?? payload.message ?? "";
3483
+ Date.now();
3484
+ const chunkText = payload.delta;
3713
3485
  if (chunkText) {
3714
3486
  tool.chunks = tool.chunks ?? [];
3715
3487
  tool.chunks.push(String(chunkText));
@@ -3717,19 +3489,15 @@ export class AgentWidgetClient {
3717
3489
  tool.status = "running";
3718
3490
  toolMessage.toolCall = tool;
3719
3491
  toolMessage.streaming = true;
3720
- const agentCtxChunk = payload.agentContext;
3721
- if (agentCtxChunk || payload.executionId) {
3492
+ if (payload.executionId) {
3722
3493
  toolMessage.agentMetadata = toolMessage.agentMetadata ?? {
3723
- executionId: agentCtxChunk?.executionId ?? payload.executionId,
3724
- iteration: agentCtxChunk?.iteration ?? payload.iteration,
3494
+ executionId: payload.executionId,
3495
+ iteration: lastIterationSeen,
3725
3496
  };
3726
3497
  }
3727
3498
  emitMessage(toolMessage);
3728
- } else if (payloadType === "tool_complete") {
3729
- const toolId =
3730
- resolveToolId(payload, false) ??
3731
- resolveToolId(payload, true) ??
3732
- `tool-${nextSequence()}`;
3499
+ } else if (payload.type === "tool_complete") {
3500
+ const toolId = payload.toolCallId;
3733
3501
  if (artifactToolCallIds.has(toolId)) {
3734
3502
  artifactToolCallIds.delete(toolId);
3735
3503
  continue;
@@ -3743,15 +3511,14 @@ export class AgentWidgetClient {
3743
3511
  if (payload.result !== undefined) {
3744
3512
  tool.result = payload.result;
3745
3513
  }
3746
- if (typeof payload.duration === "number") {
3747
- tool.duration = payload.duration;
3748
- }
3749
- tool.completedAt = resolveTimestamp(
3750
- payload.completedAt ?? payload.timestamp
3751
- );
3752
- const durationValue = payload.duration ?? payload.executionTime;
3514
+ tool.name = payload.toolName ?? tool.name;
3515
+ tool.success = payload.success;
3516
+ tool.error = payload.error;
3517
+ tool.completedAt = Date.now();
3518
+ const durationValue = payload.executionTime;
3753
3519
  if (typeof durationValue === "number") {
3754
3520
  tool.durationMs = durationValue;
3521
+ tool.duration = durationValue;
3755
3522
  } else {
3756
3523
  const start = tool.startedAt ?? Date.now();
3757
3524
  tool.durationMs = Math.max(
@@ -3761,29 +3528,17 @@ export class AgentWidgetClient {
3761
3528
  }
3762
3529
  toolMessage.toolCall = tool;
3763
3530
  toolMessage.streaming = false;
3764
- const agentCtxComplete = payload.agentContext;
3765
- if (agentCtxComplete || payload.executionId) {
3531
+ if (payload.executionId) {
3766
3532
  toolMessage.agentMetadata = toolMessage.agentMetadata ?? {
3767
- executionId: agentCtxComplete?.executionId ?? payload.executionId,
3768
- iteration: agentCtxComplete?.iteration ?? payload.iteration,
3533
+ executionId: payload.executionId,
3534
+ iteration: payload.iteration ?? lastIterationSeen,
3769
3535
  };
3770
3536
  }
3771
3537
  emitMessage(toolMessage);
3772
- const callKey = getToolCallKey(payload);
3773
- if (callKey) {
3774
- toolContext.byCall.delete(callKey);
3775
- }
3776
- } else if (payloadType === "await" && payload.toolName) {
3777
- // LOCAL tool pause. Two wire shapes resolve here, by dispatch target:
3778
- // - FLOW dispatch → `step_await` + `awaitReason: "local_tool_required"`
3779
- // (Runtype's prompt step throws LocalToolRequiredError when the model
3780
- // calls a `toolType: "local"` tool).
3781
- // - AGENT dispatch → `agent_await` (the agent runtime's native pause).
3782
- // Either way the server emits the tool name, params, and execution id;
3783
- // the execution pauses until the client POSTs /resume with toolOutputs.
3784
- // `agent_await` carries a BARE tool name plus an `origin`; page tools
3785
- // (origin "webmcp") are normalized to the `webmcp:`-prefixed form below
3786
- // so the bridge + session.ts `/resume` keying are identical for both.
3538
+ } else if (payload.type === "await" && payload.toolName) {
3539
+ // Unified LOCAL tool pause for either dispatch kind. The execution
3540
+ // waits for /resume with toolOutputs. Page tools with origin "webmcp"
3541
+ // may carry a bare name, normalized to the internal webmcp: prefix.
3787
3542
  //
3788
3543
  // Upsert a fully-populated tool-variant message so the existing
3789
3544
  // ask_user_question bubble + sheet paths fire. Mark the message with
@@ -3807,9 +3562,9 @@ export class AgentWidgetClient {
3807
3562
  toolCallId ?? (payload.toolId as string) ?? `local-${nextSequence()}`;
3808
3563
  const toolMessage = ensureToolMessage(toolId);
3809
3564
  const rawToolName = payload.toolName as string;
3810
- // `agent_await` page tools arrive with a bare name; synthesize the
3565
+ // Page tools may arrive with a bare name; synthesize the
3811
3566
  // `webmcp:` prefix so isWebMcpToolName (and the bridge's prefix-strip on
3812
- // resume) treat them identically to a flow `step_await`.
3567
+ // resume) treat them identically to a flow `await`.
3813
3568
  const toolName =
3814
3569
  payload.origin === "webmcp" &&
3815
3570
  !isWebMcpToolName(rawToolName)
@@ -3820,7 +3575,7 @@ export class AgentWidgetClient {
3820
3575
  tool.name = toolName;
3821
3576
  tool.args = payload.parameters;
3822
3577
  // WebMCP tools are executed asynchronously by the browser AFTER this
3823
- // `step_await` arrives. Keep them running until session.ts resolves
3578
+ // `await` arrives. Keep them running until session.ts resolves
3824
3579
  // the page tool and records its actual elapsed time. Other local
3825
3580
  // tools (for example ask_user_question) keep the existing complete
3826
3581
  // state because they are waiting for a user interaction, not an
@@ -3829,7 +3584,7 @@ export class AgentWidgetClient {
3829
3584
  tool.chunks = tool.chunks ?? [];
3830
3585
  tool.startedAt =
3831
3586
  tool.startedAt ??
3832
- resolveTimestamp(payload.startedAt ?? payload.timestamp ?? payload.awaitedAt);
3587
+ resolveTimestamp(payload.awaitedAt);
3833
3588
  if (webMcpTool) {
3834
3589
  tool.completedAt = undefined;
3835
3590
  tool.duration = undefined;
@@ -3849,7 +3604,7 @@ export class AgentWidgetClient {
3849
3604
  ...(toolCallId ? { webMcpToolCallId: toolCallId } : {}),
3850
3605
  };
3851
3606
  emitMessage(toolMessage);
3852
- } else if (payloadType === "text_start") {
3607
+ } else if (payload.type === "text_start") {
3853
3608
  // Nested flow-as-tool text (PR #4602): a `parentToolCallId` means this
3854
3609
  // block belongs to a flow running as that tool — record the mapping and
3855
3610
  // leave the top-level assistant bubble untouched (the nested deltas route
@@ -3883,7 +3638,7 @@ export class AgentWidgetClient {
3883
3638
  currentTextBlockId =
3884
3639
  typeof payload.id === "string" ? payload.id : currentTextBlockId;
3885
3640
  pendingTextRaw = "";
3886
- } else if (payloadType === "text_delta") {
3641
+ } else if (payload.type === "text_delta") {
3887
3642
  // Nested flow-as-tool text: route to the parent tool's row, through the
3888
3643
  // same structured-content parser, never the top-level assistant channel.
3889
3644
  const deltaBlockId = typeof payload.id === "string" ? payload.id : null;
@@ -3918,7 +3673,7 @@ export class AgentWidgetClient {
3918
3673
  const assistant = ensureAssistantMessage();
3919
3674
  assistant.agentMetadata = {
3920
3675
  executionId: payload.executionId,
3921
- iteration: payload.iteration,
3676
+ iteration: lastIterationSeen,
3922
3677
  };
3923
3678
  applyTextChunk(assistant, pendingTextRaw, delta, undefined);
3924
3679
  lastAssistantInTurn = assistant;
@@ -3937,13 +3692,13 @@ export class AgentWidgetClient {
3937
3692
  const assistant = ensureAssistantMessage();
3938
3693
  assistant.agentMetadata = {
3939
3694
  executionId: payload.executionId,
3940
- iteration: payload.iteration,
3695
+ iteration: lastIterationSeen,
3941
3696
  turnId: openTurnId ?? undefined,
3942
3697
  agentName: agentExecution?.agentName
3943
3698
  };
3944
3699
  applyTextChunk(assistant, pendingTextRaw, agentDelta, undefined);
3945
3700
  lastAssistantInTurn = assistant;
3946
- } else if (payloadType === "text_complete") {
3701
+ } else if (payload.type === "text_complete") {
3947
3702
  // Nested flow-as-tool text block close: seal its parent-tool-row message.
3948
3703
  const completeBlockId = typeof payload.id === "string" ? payload.id : null;
3949
3704
  if (completeBlockId && nestedBlockParent.has(completeBlockId)) {
@@ -3976,11 +3731,9 @@ export class AgentWidgetClient {
3976
3731
  }
3977
3732
  currentTextBlockId = null;
3978
3733
  pendingTextRaw = "";
3979
- } else if (payloadType === "step_complete") {
3734
+ } else if (payload.type === "step_complete") {
3980
3735
  // Only process completions for prompt steps, not tool/context steps
3981
- const stepType = (payload as any).stepType;
3982
- const executionType = (payload as any).executionType;
3983
- if (stepType === "tool" || executionType === "context") {
3736
+ if (payload.stepType === "tool") {
3984
3737
  // Skip tool-related completions - they're handled by tool_complete
3985
3738
  continue;
3986
3739
  }
@@ -3989,17 +3742,7 @@ export class AgentWidgetClient {
3989
3742
  // event, which the wire encoder folds into a failed `step_complete`
3990
3743
  // — surfaces as a terminal error and finalizes the stream.
3991
3744
  if (payload.success === false) {
3992
- const e = payload.error;
3993
- const message =
3994
- typeof e === "string" && e !== ""
3995
- ? e
3996
- : // Reflect.has, not `in`: the `in` operator inside an arrow body can
3997
- // be minified into a `for(init;;)` head, which Oxc mis-parses and
3998
- // Rolldown (Vite 8) silently emits as an empty chunk. Same
3999
- // [[HasProperty]] semantics. Enforced by scripts/check-dist-no-in-for-init.mjs.
4000
- e != null && typeof e === "object" && Reflect.has(e, "message")
4001
- ? String((e as { message?: unknown }).message ?? "Step failed")
4002
- : "Step failed";
3745
+ const message = payload.error || "Step failed";
4003
3746
  onEvent({ type: "error", error: new Error(message) });
4004
3747
  const finalMsg = assistantMessage as AgentWidgetMessage | null;
4005
3748
  if (finalMsg && finalMsg.streaming) {
@@ -4018,10 +3761,13 @@ export class AgentWidgetClient {
4018
3761
  {
4019
3762
  const sealed = lastSealedFlowBubble;
4020
3763
  lastSealedFlowBubble = null;
4021
- const flowStopReason = (payload as any).stopReason as
3764
+ const flowStopReason = payload.stopReason as
4022
3765
  | StopReasonKind
4023
3766
  | undefined;
4024
- const finalResponse = payload.result?.response;
3767
+ const result = payload.result && typeof payload.result === "object"
3768
+ ? payload.result as Record<string, unknown>
3769
+ : null;
3770
+ const finalResponse = result?.response;
4025
3771
  if (sealed) {
4026
3772
  if (flowStopReason) sealed.stopReason = flowStopReason;
4027
3773
  if (finalResponse !== undefined && finalResponse !== null) {
@@ -4057,21 +3803,17 @@ export class AgentWidgetClient {
4057
3803
  // ================================================================
4058
3804
  // Agent Loop Execution Events
4059
3805
  // ================================================================
4060
- } else if (payloadType === "execution_start") {
3806
+ } else if (payload.type === "execution_start") {
4061
3807
  executionKind = payload.kind === "flow" ? "flow" : "agent";
4062
3808
  executionKindResolved = true;
4063
3809
  if (executionKind === "agent") {
4064
3810
  agentExecution = {
4065
3811
  executionId: payload.executionId,
4066
3812
  agentId: payload.agentId ?? 'virtual',
4067
- agentName: payload.agentName ?? '',
4068
- status: 'running',
4069
- currentIteration: 0,
4070
- maxTurns: payload.maxTurns ?? 1,
4071
- startedAt: resolveTimestamp(payload.startedAt)
3813
+ agentName: payload.agentName ?? ''
4072
3814
  };
4073
3815
  }
4074
- } else if (payloadType === "turn_start") {
3816
+ } else if (payload.type === "turn_start") {
4075
3817
  // Unified collapsed `agent_iteration_*` into a denormalized `iteration`
4076
3818
  // field on the turn (merged spec §2). Reconstruct the per-iteration
4077
3819
  // message boundary the 'separate' renderer keys off: when the iteration
@@ -4079,13 +3821,11 @@ export class AgentWidgetClient {
4079
3821
  const iteration =
4080
3822
  typeof payload.iteration === "number" ? payload.iteration : lastIterationSeen;
4081
3823
  if (iteration !== lastIterationSeen) {
4082
- if (agentExecution) agentExecution.currentIteration = iteration;
4083
3824
  if (iterationDisplay === 'separate' && iteration > 1) {
4084
3825
  const prevMsg = assistantMessage as AgentWidgetMessage | null;
4085
3826
  if (prevMsg) {
4086
3827
  prevMsg.streaming = false;
4087
3828
  emitMessage(prevMsg);
4088
- agentIterationMessages.set(iteration - 1, prevMsg);
4089
3829
  assistantMessage = null;
4090
3830
  }
4091
3831
  }
@@ -4096,10 +3836,10 @@ export class AgentWidgetClient {
4096
3836
  // turn_complete to attach stopReason to the final text segment of the
4097
3837
  // turn even if that segment was sealed by an intervening tool boundary.
4098
3838
  lastAssistantInTurn = null;
4099
- } else if (payloadType === "tool_input_delta") {
3839
+ } else if (payload.type === "tool_input_delta") {
4100
3840
  // Streamed tool arguments (display-only; authoritative args ride
4101
3841
  // tool_input_complete / tool_start).
4102
- const toolId = payload.toolCallId ?? toolContext.lastId;
3842
+ const toolId = payload.toolCallId;
4103
3843
  if (toolId) {
4104
3844
  const toolMessage = toolMessages.get(toolId);
4105
3845
  if (toolMessage?.toolCall) {
@@ -4108,16 +3848,20 @@ export class AgentWidgetClient {
4108
3848
  emitMessage(toolMessage);
4109
3849
  }
4110
3850
  }
4111
- } else if (payloadType === "tool_input_complete") {
4112
- // Authoritative args are set at tool_start; nothing to render here.
4113
- continue;
4114
- } else if (payloadType === "turn_complete") {
3851
+ } else if (payload.type === "tool_input_complete") {
3852
+ if (artifactToolCallIds.has(payload.toolCallId)) continue;
3853
+ const toolMessage = ensureToolMessage(payload.toolCallId);
3854
+ const tool = toolMessage.toolCall!;
3855
+ tool.args = payload.parameters;
3856
+ tool.name = payload.toolName ?? tool.name;
3857
+ emitMessage(toolMessage);
3858
+ } else if (payload.type === "turn_complete") {
4115
3859
  // Reasoning is sealed by its own reasoning_complete on the wire
4116
3860
  // vocabulary; this only attaches the turn-level stopReason to the
4117
3861
  // assistant message produced by this turn. Falls back to
4118
3862
  // lastAssistantInTurn when the bubble was sealed at a tool boundary
4119
3863
  // mid-turn, so the notice still attaches to the final visible segment.
4120
- const turnStopReason = (payload as any).stopReason as
3864
+ const turnStopReason = payload.stopReason as
4121
3865
  | StopReasonKind
4122
3866
  | undefined;
4123
3867
  const stopReasonTarget = assistantMessage ?? lastAssistantInTurn;
@@ -4131,19 +3875,18 @@ export class AgentWidgetClient {
4131
3875
  }
4132
3876
  }
4133
3877
  if (openTurnId === payload.id) openTurnId = null;
4134
- } else if (payloadType === "media_start") {
3878
+ } else if (payload.type === "media_start") {
4135
3879
  // Open a media block; buffer fragments until media_complete.
4136
3880
  const id = String(payload.id);
4137
3881
  mediaBuffers.set(id, {
4138
3882
  mediaType: typeof payload.mediaType === "string" ? payload.mediaType : undefined,
4139
- role: typeof payload.role === "string" ? payload.role : undefined,
4140
3883
  toolCallId: payload.toolCallId,
4141
3884
  parts: [],
4142
3885
  });
4143
- } else if (payloadType === "media_delta") {
3886
+ } else if (payload.type === "media_delta") {
4144
3887
  const buf = mediaBuffers.get(String(payload.id));
4145
3888
  if (buf && typeof payload.delta === "string") buf.parts.push(payload.delta);
4146
- } else if (payloadType === "media_complete") {
3889
+ } else if (payload.type === "media_complete") {
4147
3890
  // Reassemble the buffered media triad into a single AI SDK–aligned
4148
3891
  // `MediaContentPart`, then render it as a synthetic assistant message
4149
3892
  // inserted between the tool bubble and the next text turn:
@@ -4275,17 +4018,14 @@ export class AgentWidgetClient {
4275
4018
  executionId: payload.executionId,
4276
4019
  // Media blocks carry no iteration of their own; stamp the
4277
4020
  // enclosing iteration tracked from turn/tool frames.
4278
- iteration:
4279
- typeof payload.iteration === "number"
4280
- ? payload.iteration
4281
- : lastIterationSeen,
4021
+ iteration: lastIterationSeen,
4282
4022
  },
4283
4023
  };
4284
4024
  emitMessage(mediaMessage);
4285
4025
 
4286
4026
  // Seal any in-flight assistant text bubble before splitting the
4287
4027
  // stream. Without this, an orphan bubble retains `streaming: true`
4288
- // forever: `agent_complete` only finalizes the latest
4028
+ // forever: `execution_complete` only finalizes the latest
4289
4029
  // `assistantMessage`, so the typing/caret indicator would stay on
4290
4030
  // the prior bubble even though no more deltas will arrive.
4291
4031
  const prevAssistant = assistantMessage as AgentWidgetMessage | null;
@@ -4296,21 +4036,14 @@ export class AgentWidgetClient {
4296
4036
  assistantMessage = null;
4297
4037
  assistantMessageRef.current = null;
4298
4038
  }
4299
- } else if (payloadType === "execution_complete") {
4300
- const kind = payload.kind ?? executionKind;
4301
- if (kind === "agent" && agentExecution) {
4302
- agentExecution.status = payload.success ? 'complete' : 'error';
4303
- agentExecution.completedAt = resolveTimestamp(payload.completedAt);
4304
- agentExecution.stopReason = payload.stopReason;
4305
- }
4306
-
4039
+ } else if (payload.type === "execution_complete") {
4307
4040
  // Finalize any still-open assistant message. Per-step reconciliation
4308
4041
  // (step_complete.result.response) normally sealed the flow blocks
4309
4042
  // already; this is the defensive close for an unterminated block, and
4310
4043
  // for flow it runs the final structured extraction off the raw buffer.
4311
4044
  const finalMsg = assistantMessage as AgentWidgetMessage | null;
4312
4045
  if (finalMsg) {
4313
- if (kind === "flow" && finalMsg.streaming !== false) {
4046
+ if (payload.kind === "flow" && finalMsg.streaming !== false) {
4314
4047
  finalizeFlowTextBlock(finalMsg);
4315
4048
  } else {
4316
4049
  finalMsg.streaming = false;
@@ -4327,7 +4060,7 @@ export class AgentWidgetClient {
4327
4060
  // `idle` the dispatch wrappers emit in their `finally` when a durable
4328
4061
  // connection drops mid-stream (durable-reconnect drop detection).
4329
4062
  onEvent({ type: "status", status: "idle", terminal: true });
4330
- } else if (payloadType === "execution_error") {
4063
+ } else if (payload.type === "execution_error") {
4331
4064
  // Terminal failure. The non-terminal `error` is handled
4332
4065
  // separately (recoverable → warn).
4333
4066
  const errorMessage = typeof payload.error === 'string'
@@ -4337,37 +4070,12 @@ export class AgentWidgetClient {
4337
4070
  type: "error",
4338
4071
  error: new Error(errorMessage)
4339
4072
  });
4340
- } else if (payloadType === "ping") {
4073
+ } else if (payload.type === "ping") {
4341
4074
  // Keep-alive heartbeat - no action needed
4342
4075
  // ================================================================
4343
4076
  // Tool Approval Events
4344
4077
  // ================================================================
4345
- } else if (payloadType === "approval_start") {
4346
- const approvalId = payload.approvalId ?? `approval-${nextSequence()}`;
4347
- const approvalMessage: AgentWidgetMessage = {
4348
- id: `approval-${approvalId}`,
4349
- role: "assistant",
4350
- content: "",
4351
- createdAt: new Date().toISOString(),
4352
- streaming: false,
4353
- variant: "approval",
4354
- sequence: nextSequence(),
4355
- approval: {
4356
- id: approvalId,
4357
- status: "pending",
4358
- agentId: agentExecution?.agentId ?? 'virtual',
4359
- executionId: payload.executionId ?? agentExecution?.executionId ?? '',
4360
- toolName: payload.toolName ?? '',
4361
- toolType: payload.toolType,
4362
- description: payload.description ?? `Execute ${payload.toolName ?? 'tool'}`,
4363
- ...(typeof payload.reason === "string" && payload.reason
4364
- ? { reason: payload.reason }
4365
- : {}),
4366
- parameters: payload.parameters,
4367
- },
4368
- };
4369
- emitMessage(approvalMessage);
4370
- } else if (payloadType === "step_await" && payload.awaitReason === "approval_required") {
4078
+ } else if (payload.type === "approval_start") {
4371
4079
  const approvalId = payload.approvalId ?? `approval-${nextSequence()}`;
4372
4080
  const approvalMessage: AgentWidgetMessage = {
4373
4081
  id: `approval-${approvalId}`,
@@ -4392,7 +4100,7 @@ export class AgentWidgetClient {
4392
4100
  },
4393
4101
  };
4394
4102
  emitMessage(approvalMessage);
4395
- } else if (payloadType === "approval_complete") {
4103
+ } else if (payload.type === "approval_complete") {
4396
4104
  const approvalId = payload.approvalId;
4397
4105
  if (approvalId) {
4398
4106
  // Find and update the existing approval message
@@ -4410,20 +4118,20 @@ export class AgentWidgetClient {
4410
4118
  status: (payload.decision as "approved" | "denied") ?? "approved",
4411
4119
  agentId: agentExecution?.agentId ?? 'virtual',
4412
4120
  executionId: payload.executionId ?? agentExecution?.executionId ?? '',
4413
- toolName: payload.toolName ?? '',
4414
- description: payload.description ?? '',
4121
+ toolName: '',
4122
+ description: '',
4415
4123
  resolvedAt: Date.now(),
4416
4124
  },
4417
4125
  };
4418
4126
  emitMessage(existingMessage);
4419
4127
  }
4420
4128
  } else if (
4421
- payloadType === "artifact_start" ||
4422
- payloadType === "artifact_delta" ||
4423
- payloadType === "artifact_update" ||
4424
- payloadType === "artifact_complete"
4129
+ payload.type === "artifact_start" ||
4130
+ payload.type === "artifact_delta" ||
4131
+ payload.type === "artifact_update" ||
4132
+ payload.type === "artifact_complete"
4425
4133
  ) {
4426
- if (payloadType === "artifact_start") {
4134
+ if (payload.type === "artifact_start") {
4427
4135
  const at = payload.artifactType as PersonaArtifactKind;
4428
4136
  const artId = String(payload.id);
4429
4137
  const artTitle = typeof payload.title === "string" ? payload.title : undefined;
@@ -4453,20 +4161,13 @@ export class AgentWidgetClient {
4453
4161
  component: typeof payload.component === "string" ? payload.component : undefined,
4454
4162
  ...(artFile ? { file: artFile } : {})
4455
4163
  });
4456
- // Seed component props from artifact_start when the payload
4457
- // carries them; artifact_update events accumulate the rest below.
4458
- const startProps =
4459
- payload.props && typeof payload.props === "object" && !Array.isArray(payload.props)
4460
- ? { ...(payload.props as Record<string, unknown>) }
4461
- : undefined;
4462
4164
  artifactContent.set(artId, {
4463
4165
  markdown: "",
4464
4166
  title: artTitle,
4465
4167
  file: artFile,
4466
- ...(startProps ? { props: startProps } : {})
4467
4168
  });
4468
- // Insert the in-thread artifact block (skip if already present from
4469
- // transcript_insert). The resolved display mode picks the component:
4169
+ // Insert the in-thread artifact block once per ID.
4170
+ // The resolved display mode picks the component:
4470
4171
  // "card"/"panel" inject the reference card; "inline" injects the
4471
4172
  // inline preview block. Both share the rawContent JSON-component
4472
4173
  // shape so transcript persistence and hydration work unchanged.
@@ -4500,7 +4201,7 @@ export class AgentWidgetClient {
4500
4201
  artifactCardMessages.set(artId, cardMsg);
4501
4202
  emitMessage(cardMsg);
4502
4203
  }
4503
- } else if (payloadType === "artifact_delta") {
4204
+ } else if (payload.type === "artifact_delta") {
4504
4205
  const deltaId = String(payload.id);
4505
4206
  const deltaText = typeof payload.delta === "string" ? payload.delta : String(payload.delta ?? "");
4506
4207
  onEvent({
@@ -4510,7 +4211,7 @@ export class AgentWidgetClient {
4510
4211
  });
4511
4212
  const acc = artifactContent.get(deltaId);
4512
4213
  if (acc) acc.markdown += deltaText;
4513
- } else if (payloadType === "artifact_update") {
4214
+ } else if (payload.type === "artifact_update") {
4514
4215
  const props =
4515
4216
  payload.props && typeof payload.props === "object" && !Array.isArray(payload.props)
4516
4217
  ? (payload.props as Record<string, unknown>)
@@ -4528,7 +4229,7 @@ export class AgentWidgetClient {
4528
4229
  if (updateAcc) {
4529
4230
  updateAcc.props = { ...(updateAcc.props ?? {}), ...props };
4530
4231
  }
4531
- } else if (payloadType === "artifact_complete") {
4232
+ } else if (payload.type === "artifact_complete") {
4532
4233
  const artCompleteId = String(payload.id);
4533
4234
  onEvent({ type: "artifact_complete", id: artCompleteId });
4534
4235
  // Update the inline card to show completed state
@@ -4566,46 +4267,7 @@ export class AgentWidgetClient {
4566
4267
  artifactCardMessages.delete(artCompleteId);
4567
4268
  }
4568
4269
  }
4569
- } else if (payloadType === "transcript_insert") {
4570
- const m = payload.message as Record<string, unknown> | undefined;
4571
- if (!m || typeof m !== "object") {
4572
- continue;
4573
- }
4574
- const id = String(m.id ?? `msg-${nextSequence()}`);
4575
- const roleRaw = m.role;
4576
- const role =
4577
- roleRaw === "user" ? "user" : roleRaw === "system" ? "system" : "assistant";
4578
- const msg: AgentWidgetMessage = {
4579
- id,
4580
- role,
4581
- content: typeof m.content === "string" ? m.content : "",
4582
- rawContent: typeof m.rawContent === "string" ? m.rawContent : undefined,
4583
- createdAt:
4584
- typeof m.createdAt === "string" ? m.createdAt : new Date().toISOString(),
4585
- streaming: m.streaming === true,
4586
- // Omit variant unless the stream specifies it. Do not default to `"assistant"`:
4587
- // that value is truthy and skips the component-directive branch (`!message.variant` in ui.ts).
4588
- ...(typeof m.variant === "string"
4589
- ? { variant: m.variant as AgentWidgetMessage["variant"] }
4590
- : {}),
4591
- sequence: nextSequence()
4592
- };
4593
- emitMessage(msg);
4594
- // Detect artifact references in transcript_insert to prevent duplicate auto-cards
4595
- if (msg.rawContent) {
4596
- try {
4597
- const parsed = JSON.parse(msg.rawContent);
4598
- const refArtId = parsed?.props?.artifactId;
4599
- if (typeof refArtId === "string") {
4600
- artifactIdsWithCards.add(refArtId);
4601
- }
4602
- } catch { /* not JSON or no artifactId */ }
4603
- }
4604
- assistantMessage = null;
4605
- assistantMessageRef.current = null;
4606
- streamParsers.delete(id);
4607
- rawContentBuffers.delete(id);
4608
- } else if (payloadType === "error") {
4270
+ } else if (payload.type === "error") {
4609
4271
  // Unified non-terminal error (merged spec). A bare `error` is
4610
4272
  // recoverable by default — a transient notice such as "rate limited,
4611
4273
  // retrying" — and the execution continues, so it must NOT surface as a
@@ -4631,41 +4293,10 @@ export class AgentWidgetClient {
4631
4293
  }
4632
4294
  onEvent({ type: "status", status: "idle" });
4633
4295
  }
4634
- } else if (
4635
- payloadType === "step_error" ||
4636
- payloadType === "dispatch_error" ||
4637
- payloadType === "flow_error"
4638
- ) {
4639
- let resolvedError: Error | null = null;
4640
- if (payload.error instanceof Error) {
4641
- resolvedError = payload.error;
4642
- } else if (payloadType === "dispatch_error") {
4643
- const msg = payload.message ?? payload.error;
4644
- if (msg != null && msg !== "") {
4645
- resolvedError = new Error(String(msg));
4646
- }
4647
- } else {
4648
- const e = payload.error;
4649
- if (typeof e === "string" && e !== "") {
4650
- resolvedError = new Error(e);
4651
- } else if (e != null && typeof e === "object" && Reflect.has(e, "message")) {
4652
- // Reflect.has, not `in` — see the note on the equivalent guard above.
4653
- resolvedError = new Error(String((e as { message?: unknown }).message ?? e));
4654
- }
4655
- }
4656
4296
 
4657
- if (resolvedError) {
4658
- onEvent({ type: "error", error: resolvedError });
4659
- const finalMsg = assistantMessage as AgentWidgetMessage | null;
4660
- if (finalMsg && finalMsg.streaming) {
4661
- finalMsg.streaming = false;
4662
- emitMessage(finalMsg);
4663
- }
4664
- onEvent({ type: "status", status: "idle" });
4665
- }
4666
4297
  }
4667
4298
  }
4668
- seqReadyQueue.length = 0;
4299
+ eventQueue.length = 0;
4669
4300
  };
4670
4301
 
4671
4302
  // eslint-disable-next-line no-constant-condition
@@ -4712,7 +4343,7 @@ export class AgentWidgetClient {
4712
4343
  advanceCursor();
4713
4344
  continue;
4714
4345
  }
4715
- let payload: any;
4346
+ let payload: unknown;
4716
4347
  try {
4717
4348
  payload = JSON.parse(data);
4718
4349
  } catch (error) {
@@ -4728,8 +4359,12 @@ export class AgentWidgetClient {
4728
4359
  continue;
4729
4360
  }
4730
4361
 
4731
- const payloadType =
4732
- eventType !== "message" ? eventType : payload.type ?? "message";
4362
+ const record = payload !== null && typeof payload === "object" && !Array.isArray(payload)
4363
+ ? payload as Record<string, unknown>
4364
+ : null;
4365
+ const payloadType = eventType !== "message"
4366
+ ? eventType
4367
+ : typeof record?.type === "string" ? record.type : "message";
4733
4368
 
4734
4369
  // Tap: capture raw SSE event for event stream inspector
4735
4370
  this.onSSEEvent?.(payloadType, payload);
@@ -4759,8 +4394,13 @@ export class AgentWidgetClient {
4759
4394
  // The wire is the wire vocabulary; the handler consumes it
4760
4395
  // natively. The stream is single-connection and in order, so each frame
4761
4396
  // drains straight through.
4762
- seqReadyQueue.push({ payloadType, payload });
4763
- drainReadyQueue();
4397
+ if (record) {
4398
+ // The raw tap and custom parser above retain the original payload.
4399
+ // Known native branches use the generated union; unknown event types
4400
+ // fall through without rejecting a custom backend's stream.
4401
+ eventQueue.push({ ...record, type: payloadType } as RuntypeExecutionStreamEvent);
4402
+ drainReadyQueue();
4403
+ }
4764
4404
  advanceCursor();
4765
4405
  }
4766
4406
  }