@runtypelabs/persona 4.21.1 → 4.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) 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-RNNBR5R3.js → approval-ui-K7R5Q5OC.js} +1 -1
  10. package/dist/approval-ui.cjs +2 -2
  11. package/dist/approval-ui.d.cts +221 -16
  12. package/dist/approval-ui.d.ts +221 -16
  13. package/dist/approval-ui.js +2 -2
  14. package/dist/artifacts-ui-F4N6VMKF.js +7 -0
  15. package/dist/artifacts-ui.cjs +1 -1
  16. package/dist/artifacts-ui.d.cts +221 -16
  17. package/dist/artifacts-ui.d.ts +221 -16
  18. package/dist/artifacts-ui.js +1 -1
  19. package/dist/chunk-NQLDCJAP.js +4 -0
  20. package/dist/codegen.cjs +1 -1
  21. package/dist/codegen.js +1 -1
  22. package/dist/context-mentions.d.cts +221 -16
  23. package/dist/context-mentions.d.ts +221 -16
  24. package/dist/event-stream-view-GL3ADRY2.js +1 -0
  25. package/dist/event-stream-view.cjs +1 -1
  26. package/dist/event-stream-view.d.cts +221 -16
  27. package/dist/event-stream-view.d.ts +221 -16
  28. package/dist/event-stream-view.js +1 -1
  29. package/dist/forms-ui.d.cts +234 -25
  30. package/dist/forms-ui.d.ts +234 -25
  31. package/dist/history-view.d.cts +136 -6
  32. package/dist/history-view.d.ts +136 -6
  33. package/dist/index.cjs +70 -70
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +302 -32
  36. package/dist/index.d.ts +302 -32
  37. package/dist/index.global.js +54 -54
  38. package/dist/index.global.js.map +1 -1
  39. package/dist/index.js +69 -69
  40. package/dist/index.js.map +1 -1
  41. package/dist/launcher.global.js +2 -2
  42. package/dist/launcher.global.js.map +1 -1
  43. package/dist/session-reconnect.d.cts +221 -16
  44. package/dist/session-reconnect.d.ts +221 -16
  45. package/dist/smart-dom-reader.d.cts +221 -16
  46. package/dist/smart-dom-reader.d.ts +221 -16
  47. package/dist/testing.cjs +3 -3
  48. package/dist/testing.d.cts +3 -4
  49. package/dist/testing.d.ts +3 -4
  50. package/dist/testing.js +3 -3
  51. package/dist/theme-editor-preview.cjs +73 -73
  52. package/dist/theme-editor-preview.d.cts +221 -16
  53. package/dist/theme-editor-preview.d.ts +221 -16
  54. package/dist/theme-editor-preview.js +51 -51
  55. package/dist/theme-editor.cjs +4 -4
  56. package/dist/theme-editor.d.cts +221 -16
  57. package/dist/theme-editor.d.ts +221 -16
  58. package/dist/theme-editor.js +2 -2
  59. package/dist/theme-reference.cjs +1 -1
  60. package/dist/theme-reference.d.cts +65 -1
  61. package/dist/theme-reference.d.ts +65 -1
  62. package/dist/theme-reference.js +1 -1
  63. package/dist/voice-runtime.d.cts +6 -2
  64. package/dist/voice-runtime.d.ts +6 -2
  65. package/dist/webmcp-polyfill.js +1 -4
  66. package/dist/webmcp-runtime-JI7T4QYF.js +6 -0
  67. package/dist/webmcp-runtime.cjs +2 -2
  68. package/dist/webmcp-runtime.d.cts +34 -2
  69. package/dist/webmcp-runtime.d.ts +34 -2
  70. package/dist/webmcp-runtime.js +2 -2
  71. package/package.json +2 -2
  72. package/src/ask-user-question-tool.ts +1 -1
  73. package/src/client-history-rest.test.ts +2 -1
  74. package/src/client-visitor-history.test.ts +20 -4
  75. package/src/client.composer-options.test.ts +1 -1
  76. package/src/client.interrupt.test.ts +2 -2
  77. package/src/client.test.ts +477 -915
  78. package/src/client.ts +134 -494
  79. package/src/client.unified-tools.test.ts +76 -0
  80. package/src/components/artifact-preview.ts +5 -0
  81. package/src/components/event-stream-badge-theming.test.ts +162 -0
  82. package/src/components/event-stream-view.test.ts +76 -76
  83. package/src/components/event-stream-view.ts +55 -20
  84. package/src/components/tool-bubble.test.ts +12 -0
  85. package/src/components/tool-bubble.ts +13 -2
  86. package/src/event-stream-view-bundle.test.ts +2 -2
  87. package/src/generated/runtype-openapi-contract.ts +137 -1
  88. package/src/index-core.ts +2 -0
  89. package/src/internal/runtype-history-provider.test.ts +2 -2
  90. package/src/runtime/init.registry.test.ts +56 -0
  91. package/src/runtime/init.ts +17 -0
  92. package/src/session-history.test.ts +1 -1
  93. package/src/session.quote.test.ts +1 -1
  94. package/src/session.resubmit-from.test.ts +1 -1
  95. package/src/session.test.ts +5 -5
  96. package/src/session.ts +95 -36
  97. package/src/suggest-replies-tool.ts +1 -1
  98. package/src/testing/mock-stream.test.ts +37 -16
  99. package/src/testing/mock-stream.ts +14 -9
  100. package/src/types/theme.ts +41 -1
  101. package/src/types.ts +57 -15
  102. package/src/ui.ask-user-question-plugin.test.ts +7 -7
  103. package/src/ui.composer-defer-one.test.ts +6 -34
  104. package/src/ui.composer-draft-persistence.test.ts +1 -1
  105. package/src/ui.composer-quote.test.ts +1 -1
  106. package/src/ui.message-history-actions.test.ts +1 -1
  107. package/src/ui.reasoning-display.test.ts +136 -2
  108. package/src/ui.streaming-coalescing.test.ts +10 -13
  109. package/src/ui.ts +101 -35
  110. package/src/ui.typing-indicator.test.ts +216 -0
  111. package/src/ui.voice-custom-provider.test.ts +211 -0
  112. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +1740 -0
  113. package/src/utils/event-stream-buffer.test.ts +13 -13
  114. package/src/utils/event-stream-capture.test.ts +20 -20
  115. package/src/utils/formatting.ts +22 -0
  116. package/src/utils/theme.ts +16 -0
  117. package/src/utils/throughput-tracker.test.ts +100 -142
  118. package/src/utils/throughput-tracker.ts +18 -132
  119. package/src/utils/tokens.ts +59 -0
  120. package/src/utils/voice-support.ts +18 -1
  121. package/src/voice/voice-turn-tracker.ts +18 -0
  122. package/src/webmcp-bridge.test.ts +132 -23
  123. package/src/webmcp-bridge.ts +27 -3
  124. package/src/webmcp-runtime-entry.ts +71 -11
  125. package/dist/artifacts-ui-J6AKUGTS.js +0 -7
  126. package/dist/chunk-HBE7LFNT.js +0 -4
  127. package/dist/event-stream-view-73NWKWVQ.js +0 -1
  128. package/dist/webmcp-runtime-P3CHZRHF.js +0 -6
  129. package/src/utils/__fixtures__/unified-translator.oracle.ts +0 -717
@@ -37,7 +37,7 @@ describe("EventStreamBuffer", () => {
37
37
 
38
38
  it("should push and retrieve events", () => {
39
39
  const buf = new EventStreamBuffer(10);
40
- const evt = makeEvent("step_chunk", 1);
40
+ const evt = makeEvent("text_delta", 1);
41
41
  buf.push(evt);
42
42
  expect(buf.getSize()).toBe(1);
43
43
  expect(buf.getAll()).toEqual([evt]);
@@ -85,30 +85,30 @@ describe("EventStreamBuffer", () => {
85
85
 
86
86
  it("should track unique event types", () => {
87
87
  const buf = new EventStreamBuffer(10);
88
- buf.push(makeEvent("step_chunk", 1));
89
- buf.push(makeEvent("flow_complete", 2));
90
- buf.push(makeEvent("step_chunk", 3));
88
+ buf.push(makeEvent("text_delta", 1));
89
+ buf.push(makeEvent("execution_complete", 2));
90
+ buf.push(makeEvent("text_delta", 3));
91
91
  const types = buf.getEventTypes();
92
- expect(types).toContain("step_chunk");
93
- expect(types).toContain("flow_complete");
92
+ expect(types).toContain("text_delta");
93
+ expect(types).toContain("execution_complete");
94
94
  expect(types).toHaveLength(2);
95
95
  });
96
96
 
97
97
  it("should preserve event types after eviction", () => {
98
98
  const buf = new EventStreamBuffer(3);
99
- buf.push(makeEvent("step_chunk", 1));
99
+ buf.push(makeEvent("text_delta", 1));
100
100
  buf.push(makeEvent("tool_start", 2));
101
- buf.push(makeEvent("flow_complete", 3));
102
- // Evict the step_chunk event
101
+ buf.push(makeEvent("execution_complete", 3));
102
+ // Evict the text_delta event
103
103
  buf.push(makeEvent("tool_end", 4));
104
104
  buf.push(makeEvent("tool_end", 5));
105
- // step_chunk is evicted from the buffer but still tracked in types
105
+ // text_delta is evicted from the buffer but still tracked in types
106
106
  const all = buf.getAll();
107
- expect(all.every(e => e.type !== "step_chunk")).toBe(true);
107
+ expect(all.every(e => e.type !== "text_delta")).toBe(true);
108
108
  const types = buf.getEventTypes();
109
- expect(types).toContain("step_chunk");
109
+ expect(types).toContain("text_delta");
110
110
  expect(types).toContain("tool_start");
111
- expect(types).toContain("flow_complete");
111
+ expect(types).toContain("execution_complete");
112
112
  expect(types).toContain("tool_end");
113
113
  expect(types).toHaveLength(4);
114
114
  });
@@ -45,10 +45,10 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
45
45
  });
46
46
  });
47
47
 
48
- it("should fire onSSEEvent for step_chunk events", async () => {
48
+ it("should fire onSSEEvent for text_delta events", async () => {
49
49
  global.fetch = createMockFetch([
50
50
  sseData({
51
- type: "step_chunk",
51
+ type: "text_delta",
52
52
  id: "step_1",
53
53
  name: "Prompt 1",
54
54
  executionType: "prompt",
@@ -56,7 +56,7 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
56
56
  text: "Hello world"
57
57
  }),
58
58
  sseData({
59
- type: "flow_complete",
59
+ type: "execution_complete",
60
60
  flowId: "flow_1",
61
61
  success: true,
62
62
  duration: 100
@@ -79,11 +79,11 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
79
79
 
80
80
  // Should capture both events
81
81
  const stepChunkEvents = capturedSSEEvents.filter(
82
- (e) => e.eventType === "step_chunk"
82
+ (e) => e.eventType === "text_delta"
83
83
  );
84
84
  expect(stepChunkEvents).toHaveLength(1);
85
85
  expect(stepChunkEvents[0].payload).toMatchObject({
86
- type: "step_chunk",
86
+ type: "text_delta",
87
87
  text: "Hello world"
88
88
  });
89
89
  });
@@ -106,7 +106,7 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
106
106
  completedAt: "2025-01-01T00:00:01.000Z"
107
107
  }),
108
108
  sseData({
109
- type: "flow_complete",
109
+ type: "execution_complete",
110
110
  flowId: "flow_1",
111
111
  success: true,
112
112
  duration: 200
@@ -148,10 +148,10 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
148
148
  });
149
149
  });
150
150
 
151
- it("should fire onSSEEvent for flow_complete events", async () => {
151
+ it("should fire onSSEEvent for execution_complete events", async () => {
152
152
  global.fetch = createMockFetch([
153
153
  sseData({
154
- type: "flow_complete",
154
+ type: "execution_complete",
155
155
  flowId: "flow_1",
156
156
  success: true,
157
157
  duration: 500,
@@ -175,11 +175,11 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
175
175
  );
176
176
 
177
177
  const flowCompleteEvents = capturedSSEEvents.filter(
178
- (e) => e.eventType === "flow_complete"
178
+ (e) => e.eventType === "execution_complete"
179
179
  );
180
180
  expect(flowCompleteEvents).toHaveLength(1);
181
181
  expect(flowCompleteEvents[0].payload).toMatchObject({
182
- type: "flow_complete",
182
+ type: "execution_complete",
183
183
  flowId: "flow_1",
184
184
  success: true,
185
185
  duration: 500
@@ -189,7 +189,7 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
189
189
  it("should fire onSSEEvent for every event in a multi-event stream", async () => {
190
190
  global.fetch = createMockFetch([
191
191
  sseData({
192
- type: "step_chunk",
192
+ type: "text_delta",
193
193
  id: "step_1",
194
194
  name: "Prompt 1",
195
195
  executionType: "prompt",
@@ -197,7 +197,7 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
197
197
  text: "Hello"
198
198
  }),
199
199
  sseData({
200
- type: "step_chunk",
200
+ type: "text_delta",
201
201
  id: "step_1",
202
202
  name: "Prompt 1",
203
203
  executionType: "prompt",
@@ -218,7 +218,7 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
218
218
  duration: 50
219
219
  }),
220
220
  sseData({
221
- type: "flow_complete",
221
+ type: "execution_complete",
222
222
  flowId: "flow_1",
223
223
  success: true,
224
224
  duration: 300
@@ -244,11 +244,11 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
244
244
 
245
245
  const types = capturedSSEEvents.map((e) => e.eventType);
246
246
  expect(types).toEqual([
247
- "step_chunk",
248
- "step_chunk",
247
+ "text_delta",
248
+ "text_delta",
249
249
  "tool_start",
250
250
  "tool_complete",
251
- "flow_complete"
251
+ "execution_complete"
252
252
  ]);
253
253
  });
254
254
 
@@ -257,11 +257,11 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
257
257
  // When SSE has no event prefix, payloadType = payload.type
258
258
  global.fetch = createMockFetch([
259
259
  sseData({
260
- type: "step_chunk",
260
+ type: "text_delta",
261
261
  text: "chunk1"
262
262
  }),
263
263
  sseData({
264
- type: "flow_complete",
264
+ type: "execution_complete",
265
265
  success: true
266
266
  })
267
267
  ]);
@@ -281,8 +281,8 @@ describe("Event Capture Pipeline - onSSEEvent callback", () => {
281
281
  );
282
282
 
283
283
  // Event types should be resolved from payload.type
284
- expect(capturedSSEEvents[0].eventType).toBe("step_chunk");
285
- expect(capturedSSEEvents[1].eventType).toBe("flow_complete");
284
+ expect(capturedSSEEvents[0].eventType).toBe("text_delta");
285
+ expect(capturedSSEEvents[1].eventType).toBe("execution_complete");
286
286
  });
287
287
  });
288
288
 
@@ -52,6 +52,28 @@ export const describeReasonStatus = (reasoning: AgentWidgetReasoning) => {
52
52
  return "";
53
53
  };
54
54
 
55
+ /** Defaults for `features.reasoningDisplay.shortThinkThreshold`. */
56
+ export const DEFAULT_SHORT_THINK_DURATION_MS = 15_000;
57
+ export const DEFAULT_SHORT_THINK_CHARS = 1_200;
58
+
59
+ /**
60
+ * A completed reasoning trace is short when its duration is under the
61
+ * threshold, or (with no reported duration) its streamed text is under the
62
+ * character threshold. Duration is preferred when available.
63
+ */
64
+ export const isShortReasoning = (
65
+ reasoning: AgentWidgetReasoning,
66
+ threshold?: { durationMs?: number; chars?: number }
67
+ ): boolean => {
68
+ const durationThreshold = threshold?.durationMs ?? DEFAULT_SHORT_THINK_DURATION_MS;
69
+ const charsThreshold = threshold?.chars ?? DEFAULT_SHORT_THINK_CHARS;
70
+ if (typeof reasoning.durationMs === "number") {
71
+ return reasoning.durationMs < durationThreshold;
72
+ }
73
+ const chars = (reasoning.chunks ?? []).join("").length;
74
+ return chars < charsThreshold;
75
+ };
76
+
55
77
  export const formatToolDuration = (tool: AgentWidgetToolCall) => {
56
78
  const durationMs =
57
79
  typeof tool.duration === "number"
@@ -128,6 +128,22 @@ const DARK_COMPONENTS: DeepPartial<PersonaTheme> = {
128
128
  foreground: 'palette.colors.primary.300',
129
129
  },
130
130
  },
131
+ // Event-stream badge chips flip to a 900-tone fill with a 300-tone text
132
+ // (primary is the inverted dark ramp, so 800/200 lands the same way).
133
+ // The light defaults are 100-tone fills that read as glowing swatches on
134
+ // a dark panel; every pair here keeps >=4.5:1 text contrast (locked by
135
+ // event-stream-badge-theming.test.ts).
136
+ eventStream: {
137
+ badge: {
138
+ flow: { background: 'palette.colors.success.900', foreground: 'palette.colors.success.300' },
139
+ step: { background: 'palette.colors.primary.800', foreground: 'palette.colors.primary.200' },
140
+ reasoning: { background: 'palette.colors.warning.900', foreground: 'palette.colors.warning.300' },
141
+ tool: { background: 'palette.colors.purple.900', foreground: 'palette.colors.purple.300' },
142
+ agent: { background: 'palette.colors.teal.900', foreground: 'palette.colors.teal.300' },
143
+ error: { background: 'palette.colors.error.900', foreground: 'palette.colors.error.300' },
144
+ default: { background: 'palette.colors.gray.800', foreground: 'palette.colors.gray.300' },
145
+ },
146
+ },
131
147
  history: {
132
148
  // error-400: the light-surface error-600 red lands ~3:1 on dark
133
149
  // surfaces, under the 4.5:1 AA floor for the 14px destructive labels.
@@ -40,9 +40,9 @@ describe("ThroughputTracker: live estimate", () => {
40
40
  const h = makeTracker();
41
41
 
42
42
  // First delta: lazily starts the run. Duration is 0 so no rate yet.
43
- h.at(1000).processEvent("step_delta", {
44
- type: "step_delta",
45
- text: text(40),
43
+ h.at(1000).processEvent("text_delta", {
44
+ type: "text_delta",
45
+ delta: text(40),
46
46
  });
47
47
  let m = h.metric();
48
48
  expect(m.status).toBe("running");
@@ -51,7 +51,7 @@ describe("ThroughputTracker: live estimate", () => {
51
51
  expect(m.tokensPerSecond).toBeUndefined();
52
52
 
53
53
  // Second delta 1s later via a different visible event type.
54
- h.at(2000).processEvent("chunk", { type: "chunk", text: text(40) });
54
+ h.at(2000).processEvent("text_delta", { type: "text_delta", delta: text(40) });
55
55
  m = h.metric();
56
56
  expect(m.outputTokens).toBe(20);
57
57
  expect(m.durationMs).toBe(1000);
@@ -59,17 +59,15 @@ describe("ThroughputTracker: live estimate", () => {
59
59
  expect(m.tokensPerSecond).toBeCloseTo(20);
60
60
  });
61
61
 
62
- it("counts agent_turn_delta text deltas as visible output", () => {
62
+ it("counts text_delta text deltas as visible output", () => {
63
63
  const h = makeTracker();
64
- h.at(0).processEvent("agent_turn_delta", {
65
- type: "agent_turn_delta",
66
- contentType: "text",
67
- text: text(40),
64
+ h.at(0).processEvent("text_delta", {
65
+ type: "text_delta",
66
+ delta: text(40),
68
67
  });
69
- h.at(1000).processEvent("agent_turn_delta", {
70
- type: "agent_turn_delta",
71
- contentType: "text",
72
- text: text(40),
68
+ h.at(1000).processEvent("text_delta", {
69
+ type: "text_delta",
70
+ delta: text(40),
73
71
  });
74
72
  const m = h.metric();
75
73
  expect(m.status).toBe("running");
@@ -77,22 +75,11 @@ describe("ThroughputTracker: live estimate", () => {
77
75
  expect(m.tokensPerSecond).toBeCloseTo(20);
78
76
  });
79
77
 
80
- it("ignores agent_turn_delta with a missing contentType (matches client)", () => {
78
+ it("ignores text_delta without a string delta", () => {
81
79
  const h = makeTracker();
82
- // The client only renders agent text when contentType === "text"; a delta
83
- // without a contentType is not visible output, so it must not be counted.
84
- h.at(0).processEvent("agent_turn_delta", {
85
- type: "agent_turn_delta",
86
- text: text(400),
87
- });
80
+ h.at(0).processEvent("text_delta", { type: "text_delta", text: text(400) });
88
81
  expect(h.metric().status).toBe("idle");
89
-
90
- // An explicit text delta is still counted.
91
- h.at(1000).processEvent("agent_turn_delta", {
92
- type: "agent_turn_delta",
93
- contentType: "text",
94
- text: text(40),
95
- });
82
+ h.at(1000).processEvent("text_delta", { type: "text_delta", delta: text(40) });
96
83
  expect(h.metric().outputTokens).toBe(10);
97
84
  });
98
85
  });
@@ -100,17 +87,16 @@ describe("ThroughputTracker: live estimate", () => {
100
87
  describe("ThroughputTracker: exact usage finalization", () => {
101
88
  it("prefers exact output tokens from the terminal event over the estimate", () => {
102
89
  const h = makeTracker();
103
- h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
104
- h.at(2000).processEvent("agent_turn_delta", {
105
- type: "agent_turn_delta",
106
- contentType: "text",
107
- text: text(40),
90
+ h.at(1000).processEvent("text_delta", { type: "text_delta", delta: text(40) });
91
+ h.at(2000).processEvent("text_delta", {
92
+ type: "text_delta",
93
+ delta: text(40),
108
94
  });
109
95
 
110
96
  // estimate so far would be 20 tokens; terminal usage overrides it.
111
- h.at(2000).processEvent("flow_complete", {
112
- type: "flow_complete",
113
- usage: { outputTokens: 123 },
97
+ h.at(2000).processEvent("execution_complete", {
98
+ type: "execution_complete",
99
+ totalTokens: { input: 0, output: 123 },
114
100
  });
115
101
 
116
102
  const m = h.metric();
@@ -123,18 +109,18 @@ describe("ThroughputTracker: exact usage finalization", () => {
123
109
 
124
110
  it("accumulates exact usage from intermediate completes and uses it on terminal", () => {
125
111
  const h = makeTracker();
126
- h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
127
- h.at(1500).processEvent("step_complete", {
128
- type: "step_complete",
129
- result: { tokens: { output: 50 } },
112
+ h.at(1000).processEvent("text_delta", { type: "text_delta", delta: text(40) });
113
+ h.at(1500).processEvent("turn_complete", {
114
+ type: "turn_complete",
115
+ tokens: { input: 0, output: 50 },
130
116
  });
131
- h.at(2000).processEvent("agent_turn_complete", {
132
- type: "agent_turn_complete",
133
- usage: { outputTokens: 30 },
117
+ h.at(2000).processEvent("turn_complete", {
118
+ type: "turn_complete",
119
+ tokens: { input: 0, output: 30 },
134
120
  });
135
121
 
136
122
  // Terminal carries no usage of its own → fall back to accumulated 80.
137
- h.at(2000).processEvent("agent_complete", { type: "agent_complete" });
123
+ h.at(2000).processEvent("execution_complete", { type: "execution_complete" });
138
124
 
139
125
  const m = h.metric();
140
126
  expect(m.status).toBe("complete");
@@ -144,12 +130,12 @@ describe("ThroughputTracker: exact usage finalization", () => {
144
130
 
145
131
  it("falls back to provider execution time when the streamed window is too short", () => {
146
132
  const h = makeTracker();
147
- h.at(1000).processEvent("flow_start", { type: "flow_start" });
148
- h.at(1100).processEvent("step_delta", { type: "step_delta", text: text(40) });
149
- // streamed window = 50ms (< 250ms threshold) → use executionTime.
150
- h.at(1150).processEvent("flow_complete", {
151
- type: "flow_complete",
152
- executionTime: 5000,
133
+ h.at(1000).processEvent("execution_start", { type: "execution_start" });
134
+ h.at(1100).processEvent("text_delta", { type: "text_delta", delta: text(40) });
135
+ // streamed window = 50ms (< 250ms threshold) → use durationMs.
136
+ h.at(1150).processEvent("execution_complete", {
137
+ type: "execution_complete",
138
+ durationMs: 5000,
153
139
  });
154
140
 
155
141
  const m = h.metric();
@@ -161,72 +147,22 @@ describe("ThroughputTracker: exact usage finalization", () => {
161
147
  });
162
148
 
163
149
  describe("ThroughputTracker: non-visible deltas ignored", () => {
164
- it("ignores agent thinking and tool_input deltas", () => {
165
- const h = makeTracker();
166
-
167
- h.at(1000).processEvent("agent_turn_delta", {
168
- type: "agent_turn_delta",
169
- contentType: "thinking",
170
- text: text(400),
171
- });
172
- expect(h.metric().status).toBe("idle");
173
-
174
- h.at(1000).processEvent("agent_turn_delta", {
175
- type: "agent_turn_delta",
176
- contentType: "tool_input",
177
- text: text(400),
178
- });
179
- expect(h.metric().status).toBe("idle");
180
-
181
- // Only the visible text delta is counted.
182
- h.at(1000).processEvent("agent_turn_delta", {
183
- type: "agent_turn_delta",
184
- contentType: "text",
185
- text: text(40),
186
- });
187
- h.at(2000).processEvent("agent_turn_delta", {
188
- type: "agent_turn_delta",
189
- contentType: "thinking",
190
- text: text(400),
191
- });
192
-
193
- const m = h.metric();
194
- expect(m.status).toBe("running");
195
- expect(m.outputTokens).toBe(10); // thinking text excluded
196
- });
197
-
198
- it("ignores tool and context step deltas", () => {
199
- const h = makeTracker();
200
-
201
- h.at(1000).processEvent("step_delta", {
202
- type: "step_delta",
203
- stepType: "tool",
204
- text: text(400),
205
- });
206
- expect(h.metric().status).toBe("idle");
207
-
208
- h.at(1000).processEvent("step_delta", {
209
- type: "step_delta",
210
- executionType: "context",
211
- text: text(400),
212
- });
213
- expect(h.metric().status).toBe("idle");
214
-
215
- // A prompt-step delta is counted.
216
- h.at(1000).processEvent("step_delta", {
217
- type: "step_delta",
218
- stepType: "prompt",
219
- text: text(40),
220
- });
221
- expect(h.metric().status).toBe("running");
222
- expect(h.metric().outputTokens).toBe(10);
223
- });
150
+ it.each(["reasoning_delta", "tool_input_delta", "tool_output_delta", "media_delta"])(
151
+ "does not count %s as visible text", (type) => {
152
+ const h = makeTracker();
153
+ h.at(1000).processEvent(type, { type, delta: text(400) });
154
+ expect(h.metric().status).toBe("idle");
155
+ h.at(1500).processEvent("text_delta", { type: "text_delta", delta: text(40) });
156
+ h.at(2000).processEvent(type, { type, delta: text(400) });
157
+ expect(h.metric().outputTokens).toBe(10);
158
+ }
159
+ );
224
160
  });
225
161
 
226
162
  describe("ThroughputTracker: intermediate completes do not finalize", () => {
227
- it("keeps the run running across step_complete / agent_turn_complete", () => {
163
+ it("keeps the run running across step_complete / turn_complete", () => {
228
164
  const h = makeTracker();
229
- h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
165
+ h.at(1000).processEvent("text_delta", { type: "text_delta", delta: text(40) });
230
166
 
231
167
  h.at(1500).processEvent("step_complete", {
232
168
  type: "step_complete",
@@ -234,29 +170,29 @@ describe("ThroughputTracker: intermediate completes do not finalize", () => {
234
170
  });
235
171
  expect(h.metric().status).toBe("running");
236
172
 
237
- h.at(1800).processEvent("agent_turn_complete", {
238
- type: "agent_turn_complete",
239
- usage: { outputTokens: 5 },
173
+ h.at(1800).processEvent("turn_complete", {
174
+ type: "turn_complete",
175
+ tokens: { input: 0, output: 5 },
240
176
  });
241
177
  expect(h.metric().status).toBe("running");
242
178
 
243
- h.at(2000).processEvent("flow_complete", { type: "flow_complete" });
179
+ h.at(2000).processEvent("execution_complete", { type: "execution_complete" });
244
180
  expect(h.metric().status).toBe("complete");
245
181
  });
246
182
  });
247
183
 
248
184
  describe("ThroughputTracker: error handling", () => {
249
- it.each(["step_error", "flow_error", "agent_error", "error"])(
185
+ it.each(["execution_error", "error"])(
250
186
  "marks the metric unavailable on %s",
251
187
  (errorType: string) => {
252
188
  const h = makeTracker();
253
- h.at(1000).processEvent("step_delta", {
254
- type: "step_delta",
255
- text: text(40),
189
+ h.at(1000).processEvent("text_delta", {
190
+ type: "text_delta",
191
+ delta: text(40),
256
192
  });
257
193
  expect(h.metric().status).toBe("running");
258
194
 
259
- h.at(1500).processEvent(errorType, { type: errorType });
195
+ h.at(1500).processEvent(errorType, { type: errorType, recoverable: false });
260
196
  expect(h.metric().status).toBe("error");
261
197
  expect(h.metric().tokensPerSecond).toBeUndefined();
262
198
  }
@@ -264,16 +200,26 @@ describe("ThroughputTracker: error handling", () => {
264
200
 
265
201
  it("treats a bare non-object error payload as an error", () => {
266
202
  const h = makeTracker();
267
- h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
268
- h.at(1500).processEvent("error", "boom");
203
+ h.at(1000).processEvent("text_delta", { type: "text_delta", delta: text(40) });
204
+ h.at(1500).processEvent("execution_error", "boom");
269
205
  expect(h.metric().status).toBe("error");
270
206
  });
271
207
 
208
+ it.each([undefined, true])("keeps running after a recoverable error (%s)", (recoverable) => {
209
+ const h = makeTracker();
210
+ h.at(1000).processEvent("text_delta", { delta: text(40) });
211
+ h.at(1200).processEvent("error", { error: "Retrying", recoverable });
212
+ expect(h.metric().status).toBe("running");
213
+ h.at(1500).processEvent("text_delta", { delta: text(40) });
214
+ h.at(2000).processEvent("execution_complete", { totalTokens: { input: 0, output: 25 } });
215
+ expect(h.metric()).toMatchObject({ status: "complete", outputTokens: 25 });
216
+ });
217
+
272
218
  it("ignores terminal/error events when no run is active", () => {
273
219
  const h = makeTracker();
274
- h.at(1000).processEvent("flow_complete", { type: "flow_complete" });
220
+ h.at(1000).processEvent("execution_complete", { type: "execution_complete" });
275
221
  expect(h.metric().status).toBe("idle");
276
- h.at(1000).processEvent("flow_error", { type: "flow_error" });
222
+ h.at(1000).processEvent("execution_error", { type: "execution_error" });
277
223
  expect(h.metric().status).toBe("idle");
278
224
  });
279
225
  });
@@ -281,19 +227,19 @@ describe("ThroughputTracker: error handling", () => {
281
227
  describe("ThroughputTracker: reset & re-run", () => {
282
228
  it("reset() returns to idle", () => {
283
229
  const h = makeTracker();
284
- h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
230
+ h.at(1000).processEvent("text_delta", { type: "text_delta", delta: text(40) });
285
231
  h.tracker.reset();
286
232
  expect(h.metric()).toEqual({ status: "idle" });
287
233
  });
288
234
 
289
235
  it("starts a fresh run after a completed one", () => {
290
236
  const h = makeTracker();
291
- h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(80) });
292
- h.at(2000).processEvent("flow_complete", { type: "flow_complete" });
237
+ h.at(1000).processEvent("text_delta", { type: "text_delta", delta: text(80) });
238
+ h.at(2000).processEvent("execution_complete", { type: "execution_complete" });
293
239
  expect(h.metric().status).toBe("complete");
294
240
 
295
241
  // New stream → accumulation resets, does not carry the prior 20 tokens.
296
- h.at(3000).processEvent("step_delta", { type: "step_delta", text: text(40) });
242
+ h.at(3000).processEvent("text_delta", { type: "text_delta", delta: text(40) });
297
243
  const m = h.metric();
298
244
  expect(m.status).toBe("running");
299
245
  expect(m.outputTokens).toBe(10);
@@ -302,15 +248,15 @@ describe("ThroughputTracker: reset & re-run", () => {
302
248
  it("resets a stale run on the next request's start event (no bleed)", () => {
303
249
  const h = makeTracker();
304
250
  // First request streams visible output but never terminates (e.g. the
305
- // user cancels mid-stream: no flow_complete / error frame is emitted).
306
- h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(120) });
251
+ // user cancels mid-stream: no execution_complete / error frame is emitted).
252
+ h.at(1000).processEvent("text_delta", { type: "text_delta", delta: text(120) });
307
253
  expect(h.metric().outputTokens).toBe(30);
308
254
  expect(h.metric().status).toBe("running");
309
255
 
310
- // Next request begins. Its flow_start must discard the stale run so the
256
+ // Next request begins. Its execution_start must discard the stale run so the
311
257
  // prior request's 30 tokens don't bleed into this one.
312
- h.at(5000).processEvent("flow_start", { type: "flow_start" });
313
- h.at(5200).processEvent("step_delta", { type: "step_delta", text: text(40) });
258
+ h.at(5000).processEvent("execution_start", { type: "execution_start" });
259
+ h.at(5200).processEvent("text_delta", { type: "text_delta", delta: text(40) });
314
260
  const m = h.metric();
315
261
  expect(m.status).toBe("running");
316
262
  expect(m.outputTokens).toBe(10); // only the new request's text
@@ -318,12 +264,12 @@ describe("ThroughputTracker: reset & re-run", () => {
318
264
 
319
265
  it("does not reset between per-step starts within one request", () => {
320
266
  const h = makeTracker();
321
- h.at(1000).processEvent("flow_start", { type: "flow_start" });
267
+ h.at(1000).processEvent("execution_start", { type: "execution_start" });
322
268
  h.at(1100).processEvent("step_start", { type: "step_start" });
323
- h.at(1200).processEvent("step_delta", { type: "step_delta", text: text(40) });
269
+ h.at(1200).processEvent("text_delta", { type: "text_delta", delta: text(40) });
324
270
  // A second step within the same request must not restart accumulation.
325
271
  h.at(1300).processEvent("step_start", { type: "step_start" });
326
- h.at(1400).processEvent("step_delta", { type: "step_delta", text: text(40) });
272
+ h.at(1400).processEvent("text_delta", { type: "text_delta", delta: text(40) });
327
273
  expect(h.metric().outputTokens).toBe(20);
328
274
  });
329
275
  });
@@ -331,12 +277,12 @@ describe("ThroughputTracker: reset & re-run", () => {
331
277
  describe("ThroughputTracker: exact usage never drops mid-run", () => {
332
278
  it("keeps exact tokens as a floor when later steps stream more text", () => {
333
279
  const h = makeTracker();
334
- h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
280
+ h.at(1000).processEvent("text_delta", { type: "text_delta", delta: text(40) });
335
281
 
336
282
  // Step 1 reports exact usage; the running total switches to it.
337
- h.at(1500).processEvent("step_complete", {
338
- type: "step_complete",
339
- result: { tokens: { output: 50 } },
283
+ h.at(1500).processEvent("turn_complete", {
284
+ type: "turn_complete",
285
+ tokens: { input: 0, output: 50 },
340
286
  });
341
287
  let m = h.metric();
342
288
  expect(m.outputTokens).toBe(50);
@@ -344,7 +290,7 @@ describe("ThroughputTracker: exact usage never drops mid-run", () => {
344
290
 
345
291
  // Step 2 streams more visible text: the total must grow from 50, not
346
292
  // collapse back to a bare 10-token estimate of the new text.
347
- h.at(2000).processEvent("step_delta", { type: "step_delta", text: text(40) });
293
+ h.at(2000).processEvent("text_delta", { type: "text_delta", delta: text(40) });
348
294
  m = h.metric();
349
295
  expect(m.outputTokens).toBe(60); // 50 exact + 10 estimated
350
296
  expect(m.source).toBe("usage");
@@ -354,7 +300,7 @@ describe("ThroughputTracker: exact usage never drops mid-run", () => {
354
300
  describe("ThroughputTracker: live rate decays while paused", () => {
355
301
  it("recomputes duration/tok-s from the clock between events", () => {
356
302
  const h = makeTracker();
357
- h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(400) });
303
+ h.at(1000).processEvent("text_delta", { type: "text_delta", delta: text(400) });
358
304
  // 100 tokens over a 1s window read at t=2000 → ~100 tok/s.
359
305
  h.at(2000);
360
306
  expect(h.metric().tokensPerSecond).toBeCloseTo(100);
@@ -364,3 +310,15 @@ describe("ThroughputTracker: live rate decays while paused", () => {
364
310
  expect(h.metric().tokensPerSecond).toBeCloseTo(25);
365
311
  });
366
312
  });
313
+
314
+
315
+ it("does not mistake total token counts or user result data for output usage", () => {
316
+ const h = makeTracker();
317
+ h.at(1000).processEvent("text_delta", { type: "text_delta", delta: text(40) });
318
+ h.at(1500).processEvent("step_complete", {
319
+ type: "step_complete", tokensUsed: 900,
320
+ result: { tokens: { output: 500 }, usage: { outputTokens: 500 } },
321
+ });
322
+ h.at(2000).processEvent("execution_complete", { type: "execution_complete", totalTokensUsed: 900 });
323
+ expect(h.metric()).toMatchObject({ outputTokens: 10, source: "estimate" });
324
+ });