@rulvar/rulvar 1.21.0 → 1.22.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/index.d.ts +1 -1
- package/dist/index.js +81 -23
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ type ProgressMode = "auto" | "tty" | "lines" | "off";
|
|
|
33
33
|
interface ProgressOptions {
|
|
34
34
|
/** Defaults to process.stderr so application stdout stays clean. */
|
|
35
35
|
sink?: ProgressSink;
|
|
36
|
-
/** Defaults to
|
|
36
|
+
/** Defaults to a monotonic clock (performance.now) plus setInterval. */
|
|
37
37
|
clock?: ProgressClock;
|
|
38
38
|
/**
|
|
39
39
|
* 'auto' (default) picks 'tty' when the sink reports a TTY and the
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
|
+
import { sanitizeTerminalText } from "@rulvar/core";
|
|
1
2
|
import { ANTHROPIC_MODELS, anthropic } from "@rulvar/anthropic";
|
|
2
3
|
import { OPENAI_MODELS, openai } from "@rulvar/openai";
|
|
3
4
|
export * from "@rulvar/core";
|
|
4
5
|
//#region src/render-progress.ts
|
|
6
|
+
/**
|
|
7
|
+
* Minimal terminal progress renderer (M1-T10): consumes the WorkflowEvent
|
|
8
|
+
* stream of a RunHandle and writes one line per lifecycle fact. Plain
|
|
9
|
+
* lines, no cursor control: readable in CI logs and pipes as well as TTYs.
|
|
10
|
+
*
|
|
11
|
+
* Event stream contract: https://docs.rulvar.com/guide/observability
|
|
12
|
+
* (the terminal progress renderer is one of the four stream consumers).
|
|
13
|
+
*
|
|
14
|
+
* Every emitted line passes through the shared terminal sanitizer before
|
|
15
|
+
* it reaches the sink, so an untrusted provider/tool/log string can never
|
|
16
|
+
* inject a control sequence or a second physical line (v1.21.0 review
|
|
17
|
+
* P2-1).
|
|
18
|
+
*/
|
|
5
19
|
function usd(amount) {
|
|
6
20
|
return `${amount.toFixed(4)} USD`;
|
|
7
21
|
}
|
|
@@ -10,9 +24,10 @@ function usd(amount) {
|
|
|
10
24
|
* the final run:end line.
|
|
11
25
|
*/
|
|
12
26
|
async function renderProgress(events, options) {
|
|
13
|
-
const
|
|
27
|
+
const sink = options?.write ?? ((line) => {
|
|
14
28
|
process.stderr.write(`${line}\n`);
|
|
15
29
|
});
|
|
30
|
+
const write = (line) => sink(sanitizeTerminalText(line));
|
|
16
31
|
const logs = options?.logs ?? true;
|
|
17
32
|
for await (const event of events) switch (event.type) {
|
|
18
33
|
case "run:start":
|
|
@@ -50,7 +65,45 @@ async function renderProgress(events, options) {
|
|
|
50
65
|
}
|
|
51
66
|
//#endregion
|
|
52
67
|
//#region src/live-progress.ts
|
|
68
|
+
/**
|
|
69
|
+
* Live terminal progress view (v1.21.0): a claude-workflows-style tree
|
|
70
|
+
* over the WorkflowEvent stream, one row per agent with a status glyph,
|
|
71
|
+
* a running timer, token counts, and USD, plus per-role sub-timings when
|
|
72
|
+
* one agent call spans several invocation phases (loop, summarize,
|
|
73
|
+
* finalize, extract). The minimal line-per-event `renderProgress` stays
|
|
74
|
+
* untouched next door; this renderer is the rich, cursor-addressed
|
|
75
|
+
* sibling with an append-only fallback for pipes and CI.
|
|
76
|
+
*
|
|
77
|
+
* Honesty rules inherited from the event contract
|
|
78
|
+
* (https://docs.rulvar.com/guide/observability): exact token counts
|
|
79
|
+
* exist only at `agent:end`, so running rows show elapsed time and a
|
|
80
|
+
* tilde-marked character estimate from `agent:stream` deltas; run-level
|
|
81
|
+
* USD is live through `budget:update`; per-role dollars appear in the
|
|
82
|
+
* final summary only when the source is a RunHandle (they come from
|
|
83
|
+
* `RunOutcome.cost.byRole`, not from any event). Replayed lifecycle
|
|
84
|
+
* events render dim with a `replay` tag, never spin, and never add to
|
|
85
|
+
* totals: the authoritative money numbers are `budget:update.spentUsd`
|
|
86
|
+
* and `run:end.totalUsd`, which are immune to replay double counting.
|
|
87
|
+
*
|
|
88
|
+
* The reducer is defensive by contract: unknown event types are ignored
|
|
89
|
+
* and every dynamic field, including the required ones, is read
|
|
90
|
+
* defensively (optional chaining with fallbacks), so a malformed event
|
|
91
|
+
* degrades a row rather than throwing and stopping the view; an unknown
|
|
92
|
+
* parent span attaches at the root, and a mid-run attach synthesizes a
|
|
93
|
+
* root instead of failing.
|
|
94
|
+
*/
|
|
95
|
+
/**
|
|
96
|
+
* Positive-integer option normalization (v1.21.0 review P3-2): a
|
|
97
|
+
* non-finite or below-minimum caller value falls back rather than
|
|
98
|
+
* poisoning the geometry (a NaN width breaks the clip, a NaN fps yields
|
|
99
|
+
* a NaN interval). Fractions floor.
|
|
100
|
+
*/
|
|
101
|
+
function posIntOption(value, fallback, min) {
|
|
102
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
103
|
+
return Math.max(min, Math.floor(value));
|
|
104
|
+
}
|
|
53
105
|
function fmtDuration(ms) {
|
|
106
|
+
if (!Number.isFinite(ms) || ms < 0) ms = 0;
|
|
54
107
|
const s = ms / 1e3;
|
|
55
108
|
if (s < 10) return `${s.toFixed(1)}s`;
|
|
56
109
|
if (s < 60) return `${String(Math.floor(s))}s`;
|
|
@@ -82,6 +135,7 @@ const SPINNER = [
|
|
|
82
135
|
"-",
|
|
83
136
|
"\\"
|
|
84
137
|
];
|
|
138
|
+
const SGR_STRIP = /* @__PURE__ */ new RegExp("\\u001B\\[[0-9;]*m", "gu");
|
|
85
139
|
function newState(title) {
|
|
86
140
|
return {
|
|
87
141
|
...title === void 0 ? {} : { title },
|
|
@@ -120,15 +174,15 @@ function nodeOf(state, event, kind, title) {
|
|
|
120
174
|
return node;
|
|
121
175
|
}
|
|
122
176
|
/**
|
|
123
|
-
* Untrusted wire strings (model ids, tool names, error messages
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
177
|
+
* Untrusted wire strings (model ids, tool names, error messages, log
|
|
178
|
+
* text, workflow and label metadata) may carry control characters and
|
|
179
|
+
* escape sequences that break the repaint arithmetic or leak terminal
|
|
180
|
+
* control. The shared core sanitizer strips C0, DEL, C1, and whole
|
|
181
|
+
* ESC-initiated CSI/OSC/DCS sequences before interpolation; the
|
|
182
|
+
* renderer's own SGR is added by paint() afterward (v1.21.0 review
|
|
183
|
+
* P2-1).
|
|
127
184
|
*/
|
|
128
|
-
const
|
|
129
|
-
function scrub(text) {
|
|
130
|
-
return text.replace(CONTROL_CHARS, " ");
|
|
131
|
-
}
|
|
185
|
+
const scrub = sanitizeTerminalText;
|
|
132
186
|
function agentTitle(event) {
|
|
133
187
|
const base = event.agentType === void 0 || event.agentType === "" ? "agent" : scrub(event.agentType);
|
|
134
188
|
return event.label === void 0 || event.label === "" ? base : `${base} (${scrub(event.label)})`;
|
|
@@ -222,7 +276,7 @@ function applyEvent(state, event, now) {
|
|
|
222
276
|
}
|
|
223
277
|
case "agent:error": {
|
|
224
278
|
const node = state.nodes.get(event.spanId);
|
|
225
|
-
if (node !== void 0) node.badge = event.willRetry ? "retry" : scrub(`error: ${event.error
|
|
279
|
+
if (node !== void 0) node.badge = event.willRetry ? "retry" : scrub(`error: ${event.error?.message ?? ""}`);
|
|
226
280
|
break;
|
|
227
281
|
}
|
|
228
282
|
case "agent:schema-retry": {
|
|
@@ -236,8 +290,8 @@ function applyEvent(state, event, now) {
|
|
|
236
290
|
node.status = event.status;
|
|
237
291
|
node.endedAt = now;
|
|
238
292
|
node.usage = {
|
|
239
|
-
input: event.usage
|
|
240
|
-
output: event.usage
|
|
293
|
+
input: event.usage?.inputTokens ?? 0,
|
|
294
|
+
output: event.usage?.outputTokens ?? 0
|
|
241
295
|
};
|
|
242
296
|
node.costUsd = event.costUsd;
|
|
243
297
|
if (event.replayed === true) node.replayed = true;
|
|
@@ -375,10 +429,11 @@ function composeFrame(state, now, tick, style, width, maxRows, maxHeight) {
|
|
|
375
429
|
...lines.slice(lines.length - keepTail)
|
|
376
430
|
];
|
|
377
431
|
}
|
|
432
|
+
const limit = Math.max(0, width - 1);
|
|
378
433
|
return clamped.map((line) => {
|
|
379
|
-
const plain = line.replace(
|
|
380
|
-
if (plain.length <=
|
|
381
|
-
return plain.slice(0,
|
|
434
|
+
const plain = line.replace(SGR_STRIP, "");
|
|
435
|
+
if (plain.length <= limit) return line;
|
|
436
|
+
return limit >= 4 ? plain.slice(0, limit - 3) + "..." : plain.slice(0, limit);
|
|
382
437
|
});
|
|
383
438
|
}
|
|
384
439
|
function defaultSink() {
|
|
@@ -440,10 +495,12 @@ function progress(source, options) {
|
|
|
440
495
|
const clock = options?.clock ?? defaultClock();
|
|
441
496
|
const mode = resolveMode(options?.mode, sink);
|
|
442
497
|
const style = { color: options?.color ?? (mode === "tty" && process.env.NO_COLOR === void 0) };
|
|
443
|
-
const
|
|
444
|
-
const
|
|
445
|
-
const
|
|
446
|
-
const
|
|
498
|
+
const columns = posIntOption(sink.columns, 80, 1);
|
|
499
|
+
const rows = posIntOption(sink.rows, 32, 3);
|
|
500
|
+
const width = posIntOption(options?.width, columns, 1);
|
|
501
|
+
const maxRows = posIntOption(options?.maxRows, Math.max(6, Math.min(24, rows - 8)), 1);
|
|
502
|
+
const fps = Math.min(30, posIntOption(options?.fps, 10, 1));
|
|
503
|
+
const state = newState(options?.title === void 0 ? void 0 : scrub(options.title));
|
|
447
504
|
let settled = false;
|
|
448
505
|
let resolveDone = () => void 0;
|
|
449
506
|
const done = new Promise((resolve) => {
|
|
@@ -464,7 +521,8 @@ function progress(source, options) {
|
|
|
464
521
|
let paintedLines = 0;
|
|
465
522
|
let lastLinesBudgetAt;
|
|
466
523
|
const paintFrame = () => {
|
|
467
|
-
const
|
|
524
|
+
const rawRows = sink.rows;
|
|
525
|
+
const maxHeight = typeof rawRows === "number" && Number.isFinite(rawRows) ? Math.max(3, Math.floor(rawRows) - 1) : void 0;
|
|
468
526
|
const frame = composeFrame(state, clock.now(), tick, style, width, maxRows, maxHeight);
|
|
469
527
|
const erase = paintedLines > 0 ? `[${String(paintedLines)}A[0J` : "";
|
|
470
528
|
sink.write(erase + frame.join("\n") + "\n");
|
|
@@ -484,9 +542,9 @@ function progress(source, options) {
|
|
|
484
542
|
const node = state.nodes.get(event.spanId);
|
|
485
543
|
const elapsed = node === void 0 || node.replayed || node.startedAt <= 0 ? "" : ` in ${fmtDuration((node.endedAt ?? now) - node.startedAt)}`;
|
|
486
544
|
const roles = node !== void 0 && node.roles.length > 1 ? ` [${node.roles.map((slice) => slice.role).join(" > ")}]` : "";
|
|
487
|
-
return `agent ${agentTitle(event)} ${event.status}${elapsed}: in ${fmtTokens(event.usage
|
|
545
|
+
return `agent ${agentTitle(event)} ${event.status}${elapsed}: in ${fmtTokens(event.usage?.inputTokens ?? 0)} out ${fmtTokens(event.usage?.outputTokens ?? 0)}, ${fmtUsd(event.costUsd)}${roles}${event.replayed === true ? " (replay)" : ""}`;
|
|
488
546
|
}
|
|
489
|
-
case "agent:error": return `agent ${agentTitle(event)} error: ${event.error
|
|
547
|
+
case "agent:error": return `agent ${agentTitle(event)} error: ${event.error?.message ?? ""}` + (event.willRetry ? " (will retry)" : "");
|
|
490
548
|
case "budget:update":
|
|
491
549
|
if (lastLinesBudgetAt !== void 0 && now - lastLinesBudgetAt < 1e3) return;
|
|
492
550
|
lastLinesBudgetAt = now;
|
|
@@ -503,7 +561,7 @@ function progress(source, options) {
|
|
|
503
561
|
applyEvent(state, event, now);
|
|
504
562
|
if (mode === "lines") {
|
|
505
563
|
const line = lineFor(event, now);
|
|
506
|
-
if (line !== void 0) sink.write(line + "\n");
|
|
564
|
+
if (line !== void 0) sink.write(scrub(line) + "\n");
|
|
507
565
|
}
|
|
508
566
|
};
|
|
509
567
|
const finishLines = () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/rulvar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Rulvar umbrella package: re-exports @rulvar/core, both first-class adapters, the file store, and the terminal progress renderer. Also installable through the unscoped alias package rulvar, which re-exports this one.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@rulvar/core": "1.
|
|
26
|
-
"@rulvar/anthropic": "1.
|
|
27
|
-
"@rulvar/openai": "1.
|
|
25
|
+
"@rulvar/core": "1.22.0",
|
|
26
|
+
"@rulvar/anthropic": "1.22.0",
|
|
27
|
+
"@rulvar/openai": "1.22.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "^22.20.0",
|
|
31
31
|
"tsdown": "^0.22.3",
|
|
32
32
|
"typescript": "~6.0.3",
|
|
33
33
|
"zod": "^4.4.3",
|
|
34
|
-
"@rulvar/testing": "1.
|
|
34
|
+
"@rulvar/testing": "1.22.0"
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|