@runtypelabs/persona 3.21.3 → 3.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/README.md +67 -0
- package/dist/animations/glyph-cycle.cjs +2 -262
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/glyph-cycle.js +2 -235
- package/dist/animations/{types-CWPIj66R.d.cts → types-BZVr1YOV.d.cts} +10 -0
- package/dist/animations/{types-CWPIj66R.d.ts → types-BZVr1YOV.d.ts} +10 -0
- package/dist/animations/wipe.cjs +2 -72
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations/wipe.js +2 -45
- package/dist/index.cjs +52 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +474 -6
- package/dist/index.d.ts +474 -6
- package/dist/index.global.js +107 -97
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +52 -45
- package/dist/index.js.map +1 -1
- package/dist/smart-dom-reader.cjs +23 -0
- package/dist/smart-dom-reader.d.cts +4521 -0
- package/dist/smart-dom-reader.d.ts +4521 -0
- package/dist/smart-dom-reader.js +23 -0
- package/dist/testing.cjs +3 -84
- package/dist/testing.js +3 -55
- package/dist/theme-editor.cjs +57 -22501
- package/dist/theme-editor.d.cts +348 -1
- package/dist/theme-editor.d.ts +348 -1
- package/dist/theme-editor.js +57 -22503
- package/package.json +16 -6
- package/src/client.test.ts +165 -0
- package/src/client.ts +144 -23
- package/src/components/event-stream-view.ts +122 -1
- package/src/index.ts +26 -0
- package/src/session.test.ts +258 -0
- package/src/session.ts +886 -30
- package/src/session.webmcp.test.ts +815 -0
- package/src/smart-dom-reader.test.ts +135 -0
- package/src/smart-dom-reader.ts +135 -0
- package/src/theme-editor/color-utils.test.ts +59 -0
- package/src/theme-editor/color-utils.ts +38 -2
- package/src/theme-editor/index.ts +35 -0
- package/src/theme-editor/webmcp/coerce.test.ts +86 -0
- package/src/theme-editor/webmcp/coerce.ts +286 -0
- package/src/theme-editor/webmcp/index.ts +45 -0
- package/src/theme-editor/webmcp/summary.ts +324 -0
- package/src/theme-editor/webmcp/tools.test.ts +205 -0
- package/src/theme-editor/webmcp/tools.ts +795 -0
- package/src/theme-editor/webmcp/types.ts +87 -0
- package/src/types.ts +186 -0
- package/src/ui.composer-keyboard.test.ts +229 -0
- package/src/ui.ts +151 -8
- package/src/utils/composer-history.test.ts +128 -0
- package/src/utils/composer-history.ts +113 -0
- package/src/utils/message-fingerprint.test.ts +20 -0
- package/src/utils/message-fingerprint.ts +2 -0
- package/src/utils/smart-dom-adapter.test.ts +257 -0
- package/src/utils/smart-dom-adapter.ts +217 -0
- package/src/utils/throughput-tracker.test.ts +366 -0
- package/src/utils/throughput-tracker.ts +427 -0
- package/{LICENSE → src/vendor/smart-dom-reader/LICENSE} +2 -2
- package/src/vendor/smart-dom-reader/README.md +61 -0
- package/src/vendor/smart-dom-reader/index.d.ts +476 -0
- package/src/vendor/smart-dom-reader/index.js +1618 -0
- package/src/webmcp-bridge.test.ts +429 -0
- package/src/webmcp-bridge.ts +547 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
ThroughputTracker,
|
|
4
|
+
estimateOutputTokens,
|
|
5
|
+
type ThroughputMetric,
|
|
6
|
+
} from "./throughput-tracker";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds a tracker with a manually-advanced clock so durations are
|
|
10
|
+
* deterministic. Call `clock.set(ms)` before feeding an event.
|
|
11
|
+
*/
|
|
12
|
+
function makeTracker() {
|
|
13
|
+
let nowMs = 0;
|
|
14
|
+
const tracker = new ThroughputTracker(() => nowMs);
|
|
15
|
+
return {
|
|
16
|
+
tracker,
|
|
17
|
+
at(ms: number) {
|
|
18
|
+
nowMs = ms;
|
|
19
|
+
return tracker;
|
|
20
|
+
},
|
|
21
|
+
metric(): ThroughputMetric {
|
|
22
|
+
return tracker.getMetric();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const text = (n: number) => "a".repeat(n);
|
|
28
|
+
|
|
29
|
+
describe("estimateOutputTokens", () => {
|
|
30
|
+
it("uses the ~4 chars/token heuristic with a floor of 1", () => {
|
|
31
|
+
expect(estimateOutputTokens("")).toBe(0);
|
|
32
|
+
expect(estimateOutputTokens(" ")).toBe(0);
|
|
33
|
+
expect(estimateOutputTokens("a")).toBe(1);
|
|
34
|
+
expect(estimateOutputTokens(text(40))).toBe(10);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("ThroughputTracker — live estimate", () => {
|
|
39
|
+
it("estimates output tokens live from visible text deltas", () => {
|
|
40
|
+
const h = makeTracker();
|
|
41
|
+
|
|
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),
|
|
46
|
+
});
|
|
47
|
+
let m = h.metric();
|
|
48
|
+
expect(m.status).toBe("running");
|
|
49
|
+
expect(m.outputTokens).toBe(10);
|
|
50
|
+
expect(m.source).toBe("estimate");
|
|
51
|
+
expect(m.tokensPerSecond).toBeUndefined();
|
|
52
|
+
|
|
53
|
+
// Second delta 1s later via a different visible event type.
|
|
54
|
+
h.at(2000).processEvent("chunk", { type: "chunk", text: text(40) });
|
|
55
|
+
m = h.metric();
|
|
56
|
+
expect(m.outputTokens).toBe(20);
|
|
57
|
+
expect(m.durationMs).toBe(1000);
|
|
58
|
+
expect(m.source).toBe("estimate");
|
|
59
|
+
expect(m.tokensPerSecond).toBeCloseTo(20);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("counts agent_turn_delta text deltas as visible output", () => {
|
|
63
|
+
const h = makeTracker();
|
|
64
|
+
h.at(0).processEvent("agent_turn_delta", {
|
|
65
|
+
type: "agent_turn_delta",
|
|
66
|
+
contentType: "text",
|
|
67
|
+
text: text(40),
|
|
68
|
+
});
|
|
69
|
+
h.at(1000).processEvent("agent_turn_delta", {
|
|
70
|
+
type: "agent_turn_delta",
|
|
71
|
+
contentType: "text",
|
|
72
|
+
text: text(40),
|
|
73
|
+
});
|
|
74
|
+
const m = h.metric();
|
|
75
|
+
expect(m.status).toBe("running");
|
|
76
|
+
expect(m.outputTokens).toBe(20);
|
|
77
|
+
expect(m.tokensPerSecond).toBeCloseTo(20);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("ignores agent_turn_delta with a missing contentType (matches client)", () => {
|
|
81
|
+
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
|
+
});
|
|
88
|
+
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
|
+
});
|
|
96
|
+
expect(h.metric().outputTokens).toBe(10);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("ThroughputTracker — exact usage finalization", () => {
|
|
101
|
+
it("prefers exact output tokens from the terminal event over the estimate", () => {
|
|
102
|
+
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),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 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 },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const m = h.metric();
|
|
117
|
+
expect(m.status).toBe("complete");
|
|
118
|
+
expect(m.outputTokens).toBe(123);
|
|
119
|
+
expect(m.source).toBe("usage");
|
|
120
|
+
expect(m.durationMs).toBe(1000); // streamed window 1000ms
|
|
121
|
+
expect(m.tokensPerSecond).toBeCloseTo(123);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("accumulates exact usage from intermediate completes and uses it on terminal", () => {
|
|
125
|
+
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 } },
|
|
130
|
+
});
|
|
131
|
+
h.at(2000).processEvent("agent_turn_complete", {
|
|
132
|
+
type: "agent_turn_complete",
|
|
133
|
+
usage: { outputTokens: 30 },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Terminal carries no usage of its own → fall back to accumulated 80.
|
|
137
|
+
h.at(2000).processEvent("agent_complete", { type: "agent_complete" });
|
|
138
|
+
|
|
139
|
+
const m = h.metric();
|
|
140
|
+
expect(m.status).toBe("complete");
|
|
141
|
+
expect(m.outputTokens).toBe(80);
|
|
142
|
+
expect(m.source).toBe("usage");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("falls back to provider execution time when the streamed window is too short", () => {
|
|
146
|
+
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,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const m = h.metric();
|
|
156
|
+
expect(m.status).toBe("complete");
|
|
157
|
+
expect(m.outputTokens).toBe(10);
|
|
158
|
+
expect(m.durationMs).toBe(5000);
|
|
159
|
+
expect(m.tokensPerSecond).toBeCloseTo(2);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
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
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("ThroughputTracker — intermediate completes do not finalize", () => {
|
|
227
|
+
it("keeps the run running across step_complete / agent_turn_complete", () => {
|
|
228
|
+
const h = makeTracker();
|
|
229
|
+
h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
|
|
230
|
+
|
|
231
|
+
h.at(1500).processEvent("step_complete", {
|
|
232
|
+
type: "step_complete",
|
|
233
|
+
result: { tokens: { output: 10 } },
|
|
234
|
+
});
|
|
235
|
+
expect(h.metric().status).toBe("running");
|
|
236
|
+
|
|
237
|
+
h.at(1800).processEvent("agent_turn_complete", {
|
|
238
|
+
type: "agent_turn_complete",
|
|
239
|
+
usage: { outputTokens: 5 },
|
|
240
|
+
});
|
|
241
|
+
expect(h.metric().status).toBe("running");
|
|
242
|
+
|
|
243
|
+
h.at(2000).processEvent("flow_complete", { type: "flow_complete" });
|
|
244
|
+
expect(h.metric().status).toBe("complete");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("ThroughputTracker — error handling", () => {
|
|
249
|
+
it.each(["step_error", "flow_error", "agent_error", "error"])(
|
|
250
|
+
"marks the metric unavailable on %s",
|
|
251
|
+
(errorType: string) => {
|
|
252
|
+
const h = makeTracker();
|
|
253
|
+
h.at(1000).processEvent("step_delta", {
|
|
254
|
+
type: "step_delta",
|
|
255
|
+
text: text(40),
|
|
256
|
+
});
|
|
257
|
+
expect(h.metric().status).toBe("running");
|
|
258
|
+
|
|
259
|
+
h.at(1500).processEvent(errorType, { type: errorType });
|
|
260
|
+
expect(h.metric().status).toBe("error");
|
|
261
|
+
expect(h.metric().tokensPerSecond).toBeUndefined();
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
it("treats a bare non-object error payload as an error", () => {
|
|
266
|
+
const h = makeTracker();
|
|
267
|
+
h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
|
|
268
|
+
h.at(1500).processEvent("error", "boom");
|
|
269
|
+
expect(h.metric().status).toBe("error");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("ignores terminal/error events when no run is active", () => {
|
|
273
|
+
const h = makeTracker();
|
|
274
|
+
h.at(1000).processEvent("flow_complete", { type: "flow_complete" });
|
|
275
|
+
expect(h.metric().status).toBe("idle");
|
|
276
|
+
h.at(1000).processEvent("flow_error", { type: "flow_error" });
|
|
277
|
+
expect(h.metric().status).toBe("idle");
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("ThroughputTracker — reset & re-run", () => {
|
|
282
|
+
it("reset() returns to idle", () => {
|
|
283
|
+
const h = makeTracker();
|
|
284
|
+
h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
|
|
285
|
+
h.tracker.reset();
|
|
286
|
+
expect(h.metric()).toEqual({ status: "idle" });
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("starts a fresh run after a completed one", () => {
|
|
290
|
+
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" });
|
|
293
|
+
expect(h.metric().status).toBe("complete");
|
|
294
|
+
|
|
295
|
+
// New stream → accumulation resets, does not carry the prior 20 tokens.
|
|
296
|
+
h.at(3000).processEvent("step_delta", { type: "step_delta", text: text(40) });
|
|
297
|
+
const m = h.metric();
|
|
298
|
+
expect(m.status).toBe("running");
|
|
299
|
+
expect(m.outputTokens).toBe(10);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("resets a stale run on the next request's start event (no bleed)", () => {
|
|
303
|
+
const h = makeTracker();
|
|
304
|
+
// 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) });
|
|
307
|
+
expect(h.metric().outputTokens).toBe(30);
|
|
308
|
+
expect(h.metric().status).toBe("running");
|
|
309
|
+
|
|
310
|
+
// Next request begins. Its flow_start must discard the stale run so the
|
|
311
|
+
// 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) });
|
|
314
|
+
const m = h.metric();
|
|
315
|
+
expect(m.status).toBe("running");
|
|
316
|
+
expect(m.outputTokens).toBe(10); // only the new request's text
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("does not reset between per-step starts within one request", () => {
|
|
320
|
+
const h = makeTracker();
|
|
321
|
+
h.at(1000).processEvent("flow_start", { type: "flow_start" });
|
|
322
|
+
h.at(1100).processEvent("step_start", { type: "step_start" });
|
|
323
|
+
h.at(1200).processEvent("step_delta", { type: "step_delta", text: text(40) });
|
|
324
|
+
// A second step within the same request must not restart accumulation.
|
|
325
|
+
h.at(1300).processEvent("step_start", { type: "step_start" });
|
|
326
|
+
h.at(1400).processEvent("step_delta", { type: "step_delta", text: text(40) });
|
|
327
|
+
expect(h.metric().outputTokens).toBe(20);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("ThroughputTracker — exact usage never drops mid-run", () => {
|
|
332
|
+
it("keeps exact tokens as a floor when later steps stream more text", () => {
|
|
333
|
+
const h = makeTracker();
|
|
334
|
+
h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(40) });
|
|
335
|
+
|
|
336
|
+
// 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 } },
|
|
340
|
+
});
|
|
341
|
+
let m = h.metric();
|
|
342
|
+
expect(m.outputTokens).toBe(50);
|
|
343
|
+
expect(m.source).toBe("usage");
|
|
344
|
+
|
|
345
|
+
// Step 2 streams more visible text — the total must grow from 50, not
|
|
346
|
+
// 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) });
|
|
348
|
+
m = h.metric();
|
|
349
|
+
expect(m.outputTokens).toBe(60); // 50 exact + 10 estimated
|
|
350
|
+
expect(m.source).toBe("usage");
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe("ThroughputTracker — live rate decays while paused", () => {
|
|
355
|
+
it("recomputes duration/tok-s from the clock between events", () => {
|
|
356
|
+
const h = makeTracker();
|
|
357
|
+
h.at(1000).processEvent("step_delta", { type: "step_delta", text: text(400) });
|
|
358
|
+
// 100 tokens over a 1s window read at t=2000 → ~100 tok/s.
|
|
359
|
+
h.at(2000);
|
|
360
|
+
expect(h.metric().tokensPerSecond).toBeCloseTo(100);
|
|
361
|
+
// Same tokens, but the model has paused — reading at t=5000 (4s window)
|
|
362
|
+
// must decay the displayed rate even though no new event arrived.
|
|
363
|
+
h.at(5000);
|
|
364
|
+
expect(h.metric().tokensPerSecond).toBeCloseTo(25);
|
|
365
|
+
});
|
|
366
|
+
});
|