@rulvar/rulvar 1.21.0 → 1.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/index.d.ts +1 -1
- package/dist/index.js +196 -71
- 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,56 +1,128 @@
|
|
|
1
|
+
import { maskSecrets, 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
|
-
return `${amount.toFixed(4)} USD`;
|
|
20
|
+
return `${(typeof amount === "number" && Number.isFinite(amount) ? amount : 0).toFixed(4)} USD`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Defensive readers: the input is a raw iterable by contract, so a
|
|
24
|
+
* recognized event with a missing or mistyped field degrades its own
|
|
25
|
+
* line instead of throwing out of the render loop (v1.22.0 review
|
|
26
|
+
* P2-3; same discipline as the live progress reducer).
|
|
27
|
+
*/
|
|
28
|
+
function str$1(value) {
|
|
29
|
+
return typeof value === "string" ? value : "";
|
|
30
|
+
}
|
|
31
|
+
function num$1(value) {
|
|
32
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
33
|
+
}
|
|
34
|
+
function name(event) {
|
|
35
|
+
const agentType = str$1(event.agentType) || "anonymous";
|
|
36
|
+
const label = str$1(event.label);
|
|
37
|
+
return `${agentType}${label === "" ? "" : ` [${label}]`}`;
|
|
7
38
|
}
|
|
8
39
|
/**
|
|
9
40
|
* Renders events until the stream ends (the run settled). Returns after
|
|
10
41
|
* the final run:end line.
|
|
11
42
|
*/
|
|
12
43
|
async function renderProgress(events, options) {
|
|
13
|
-
const
|
|
44
|
+
const sink = options?.write ?? ((line) => {
|
|
14
45
|
process.stderr.write(`${line}\n`);
|
|
15
46
|
});
|
|
47
|
+
const write = (line) => sink(sanitizeTerminalText(line));
|
|
16
48
|
const logs = options?.logs ?? true;
|
|
17
49
|
for await (const event of events) switch (event.type) {
|
|
18
50
|
case "run:start":
|
|
19
|
-
write(`run ${event.runId} started: ${event.workflow}${event.resumed ? " (resumed)" : ""}`);
|
|
51
|
+
write(`run ${str$1(event.runId)} started: ${str$1(event.workflow)}${event.resumed === true ? " (resumed)" : ""}`);
|
|
20
52
|
break;
|
|
21
53
|
case "phase:start":
|
|
22
|
-
write(`phase: ${event.phase}`);
|
|
54
|
+
write(`phase: ${str$1(event.phase)}`);
|
|
23
55
|
break;
|
|
24
56
|
case "agent:queued":
|
|
25
|
-
write(`agent ${event
|
|
57
|
+
write(`agent ${name(event)} queued`);
|
|
26
58
|
break;
|
|
27
59
|
case "agent:start":
|
|
28
|
-
write(`agent ${event
|
|
60
|
+
write(`agent ${name(event)} -> ${str$1(event.model)} (${str$1(event.role)})`);
|
|
29
61
|
break;
|
|
30
62
|
case "agent:end":
|
|
31
|
-
write(`agent ${event
|
|
63
|
+
write(`agent ${name(event)} ${str$1(event.status)} (${usd(event.costUsd)}, ${String(num$1(event.usage?.outputTokens))} out tokens)`);
|
|
32
64
|
break;
|
|
33
65
|
case "agent:error":
|
|
34
|
-
write(`agent ${event.agentType || "anonymous"} error: ${event.error
|
|
66
|
+
write(`agent ${str$1(event.agentType) || "anonymous"} error: ${str$1(event.error?.message)}${event.willRetry === true ? " (will retry)" : ""}`);
|
|
35
67
|
break;
|
|
36
68
|
case "agent:schema-retry":
|
|
37
|
-
write(`agent ${event.agentType || "anonymous"} schema retry ${event.attempt}/${event.maxAttempts}`);
|
|
69
|
+
write(`agent ${str$1(event.agentType) || "anonymous"} schema retry ${String(num$1(event.attempt))}/${String(num$1(event.maxAttempts))}`);
|
|
38
70
|
break;
|
|
39
71
|
case "budget:update":
|
|
40
|
-
write(`budget: spent ${usd(event.spentUsd)}` + (event.remainingUsd ===
|
|
72
|
+
write(`budget: spent ${usd(event.spentUsd)}` + (typeof event.remainingUsd === "number" ? `, remaining ${usd(event.remainingUsd)}` : ""));
|
|
41
73
|
break;
|
|
42
74
|
case "log":
|
|
43
|
-
if (logs && event.level !== "debug") write(`[${event.level}] ${event.msg}`);
|
|
75
|
+
if (logs && event.level !== "debug") write(`[${str$1(event.level)}] ${str$1(event.msg)}`);
|
|
44
76
|
break;
|
|
45
77
|
case "run:end":
|
|
46
|
-
write(`run finished: ${event.status} (total ${usd(event.totalUsd)})`);
|
|
78
|
+
write(`run finished: ${str$1(event.status)} (total ${usd(event.totalUsd)})`);
|
|
47
79
|
break;
|
|
48
80
|
default: break;
|
|
49
81
|
}
|
|
50
82
|
}
|
|
51
83
|
//#endregion
|
|
52
84
|
//#region src/live-progress.ts
|
|
85
|
+
/**
|
|
86
|
+
* Live terminal progress view (v1.21.0): a claude-workflows-style tree
|
|
87
|
+
* over the WorkflowEvent stream, one row per agent with a status glyph,
|
|
88
|
+
* a running timer, token counts, and USD, plus per-role sub-timings when
|
|
89
|
+
* one agent call spans several invocation phases (loop, summarize,
|
|
90
|
+
* finalize, extract). The minimal line-per-event `renderProgress` stays
|
|
91
|
+
* untouched next door; this renderer is the rich, cursor-addressed
|
|
92
|
+
* sibling with an append-only fallback for pipes and CI.
|
|
93
|
+
*
|
|
94
|
+
* Honesty rules inherited from the event contract
|
|
95
|
+
* (https://docs.rulvar.com/guide/observability): exact token counts
|
|
96
|
+
* exist only at `agent:end`, so running rows show elapsed time and a
|
|
97
|
+
* tilde-marked character estimate from `agent:stream` deltas; run-level
|
|
98
|
+
* USD is live through `budget:update`; per-role dollars appear in the
|
|
99
|
+
* final summary only when the source is a RunHandle (they come from
|
|
100
|
+
* `RunOutcome.cost.byRole`, not from any event). Replayed lifecycle
|
|
101
|
+
* events render dim with a `replay` tag, never spin, and never add to
|
|
102
|
+
* totals: the authoritative money numbers are `budget:update.spentUsd`
|
|
103
|
+
* and `run:end.totalUsd`, which are immune to replay double counting.
|
|
104
|
+
*
|
|
105
|
+
* The reducer is defensive by contract: unknown event types are ignored
|
|
106
|
+
* and every dynamic field, including the required ones, is read
|
|
107
|
+
* defensively (optional chaining with fallbacks), so a malformed event
|
|
108
|
+
* degrades a row rather than throwing and stopping the view; an unknown
|
|
109
|
+
* parent span attaches at the root, and a mid-run attach synthesizes a
|
|
110
|
+
* root instead of failing.
|
|
111
|
+
*/
|
|
112
|
+
/**
|
|
113
|
+
* Positive-integer option normalization (v1.21.0 review P3-2): a
|
|
114
|
+
* non-finite caller value falls back to the default and a below-minimum
|
|
115
|
+
* value CLAMPS to the minimum (wording fixed in the v1.22.0 review
|
|
116
|
+
* P4-2; the behavior always clamped), so no caller value can poison the
|
|
117
|
+
* geometry (a NaN width breaks the clip, a NaN fps yields a NaN
|
|
118
|
+
* interval). Fractions floor.
|
|
119
|
+
*/
|
|
120
|
+
function posIntOption(value, fallback, min) {
|
|
121
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
122
|
+
return Math.max(min, Math.floor(value));
|
|
123
|
+
}
|
|
53
124
|
function fmtDuration(ms) {
|
|
125
|
+
if (!Number.isFinite(ms) || ms < 0) ms = 0;
|
|
54
126
|
const s = ms / 1e3;
|
|
55
127
|
if (s < 10) return `${s.toFixed(1)}s`;
|
|
56
128
|
if (s < 60) return `${String(Math.floor(s))}s`;
|
|
@@ -82,6 +154,7 @@ const SPINNER = [
|
|
|
82
154
|
"-",
|
|
83
155
|
"\\"
|
|
84
156
|
];
|
|
157
|
+
const SGR_STRIP = /* @__PURE__ */ new RegExp("\\u001B\\[[0-9;]*m", "gu");
|
|
85
158
|
function newState(title) {
|
|
86
159
|
return {
|
|
87
160
|
...title === void 0 ? {} : { title },
|
|
@@ -120,18 +193,53 @@ function nodeOf(state, event, kind, title) {
|
|
|
120
193
|
return node;
|
|
121
194
|
}
|
|
122
195
|
/**
|
|
123
|
-
* Untrusted wire strings (model ids, tool names, error messages
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
196
|
+
* Untrusted wire strings (model ids, tool names, error messages, log
|
|
197
|
+
* text, workflow and label metadata) may carry control characters and
|
|
198
|
+
* escape sequences that break the repaint arithmetic or leak terminal
|
|
199
|
+
* control. The shared core sanitizer strips C0, DEL, C1, and whole
|
|
200
|
+
* ESC-initiated CSI/OSC/DCS sequences before interpolation; the
|
|
201
|
+
* renderer's own SGR is added by paint() afterward (v1.21.0 review
|
|
202
|
+
* P2-1).
|
|
127
203
|
*/
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
204
|
+
const scrub = sanitizeTerminalText;
|
|
205
|
+
/**
|
|
206
|
+
* Defensive readers for recognized event types arriving from a RAW
|
|
207
|
+
* iterable: the engine's own bus always satisfies the types, but the
|
|
208
|
+
* documented raw-iterable source can hand the reducer any shape, and a
|
|
209
|
+
* missing or mistyped field must degrade the one row, never stop the
|
|
210
|
+
* view or reach a template literal as an object whose coercion could
|
|
211
|
+
* throw (v1.22.0 review P2-3).
|
|
212
|
+
*/
|
|
213
|
+
function str(value) {
|
|
214
|
+
return typeof value === "string" ? value : "";
|
|
215
|
+
}
|
|
216
|
+
function num(value) {
|
|
217
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
218
|
+
}
|
|
219
|
+
function numOpt(value) {
|
|
220
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* One safe form for every error a catch path surfaces: the message is
|
|
224
|
+
* secret-masked FIRST (the thrown value never went through the event
|
|
225
|
+
* masking boundary) and terminal-sanitized second, so a rejected
|
|
226
|
+
* source can neither leak a key-shaped fragment nor inject control
|
|
227
|
+
* sequences or forged lines into the sink (v1.22.0 review P2-2).
|
|
228
|
+
*/
|
|
229
|
+
function errorNotice(thrown) {
|
|
230
|
+
let message;
|
|
231
|
+
try {
|
|
232
|
+
message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
233
|
+
} catch {
|
|
234
|
+
message = "unprintable thrown value";
|
|
235
|
+
}
|
|
236
|
+
return scrub(maskSecrets(`error: ${message}`));
|
|
131
237
|
}
|
|
132
238
|
function agentTitle(event) {
|
|
133
|
-
const
|
|
134
|
-
|
|
239
|
+
const agentType = str(event.agentType);
|
|
240
|
+
const label = str(event.label);
|
|
241
|
+
const base = agentType === "" ? "agent" : scrub(agentType);
|
|
242
|
+
return label === "" ? base : `${base} (${scrub(label)})`;
|
|
135
243
|
}
|
|
136
244
|
/** The tool event's span is the agent's own span or a child of it. */
|
|
137
245
|
function toolTarget(state, event) {
|
|
@@ -144,11 +252,11 @@ function applyEvent(state, event, now) {
|
|
|
144
252
|
state.runId = event.runId;
|
|
145
253
|
state.resumed = event.resumed === true;
|
|
146
254
|
state.startedAt ??= now;
|
|
147
|
-
state.title ??= scrub(event.workflow);
|
|
255
|
+
state.title ??= scrub(str(event.workflow));
|
|
148
256
|
break;
|
|
149
257
|
case "run:end":
|
|
150
|
-
state.status = event.status;
|
|
151
|
-
state.totalUsd = event.totalUsd;
|
|
258
|
+
state.status = str(event.status) || "interrupted";
|
|
259
|
+
state.totalUsd = numOpt(event.totalUsd);
|
|
152
260
|
state.endedAt = now;
|
|
153
261
|
for (const node of state.nodes.values()) {
|
|
154
262
|
if (node.endedAt === void 0 && node.kind !== "phase") {
|
|
@@ -160,36 +268,36 @@ function applyEvent(state, event, now) {
|
|
|
160
268
|
}
|
|
161
269
|
break;
|
|
162
270
|
case "phase:start": {
|
|
163
|
-
const node = nodeOf(state, event, "phase", scrub(event.phase));
|
|
271
|
+
const node = nodeOf(state, event, "phase", scrub(str(event.phase)));
|
|
164
272
|
node.startedAt = now;
|
|
165
273
|
node.status = "ok";
|
|
166
274
|
break;
|
|
167
275
|
}
|
|
168
276
|
case "budget:update":
|
|
169
|
-
state.spentUsd = event.spentUsd;
|
|
170
|
-
state.ceilingUsd = event.remainingUsd ===
|
|
277
|
+
state.spentUsd = num(event.spentUsd);
|
|
278
|
+
state.ceilingUsd = typeof event.remainingUsd === "number" && Number.isFinite(event.remainingUsd) ? num(event.spentUsd) + event.remainingUsd : void 0;
|
|
171
279
|
break;
|
|
172
280
|
case "log":
|
|
173
281
|
if (event.level === "warn" || event.level === "error") {
|
|
174
|
-
state.notices.push(scrub(`${event.level}: ${event.msg}`));
|
|
282
|
+
state.notices.push(scrub(`${str(event.level)}: ${str(event.msg)}`));
|
|
175
283
|
if (state.notices.length > 2) state.notices.shift();
|
|
176
284
|
}
|
|
177
285
|
break;
|
|
178
286
|
case "external:waiting":
|
|
179
|
-
state.banner = scrub(`waiting on external: ${event.key}`);
|
|
287
|
+
state.banner = scrub(`waiting on external: ${str(event.key)}`);
|
|
180
288
|
break;
|
|
181
289
|
case "approval:pending":
|
|
182
|
-
state.banner = scrub(`approval pending: ${event.toolName}`);
|
|
290
|
+
state.banner = scrub(`approval pending: ${str(event.toolName)}`);
|
|
183
291
|
break;
|
|
184
292
|
case "child:start": {
|
|
185
|
-
const node = nodeOf(state, event, "child", scrub(`${event.workflow} (${event.scope})`));
|
|
293
|
+
const node = nodeOf(state, event, "child", scrub(`${str(event.workflow)} (${str(event.scope)})`));
|
|
186
294
|
node.startedAt = now;
|
|
187
295
|
break;
|
|
188
296
|
}
|
|
189
297
|
case "child:end": {
|
|
190
298
|
const node = state.nodes.get(event.spanId);
|
|
191
299
|
if (node !== void 0) {
|
|
192
|
-
node.status = event.status;
|
|
300
|
+
node.status = str(event.status) || "interrupted";
|
|
193
301
|
node.endedAt = now;
|
|
194
302
|
}
|
|
195
303
|
break;
|
|
@@ -205,41 +313,41 @@ function applyEvent(state, event, now) {
|
|
|
205
313
|
const node = nodeOf(state, event, "agent", agentTitle(event));
|
|
206
314
|
if (node.status === "queued" || node.roles.length === 0) node.startedAt = now;
|
|
207
315
|
node.status = "running";
|
|
208
|
-
node.model = scrub(event.model);
|
|
316
|
+
node.model = scrub(str(event.model));
|
|
209
317
|
if (event.replayed === true) node.replayed = true;
|
|
210
318
|
const open = node.roles.at(-1);
|
|
211
319
|
if (open !== void 0 && open.endedAt === void 0) open.endedAt = now;
|
|
212
320
|
node.roles.push({
|
|
213
|
-
role: event.role,
|
|
321
|
+
role: str(event.role),
|
|
214
322
|
startedAt: now
|
|
215
323
|
});
|
|
216
324
|
break;
|
|
217
325
|
}
|
|
218
326
|
case "agent:stream": {
|
|
219
327
|
const node = state.nodes.get(event.spanId);
|
|
220
|
-
if (node !== void 0) node.streamedChars += event.delta.length;
|
|
328
|
+
if (node !== void 0) node.streamedChars += str(event.delta).length;
|
|
221
329
|
break;
|
|
222
330
|
}
|
|
223
331
|
case "agent:error": {
|
|
224
332
|
const node = state.nodes.get(event.spanId);
|
|
225
|
-
if (node !== void 0) node.badge = event.willRetry ? "retry" : scrub(`error: ${event.error
|
|
333
|
+
if (node !== void 0) node.badge = event.willRetry ? "retry" : scrub(`error: ${str(event.error?.message)}`);
|
|
226
334
|
break;
|
|
227
335
|
}
|
|
228
336
|
case "agent:schema-retry": {
|
|
229
337
|
const node = state.nodes.get(event.spanId);
|
|
230
|
-
if (node !== void 0) node.badge = `schema ${String(event.attempt)}/${String(event.maxAttempts)}`;
|
|
338
|
+
if (node !== void 0) node.badge = `schema ${String(num(event.attempt))}/${String(num(event.maxAttempts))}`;
|
|
231
339
|
break;
|
|
232
340
|
}
|
|
233
341
|
case "agent:end": {
|
|
234
342
|
state.banner = void 0;
|
|
235
343
|
const node = nodeOf(state, event, "agent", agentTitle(event));
|
|
236
|
-
node.status = event.status;
|
|
344
|
+
node.status = str(event.status) || "interrupted";
|
|
237
345
|
node.endedAt = now;
|
|
238
346
|
node.usage = {
|
|
239
|
-
input: event.usage
|
|
240
|
-
output: event.usage
|
|
347
|
+
input: num(event.usage?.inputTokens),
|
|
348
|
+
output: num(event.usage?.outputTokens)
|
|
241
349
|
};
|
|
242
|
-
node.costUsd = event.costUsd;
|
|
350
|
+
node.costUsd = numOpt(event.costUsd);
|
|
243
351
|
if (event.replayed === true) node.replayed = true;
|
|
244
352
|
if (node.status === "ok") node.badge = void 0;
|
|
245
353
|
const open = node.roles.at(-1);
|
|
@@ -248,7 +356,7 @@ function applyEvent(state, event, now) {
|
|
|
248
356
|
}
|
|
249
357
|
case "tool:start": {
|
|
250
358
|
const node = toolTarget(state, event);
|
|
251
|
-
if (node !== void 0) node.toolActive = scrub(event.toolName);
|
|
359
|
+
if (node !== void 0) node.toolActive = scrub(str(event.toolName));
|
|
252
360
|
break;
|
|
253
361
|
}
|
|
254
362
|
case "tool:end": {
|
|
@@ -256,7 +364,7 @@ function applyEvent(state, event, now) {
|
|
|
256
364
|
if (node !== void 0) {
|
|
257
365
|
node.toolActive = void 0;
|
|
258
366
|
node.toolCount += 1;
|
|
259
|
-
if (event.outcome === "denied") node.badge = scrub(`tool ${event.toolName} denied`);
|
|
367
|
+
if (event.outcome === "denied") node.badge = scrub(`tool ${str(event.toolName)} denied`);
|
|
260
368
|
}
|
|
261
369
|
break;
|
|
262
370
|
}
|
|
@@ -268,7 +376,7 @@ function applyEvent(state, event, now) {
|
|
|
268
376
|
break;
|
|
269
377
|
case "spawn:rejected":
|
|
270
378
|
state.rejected += 1;
|
|
271
|
-
state.notices.push(`spawn rejected: ${event.code}`);
|
|
379
|
+
state.notices.push(scrub(`spawn rejected: ${str(event.code)}`));
|
|
272
380
|
if (state.notices.length > 2) state.notices.shift();
|
|
273
381
|
break;
|
|
274
382
|
default: break;
|
|
@@ -285,7 +393,7 @@ const GLYPHS = {
|
|
|
285
393
|
escalated: "!"
|
|
286
394
|
};
|
|
287
395
|
function paint(text, code, style) {
|
|
288
|
-
return style.color ?
|
|
396
|
+
return style.color ? `\u001B[${code}m${text}\u001B[0m` : text;
|
|
289
397
|
}
|
|
290
398
|
function nodeRow(node, now, spinner, style) {
|
|
291
399
|
const running = node.endedAt === void 0 && node.status === "running";
|
|
@@ -375,10 +483,11 @@ function composeFrame(state, now, tick, style, width, maxRows, maxHeight) {
|
|
|
375
483
|
...lines.slice(lines.length - keepTail)
|
|
376
484
|
];
|
|
377
485
|
}
|
|
486
|
+
const limit = Math.max(0, width - 1);
|
|
378
487
|
return clamped.map((line) => {
|
|
379
|
-
const plain = line.replace(
|
|
380
|
-
if (plain.length <=
|
|
381
|
-
return plain.slice(0,
|
|
488
|
+
const plain = line.replace(SGR_STRIP, "");
|
|
489
|
+
if (plain.length <= limit) return line;
|
|
490
|
+
return limit >= 4 ? plain.slice(0, limit - 3) + "..." : plain.slice(0, limit);
|
|
382
491
|
});
|
|
383
492
|
}
|
|
384
493
|
function defaultSink() {
|
|
@@ -440,10 +549,12 @@ function progress(source, options) {
|
|
|
440
549
|
const clock = options?.clock ?? defaultClock();
|
|
441
550
|
const mode = resolveMode(options?.mode, sink);
|
|
442
551
|
const style = { color: options?.color ?? (mode === "tty" && process.env.NO_COLOR === void 0) };
|
|
443
|
-
const
|
|
444
|
-
const
|
|
445
|
-
const
|
|
446
|
-
const
|
|
552
|
+
const columns = posIntOption(sink.columns, 80, 1);
|
|
553
|
+
const rows = posIntOption(sink.rows, 32, 3);
|
|
554
|
+
const width = posIntOption(options?.width, columns, 1);
|
|
555
|
+
const maxRows = posIntOption(options?.maxRows, Math.max(6, Math.min(24, rows - 8)), 1);
|
|
556
|
+
const fps = Math.min(30, posIntOption(options?.fps, 10, 1));
|
|
557
|
+
const state = newState(options?.title === void 0 ? void 0 : scrub(options.title));
|
|
447
558
|
let settled = false;
|
|
448
559
|
let resolveDone = () => void 0;
|
|
449
560
|
const done = new Promise((resolve) => {
|
|
@@ -464,46 +575,60 @@ function progress(source, options) {
|
|
|
464
575
|
let paintedLines = 0;
|
|
465
576
|
let lastLinesBudgetAt;
|
|
466
577
|
const paintFrame = () => {
|
|
467
|
-
const
|
|
578
|
+
const rawRows = sink.rows;
|
|
579
|
+
const maxHeight = typeof rawRows === "number" && Number.isFinite(rawRows) ? Math.max(3, Math.floor(rawRows) - 1) : void 0;
|
|
468
580
|
const frame = composeFrame(state, clock.now(), tick, style, width, maxRows, maxHeight);
|
|
469
|
-
const erase = paintedLines > 0 ?
|
|
581
|
+
const erase = paintedLines > 0 ? `\u001B[${String(paintedLines)}A\u001B[0J` : "";
|
|
470
582
|
sink.write(erase + frame.join("\n") + "\n");
|
|
471
583
|
paintedLines = frame.length;
|
|
472
584
|
state.dirty = false;
|
|
473
585
|
};
|
|
474
586
|
const lineFor = (event, now) => {
|
|
475
587
|
switch (event.type) {
|
|
476
|
-
case "run:start": return `run ${event.runId} started: ${event.workflow}${event.resumed ? " (resumed)" : ""}`;
|
|
477
|
-
case "phase:start": return `phase: ${event.phase}`;
|
|
588
|
+
case "run:start": return `run ${str(event.runId)} started: ${str(event.workflow)}${event.resumed === true ? " (resumed)" : ""}`;
|
|
589
|
+
case "phase:start": return `phase: ${str(event.phase)}`;
|
|
478
590
|
case "agent:start": {
|
|
479
591
|
const node = state.nodes.get(event.spanId);
|
|
480
592
|
const inner = node !== void 0 && node.roles.length > 1;
|
|
481
|
-
return `agent ${agentTitle(event)} -> ${event.model} (${event.role})${inner ? " [inner phase]" : ""}`;
|
|
593
|
+
return `agent ${agentTitle(event)} -> ${str(event.model)} (${str(event.role)})${inner ? " [inner phase]" : ""}`;
|
|
482
594
|
}
|
|
483
595
|
case "agent:end": {
|
|
484
596
|
const node = state.nodes.get(event.spanId);
|
|
485
597
|
const elapsed = node === void 0 || node.replayed || node.startedAt <= 0 ? "" : ` in ${fmtDuration((node.endedAt ?? now) - node.startedAt)}`;
|
|
486
598
|
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
|
|
599
|
+
return `agent ${agentTitle(event)} ${str(event.status)}${elapsed}: in ${fmtTokens(num(event.usage?.inputTokens))} out ${fmtTokens(num(event.usage?.outputTokens))}, ${fmtUsd(num(event.costUsd))}${roles}${event.replayed === true ? " (replay)" : ""}`;
|
|
488
600
|
}
|
|
489
|
-
case "agent:error": return `agent ${agentTitle(event)} error: ${event.error
|
|
601
|
+
case "agent:error": return `agent ${agentTitle(event)} error: ${str(event.error?.message)}` + (event.willRetry ? " (will retry)" : "");
|
|
490
602
|
case "budget:update":
|
|
491
603
|
if (lastLinesBudgetAt !== void 0 && now - lastLinesBudgetAt < 1e3) return;
|
|
492
604
|
lastLinesBudgetAt = now;
|
|
493
|
-
return `budget: ${fmtUsd(event.spentUsd)}` + (event.remainingUsd ===
|
|
494
|
-
case "log": return event.level === "warn" || event.level === "error" ? `[${event.level}] ${event.msg}` : void 0;
|
|
495
|
-
case "external:waiting": return `waiting on external: ${event.key}`;
|
|
496
|
-
case "approval:pending": return `approval pending: ${event.toolName}`;
|
|
497
|
-
case "run:end": return `run finished: ${event.status} (total ${fmtUsd(event.totalUsd)})`;
|
|
605
|
+
return `budget: ${fmtUsd(num(event.spentUsd))}` + (typeof event.remainingUsd === "number" && Number.isFinite(event.remainingUsd) ? ` of ${fmtUsd(num(event.spentUsd) + event.remainingUsd)}` : "");
|
|
606
|
+
case "log": return event.level === "warn" || event.level === "error" ? `[${str(event.level)}] ${str(event.msg)}` : void 0;
|
|
607
|
+
case "external:waiting": return `waiting on external: ${str(event.key)}`;
|
|
608
|
+
case "approval:pending": return `approval pending: ${str(event.toolName)}`;
|
|
609
|
+
case "run:end": return `run finished: ${str(event.status)} (total ${fmtUsd(num(event.totalUsd))})`;
|
|
498
610
|
default: return;
|
|
499
611
|
}
|
|
500
612
|
};
|
|
613
|
+
const pushNotice = (notice) => {
|
|
614
|
+
state.notices.push(notice);
|
|
615
|
+
if (state.notices.length > 2) state.notices.shift();
|
|
616
|
+
if (mode === "lines") sink.write(scrub(notice) + "\n");
|
|
617
|
+
};
|
|
618
|
+
let malformedNoticed = false;
|
|
501
619
|
const onEvent = (event) => {
|
|
502
620
|
const now = clock.now();
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
621
|
+
try {
|
|
622
|
+
applyEvent(state, event, now);
|
|
623
|
+
if (mode === "lines") {
|
|
624
|
+
const line = lineFor(event, now);
|
|
625
|
+
if (line !== void 0) sink.write(scrub(line) + "\n");
|
|
626
|
+
}
|
|
627
|
+
} catch {
|
|
628
|
+
if (!malformedNoticed) {
|
|
629
|
+
malformedNoticed = true;
|
|
630
|
+
pushNotice("progress: skipped malformed event(s); view may be incomplete");
|
|
631
|
+
}
|
|
507
632
|
}
|
|
508
633
|
};
|
|
509
634
|
const finishLines = () => {
|
|
@@ -557,7 +682,7 @@ function progress(source, options) {
|
|
|
557
682
|
state.totalUsd = outcome.cost.totalUsd;
|
|
558
683
|
state.endedAt ??= clock.now();
|
|
559
684
|
}).catch((thrown) => {
|
|
560
|
-
|
|
685
|
+
pushNotice(errorNotice(thrown));
|
|
561
686
|
state.endedAt ??= clock.now();
|
|
562
687
|
}).finally(() => {
|
|
563
688
|
handleApi.stop(true);
|
|
@@ -567,7 +692,7 @@ function progress(source, options) {
|
|
|
567
692
|
else if (typeof source.then === "function") source.then((handle) => {
|
|
568
693
|
attachHandle(handle);
|
|
569
694
|
}).catch((thrown) => {
|
|
570
|
-
|
|
695
|
+
pushNotice(errorNotice(thrown));
|
|
571
696
|
state.endedAt ??= clock.now();
|
|
572
697
|
handleApi.stop(true);
|
|
573
698
|
});
|
|
@@ -578,7 +703,7 @@ function progress(source, options) {
|
|
|
578
703
|
onEvent(event);
|
|
579
704
|
}
|
|
580
705
|
} catch (thrown) {
|
|
581
|
-
|
|
706
|
+
pushNotice(errorNotice(thrown));
|
|
582
707
|
} finally {
|
|
583
708
|
state.endedAt ??= clock.now();
|
|
584
709
|
handleApi.stop(true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/rulvar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.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.23.0",
|
|
26
|
+
"@rulvar/anthropic": "1.23.0",
|
|
27
|
+
"@rulvar/openai": "1.23.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.23.0"
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|