@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.
- 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-RNNBR5R3.js → approval-ui-K7R5Q5OC.js} +1 -1
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +221 -16
- package/dist/approval-ui.d.ts +221 -16
- package/dist/approval-ui.js +2 -2
- package/dist/artifacts-ui-F4N6VMKF.js +7 -0
- package/dist/artifacts-ui.cjs +1 -1
- package/dist/artifacts-ui.d.cts +221 -16
- package/dist/artifacts-ui.d.ts +221 -16
- package/dist/artifacts-ui.js +1 -1
- package/dist/chunk-NQLDCJAP.js +4 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +221 -16
- package/dist/context-mentions.d.ts +221 -16
- package/dist/event-stream-view-GL3ADRY2.js +1 -0
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +221 -16
- package/dist/event-stream-view.d.ts +221 -16
- package/dist/event-stream-view.js +1 -1
- package/dist/forms-ui.d.cts +234 -25
- package/dist/forms-ui.d.ts +234 -25
- package/dist/history-view.d.cts +136 -6
- package/dist/history-view.d.ts +136 -6
- package/dist/index.cjs +70 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +302 -32
- package/dist/index.d.ts +302 -32
- package/dist/index.global.js +54 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +69 -69
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +221 -16
- package/dist/session-reconnect.d.ts +221 -16
- package/dist/smart-dom-reader.d.cts +221 -16
- package/dist/smart-dom-reader.d.ts +221 -16
- 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 +73 -73
- package/dist/theme-editor-preview.d.cts +221 -16
- package/dist/theme-editor-preview.d.ts +221 -16
- package/dist/theme-editor-preview.js +51 -51
- package/dist/theme-editor.cjs +4 -4
- package/dist/theme-editor.d.cts +221 -16
- package/dist/theme-editor.d.ts +221 -16
- package/dist/theme-editor.js +2 -2
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +65 -1
- package/dist/theme-reference.d.ts +65 -1
- package/dist/theme-reference.js +1 -1
- package/dist/voice-runtime.d.cts +6 -2
- package/dist/voice-runtime.d.ts +6 -2
- package/dist/webmcp-polyfill.js +1 -4
- package/dist/webmcp-runtime-JI7T4QYF.js +6 -0
- package/dist/webmcp-runtime.cjs +2 -2
- package/dist/webmcp-runtime.d.cts +34 -2
- package/dist/webmcp-runtime.d.ts +34 -2
- package/dist/webmcp-runtime.js +2 -2
- package/package.json +2 -2
- 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 +20 -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/artifact-preview.ts +5 -0
- package/src/components/event-stream-badge-theming.test.ts +162 -0
- package/src/components/event-stream-view.test.ts +76 -76
- package/src/components/event-stream-view.ts +55 -20
- 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 +137 -1
- package/src/index-core.ts +2 -0
- package/src/internal/runtype-history-provider.test.ts +2 -2
- package/src/runtime/init.registry.test.ts +56 -0
- package/src/runtime/init.ts +17 -0
- 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/theme.ts +41 -1
- package/src/types.ts +57 -15
- 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.reasoning-display.test.ts +136 -2
- package/src/ui.streaming-coalescing.test.ts +10 -13
- package/src/ui.ts +101 -35
- package/src/ui.typing-indicator.test.ts +216 -0
- package/src/ui.voice-custom-provider.test.ts +211 -0
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +1740 -0
- package/src/utils/event-stream-buffer.test.ts +13 -13
- package/src/utils/event-stream-capture.test.ts +20 -20
- package/src/utils/formatting.ts +22 -0
- package/src/utils/theme.ts +16 -0
- package/src/utils/throughput-tracker.test.ts +100 -142
- package/src/utils/throughput-tracker.ts +18 -132
- package/src/utils/tokens.ts +59 -0
- package/src/utils/voice-support.ts +18 -1
- package/src/voice/voice-turn-tracker.ts +18 -0
- package/src/webmcp-bridge.test.ts +132 -23
- package/src/webmcp-bridge.ts +27 -3
- package/src/webmcp-runtime-entry.ts +71 -11
- package/dist/artifacts-ui-J6AKUGTS.js +0 -7
- package/dist/chunk-HBE7LFNT.js +0 -4
- package/dist/event-stream-view-73NWKWVQ.js +0 -1
- package/dist/webmcp-runtime-P3CHZRHF.js +0 -6
- 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("
|
|
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("
|
|
89
|
-
buf.push(makeEvent("
|
|
90
|
-
buf.push(makeEvent("
|
|
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("
|
|
93
|
-
expect(types).toContain("
|
|
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("
|
|
99
|
+
buf.push(makeEvent("text_delta", 1));
|
|
100
100
|
buf.push(makeEvent("tool_start", 2));
|
|
101
|
-
buf.push(makeEvent("
|
|
102
|
-
// Evict the
|
|
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
|
-
//
|
|
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 !== "
|
|
107
|
+
expect(all.every(e => e.type !== "text_delta")).toBe(true);
|
|
108
108
|
const types = buf.getEventTypes();
|
|
109
|
-
expect(types).toContain("
|
|
109
|
+
expect(types).toContain("text_delta");
|
|
110
110
|
expect(types).toContain("tool_start");
|
|
111
|
-
expect(types).toContain("
|
|
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
|
|
48
|
+
it("should fire onSSEEvent for text_delta events", async () => {
|
|
49
49
|
global.fetch = createMockFetch([
|
|
50
50
|
sseData({
|
|
51
|
-
type: "
|
|
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: "
|
|
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 === "
|
|
82
|
+
(e) => e.eventType === "text_delta"
|
|
83
83
|
);
|
|
84
84
|
expect(stepChunkEvents).toHaveLength(1);
|
|
85
85
|
expect(stepChunkEvents[0].payload).toMatchObject({
|
|
86
|
-
type: "
|
|
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: "
|
|
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
|
|
151
|
+
it("should fire onSSEEvent for execution_complete events", async () => {
|
|
152
152
|
global.fetch = createMockFetch([
|
|
153
153
|
sseData({
|
|
154
|
-
type: "
|
|
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 === "
|
|
178
|
+
(e) => e.eventType === "execution_complete"
|
|
179
179
|
);
|
|
180
180
|
expect(flowCompleteEvents).toHaveLength(1);
|
|
181
181
|
expect(flowCompleteEvents[0].payload).toMatchObject({
|
|
182
|
-
type: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
"
|
|
248
|
-
"
|
|
247
|
+
"text_delta",
|
|
248
|
+
"text_delta",
|
|
249
249
|
"tool_start",
|
|
250
250
|
"tool_complete",
|
|
251
|
-
"
|
|
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: "
|
|
260
|
+
type: "text_delta",
|
|
261
261
|
text: "chunk1"
|
|
262
262
|
}),
|
|
263
263
|
sseData({
|
|
264
|
-
type: "
|
|
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("
|
|
285
|
-
expect(capturedSSEEvents[1].eventType).toBe("
|
|
284
|
+
expect(capturedSSEEvents[0].eventType).toBe("text_delta");
|
|
285
|
+
expect(capturedSSEEvents[1].eventType).toBe("execution_complete");
|
|
286
286
|
});
|
|
287
287
|
});
|
|
288
288
|
|
package/src/utils/formatting.ts
CHANGED
|
@@ -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"
|
package/src/utils/theme.ts
CHANGED
|
@@ -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("
|
|
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
|
+
});
|