@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.
@@ -201,18 +201,80 @@ export function recordRun(record, cwd = process.cwd()) {
201
201
  pruneRuns(cwd);
202
202
  return true;
203
203
  }
204
- function isRecord(v) {
205
- const r = v;
206
- return (!!r &&
207
- typeof r === "object" &&
208
- r.schema_version === RUN_HISTORY_SCHEMA_VERSION &&
209
- typeof r.run_id === "string" &&
210
- typeof r.workflow_id === "string" &&
211
- typeof r.started_at === "string" &&
212
- Array.isArray(r.nodes) &&
213
- Array.isArray(r.routes) &&
214
- !!r.totals &&
215
- typeof r.totals === "object");
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
- if (isRecord(parsed) && RUN_ID_RE.test(parsed.run_id) && f === `${parsed.run_id}.json`)
242
- out.push(parsed);
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
@@ -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 agent, so every
14
- * string read from a record goes through {@link sanitizeTerminal} (control sequences stripped, length
15
- * capped) and every number is coerced to a finite number. Only for display: matching and `--json`
16
- * use the record as read.
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 agent, so every
56
- * string read from a record goes through {@link sanitizeTerminal} (control sequences stripped, length
57
- * capped) and every number is coerced to a finite number. Only for display: matching and `--json`
58
- * use the record as read.
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
- return {
63
- ...r,
64
- run_id: t(r.run_id),
65
- workflow_id: t(r.workflow_id),
66
- workflow_hash: t(r.workflow_hash),
67
- started_at: t(r.started_at),
68
- status: t(r.status, 16),
69
- duration_ms: finite(r.duration_ms),
70
- nodes: r.nodes.map((n) => ({
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: r.routes.map((e) => ({ from: t(e.from), to: t(e.to) })),
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(r.totals.nodes_total),
82
- nodes_ok: finite(r.totals.nodes_ok),
83
- nodes_skipped: finite(r.totals.nodes_skipped),
84
- tool_calls: finite(r.totals.tool_calls),
85
- tokens: finiteOrNull(r.totals.tokens),
86
- cost_usd: finiteOrNull(r.totals.cost_usd),
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
- io.stdout.write(JSON.stringify({ schema_version: 1, a, b, answers }, null, 2) + "\n");
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(JSON.stringify(runs, null, 2) + "\n");
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.206",
3
+ "version": "0.1.207",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"