@runtypelabs/persona 4.22.0 → 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 (93) 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 +166 -27
  29. package/dist/index.d.ts +166 -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/session-reconnect.d.cts +148 -14
  35. package/dist/session-reconnect.d.ts +148 -14
  36. package/dist/smart-dom-reader.d.cts +148 -14
  37. package/dist/smart-dom-reader.d.ts +148 -14
  38. package/dist/testing.cjs +3 -3
  39. package/dist/testing.d.cts +3 -4
  40. package/dist/testing.d.ts +3 -4
  41. package/dist/testing.js +3 -3
  42. package/dist/theme-editor-preview.cjs +71 -71
  43. package/dist/theme-editor-preview.d.cts +148 -14
  44. package/dist/theme-editor-preview.d.ts +148 -14
  45. package/dist/theme-editor-preview.js +48 -48
  46. package/dist/theme-editor.d.cts +148 -14
  47. package/dist/theme-editor.d.ts +148 -14
  48. package/dist/voice-runtime.d.cts +6 -2
  49. package/dist/voice-runtime.d.ts +6 -2
  50. package/package.json +1 -1
  51. package/src/ask-user-question-tool.ts +1 -1
  52. package/src/client-history-rest.test.ts +2 -1
  53. package/src/client-visitor-history.test.ts +20 -4
  54. package/src/client.composer-options.test.ts +1 -1
  55. package/src/client.interrupt.test.ts +2 -2
  56. package/src/client.test.ts +477 -915
  57. package/src/client.ts +134 -494
  58. package/src/client.unified-tools.test.ts +76 -0
  59. package/src/components/event-stream-badge-theming.test.ts +3 -0
  60. package/src/components/event-stream-view.test.ts +76 -76
  61. package/src/components/event-stream-view.ts +5 -0
  62. package/src/components/tool-bubble.test.ts +12 -0
  63. package/src/components/tool-bubble.ts +13 -2
  64. package/src/event-stream-view-bundle.test.ts +2 -2
  65. package/src/generated/runtype-openapi-contract.ts +129 -1
  66. package/src/internal/runtype-history-provider.test.ts +2 -2
  67. package/src/session-history.test.ts +1 -1
  68. package/src/session.quote.test.ts +1 -1
  69. package/src/session.resubmit-from.test.ts +1 -1
  70. package/src/session.test.ts +5 -5
  71. package/src/session.ts +95 -36
  72. package/src/suggest-replies-tool.ts +1 -1
  73. package/src/testing/mock-stream.test.ts +37 -16
  74. package/src/testing/mock-stream.ts +14 -9
  75. package/src/types.ts +19 -14
  76. package/src/ui.ask-user-question-plugin.test.ts +7 -7
  77. package/src/ui.composer-defer-one.test.ts +6 -34
  78. package/src/ui.composer-draft-persistence.test.ts +1 -1
  79. package/src/ui.composer-quote.test.ts +1 -1
  80. package/src/ui.message-history-actions.test.ts +1 -1
  81. package/src/ui.streaming-coalescing.test.ts +10 -13
  82. package/src/ui.ts +67 -32
  83. package/src/ui.typing-indicator.test.ts +216 -0
  84. package/src/ui.voice-custom-provider.test.ts +211 -0
  85. package/src/utils/event-stream-buffer.test.ts +13 -13
  86. package/src/utils/event-stream-capture.test.ts +20 -20
  87. package/src/utils/throughput-tracker.test.ts +100 -142
  88. package/src/utils/throughput-tracker.ts +18 -132
  89. package/src/utils/voice-support.ts +18 -1
  90. package/src/voice/voice-turn-tracker.ts +18 -0
  91. package/src/webmcp-bridge.ts +1 -1
  92. package/dist/event-stream-view-FKWDY3UB.js +0 -1
  93. package/src/utils/__fixtures__/unified-translator.oracle.ts +0 -717
@@ -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
+ });
@@ -12,7 +12,7 @@
12
12
  // then prefers exact provider usage (output tokens) when terminal events carry
13
13
  // it. A run starts when the stream starts (or lazily on the first visible
14
14
  // delta), stays "running" across intermediate step/turn completions, and only
15
- // finalizes on terminal `flow_complete` / `agent_complete`. Stream errors mark
15
+ // finalizes on terminal `execution_complete`. Stream errors mark
16
16
  // the metric unavailable rather than leaving it stuck "running".
17
17
 
18
18
  export type ThroughputMetricStatus = "idle" | "running" | "complete" | "error";
@@ -47,47 +47,10 @@ interface ThroughputRunStats {
47
47
  // execution time or whole-request duration instead.
48
48
  const THROUGHPUT_MIN_DURATION_MS = 250;
49
49
 
50
- // Request-level lifecycle events: each marks the beginning of a NEW request.
51
- // The SSE tap fires for every payload type regardless of whether the client has
52
- // a handler for it, so any of these that the server emits starts the run with
53
- // an accurate `startedAt` (capturing time-to-first-token). These RESET any run
54
- // already in progress, so a prior stream that ended without a terminal/error
55
- // frame (e.g. `session.cancel()`) doesn't bleed its tokens into the next one.
56
- const REQUEST_START_EVENTS = new Set([
57
- "flow_start",
58
- "flow_run_start",
59
- "agent_start",
60
- "dispatch_start",
61
- "run_start",
62
- ]);
63
-
64
- // Per-step markers that fire repeatedly WITHIN a single request (a flow emits
65
- // one per step). These only lazily begin a run: they must never reset, or a
66
- // multi-step response would restart the metric between steps. If no request- or
67
- // step-start event is emitted, the first visible delta lazily starts the run.
68
- const STEP_START_EVENTS = new Set(["step_start", "execution_start"]);
69
-
70
- const VISIBLE_DELTA_EVENTS = new Set([
71
- "step_delta",
72
- "step_chunk",
73
- "chunk",
74
- "agent_turn_delta",
75
- ]);
76
-
77
- const INTERMEDIATE_COMPLETE_EVENTS = new Set([
78
- "step_complete",
79
- "agent_turn_complete",
80
- ]);
81
-
82
- const TERMINAL_COMPLETE_EVENTS = new Set(["flow_complete", "agent_complete"]);
83
-
84
- const ERROR_EVENTS = new Set([
85
- "step_error",
86
- "flow_error",
87
- "agent_error",
88
- "dispatch_error",
89
- "error",
90
- ]);
50
+ // Execution starts reset a stale/cancelled run; step and turn starts only
51
+ // lazily initialize it, since they can repeat within the same execution.
52
+ const STEP_START_EVENTS = new Set(["step_start", "turn_start"]);
53
+ const INTERMEDIATE_COMPLETE_EVENTS = new Set(["step_complete", "turn_complete"]);
91
54
 
92
55
  const isRecord = (value: unknown): value is Record<string, unknown> =>
93
56
  typeof value === "object" && value !== null && !Array.isArray(value);
@@ -134,91 +97,15 @@ function resolveEventType(
134
97
  return typeof payload.type === "string" ? payload.type : eventType;
135
98
  }
136
99
 
137
- function getTextDelta(payload: Record<string, unknown>): string {
138
- if (typeof payload.text === "string") return payload.text;
139
- if (typeof payload.delta === "string") return payload.delta;
140
- if (typeof payload.content === "string") return payload.content;
141
- if (typeof payload.chunk === "string") return payload.chunk;
142
- return "";
143
- }
144
-
145
- /**
146
- * Only count visible model output.
147
- *
148
- * For `agent_turn_delta`, count only a contentType of exactly `text` as
149
- * visible, matching the client renderer (which appends streaming assistant
150
- * text only when `contentType === "text"`); `thinking`, `tool_input`, any
151
- * other value, and a missing contentType are ignored so throughput never
152
- * includes deltas the chat UI doesn't render.
153
- *
154
- * For `step_delta` / `step_chunk`, skip tool and context steps: those carry
155
- * tool I/O, not model-visible text: mirroring the widget's own renderer.
156
- */
157
- function isVisibleTextDelta(
158
- type: string,
159
- payload: Record<string, unknown>
160
- ): boolean {
161
- if (type === "step_delta" || type === "step_chunk") {
162
- return payload.stepType !== "tool" && payload.executionType !== "context";
163
- }
164
-
165
- if (type !== "agent_turn_delta") return true;
166
-
167
- const contentType =
168
- typeof payload.contentType === "string"
169
- ? payload.contentType
170
- : typeof payload.content_type === "string"
171
- ? payload.content_type
172
- : undefined;
173
-
174
- return contentType === "text";
175
- }
176
-
177
- /** Extract exact output tokens from a variety of usage payload shapes. */
100
+ /** Exact output counts are carried by turn and execution completions. */
178
101
  function getOutputTokens(payload: Record<string, unknown>): number | undefined {
179
- const result = getRecord(payload, "result");
180
- const candidates = [
181
- getRecord(payload, "tokens"),
182
- getRecord(payload, "totalTokens"),
183
- result ? getRecord(result, "tokens") : undefined,
184
- getRecord(payload, "usage"),
185
- result ? getRecord(result, "usage") : undefined,
186
- ];
187
-
188
- for (const candidate of candidates) {
189
- if (!candidate) continue;
190
- const outputTokens =
191
- toFiniteNumber(candidate.output) ??
192
- toFiniteNumber(candidate.outputTokens) ??
193
- toFiniteNumber(candidate.completionTokens);
194
- if (outputTokens !== undefined) return outputTokens;
195
- }
196
-
197
- return (
198
- toFiniteNumber(payload.outputTokens) ??
199
- toFiniteNumber(payload.completionTokens) ??
200
- (result
201
- ? (toFiniteNumber(result.outputTokens) ??
202
- toFiniteNumber(result.completionTokens))
203
- : undefined)
204
- );
102
+ const tokens = getRecord(payload, "tokens") ?? getRecord(payload, "totalTokens");
103
+ return tokens ? toFiniteNumber(tokens.output) : undefined;
205
104
  }
206
105
 
207
- /** Extract provider execution time (ms) from a variety of payload shapes. */
208
- function getExecutionTimeMs(
209
- payload: Record<string, unknown>
210
- ): number | undefined {
211
- const result = getRecord(payload, "result");
212
- return (
213
- toFiniteNumber(payload.executionTime) ??
214
- toFiniteNumber(payload.executionTimeMs) ??
215
- toFiniteNumber(payload.execution_time) ??
216
- toFiniteNumber(payload.duration) ??
217
- (result
218
- ? (toFiniteNumber(result.executionTime) ??
219
- toFiniteNumber(result.executionTimeMs))
220
- : undefined)
221
- );
106
+ /** Current step/execution completions report elapsed milliseconds as durationMs. */
107
+ function getExecutionTimeMs(payload: Record<string, unknown>): number | undefined {
108
+ return toFiniteNumber(payload.durationMs);
222
109
  }
223
110
 
224
111
  function defaultClock(): number {
@@ -286,8 +173,8 @@ export class ThroughputTracker {
286
173
 
287
174
  processEvent(eventType: string, payload: unknown): void {
288
175
  if (!isRecord(payload)) {
289
- // Non-object payloads can still signal lifecycle (e.g. bare "error").
290
- if (ERROR_EVENTS.has(eventType) && this.run) {
176
+ // Non-object payloads can still signal lifecycle (e.g. bare "execution_error").
177
+ if (eventType === "execution_error" && this.run) {
291
178
  this.run = null;
292
179
  this.metric = { status: "error" };
293
180
  }
@@ -297,7 +184,7 @@ export class ThroughputTracker {
297
184
  const type = resolveEventType(eventType, payload);
298
185
  const now = this.now();
299
186
 
300
- if (REQUEST_START_EVENTS.has(type)) {
187
+ if (type === "execution_start") {
301
188
  // New request: start fresh, discarding any incomplete prior run.
302
189
  this.startRun(now);
303
190
  return;
@@ -309,9 +196,8 @@ export class ThroughputTracker {
309
196
  return;
310
197
  }
311
198
 
312
- if (VISIBLE_DELTA_EVENTS.has(type)) {
313
- if (!isVisibleTextDelta(type, payload)) return;
314
- const text = getTextDelta(payload);
199
+ if (type === "text_delta") {
200
+ const text = typeof payload.delta === "string" ? payload.delta : "";
315
201
  if (!text) return;
316
202
 
317
203
  // Lazily start a run if the stream began without a recognized start event.
@@ -367,7 +253,7 @@ export class ThroughputTracker {
367
253
  return;
368
254
  }
369
255
 
370
- if (TERMINAL_COMPLETE_EVENTS.has(type)) {
256
+ if (type === "execution_complete") {
371
257
  if (!this.run) return;
372
258
  const stats = this.run;
373
259
  // Prefer exact output tokens from this terminal event, else accumulated
@@ -396,7 +282,7 @@ export class ThroughputTracker {
396
282
  return;
397
283
  }
398
284
 
399
- if (ERROR_EVENTS.has(type)) {
285
+ if (type === "execution_error" || (type === "error" && payload.recoverable === false)) {
400
286
  if (!this.run) return;
401
287
  this.run = null;
402
288
  this.metric = { status: "error" };
@@ -1,4 +1,21 @@
1
- import type { VoiceConfig } from "../types";
1
+ import type { AgentWidgetConfig, VoiceConfig } from "../types";
2
+
3
+ /** Changes that require replacing a session's capture and playback provider. */
4
+ export function voiceConnectionChanged(previous: AgentWidgetConfig, next: AgentWidgetConfig): boolean {
5
+ const before = previous.voiceRecognition;
6
+ const after = next.voiceRecognition;
7
+ return (before?.enabled === true) !== (after?.enabled === true) ||
8
+ before?.provider?.type !== after?.provider?.type ||
9
+ before?.provider?.custom !== after?.provider?.custom ||
10
+ before?.provider?.runtype?.createPlaybackEngine !== after?.provider?.runtype?.createPlaybackEngine ||
11
+ JSON.stringify(before?.provider?.runtype) !== JSON.stringify(after?.provider?.runtype) ||
12
+ previous.agentId !== next.agentId || previous.clientToken !== next.clientToken || previous.apiUrl !== next.apiUrl;
13
+ }
14
+
15
+ /** Providers whose microphone lifecycle is owned by ChatSession instead of browser dictation. */
16
+ export function usesSessionVoice(config?: Partial<VoiceConfig>): boolean {
17
+ return config?.type === "runtype" || config?.type === "custom";
18
+ }
2
19
 
3
20
  /**
4
21
  * Eager, construction-free replica of the voice factory's `isVoiceSupported`
@@ -0,0 +1,18 @@
1
+ /** Correlate provider transcripts across cancellation and overlapping turns. */
2
+ export class VoiceTurnTracker {
3
+ private turnId?: string;
4
+ private cancelled = false;
5
+
6
+ start(turnId?: string): void {
7
+ this.turnId = turnId;
8
+ this.cancelled = false;
9
+ }
10
+
11
+ cancel(): void {
12
+ this.cancelled = true;
13
+ }
14
+
15
+ accepts(turnId?: string): boolean {
16
+ return !this.cancelled && (!turnId || !this.turnId || turnId === this.turnId);
17
+ }
18
+ }