@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.
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-B_JdVK9o.d.cts → types-9Vxm-PPz.d.cts} +136 -6
- package/dist/animations/{types-B_JdVK9o.d.ts → types-9Vxm-PPz.d.ts} +136 -6
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +136 -6
- package/dist/animations-extra.d.ts +136 -6
- package/dist/approval-ui.d.cts +148 -14
- package/dist/approval-ui.d.ts +148 -14
- package/dist/artifacts-ui.d.cts +148 -14
- package/dist/artifacts-ui.d.ts +148 -14
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +148 -14
- package/dist/context-mentions.d.ts +148 -14
- package/dist/event-stream-view-GL3ADRY2.js +1 -0
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +148 -14
- package/dist/event-stream-view.d.ts +148 -14
- package/dist/event-stream-view.js +1 -1
- package/dist/forms-ui.d.cts +161 -23
- package/dist/forms-ui.d.ts +161 -23
- package/dist/history-view.d.cts +136 -6
- package/dist/history-view.d.ts +136 -6
- package/dist/index.cjs +67 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +172 -27
- package/dist/index.d.ts +172 -27
- package/dist/index.global.js +51 -51
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +67 -67
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +148 -14
- package/dist/session-reconnect.d.ts +148 -14
- package/dist/smart-dom-reader.d.cts +148 -14
- package/dist/smart-dom-reader.d.ts +148 -14
- package/dist/testing.cjs +3 -3
- package/dist/testing.d.cts +3 -4
- package/dist/testing.d.ts +3 -4
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +71 -71
- package/dist/theme-editor-preview.d.cts +154 -14
- package/dist/theme-editor-preview.d.ts +154 -14
- package/dist/theme-editor-preview.js +48 -48
- package/dist/theme-editor.d.cts +148 -14
- package/dist/theme-editor.d.ts +148 -14
- package/dist/voice-runtime.d.cts +6 -2
- package/dist/voice-runtime.d.ts +6 -2
- package/package.json +1 -1
- package/src/ask-user-question-tool.ts +1 -1
- package/src/client-history-rest.test.ts +2 -1
- package/src/client-visitor-history.test.ts +106 -4
- package/src/client.composer-options.test.ts +1 -1
- package/src/client.interrupt.test.ts +2 -2
- package/src/client.test.ts +477 -915
- package/src/client.ts +134 -494
- package/src/client.unified-tools.test.ts +76 -0
- package/src/components/event-stream-badge-theming.test.ts +3 -0
- package/src/components/event-stream-view.test.ts +76 -76
- package/src/components/event-stream-view.ts +5 -0
- package/src/components/tool-bubble.test.ts +12 -0
- package/src/components/tool-bubble.ts +13 -2
- package/src/event-stream-view-bundle.test.ts +2 -2
- package/src/generated/runtype-openapi-contract.ts +129 -1
- package/src/internal/runtype-history-provider.test.ts +2 -2
- package/src/launcher-global.ts +1 -1
- package/src/session-history.test.ts +1 -1
- package/src/session.quote.test.ts +1 -1
- package/src/session.resubmit-from.test.ts +1 -1
- package/src/session.test.ts +5 -5
- package/src/session.ts +95 -36
- package/src/suggest-replies-tool.ts +1 -1
- package/src/testing/mock-stream.test.ts +37 -16
- package/src/testing/mock-stream.ts +14 -9
- package/src/types.ts +19 -14
- package/src/ui.ask-user-question-plugin.test.ts +7 -7
- package/src/ui.composer-defer-one.test.ts +6 -34
- package/src/ui.composer-draft-persistence.test.ts +1 -1
- package/src/ui.composer-quote.test.ts +1 -1
- package/src/ui.message-history-actions.test.ts +1 -1
- package/src/ui.streaming-coalescing.test.ts +10 -13
- package/src/ui.ts +80 -32
- package/src/ui.typing-indicator.test.ts +216 -0
- package/src/ui.voice-custom-provider.test.ts +211 -0
- package/src/utils/event-stream-buffer.test.ts +13 -13
- package/src/utils/event-stream-capture.test.ts +20 -20
- package/src/utils/throughput-tracker.test.ts +100 -142
- package/src/utils/throughput-tracker.ts +18 -132
- package/src/utils/voice-support.ts +18 -1
- package/src/voice/voice-turn-tracker.ts +18 -0
- package/src/webmcp-bridge.ts +1 -1
- package/dist/event-stream-view-FKWDY3UB.js +0 -1
- 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("
|
|
44
|
-
type: "
|
|
45
|
-
|
|
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("
|
|
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
|
|
62
|
+
it("counts text_delta text deltas as visible output", () => {
|
|
63
63
|
const h = makeTracker();
|
|
64
|
-
h.at(0).processEvent("
|
|
65
|
-
type: "
|
|
66
|
-
|
|
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("
|
|
70
|
-
type: "
|
|
71
|
-
|
|
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
|
|
78
|
+
it("ignores text_delta without a string delta", () => {
|
|
81
79
|
const h = makeTracker();
|
|
82
|
-
|
|
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("
|
|
104
|
-
h.at(2000).processEvent("
|
|
105
|
-
type: "
|
|
106
|
-
|
|
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("
|
|
112
|
-
type: "
|
|
113
|
-
|
|
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("
|
|
127
|
-
h.at(1500).processEvent("
|
|
128
|
-
type: "
|
|
129
|
-
|
|
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("
|
|
132
|
-
type: "
|
|
133
|
-
|
|
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("
|
|
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("
|
|
148
|
-
h.at(1100).processEvent("
|
|
149
|
-
// streamed window = 50ms (< 250ms threshold) → use
|
|
150
|
-
h.at(1150).processEvent("
|
|
151
|
-
type: "
|
|
152
|
-
|
|
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("
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 /
|
|
163
|
+
it("keeps the run running across step_complete / turn_complete", () => {
|
|
228
164
|
const h = makeTracker();
|
|
229
|
-
h.at(1000).processEvent("
|
|
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("
|
|
238
|
-
type: "
|
|
239
|
-
|
|
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("
|
|
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(["
|
|
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("
|
|
254
|
-
type: "
|
|
255
|
-
|
|
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("
|
|
268
|
-
h.at(1500).processEvent("
|
|
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("
|
|
220
|
+
h.at(1000).processEvent("execution_complete", { type: "execution_complete" });
|
|
275
221
|
expect(h.metric().status).toBe("idle");
|
|
276
|
-
h.at(1000).processEvent("
|
|
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("
|
|
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("
|
|
292
|
-
h.at(2000).processEvent("
|
|
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("
|
|
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
|
|
306
|
-
h.at(1000).processEvent("
|
|
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
|
|
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("
|
|
313
|
-
h.at(5200).processEvent("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
338
|
-
type: "
|
|
339
|
-
|
|
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("
|
|
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("
|
|
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 `
|
|
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
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
180
|
-
|
|
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
|
-
/**
|
|
208
|
-
function getExecutionTimeMs(
|
|
209
|
-
payload
|
|
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 "
|
|
290
|
-
if (
|
|
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 (
|
|
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 (
|
|
313
|
-
|
|
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 (
|
|
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 (
|
|
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
|
+
}
|