@sweny-ai/core 0.1.206 → 0.1.207
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/cli/run-history.js +77 -14
- package/dist/cli/runs.d.ts +7 -4
- package/dist/cli/runs.js +49 -23
- package/package.json +1 -1
package/dist/cli/run-history.js
CHANGED
|
@@ -201,18 +201,80 @@ export function recordRun(record, cwd = process.cwd()) {
|
|
|
201
201
|
pruneRuns(cwd);
|
|
202
202
|
return true;
|
|
203
203
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
204
|
+
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
205
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
206
|
+
const numOrNull = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
207
|
+
const RUNGS = new Set(["expr", "decider", "agent"]);
|
|
208
|
+
/**
|
|
209
|
+
* A typed, allow-listed copy of a stored record, or null when it is not a run record. History is
|
|
210
|
+
* agent-writable, so a record is never passed through: only the documented fields are copied, numbers
|
|
211
|
+
* are made finite, and malformed node, route, harness, degraded and spend entries are dropped. Strings
|
|
212
|
+
* are kept as read (terminal safety is the printer's job, see `printableRun`).
|
|
213
|
+
*/
|
|
214
|
+
function parseRecord(v) {
|
|
215
|
+
if (!isObj(v))
|
|
216
|
+
return null;
|
|
217
|
+
if (v.schema_version !== RUN_HISTORY_SCHEMA_VERSION ||
|
|
218
|
+
typeof v.run_id !== "string" ||
|
|
219
|
+
typeof v.workflow_id !== "string" ||
|
|
220
|
+
typeof v.started_at !== "string" ||
|
|
221
|
+
typeof v.status !== "string" ||
|
|
222
|
+
!Array.isArray(v.nodes) ||
|
|
223
|
+
!Array.isArray(v.routes) ||
|
|
224
|
+
!isObj(v.totals)) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const t = v.totals;
|
|
228
|
+
const out = {
|
|
229
|
+
schema_version: RUN_HISTORY_SCHEMA_VERSION,
|
|
230
|
+
run_id: v.run_id,
|
|
231
|
+
workflow_id: v.workflow_id,
|
|
232
|
+
workflow_hash: typeof v.workflow_hash === "string" ? v.workflow_hash : "",
|
|
233
|
+
started_at: v.started_at,
|
|
234
|
+
duration_ms: num(v.duration_ms),
|
|
235
|
+
status: v.status,
|
|
236
|
+
nodes: v.nodes
|
|
237
|
+
.filter((n) => isObj(n) && typeof n.id === "string" && typeof n.status === "string")
|
|
238
|
+
.map((n) => ({
|
|
239
|
+
id: n.id,
|
|
240
|
+
status: n.status,
|
|
241
|
+
duration_ms: num(n.duration_ms),
|
|
242
|
+
tool_calls: num(n.tool_calls),
|
|
243
|
+
tokens: numOrNull(n.tokens),
|
|
244
|
+
cost_usd: numOrNull(n.cost_usd),
|
|
245
|
+
retries: num(n.retries),
|
|
246
|
+
})),
|
|
247
|
+
routes: v.routes
|
|
248
|
+
.filter((e) => isObj(e) && typeof e.from === "string" && typeof e.to === "string")
|
|
249
|
+
.map((e) => ({
|
|
250
|
+
from: e.from,
|
|
251
|
+
to: e.to,
|
|
252
|
+
...(typeof e.rung === "string" && RUNGS.has(e.rung) ? { rung: e.rung } : {}),
|
|
253
|
+
})),
|
|
254
|
+
totals: {
|
|
255
|
+
nodes_total: num(t.nodes_total),
|
|
256
|
+
nodes_ok: num(t.nodes_ok),
|
|
257
|
+
nodes_skipped: num(t.nodes_skipped),
|
|
258
|
+
tool_calls: num(t.tool_calls),
|
|
259
|
+
tokens: numOrNull(t.tokens),
|
|
260
|
+
cost_usd: numOrNull(t.cost_usd),
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
if (isObj(v.harness) && typeof v.harness.id === "string" && typeof v.harness.version === "string") {
|
|
264
|
+
out.harness = { id: v.harness.id, version: v.harness.version };
|
|
265
|
+
}
|
|
266
|
+
if (Array.isArray(v.degraded))
|
|
267
|
+
out.degraded = v.degraded.filter((d) => typeof d === "string");
|
|
268
|
+
if (isObj(v.spend)) {
|
|
269
|
+
const spend = {};
|
|
270
|
+
for (const k of SPEND_KINDS) {
|
|
271
|
+
const e = Object.hasOwn(v.spend, k) ? v.spend[k] : undefined;
|
|
272
|
+
if (isObj(e))
|
|
273
|
+
spend[k] = { tokens: num(e.tokens), cost_usd: num(e.cost_usd), calls: num(e.calls) };
|
|
274
|
+
}
|
|
275
|
+
out.spend = spend;
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
216
278
|
}
|
|
217
279
|
/**
|
|
218
280
|
* All readable records, newest first. Corrupt files and unknown schema versions
|
|
@@ -238,8 +300,9 @@ export function listRuns(cwd = process.cwd()) {
|
|
|
238
300
|
if (!st.isFile() || st.size > RECORD_READ_MAX_BYTES)
|
|
239
301
|
continue;
|
|
240
302
|
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
241
|
-
|
|
242
|
-
|
|
303
|
+
const rec = parseRecord(parsed);
|
|
304
|
+
if (rec && RUN_ID_RE.test(rec.run_id) && f === `${rec.run_id}.json`)
|
|
305
|
+
out.push(rec);
|
|
243
306
|
}
|
|
244
307
|
catch {
|
|
245
308
|
// corrupt record: skip
|
package/dist/cli/runs.d.ts
CHANGED
|
@@ -10,12 +10,15 @@ import { type RunRecord } from "./run-history.js";
|
|
|
10
10
|
import { type AnswersComparison } from "./run-answers.js";
|
|
11
11
|
export declare function formatAge(startedAt: string, nowMs?: number): string;
|
|
12
12
|
/**
|
|
13
|
-
* A copy of a history record that is safe to print. `.sweny/runs/` is writable by an
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* A copy of a history record that is safe to print and to export. `.sweny/runs/` is writable by an
|
|
14
|
+
* agent, so the copy is built from an explicit allow-list of typed fields, never by spreading the
|
|
15
|
+
* record: unknown keys are dropped at every level, every string goes through {@link sanitizeTerminal}
|
|
16
|
+
* (control sequences stripped, length capped), every number is coerced finite, and malformed node and
|
|
17
|
+
* route entries are skipped. The table, the diff and every `--json` output use it.
|
|
17
18
|
*/
|
|
18
19
|
export declare function printableRun(r: RunRecord): RunRecord;
|
|
20
|
+
/** `sweny runs --json`: the validated records, never the raw ones. */
|
|
21
|
+
export declare function formatRunsJson(records: RunRecord[]): string;
|
|
19
22
|
export declare function formatRunsTable(records: RunRecord[], color: boolean, nowMs?: number, unicode?: boolean): string;
|
|
20
23
|
export interface RunDiffOptions {
|
|
21
24
|
/** Answer comparison for the pair; when set the diff leads with it and ends on its summary line. */
|
package/dist/cli/runs.js
CHANGED
|
@@ -51,23 +51,31 @@ function pad(s, w, right = false) {
|
|
|
51
51
|
// ── History records are untrusted ───────────────────────────────
|
|
52
52
|
const finite = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
53
53
|
const finiteOrNull = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
54
|
+
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
55
|
+
const RUNG = new Set(["expr", "decider", "agent"]);
|
|
54
56
|
/**
|
|
55
|
-
* A copy of a history record that is safe to print. `.sweny/runs/` is writable by an
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* A copy of a history record that is safe to print and to export. `.sweny/runs/` is writable by an
|
|
58
|
+
* agent, so the copy is built from an explicit allow-list of typed fields, never by spreading the
|
|
59
|
+
* record: unknown keys are dropped at every level, every string goes through {@link sanitizeTerminal}
|
|
60
|
+
* (control sequences stripped, length capped), every number is coerced finite, and malformed node and
|
|
61
|
+
* route entries are skipped. The table, the diff and every `--json` output use it.
|
|
59
62
|
*/
|
|
60
63
|
export function printableRun(r) {
|
|
61
64
|
const t = (v, max = 64) => sanitizeTerminal(v, max);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
const rec = r;
|
|
66
|
+
const list = (v) => (Array.isArray(v) ? v : []);
|
|
67
|
+
const totals = isObj(rec.totals) ? rec.totals : {};
|
|
68
|
+
const out = {
|
|
69
|
+
schema_version: finite(rec.schema_version),
|
|
70
|
+
run_id: t(rec.run_id),
|
|
71
|
+
workflow_id: t(rec.workflow_id),
|
|
72
|
+
workflow_hash: t(rec.workflow_hash),
|
|
73
|
+
started_at: t(rec.started_at),
|
|
74
|
+
duration_ms: finite(rec.duration_ms),
|
|
75
|
+
status: t(rec.status, 16),
|
|
76
|
+
nodes: list(rec.nodes)
|
|
77
|
+
.filter(isObj)
|
|
78
|
+
.map((n) => ({
|
|
71
79
|
id: t(n.id),
|
|
72
80
|
status: t(n.status, 16),
|
|
73
81
|
duration_ms: finite(n.duration_ms),
|
|
@@ -76,17 +84,34 @@ export function printableRun(r) {
|
|
|
76
84
|
cost_usd: finiteOrNull(n.cost_usd),
|
|
77
85
|
retries: finite(n.retries),
|
|
78
86
|
})),
|
|
79
|
-
routes:
|
|
87
|
+
routes: list(rec.routes)
|
|
88
|
+
.filter((e) => isObj(e) && typeof e.from === "string" && typeof e.to === "string")
|
|
89
|
+
.map((e) => ({
|
|
90
|
+
from: t(e.from),
|
|
91
|
+
to: t(e.to),
|
|
92
|
+
...(typeof e.rung === "string" && RUNG.has(e.rung) ? { rung: e.rung } : {}),
|
|
93
|
+
})),
|
|
80
94
|
totals: {
|
|
81
|
-
nodes_total: finite(
|
|
82
|
-
nodes_ok: finite(
|
|
83
|
-
nodes_skipped: finite(
|
|
84
|
-
tool_calls: finite(
|
|
85
|
-
tokens: finiteOrNull(
|
|
86
|
-
cost_usd: finiteOrNull(
|
|
95
|
+
nodes_total: finite(totals.nodes_total),
|
|
96
|
+
nodes_ok: finite(totals.nodes_ok),
|
|
97
|
+
nodes_skipped: finite(totals.nodes_skipped),
|
|
98
|
+
tool_calls: finite(totals.tool_calls),
|
|
99
|
+
tokens: finiteOrNull(totals.tokens),
|
|
100
|
+
cost_usd: finiteOrNull(totals.cost_usd),
|
|
87
101
|
},
|
|
88
|
-
...(r.spend && typeof r.spend === "object" ? { spend: printableSpend(r.spend) } : {}),
|
|
89
102
|
};
|
|
103
|
+
if (isObj(rec.harness))
|
|
104
|
+
out.harness = { id: t(rec.harness.id), version: t(rec.harness.version) };
|
|
105
|
+
if (Array.isArray(rec.degraded)) {
|
|
106
|
+
out.degraded = rec.degraded.filter((d) => typeof d === "string").map((d) => t(d));
|
|
107
|
+
}
|
|
108
|
+
if (isObj(rec.spend))
|
|
109
|
+
out.spend = printableSpend(rec.spend);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
/** `sweny runs --json`: the validated records, never the raw ones. */
|
|
113
|
+
export function formatRunsJson(records) {
|
|
114
|
+
return JSON.stringify(records.map(printableRun), null, 2) + "\n";
|
|
90
115
|
}
|
|
91
116
|
/** The record's split, or its totals as all node spend when it has none. */
|
|
92
117
|
function splitOf(r) {
|
|
@@ -432,7 +457,8 @@ export function runRunsDiff(refs, opts, io) {
|
|
|
432
457
|
const { a, b } = picked;
|
|
433
458
|
const answers = compareAnswers(readRunAnswers(a.run_id, io.cwd), readRunAnswers(b.run_id, io.cwd));
|
|
434
459
|
if (opts.json) {
|
|
435
|
-
|
|
460
|
+
const body = { schema_version: 1, a: printableRun(a), b: printableRun(b), answers };
|
|
461
|
+
io.stdout.write(JSON.stringify(body, null, 2) + "\n");
|
|
436
462
|
return 0;
|
|
437
463
|
}
|
|
438
464
|
io.stdout.write("\n" + formatRunDiff(a, b, colorEnabled(io.stdout, env), { answers, unicode: supportsUnicode(env) }) + "\n");
|
|
@@ -458,7 +484,7 @@ export function registerRunsCommand(program) {
|
|
|
458
484
|
runs = runs.filter((r) => r.workflow_id === opts.workflow);
|
|
459
485
|
runs = runs.slice(0, limit);
|
|
460
486
|
if (opts.json) {
|
|
461
|
-
process.stdout.write(
|
|
487
|
+
process.stdout.write(formatRunsJson(runs));
|
|
462
488
|
return;
|
|
463
489
|
}
|
|
464
490
|
process.stdout.write("\n" + formatRunsTable(runs, colorEnabled(process.stdout), Date.now(), supportsUnicode()) + "\n");
|