@zenera/cli 1.1.24 → 1.1.26
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 +65 -0
- package/dist/ask.js +28 -5
- package/dist/banner.js +2 -2
- package/dist/batch.d.ts +6 -0
- package/dist/batch.js +76 -1
- package/dist/catalog.d.ts +31 -1
- package/dist/catalog.js +140 -7
- package/dist/commands/index.js +2 -0
- package/dist/commands/inspect.d.ts +52 -2
- package/dist/commands/inspect.js +220 -31
- package/dist/commands/memory.js +156 -32
- package/dist/commands/meta.js +333 -24
- package/dist/commands/models.js +97 -12
- package/dist/commands/open.js +4 -4
- package/dist/commands/run.js +38 -4
- package/dist/commands/sandbox.js +148 -16
- package/dist/commands/view.d.ts +8 -0
- package/dist/commands/view.js +114 -0
- package/dist/engine.d.ts +8 -1
- package/dist/engine.js +7 -0
- package/dist/eventlog.d.ts +64 -0
- package/dist/eventlog.js +170 -0
- package/dist/liveness.d.ts +7 -0
- package/dist/liveness.js +35 -3
- package/dist/main.js +0 -0
- package/dist/meta/editor.d.ts +9 -0
- package/dist/meta/editor.js +43 -0
- package/dist/meta/finetune/analyze-brief.d.ts +26 -0
- package/dist/meta/finetune/analyze-brief.js +131 -0
- package/dist/meta/finetune/apply-brief.d.ts +23 -0
- package/dist/meta/finetune/apply-brief.js +247 -0
- package/dist/meta/finetune/brief.d.ts +45 -0
- package/dist/meta/finetune/brief.js +158 -0
- package/dist/meta/finetune/children.d.ts +8 -0
- package/dist/meta/finetune/children.js +76 -0
- package/dist/meta/finetune/command.d.ts +30 -0
- package/dist/meta/finetune/command.js +516 -0
- package/dist/meta/finetune/cost.d.ts +46 -0
- package/dist/meta/finetune/cost.js +105 -0
- package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
- package/dist/meta/finetune/dataset/anchors.js +239 -0
- package/dist/meta/finetune/dataset/changes.d.ts +36 -0
- package/dist/meta/finetune/dataset/changes.js +319 -0
- package/dist/meta/finetune/dataset/command.d.ts +17 -0
- package/dist/meta/finetune/dataset/command.js +1011 -0
- package/dist/meta/finetune/dataset/drift.d.ts +33 -0
- package/dist/meta/finetune/dataset/drift.js +71 -0
- package/dist/meta/finetune/dataset/export.d.ts +26 -0
- package/dist/meta/finetune/dataset/export.js +51 -0
- package/dist/meta/finetune/dataset/index.d.ts +2 -0
- package/dist/meta/finetune/dataset/index.js +2 -0
- package/dist/meta/finetune/dataset/sample.d.ts +50 -0
- package/dist/meta/finetune/dataset/sample.js +197 -0
- package/dist/meta/finetune/dataset/schema.d.ts +33 -0
- package/dist/meta/finetune/dataset/schema.js +216 -0
- package/dist/meta/finetune/dataset/store.d.ts +35 -0
- package/dist/meta/finetune/dataset/store.js +177 -0
- package/dist/meta/finetune/dataset/types.d.ts +127 -0
- package/dist/meta/finetune/dataset/types.js +39 -0
- package/dist/meta/finetune/failure.d.ts +15 -0
- package/dist/meta/finetune/failure.js +26 -0
- package/dist/meta/finetune/feedback.d.ts +57 -0
- package/dist/meta/finetune/feedback.js +104 -0
- package/dist/meta/finetune/guard.d.ts +29 -0
- package/dist/meta/finetune/guard.js +104 -0
- package/dist/meta/finetune/improvements.d.ts +98 -0
- package/dist/meta/finetune/improvements.js +501 -0
- package/dist/meta/finetune/index.d.ts +2 -0
- package/dist/meta/finetune/index.js +2 -0
- package/dist/meta/finetune/journal.d.ts +35 -0
- package/dist/meta/finetune/journal.js +135 -0
- package/dist/meta/finetune/live.d.ts +84 -0
- package/dist/meta/finetune/live.js +272 -0
- package/dist/meta/finetune/loop.d.ts +3 -0
- package/dist/meta/finetune/loop.js +163 -0
- package/dist/meta/finetune/memories.d.ts +47 -0
- package/dist/meta/finetune/memories.js +154 -0
- package/dist/meta/finetune/recheck.d.ts +58 -0
- package/dist/meta/finetune/recheck.js +311 -0
- package/dist/meta/finetune/report.d.ts +40 -0
- package/dist/meta/finetune/report.js +1397 -0
- package/dist/meta/finetune/retry.d.ts +24 -0
- package/dist/meta/finetune/retry.js +96 -0
- package/dist/meta/finetune/sampler.d.ts +18 -0
- package/dist/meta/finetune/sampler.js +47 -0
- package/dist/meta/finetune/system.d.ts +27 -0
- package/dist/meta/finetune/system.js +132 -0
- package/dist/meta/finetune/train.d.ts +43 -0
- package/dist/meta/finetune/train.js +425 -0
- package/dist/meta/finetune/tui.d.ts +16 -0
- package/dist/meta/finetune/tui.js +316 -0
- package/dist/meta/finetune/tuning.d.ts +193 -0
- package/dist/meta/finetune/tuning.js +308 -0
- package/dist/meta/finetune/usage.d.ts +66 -0
- package/dist/meta/finetune/usage.js +260 -0
- package/dist/meta/finetune/view.d.ts +9 -0
- package/dist/meta/finetune/view.js +27 -0
- package/dist/meta/host.d.ts +15 -0
- package/dist/meta/host.js +21 -0
- package/dist/meta/index.d.ts +11 -0
- package/dist/meta/index.js +11 -0
- package/dist/meta/launch.d.ts +81 -0
- package/dist/meta/launch.js +286 -0
- package/dist/meta/model.d.ts +44 -0
- package/dist/meta/model.js +119 -0
- package/dist/meta/prompts.d.ts +18 -0
- package/dist/meta/prompts.js +61 -0
- package/dist/meta/relay.d.ts +20 -0
- package/dist/meta/relay.js +123 -0
- package/dist/meta/tokens.d.ts +52 -0
- package/dist/meta/tokens.js +226 -0
- package/dist/meta/window.d.ts +61 -0
- package/dist/meta/window.js +198 -0
- package/dist/meta/wire.d.ts +42 -0
- package/dist/meta/wire.js +129 -0
- package/dist/meta.d.ts +71 -1
- package/dist/meta.js +303 -6
- package/dist/modelref.d.ts +16 -0
- package/dist/modelref.js +41 -0
- package/dist/narrate.js +5 -2
- package/dist/podman.d.ts +26 -0
- package/dist/podman.js +83 -3
- package/dist/projects.d.ts +0 -5
- package/dist/projects.js +2 -15
- package/dist/resources.d.ts +215 -0
- package/dist/resources.js +856 -0
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +10 -3
- package/dist/scaffold.d.ts +15 -7
- package/dist/scaffold.js +24 -67
- package/dist/session.js +4 -29
- package/dist/term.d.ts +2 -0
- package/dist/term.js +11 -2
- package/dist/trace.d.ts +5 -0
- package/dist/trace.js +107 -7
- package/dist/tui/app.js +19 -5
- package/dist/tui/links.d.ts +27 -0
- package/dist/tui/links.js +142 -0
- package/dist/tui/models.d.ts +35 -0
- package/dist/tui/models.js +447 -0
- package/dist/tui/sandbox.d.ts +10 -0
- package/dist/tui/sandbox.js +131 -0
- package/dist/tui/wrap.d.ts +5 -0
- package/dist/tui/wrap.js +12 -0
- package/dist/usage.d.ts +78 -0
- package/dist/usage.js +51 -0
- package/package.json +4 -2
- package/templates/editor/.github/copilot-instructions.md +60 -145
- package/templates/editor/.github/prompts/analyze.prompt.md +33 -0
- package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +270 -0
- package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
- package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
- package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
- package/templates/editor/.github/prompts/project-review.prompt.md +5 -4
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +588 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
- package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +137 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +63 -11
- package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
- package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
- package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
- package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +35 -14
- package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
- package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
- package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +1492 -660
- package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +74 -26
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +271 -0
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
- package/templates/editor/.github/skills/zen-memory/SKILL.md +82 -53
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
- package/templates/editor/.github/skills/zen-review/SKILL.md +8 -31
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -5
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +9 -4
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
- package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
- package/templates/editor/.vscode/settings.json +1 -0
- package/templates/project/agents/files-instructions.md +29 -20
- package/templates/project/agents/fork-instructions.md +78 -28
- package/templates/project/agents/memory-instructions.md +222 -494
- package/templates/project/agents/tools-instructions.md +14 -4
- package/templates/project/gitignore +2 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
- package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
//
|
|
4
|
+
// Report on one run's batch: what each case did, and where its trajectory is.
|
|
5
|
+
//
|
|
6
|
+
// `index`, `graphs` and `compare` each open by saying which memory the run had -
|
|
7
|
+
// NO MEMORY, WITH MEMORY or MEMORY OFF - because the same trajectory means
|
|
8
|
+
// opposite things either way.
|
|
9
|
+
//
|
|
10
|
+
// Run `oom` first, and do not expect `failures` to have caught it: a case whose
|
|
11
|
+
// command was killed usually recovers, answers anyway and is recorded `ok`. A
|
|
12
|
+
// run can read "16 items, 16 ok, 0 failed" with six of them OOM-killed inside,
|
|
13
|
+
// and then nothing it did is evidence about the prompt. See the
|
|
14
|
+
// zen-finetune SKILL.md: the fix is half the concurrency, never below 4.
|
|
15
|
+
//
|
|
16
|
+
// `graphs` saves the most: every case's graph.mmd with its id, verdict and
|
|
17
|
+
// rubric above it - one read instead of one `zen inspect graph` per case.
|
|
18
|
+
//
|
|
19
|
+
// `memory` is what each case did with memory: recalls, searches, commits, and
|
|
20
|
+
// how many committed nodes the graph already held. In a with-memory run every
|
|
21
|
+
// one of those is a call spent on nothing.
|
|
22
|
+
//
|
|
23
|
+
// `commits`, `recalls` and `answers` are the reading behind those numbers:
|
|
24
|
+
// the nodes each case committed (against the candidate, with re-commits
|
|
25
|
+
// marked, when it had memory), every memory step it took, and what it answered.
|
|
26
|
+
// They go through `zen memory` and `zen inspect`, never the files on disk.
|
|
27
|
+
//
|
|
28
|
+
// Run it from anywhere; it finds the project root from its own location.
|
|
29
|
+
// See .github/skills/zen-finetune/SKILL.md.
|
|
30
|
+
|
|
31
|
+
import { spawnSync } from 'node:child_process';
|
|
32
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
33
|
+
import { dirname, join, resolve } from 'node:path';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
|
|
36
|
+
const NAME = 'report.mjs';
|
|
37
|
+
|
|
38
|
+
const USAGE = `Report on one run's batch: what each case did, and where its trajectory is.
|
|
39
|
+
|
|
40
|
+
${NAME} index: ok, agent, stop reason, cost per case
|
|
41
|
+
${NAME} graphs [id...] every trajectory graph, with its rubric above it
|
|
42
|
+
${NAME} paths [id...] id <TAB> trajectory directory, for zen inspect
|
|
43
|
+
${NAME} failures only the cases that did not finish, with errors
|
|
44
|
+
${NAME} oom were commands killed - is this run gradeable at all
|
|
45
|
+
${NAME} memory per case: recalls, memory searches, commits, already known
|
|
46
|
+
${NAME} commits [id...] per case: every node it committed; with memory, against the
|
|
47
|
+
candidate, re-commits of what it already held marked
|
|
48
|
+
${NAME} recalls [id...] per case: every recall and memory tool step, top recalled nodes
|
|
49
|
+
${NAME} answers [id...] per case: the final answer, with its rubric above it
|
|
50
|
+
${NAME} compare the per-case table findings.md opens with
|
|
51
|
+
|
|
52
|
+
-d <dir> the batch directory; default the newest finetune/runs/*/batch
|
|
53
|
+
-p <dir> the batch to compare against, for \`compare\`
|
|
54
|
+
-s <file> the dataset, for rubrics; default finetune/dataset.json`;
|
|
55
|
+
|
|
56
|
+
process.stdout.on('error', (err) => {
|
|
57
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'EPIPE') {
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
process.chdir(resolve(dirname(fileURLToPath(import.meta.url)), '../../../..'));
|
|
63
|
+
|
|
64
|
+
/** @param {string} msg @param {number} [code] @returns {never} */
|
|
65
|
+
function die(msg, code = 2) {
|
|
66
|
+
console.error(`${NAME}: ${msg}`);
|
|
67
|
+
process.exit(code);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Arguments
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
const MODES = [
|
|
75
|
+
'index',
|
|
76
|
+
'graphs',
|
|
77
|
+
'paths',
|
|
78
|
+
'failures',
|
|
79
|
+
'oom',
|
|
80
|
+
'memory',
|
|
81
|
+
'commits',
|
|
82
|
+
'recalls',
|
|
83
|
+
'answers',
|
|
84
|
+
'compare',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
let mode = '';
|
|
88
|
+
let batch = '';
|
|
89
|
+
let prev = '';
|
|
90
|
+
let dataset = 'finetune/dataset.json';
|
|
91
|
+
/** @type {string[]} */
|
|
92
|
+
const wanted = [];
|
|
93
|
+
|
|
94
|
+
const argv = process.argv.slice(2);
|
|
95
|
+
/** @param {string} flag @returns {string} */
|
|
96
|
+
const next = (flag) => {
|
|
97
|
+
const value = argv.shift();
|
|
98
|
+
if (value === undefined) {
|
|
99
|
+
die(`${flag} needs a value`);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
while (argv.length > 0) {
|
|
105
|
+
const arg = /** @type {string} */ (argv.shift());
|
|
106
|
+
if (arg === '-d') {
|
|
107
|
+
batch = next('-d');
|
|
108
|
+
} else if (arg === '-p') {
|
|
109
|
+
prev = next('-p');
|
|
110
|
+
} else if (arg === '-s') {
|
|
111
|
+
dataset = next('-s');
|
|
112
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
113
|
+
console.log(USAGE);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
} else if (MODES.includes(arg)) {
|
|
116
|
+
if (mode) {
|
|
117
|
+
die('one mode at a time');
|
|
118
|
+
}
|
|
119
|
+
mode = arg;
|
|
120
|
+
} else if (arg.startsWith('-')) {
|
|
121
|
+
console.error(`${NAME}: unknown flag ${arg}`);
|
|
122
|
+
console.error(USAGE);
|
|
123
|
+
process.exit(2);
|
|
124
|
+
} else {
|
|
125
|
+
wanted.push(arg);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
mode ||= 'index';
|
|
130
|
+
|
|
131
|
+
// Without -d, the batch written most recently - by time, not by name, since
|
|
132
|
+
// `run10` sorts before `run2`.
|
|
133
|
+
if (!batch) {
|
|
134
|
+
let newest = 0;
|
|
135
|
+
/** @type {string[]} */
|
|
136
|
+
let names = [];
|
|
137
|
+
try {
|
|
138
|
+
names = readdirSync('finetune/runs');
|
|
139
|
+
} catch {
|
|
140
|
+
names = [];
|
|
141
|
+
}
|
|
142
|
+
for (const name of names) {
|
|
143
|
+
const dir = join('finetune/runs', name, 'batch');
|
|
144
|
+
const file = join(dir, 'batch.json');
|
|
145
|
+
if (existsSync(file) && statSync(file).mtimeMs > newest) {
|
|
146
|
+
newest = statSync(file).mtimeMs;
|
|
147
|
+
batch = dir;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!batch || !existsSync(join(batch, 'batch.json'))) {
|
|
153
|
+
console.error(`${NAME}: no batch.json${batch ? ` in ${batch}` : ''}`);
|
|
154
|
+
console.error(' name one with -d, or run a batch first');
|
|
155
|
+
process.exit(2);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Reading
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
/** @param {string} path @returns {any} */
|
|
163
|
+
function json(path) {
|
|
164
|
+
try {
|
|
165
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
166
|
+
} catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* `zen <args> --json`, parsed; `{ error }` when it refused.
|
|
173
|
+
*
|
|
174
|
+
* @param {string[]} args
|
|
175
|
+
* @returns {any}
|
|
176
|
+
*/
|
|
177
|
+
function zen(args) {
|
|
178
|
+
const res = spawnSync('zen', [...args, '--json'], {
|
|
179
|
+
encoding: 'utf8',
|
|
180
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
181
|
+
});
|
|
182
|
+
if (res.error) {
|
|
183
|
+
die(`cannot run zen: ${res.error.message}`);
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const out = JSON.parse(res.stdout);
|
|
187
|
+
return res.status === 0 ? out : { error: out?.error ?? `zen exited ${res.status}` };
|
|
188
|
+
} catch {
|
|
189
|
+
const said = `${res.stderr}\n${res.stdout}`.trim().split('\n').at(-1);
|
|
190
|
+
return { error: said || `zen exited ${res.status}` };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** One line, at most `n` long. @param {string} text @param {number} [n] */
|
|
195
|
+
function flat(text, n = Infinity) {
|
|
196
|
+
const one = String(text).replace(/\s+/g, ' ').trim();
|
|
197
|
+
return one.length > n ? `${one.slice(0, n - 1)}…` : one;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const batchDoc = json(join(batch, 'batch.json'));
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* `{ id, ok }` per case, in batch order, filtered to the ids asked for.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} dir
|
|
206
|
+
* @returns {{ id: string, ok: boolean }[]}
|
|
207
|
+
*/
|
|
208
|
+
function roster(dir) {
|
|
209
|
+
const doc = dir === batch ? batchDoc : json(join(dir, 'batch.json'));
|
|
210
|
+
/** @type {{ id: string, ok: boolean }[]} */
|
|
211
|
+
const all = (doc?.batch_results ?? []).map((/** @type {any} */ r) => ({
|
|
212
|
+
id: String(r.id),
|
|
213
|
+
ok: r.ok === true,
|
|
214
|
+
}));
|
|
215
|
+
return wanted.length === 0 ? all : all.filter((r) => wanted.includes(r.id));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @param {string} id */
|
|
219
|
+
const output = (id) => json(join(batch, id, 'output.json'));
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @typedef {{ tokens: number, secs: number, llm: number, tools: number, forks: number,
|
|
223
|
+
* recalls: number, reads: number, commits: number, known: number }} Metrics
|
|
224
|
+
*/
|
|
225
|
+
|
|
226
|
+
/** @type {Metrics} */
|
|
227
|
+
const ZERO = {
|
|
228
|
+
tokens: 0,
|
|
229
|
+
secs: 0,
|
|
230
|
+
llm: 0,
|
|
231
|
+
tools: 0,
|
|
232
|
+
forks: 0,
|
|
233
|
+
recalls: 0,
|
|
234
|
+
reads: 0,
|
|
235
|
+
commits: 0,
|
|
236
|
+
known: 0,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* What one case cost. The counts come from the graph's own header rows -
|
|
241
|
+
* %% nodes 103 · 33 llm · 30 tool calls · 1 forks
|
|
242
|
+
* %% tools run_command x12, memory_search x2, memory_commit x1
|
|
243
|
+
* - because counting state.json nodes over-counts tool calls about threefold.
|
|
244
|
+
* `recalls` are the automatic recall nodes; `known` is read off the commit
|
|
245
|
+
* results, which say "N already known" when the graph held what was committed.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} dir
|
|
248
|
+
* @param {string} id
|
|
249
|
+
* @returns {Metrics}
|
|
250
|
+
*/
|
|
251
|
+
function metrics(dir, id) {
|
|
252
|
+
const out = json(join(dir, id, 'output.json'));
|
|
253
|
+
if (!out) {
|
|
254
|
+
return { ...ZERO };
|
|
255
|
+
}
|
|
256
|
+
const tokens = (out.usage?.inputTokens ?? 0) + (out.usage?.outputTokens ?? 0);
|
|
257
|
+
const secs = Math.floor((out.durationMs ?? 0) / 1000);
|
|
258
|
+
const graph = out.run?.graph;
|
|
259
|
+
if (!graph || !existsSync(graph)) {
|
|
260
|
+
return { ...ZERO, tokens, secs };
|
|
261
|
+
}
|
|
262
|
+
const lines = readFileSync(graph, 'utf8').split('\n');
|
|
263
|
+
const row = lines.find((line) => /^%% nodes\s/.test(line));
|
|
264
|
+
const parts = (row ?? '').replace(/^%% nodes\s+/, '').split(' · ');
|
|
265
|
+
const n = (/** @type {number} */ i) => Number.parseInt(parts[i] ?? '', 10) || 0;
|
|
266
|
+
/** @type {Map<string, number>} */
|
|
267
|
+
const used = new Map();
|
|
268
|
+
for (const m of (lines.find((line) => /^%% tools\s/.test(line)) ?? '').matchAll(
|
|
269
|
+
/([\w.-]+) x(\d+)/g,
|
|
270
|
+
)) {
|
|
271
|
+
used.set(m[1], Number(m[2]));
|
|
272
|
+
}
|
|
273
|
+
const tool = (/** @type {string} */ name) => used.get(name) ?? 0;
|
|
274
|
+
let known = 0;
|
|
275
|
+
for (const line of lines) {
|
|
276
|
+
if (line.includes('memory_commit =')) {
|
|
277
|
+
known += Number(/(\d+) already known/.exec(line)?.[1] ?? 0);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
tokens,
|
|
282
|
+
secs,
|
|
283
|
+
llm: n(1),
|
|
284
|
+
tools: n(2),
|
|
285
|
+
forks: n(3),
|
|
286
|
+
recalls: lines.filter((line) => /\brecall \d+ nodes\b/.test(line)).length,
|
|
287
|
+
reads: tool('memory_search') + tool('memory_grep') + tool('memory_load'),
|
|
288
|
+
commits: tool('memory_commit'),
|
|
289
|
+
known,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** 1.09M, 812k, 94. @param {number} n */
|
|
294
|
+
function hum(n) {
|
|
295
|
+
if (n >= 1_000_000) {
|
|
296
|
+
return `${(n / 1_000_000).toFixed(2)}M`;
|
|
297
|
+
}
|
|
298
|
+
if (n >= 1000) {
|
|
299
|
+
return `${Math.trunc(n / 1000)}k`;
|
|
300
|
+
}
|
|
301
|
+
return String(n);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* "33 → 30 (−3)" against a previous run, "33" without one. Tokens and seconds
|
|
306
|
+
* move continuously and read as a percentage; calls and forks read as a count.
|
|
307
|
+
*
|
|
308
|
+
* @param {number} was
|
|
309
|
+
* @param {number} now
|
|
310
|
+
* @param {'tok' | 'sec' | 'num'} kind
|
|
311
|
+
* @returns {string}
|
|
312
|
+
*/
|
|
313
|
+
function cell(was, now, kind) {
|
|
314
|
+
const show = (/** @type {number} */ v) =>
|
|
315
|
+
kind === 'tok' ? hum(v) : kind === 'sec' ? `${v}s` : String(v);
|
|
316
|
+
if (!prev) {
|
|
317
|
+
return show(now);
|
|
318
|
+
}
|
|
319
|
+
const delta = now - was;
|
|
320
|
+
const size = Math.abs(delta);
|
|
321
|
+
const sign = delta >= 0 ? '+' : '−';
|
|
322
|
+
if (size === 0) {
|
|
323
|
+
return `${show(now)} (=)`;
|
|
324
|
+
}
|
|
325
|
+
if (kind === 'num') {
|
|
326
|
+
return `${show(was)} → ${show(now)} (${sign}${size})`;
|
|
327
|
+
}
|
|
328
|
+
if (was > 0) {
|
|
329
|
+
return `${show(was)} → ${show(now)} (${sign}${((size * 100) / was).toFixed(0)}%)`;
|
|
330
|
+
}
|
|
331
|
+
return `${show(was)} → ${show(now)}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The memory a batch started from, where it is now: `memory.mjs checkpoint`
|
|
336
|
+
* renames `candidate-batchNN` to `after-batchNN` once the batch passes.
|
|
337
|
+
*
|
|
338
|
+
* @param {string} source
|
|
339
|
+
*/
|
|
340
|
+
function startedFrom(source) {
|
|
341
|
+
const kept = source.replace(/candidate-(batch\d+)\/?$/, 'after-$1');
|
|
342
|
+
return source && !existsSync(source) && existsSync(kept) ? kept : source;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* `memory off` / `no memory` / `with memory`, for a batch directory.
|
|
347
|
+
*
|
|
348
|
+
* batch.json records `--memory finetune/empty` as mode `copied`, exactly like
|
|
349
|
+
* a with-memory run. What differs is whether the source held a graph.
|
|
350
|
+
*
|
|
351
|
+
* @param {string} dir
|
|
352
|
+
* @returns {{ kind: string, line: string }}
|
|
353
|
+
*/
|
|
354
|
+
function memory(dir) {
|
|
355
|
+
const mem = json(join(dir, 'batch.json'))?.batch?.memory ?? {};
|
|
356
|
+
const mode = mem.mode ?? 'none';
|
|
357
|
+
const source = startedFrom(mem.source ?? '');
|
|
358
|
+
if (mode === 'none') {
|
|
359
|
+
return {
|
|
360
|
+
kind: 'memory off',
|
|
361
|
+
line: 'MEMORY OFF — no memory was given to this batch, and none was written',
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
if (mode === 'read-only') {
|
|
365
|
+
return {
|
|
366
|
+
kind: 'with memory',
|
|
367
|
+
line: `WITH MEMORY — one shared graph, read-only, every case read it and none wrote (${source})`,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (mode === 'copied') {
|
|
371
|
+
return source && existsSync(join(source, 'graph.json'))
|
|
372
|
+
? {
|
|
373
|
+
kind: 'with memory',
|
|
374
|
+
line: `WITH MEMORY — each case got its own copy of an existing graph (${source})`,
|
|
375
|
+
}
|
|
376
|
+
: {
|
|
377
|
+
kind: 'no memory',
|
|
378
|
+
line: `NO MEMORY — every case started from an empty graph and wrote its own (${source || 'none'})`,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return { kind: mode, line: `${mode}${source ? ` (${source})` : ''}` };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const datasetDoc = json(dataset);
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The rubric from the dataset, joined on the id; empty when there is none.
|
|
388
|
+
*
|
|
389
|
+
* @param {string} id
|
|
390
|
+
* @returns {string[]}
|
|
391
|
+
*/
|
|
392
|
+
function rubric(id) {
|
|
393
|
+
const all = datasetDoc?.cases ?? datasetDoc?.samples ?? [];
|
|
394
|
+
const found = all.find((/** @type {any} */ c) => c.id === id);
|
|
395
|
+
return (found?.rubric ?? []).map((/** @type {string} */ line) => ` - ${line}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** @param {(string | number)[]} cells @param {number[]} widths */
|
|
399
|
+
const row = (cells, widths) =>
|
|
400
|
+
cells
|
|
401
|
+
.map((c, i) =>
|
|
402
|
+
widths[i] < 0 ? String(c).padStart(-widths[i]) : String(c).padEnd(widths[i]),
|
|
403
|
+
)
|
|
404
|
+
.join(' ')
|
|
405
|
+
.replace(/ +$/, '');
|
|
406
|
+
|
|
407
|
+
const say = (/** @type {string} */ line) => process.stdout.write(`${line}\n`);
|
|
408
|
+
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
// The modes
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
if (mode === 'index') {
|
|
414
|
+
const b = batchDoc?.batch ?? {};
|
|
415
|
+
say(
|
|
416
|
+
`batch ${b.items} items, ${b.ok} ok, ${b.failed} failed` +
|
|
417
|
+
` · ${Math.floor((b.durationMs ?? 0) / 1000)}s`,
|
|
418
|
+
);
|
|
419
|
+
say(`dir ${batch}`);
|
|
420
|
+
say(`memory ${memory(batch).line}`);
|
|
421
|
+
say('');
|
|
422
|
+
const widths = [24, 5, 12, 12, -7, -5, -6];
|
|
423
|
+
say(row(['ID', 'OK', 'AGENT', 'STOP', 'TOKENS', 'SEC', 'RUBRIC'], widths));
|
|
424
|
+
for (const { id, ok } of roster(batch)) {
|
|
425
|
+
const out = output(id);
|
|
426
|
+
const lines = rubric(id).length;
|
|
427
|
+
say(
|
|
428
|
+
row(
|
|
429
|
+
[
|
|
430
|
+
id,
|
|
431
|
+
String(ok),
|
|
432
|
+
out?.agent || '-',
|
|
433
|
+
out?.stopReason || '-',
|
|
434
|
+
out ? (out.usage?.inputTokens ?? 0) + (out.usage?.outputTokens ?? 0) : '-',
|
|
435
|
+
out ? Math.floor((out.durationMs ?? 0) / 1000) : '-',
|
|
436
|
+
lines === 0 ? '-' : lines,
|
|
437
|
+
],
|
|
438
|
+
widths,
|
|
439
|
+
),
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
say('');
|
|
443
|
+
say(`next: ${NAME} -d ${batch} graphs | less`);
|
|
444
|
+
} else if (mode === 'graphs') {
|
|
445
|
+
say(`== memory: ${memory(batch).line}`);
|
|
446
|
+
for (const { id, ok } of roster(batch)) {
|
|
447
|
+
const out = output(id);
|
|
448
|
+
say('================================================================');
|
|
449
|
+
say(`== ${id} ok=${ok}`);
|
|
450
|
+
const lines = rubric(id);
|
|
451
|
+
if (lines.length > 0) {
|
|
452
|
+
say('== rubric:');
|
|
453
|
+
for (const line of lines) {
|
|
454
|
+
say(`== ${line}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
say('================================================================');
|
|
458
|
+
const graph = out?.run?.graph;
|
|
459
|
+
if (graph && existsSync(graph)) {
|
|
460
|
+
process.stdout.write(readFileSync(graph, 'utf8'));
|
|
461
|
+
} else if (out) {
|
|
462
|
+
say(out.error?.message ?? 'no graph was written');
|
|
463
|
+
} else {
|
|
464
|
+
say('no output.json');
|
|
465
|
+
}
|
|
466
|
+
say('');
|
|
467
|
+
}
|
|
468
|
+
} else if (mode === 'paths') {
|
|
469
|
+
for (const { id } of roster(batch)) {
|
|
470
|
+
say(`${id}\t${output(id)?.run?.dir || '-'}`);
|
|
471
|
+
}
|
|
472
|
+
} else if (mode === 'failures') {
|
|
473
|
+
const items = roster(batch);
|
|
474
|
+
if (items.every((r) => r.ok)) {
|
|
475
|
+
say('nothing failed');
|
|
476
|
+
process.exit(0);
|
|
477
|
+
}
|
|
478
|
+
for (const { id, ok } of items) {
|
|
479
|
+
if (ok) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
say(`-- ${id}`);
|
|
483
|
+
const out = output(id);
|
|
484
|
+
if (out) {
|
|
485
|
+
say(` ${out.error?.message ?? 'unknown'}`);
|
|
486
|
+
if (out.error?.hint) {
|
|
487
|
+
say(` hint: ${out.error.hint}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
} else if (mode === 'compare') {
|
|
492
|
+
if (prev && !existsSync(join(prev, 'batch.json'))) {
|
|
493
|
+
die(`no batch.json in ${prev}`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Part of the table, not a footnote: a pasted table loses what follows it.
|
|
497
|
+
say(`memory: ${memory(batch).line}`);
|
|
498
|
+
if (prev) {
|
|
499
|
+
say(`prev memory: ${memory(prev).line}`);
|
|
500
|
+
}
|
|
501
|
+
say('');
|
|
502
|
+
say(
|
|
503
|
+
'| case | verdict | tokens | llm calls | tool calls | forks | memory reads | commits (known) | time |',
|
|
504
|
+
);
|
|
505
|
+
say('| --- | --- | --- | --- | --- | --- | --- | --- | --- |');
|
|
506
|
+
|
|
507
|
+
const sum = { ...ZERO };
|
|
508
|
+
const was = { ...ZERO };
|
|
509
|
+
/** @param {number} c @param {number} k */
|
|
510
|
+
const commits = (c, k) => (k > 0 ? `${c} (${k} known)` : String(c));
|
|
511
|
+
for (const { id, ok } of roster(batch)) {
|
|
512
|
+
const now = metrics(batch, id);
|
|
513
|
+
const before = prev ? metrics(prev, id) : { ...ZERO };
|
|
514
|
+
for (const k of /** @type {(keyof Metrics)[]} */ (Object.keys(ZERO))) {
|
|
515
|
+
sum[k] += now[k];
|
|
516
|
+
was[k] += before[k];
|
|
517
|
+
}
|
|
518
|
+
// The verdict is the grader's: no script can read it off a trajectory.
|
|
519
|
+
say(
|
|
520
|
+
`| ${id} | ${ok ? '?' : '**did not finish**'} ` +
|
|
521
|
+
`| ${cell(before.tokens, now.tokens, 'tok')} ` +
|
|
522
|
+
`| ${cell(before.llm, now.llm, 'num')} ` +
|
|
523
|
+
`| ${cell(before.tools, now.tools, 'num')} ` +
|
|
524
|
+
`| ${cell(before.forks, now.forks, 'num')} ` +
|
|
525
|
+
`| ${cell(before.recalls + before.reads, now.recalls + now.reads, 'num')} ` +
|
|
526
|
+
`| ${prev ? `${commits(before.commits, before.known)} → ` : ''}${commits(now.commits, now.known)} ` +
|
|
527
|
+
`| ${cell(before.secs, now.secs, 'sec')} |`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
say(
|
|
532
|
+
`| **total** | — ` +
|
|
533
|
+
`| **${cell(was.tokens, sum.tokens, 'tok')}** ` +
|
|
534
|
+
`| **${cell(was.llm, sum.llm, 'num')}** ` +
|
|
535
|
+
`| **${cell(was.tools, sum.tools, 'num')}** ` +
|
|
536
|
+
`| **${cell(was.forks, sum.forks, 'num')}** ` +
|
|
537
|
+
`| **${cell(was.recalls + was.reads, sum.recalls + sum.reads, 'num')}** ` +
|
|
538
|
+
`| **${prev ? `${commits(was.commits, was.known)} → ` : ''}${commits(sum.commits, sum.known)}** ` +
|
|
539
|
+
`| **${cell(was.secs, sum.secs, 'sec')}** |`,
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
say('');
|
|
543
|
+
say(`this ${batch}`);
|
|
544
|
+
if (prev) {
|
|
545
|
+
say(`prev ${prev}`);
|
|
546
|
+
}
|
|
547
|
+
say('note: wall time is concurrent — the totals are machine load, not a sum anybody waited.');
|
|
548
|
+
const a = memory(batch).kind;
|
|
549
|
+
const b = prev ? memory(prev).kind : a;
|
|
550
|
+
if (a !== b) {
|
|
551
|
+
say('');
|
|
552
|
+
say(`note: these two runs did not have the same memory (${b} → ${a}).`);
|
|
553
|
+
say(' A with-memory run against the no-memory run that passed the same batch is');
|
|
554
|
+
say(' the measurement of what memory saves, and reads as one; otherwise the wrong');
|
|
555
|
+
say(' -p was given, or the numbers will be read as the effect of an instruction');
|
|
556
|
+
say(' that did nothing.');
|
|
557
|
+
}
|
|
558
|
+
} else if (mode === 'memory') {
|
|
559
|
+
// What each case did with memory. With memory, a read that found nothing
|
|
560
|
+
// and a commit of something the graph already held are both wasted calls.
|
|
561
|
+
say(`memory ${memory(batch).line}`);
|
|
562
|
+
say('');
|
|
563
|
+
const widths = [32, -8, -6, -8, -6];
|
|
564
|
+
say(row(['CASE', 'RECALLS', 'READS', 'COMMITS', 'KNOWN'], widths));
|
|
565
|
+
const sum = { ...ZERO };
|
|
566
|
+
for (const { id } of roster(batch)) {
|
|
567
|
+
const m = metrics(batch, id);
|
|
568
|
+
for (const k of /** @type {(keyof Metrics)[]} */ (Object.keys(ZERO))) {
|
|
569
|
+
sum[k] += m[k];
|
|
570
|
+
}
|
|
571
|
+
say(row([id, m.recalls, m.reads, m.commits, m.known], widths));
|
|
572
|
+
}
|
|
573
|
+
say(row(['total', sum.recalls, sum.reads, sum.commits, sum.known], widths));
|
|
574
|
+
say('');
|
|
575
|
+
say('recalls automatic recall nodes - what memory put in front of the model');
|
|
576
|
+
say('reads memory_search + memory_grep + memory_load calls');
|
|
577
|
+
say('commits memory_commit calls');
|
|
578
|
+
say('known committed nodes the graph already held, as the commit results report them');
|
|
579
|
+
if (memory(batch).kind === 'with memory' && sum.known > 0) {
|
|
580
|
+
say('');
|
|
581
|
+
say(`${sum.known} committed node(s) were already known: a call spent re-saying what`);
|
|
582
|
+
say('the graph held. Find the commit nodes and ask why it did not check first.');
|
|
583
|
+
}
|
|
584
|
+
} else if (mode === 'commits') {
|
|
585
|
+
// Without memory a case starts empty, so everything in its graph it committed.
|
|
586
|
+
// With memory its graph is the candidate plus its commits: diff against it.
|
|
587
|
+
const mem = memory(batch);
|
|
588
|
+
const against =
|
|
589
|
+
mem.kind === 'with memory' ? startedFrom(batchDoc?.batch?.memory?.source ?? '') : '';
|
|
590
|
+
say(`memory ${mem.line}`);
|
|
591
|
+
let added = 0;
|
|
592
|
+
let twins = 0;
|
|
593
|
+
for (const { id } of roster(batch)) {
|
|
594
|
+
const dir = join(batch, id, 'memory');
|
|
595
|
+
say('');
|
|
596
|
+
if (!existsSync(join(dir, 'manifest.json'))) {
|
|
597
|
+
say(`-- ${id} committed nothing`);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const out = against
|
|
601
|
+
? zen(['memory', 'diff', against, '--dir', dir])
|
|
602
|
+
: zen(['memory', 'ls', '--dir', dir, '--limit', '100000']);
|
|
603
|
+
if (out?.error) {
|
|
604
|
+
say(`-- ${id} ${out.error}`);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
/** @type {{ node: any, nearest?: { id: string, score: number }, twin?: boolean }[]} */
|
|
608
|
+
const rows = against ? out.added : out.nodes.map((/** @type {any} */ node) => ({ node }));
|
|
609
|
+
const held = rows.filter((r) => r.twin).length;
|
|
610
|
+
added += rows.length;
|
|
611
|
+
twins += held;
|
|
612
|
+
const extra = against
|
|
613
|
+
? [
|
|
614
|
+
held ? `${held} already held` : '',
|
|
615
|
+
out.revised.length ? `${out.revised.length} revised` : '',
|
|
616
|
+
out.used.length ? `${out.used.length} loaded` : '',
|
|
617
|
+
].filter(Boolean)
|
|
618
|
+
: [];
|
|
619
|
+
say(`-- ${id} ${rows.length} committed${extra.length ? `, ${extra.join(', ')}` : ''}`);
|
|
620
|
+
for (const { node, nearest, twin } of rows) {
|
|
621
|
+
const near = nearest
|
|
622
|
+
? ` ${twin ? 'ALREADY HELD as' : 'nearest'} ${nearest.id} ${nearest.score.toFixed(2)}`
|
|
623
|
+
: '';
|
|
624
|
+
const file = node.file ? ` file .${node.file.format}` : '';
|
|
625
|
+
say(` ${node.id} ${node.kind} ${node.audience.join(',')}${file}${near}`);
|
|
626
|
+
say(` ${flat(node.text)}`);
|
|
627
|
+
}
|
|
628
|
+
for (const { before, after } of against ? out.revised : []) {
|
|
629
|
+
say(` ${after.id} ${after.kind} revised r${before.revision} -> r${after.revision}`);
|
|
630
|
+
say(` ${flat(after.text)}`);
|
|
631
|
+
}
|
|
632
|
+
for (const { node, loads } of against ? out.used : []) {
|
|
633
|
+
say(` ${node.id} ${node.kind} loaded ${loads}x`);
|
|
634
|
+
say(` ${flat(node.text)}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
say('');
|
|
638
|
+
say(`total ${added} committed${against ? `, ${twins} already held` : ''}`);
|
|
639
|
+
if (against) {
|
|
640
|
+
say(`against ${against}`);
|
|
641
|
+
if (twins > 0) {
|
|
642
|
+
say('');
|
|
643
|
+
say(`${twins} commit(s) said again what the candidate held: merge folds them,`);
|
|
644
|
+
say('so each was a call that bought nothing. Ask at the commit why it did not');
|
|
645
|
+
say('recall first.');
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
say('');
|
|
649
|
+
say(`one node in full: zen memory show <node-id> --dir ${join(batch, '<case>', 'memory')}`);
|
|
650
|
+
} else if (mode === 'recalls') {
|
|
651
|
+
say(`memory ${memory(batch).line}`);
|
|
652
|
+
const STEP =
|
|
653
|
+
/^\s*(n\d+)\S*"n\d+ ((?:recall \d+ nodes|memory_(?:search|grep|load|commit)) .*?)(?: · t\+|")/;
|
|
654
|
+
for (const { id } of roster(batch)) {
|
|
655
|
+
const out = output(id);
|
|
656
|
+
const graph = out?.run?.graph;
|
|
657
|
+
say('');
|
|
658
|
+
if (!graph || !existsSync(graph)) {
|
|
659
|
+
say(`-- ${id} no graph`);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
/** @type {{ node: string, label: string }[]} */
|
|
663
|
+
const steps = [];
|
|
664
|
+
for (const line of readFileSync(graph, 'utf8').split('\n')) {
|
|
665
|
+
const m = STEP.exec(line);
|
|
666
|
+
if (m) {
|
|
667
|
+
steps.push({ node: m[1], label: m[2] });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (steps.length === 0) {
|
|
671
|
+
say(`-- ${id} never touched memory`);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const recalls = steps.filter((s) => s.label.startsWith('recall '));
|
|
675
|
+
/** @type {Map<string, string[]>} */
|
|
676
|
+
const seeds = new Map();
|
|
677
|
+
if (recalls.length > 0) {
|
|
678
|
+
const res = zen([
|
|
679
|
+
'inspect',
|
|
680
|
+
'node',
|
|
681
|
+
...recalls.map((r) => r.node),
|
|
682
|
+
'--dir',
|
|
683
|
+
out.run.dir,
|
|
684
|
+
]);
|
|
685
|
+
for (const n of res?.nodes ?? []) {
|
|
686
|
+
const text = n.parts?.find((/** @type {any} */ p) => p.name === 'recalled')?.text;
|
|
687
|
+
const top = [
|
|
688
|
+
...String(text ?? '').matchAll(/^(\d\.\d\d) +(\S+) +(\S+)\n +(.+)$/gm),
|
|
689
|
+
];
|
|
690
|
+
seeds.set(
|
|
691
|
+
n.id,
|
|
692
|
+
top.slice(0, 3).map((m) => `${m[1]} ${m[2]} ${m[3]} ${flat(m[4], 90)}`),
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const calls = steps.filter(
|
|
697
|
+
(s) => !s.label.startsWith('recall ') && !s.label.includes(' = '),
|
|
698
|
+
);
|
|
699
|
+
say(`-- ${id} ${recalls.length} recalls, ${calls.length} memory tool calls`);
|
|
700
|
+
for (const s of steps) {
|
|
701
|
+
say(` ${s.node.padEnd(5)} ${flat(s.label, 120)}`);
|
|
702
|
+
for (const line of seeds.get(s.node) ?? []) {
|
|
703
|
+
say(` ${line}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
say('');
|
|
708
|
+
say(
|
|
709
|
+
`one step in full: zen inspect node <nN> --dir "$(${NAME} -d ${batch} paths <case> | cut -f2)"`,
|
|
710
|
+
);
|
|
711
|
+
} else if (mode === 'answers') {
|
|
712
|
+
for (const { id, ok } of roster(batch)) {
|
|
713
|
+
const out = output(id);
|
|
714
|
+
say('================================================================');
|
|
715
|
+
say(`== ${id} ok=${ok}`);
|
|
716
|
+
const lines = rubric(id);
|
|
717
|
+
if (lines.length > 0) {
|
|
718
|
+
say('== rubric:');
|
|
719
|
+
for (const line of lines) {
|
|
720
|
+
say(`== ${line}`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
say('================================================================');
|
|
724
|
+
if (!out) {
|
|
725
|
+
say('no output.json');
|
|
726
|
+
} else if (out.ok === false) {
|
|
727
|
+
say(`did not finish: ${out.error?.message ?? 'unknown'}`);
|
|
728
|
+
} else {
|
|
729
|
+
say(String(out.output ?? '').trim() || '(no answer)');
|
|
730
|
+
}
|
|
731
|
+
say('');
|
|
732
|
+
}
|
|
733
|
+
} else if (mode === 'oom') {
|
|
734
|
+
// The graph is where an exit code survives: a killed `run_command` reads
|
|
735
|
+
// `= exit code 137`. Never grep the batch directory - a workspace is full of
|
|
736
|
+
// data files with 137 in them.
|
|
737
|
+
/** @type {{ id: string, killed: number, timedout: number }[]} */
|
|
738
|
+
const scan = [];
|
|
739
|
+
for (const { id } of roster(batch)) {
|
|
740
|
+
const graph = output(id)?.run?.graph;
|
|
741
|
+
if (!graph || !existsSync(graph)) {
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const lines = readFileSync(graph, 'utf8').split('\n');
|
|
745
|
+
const killed = lines.filter((l) => l.includes('exit code 137')).length;
|
|
746
|
+
const timedout = lines.filter((l) => l.includes('exit code 124')).length;
|
|
747
|
+
if (killed > 0 || timedout > 0) {
|
|
748
|
+
scan.push({ id, killed, timedout });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (scan.length === 0) {
|
|
753
|
+
say('no killed commands in this run — it is gradeable');
|
|
754
|
+
process.exit(0);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const widths = [32, -8, -8];
|
|
758
|
+
say(row(['CASE', 'OOM 137', 'TIME 124'], widths));
|
|
759
|
+
for (const { id, killed, timedout } of scan) {
|
|
760
|
+
say(row([id, killed, timedout], widths));
|
|
761
|
+
}
|
|
762
|
+
say('');
|
|
763
|
+
|
|
764
|
+
const casesKilled = scan.filter((s) => s.killed > 0).length;
|
|
765
|
+
const totalKilled = scan.reduce((n, s) => n + s.killed, 0);
|
|
766
|
+
const totalTimedout = scan.reduce((n, s) => n + s.timedout, 0);
|
|
767
|
+
const config = json('finetune/config.json') ?? {};
|
|
768
|
+
const current = Number(config.concurrency) || 16;
|
|
769
|
+
const halved = Math.max(4, Math.floor(current / 2));
|
|
770
|
+
|
|
771
|
+
if (casesKilled > 0) {
|
|
772
|
+
say(`${totalKilled} command(s) across ${casesKilled} case(s) were OOM-killed.`);
|
|
773
|
+
say('');
|
|
774
|
+
say('THIS RUN IS VOID, NOT GRADED.');
|
|
775
|
+
say(' Nothing here is evidence about the prompt. Do not grade it, do not');
|
|
776
|
+
say(' change instructions on it, and do not compare its tokens to another');
|
|
777
|
+
say(' run.');
|
|
778
|
+
say('');
|
|
779
|
+
if (current <= 4) {
|
|
780
|
+
say(` It ran at concurrency ${current}, the floor. The machine cannot run this`);
|
|
781
|
+
say(' batch: stop and tell the user.');
|
|
782
|
+
} else {
|
|
783
|
+
say(` Halve the concurrency and run the same cases again: set`);
|
|
784
|
+
say(
|
|
785
|
+
` "concurrency": ${halved} in finetune/config.json (was ${current}; never below 4).`,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
say(`${totalTimedout} command(s) hit the timeout; none was OOM-killed.`);
|
|
792
|
+
say(' Under memory pressure 124 is 137 wearing a different number, and raising');
|
|
793
|
+
say(' the timeout converts one into the other rather than fixing either. If it');
|
|
794
|
+
say(
|
|
795
|
+
` repeats in the next run, halve the concurrency: ${current} -> ${halved} (never below 4).`,
|
|
796
|
+
);
|
|
797
|
+
}
|