@zenera/cli 1.1.23 → 1.1.25
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 +88 -2
- package/dist/ask.d.ts +51 -0
- package/dist/ask.js +166 -0
- package/dist/banner.js +2 -2
- package/dist/batch.d.ts +83 -0
- package/dist/batch.js +413 -0
- package/dist/catalog.d.ts +11 -1
- package/dist/catalog.js +80 -7
- package/dist/command.d.ts +6 -0
- package/dist/commands/inspect.d.ts +57 -0
- package/dist/commands/inspect.js +539 -91
- package/dist/commands/list.js +128 -19
- package/dist/commands/memory.js +461 -32
- package/dist/commands/meta.js +311 -21
- package/dist/commands/models.js +110 -14
- package/dist/commands/open.js +4 -4
- package/dist/commands/run.js +175 -36
- package/dist/commands/sandbox.js +148 -15
- package/dist/engine.d.ts +24 -0
- package/dist/engine.js +61 -8
- package/dist/eventlog.d.ts +59 -0
- package/dist/eventlog.js +146 -0
- package/dist/liveness.d.ts +7 -0
- package/dist/liveness.js +20 -3
- package/dist/main.js +5 -2
- package/dist/meta/editor.d.ts +9 -0
- package/dist/meta/editor.js +60 -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 +338 -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 +125 -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 +45 -0
- package/dist/meta/finetune/feedback.js +85 -0
- package/dist/meta/finetune/improvements.d.ts +51 -0
- package/dist/meta/finetune/improvements.js +244 -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 +74 -0
- package/dist/meta/finetune/live.js +226 -0
- package/dist/meta/finetune/loop.d.ts +3 -0
- package/dist/meta/finetune/loop.js +114 -0
- package/dist/meta/finetune/memories.d.ts +47 -0
- package/dist/meta/finetune/memories.js +154 -0
- package/dist/meta/finetune/report.d.ts +33 -0
- package/dist/meta/finetune/report.js +1284 -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 +41 -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 +39 -0
- package/dist/meta/finetune/train.js +391 -0
- package/dist/meta/finetune/tui.d.ts +16 -0
- package/dist/meta/finetune/tui.js +291 -0
- package/dist/meta/finetune/tuning.d.ts +167 -0
- package/dist/meta/finetune/tuning.js +289 -0
- package/dist/meta/finetune/usage.d.ts +62 -0
- package/dist/meta/finetune/usage.js +225 -0
- package/dist/meta/host.d.ts +14 -0
- package/dist/meta/host.js +20 -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/modelref.d.ts +16 -0
- package/dist/modelref.js +41 -0
- package/dist/narrate.js +5 -2
- package/dist/podman.d.ts +42 -0
- package/dist/podman.js +198 -5
- package/dist/progress.d.ts +33 -0
- package/dist/progress.js +402 -0
- package/dist/projects.d.ts +0 -5
- package/dist/projects.js +2 -15
- package/dist/request.d.ts +45 -0
- package/dist/request.js +300 -0
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +20 -3
- package/dist/scaffold.d.ts +15 -6
- package/dist/scaffold.js +5 -60
- package/dist/session.d.ts +26 -1
- package/dist/session.js +46 -32
- package/dist/term.d.ts +5 -0
- package/dist/term.js +28 -3
- package/dist/trace.d.ts +84 -0
- package/dist/trace.js +737 -0
- package/dist/tui/models.d.ts +33 -0
- package/dist/tui/models.js +389 -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/dist/validate.js +25 -3
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +41 -107
- package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
- package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
- package/templates/editor/.github/prompts/finetune.prompt.md +744 -0
- 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 +4 -3
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +18 -4
- package/templates/editor/.github/skills/zen-cli/references/check.md +7 -5
- package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +300 -21
- package/templates/editor/.github/skills/zen-cli/references/memory.md +121 -28
- package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/models.md +6 -2
- package/templates/editor/.github/skills/zen-cli/references/projects.md +12 -5
- package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
- package/templates/editor/.github/skills/zen-cli/references/run.md +201 -4
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
- 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 +1791 -0
- 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 +431 -0
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
- package/templates/editor/.github/skills/zen-memory/SKILL.md +78 -29
- package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +363 -531
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
- package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +25 -2
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +334 -0
- package/templates/editor/.github/skills/zen-sandbox-capacity/references/loadtest.md +181 -0
- package/templates/editor/.github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh +457 -0
- package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
- package/templates/editor/.vscode/agents.schema.json +1 -1
- package/templates/editor/.vscode/settings.json +2 -0
- package/templates/project/agents/files-instructions.md +35 -13
- package/templates/project/agents/fork-instructions.md +27 -0
- package/templates/project/agents/memory-instructions.md +106 -34
- package/templates/project/agents/tools-instructions.md +7 -4
- package/templates/project/gitignore +5 -0
- package/dist/meta.d.ts +0 -221
- package/dist/meta.js +0 -684
package/dist/trace.js
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { coveredIds, totalUsage, } from '@zenera/neo';
|
|
2
|
+
import { format } from "./narrate.js";
|
|
3
|
+
import { describeCall, summarise } from "./tui/wrap.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Flattening
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* Number every node in the run, branches included.
|
|
9
|
+
*
|
|
10
|
+
* A join's branches ran between the fork and the join, so they are numbered
|
|
11
|
+
* there — which means recursing into them before the join takes its own
|
|
12
|
+
* number. Nesting is free: a branch's own fork hangs off a join in its nodes.
|
|
13
|
+
*/
|
|
14
|
+
function flatten(nodes, depth, branch, acc) {
|
|
15
|
+
const hidden = coveredIds(nodes);
|
|
16
|
+
const level = [];
|
|
17
|
+
for (const node of nodes) {
|
|
18
|
+
const entry = {
|
|
19
|
+
key: '',
|
|
20
|
+
node,
|
|
21
|
+
depth,
|
|
22
|
+
branch,
|
|
23
|
+
covered: hidden.has(node.id),
|
|
24
|
+
children: [],
|
|
25
|
+
turnStart: null,
|
|
26
|
+
startMs: null,
|
|
27
|
+
endMs: null,
|
|
28
|
+
};
|
|
29
|
+
if (node.type === 'join') {
|
|
30
|
+
for (const b of node.branches) {
|
|
31
|
+
entry.children.push({
|
|
32
|
+
name: b.name,
|
|
33
|
+
agent: b.agent,
|
|
34
|
+
status: b.status,
|
|
35
|
+
entries: flatten(b.nodes, depth + 1, b.name, acc),
|
|
36
|
+
startMs: null,
|
|
37
|
+
endMs: null,
|
|
38
|
+
turnStart: null,
|
|
39
|
+
slowest: false,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
entry.key = `n${acc.length + 1}`;
|
|
44
|
+
acc.push(entry);
|
|
45
|
+
level.push(entry);
|
|
46
|
+
}
|
|
47
|
+
// Which compaction hid what, now that everything on this level has a name.
|
|
48
|
+
// "Compacted" on its own invites the wrong conclusion — that the node did
|
|
49
|
+
// not happen. Naming the summary that replaced it points at the answer.
|
|
50
|
+
const byNodeId = new Map(level.map((e) => [e.node.id, e]));
|
|
51
|
+
for (const e of level) {
|
|
52
|
+
if (e.node.type !== 'compaction') {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const id of e.node.covers) {
|
|
56
|
+
const hidden = byNodeId.get(id);
|
|
57
|
+
if (hidden) {
|
|
58
|
+
hidden.coveredBy = e.key;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return level;
|
|
63
|
+
}
|
|
64
|
+
export function traceOf(state) {
|
|
65
|
+
const entries = [];
|
|
66
|
+
const root = flatten(state.trajectory, 0, null, entries);
|
|
67
|
+
time(root, null, null);
|
|
68
|
+
return { root, entries, byKey: new Map(entries.map((e) => [e.key, e])) };
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Timing
|
|
72
|
+
//
|
|
73
|
+
// A node is stamped when it is appended, which is when the work behind it
|
|
74
|
+
// finished. So the interval between a node and the one before it in the same
|
|
75
|
+
// lane *is* that work, and every node has a start and a duration without
|
|
76
|
+
// either being stored.
|
|
77
|
+
//
|
|
78
|
+
// Offsets count from the start of the turn, not of the run: in a follow-up the
|
|
79
|
+
// question is how long this answer took, and counting from a conversation that
|
|
80
|
+
// began an hour ago answers nothing.
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
function stampOf(node) {
|
|
83
|
+
const t = Date.parse(node.ts);
|
|
84
|
+
return Number.isNaN(t) ? null : t;
|
|
85
|
+
}
|
|
86
|
+
function time(level, lanePrev, turnStart) {
|
|
87
|
+
let prev = lanePrev;
|
|
88
|
+
let t0 = turnStart;
|
|
89
|
+
for (const e of level) {
|
|
90
|
+
const ts = stampOf(e.node);
|
|
91
|
+
// A real user turn resets the clock and starts at itself: the gap since
|
|
92
|
+
// the previous run is waiting, not work. Synthetic inputs are not turns.
|
|
93
|
+
const opensTurn = !e.branch && e.node.type === 'user_input' && !e.node.synthetic;
|
|
94
|
+
const start = opensTurn || prev === null ? ts : prev;
|
|
95
|
+
// A lane that never sees a turn opener — a branch, or a trajectory that
|
|
96
|
+
// begins mid-conversation — counts from where it itself began. Anything
|
|
97
|
+
// else puts the origin after the first node and reports it as negative.
|
|
98
|
+
if (opensTurn || t0 === null) {
|
|
99
|
+
t0 = start;
|
|
100
|
+
}
|
|
101
|
+
e.turnStart = t0;
|
|
102
|
+
e.startMs = start;
|
|
103
|
+
e.endMs = ts;
|
|
104
|
+
if (e.node.type === 'join') {
|
|
105
|
+
let slowest = null;
|
|
106
|
+
for (const br of e.children) {
|
|
107
|
+
// Branches run between the fork and the join, so they start
|
|
108
|
+
// where the trunk left off and share the turn that forked them.
|
|
109
|
+
time(br.entries, prev, e.turnStart);
|
|
110
|
+
const last = br.entries.at(-1);
|
|
111
|
+
br.turnStart = e.turnStart;
|
|
112
|
+
br.startMs = prev;
|
|
113
|
+
br.endMs = last ? last.endMs : prev;
|
|
114
|
+
if (br.entries.length && (!slowest || (br.endMs ?? 0) > (slowest.endMs ?? 0))) {
|
|
115
|
+
slowest = br;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (slowest) {
|
|
119
|
+
slowest.slowest = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
prev = ts;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function secs(ms) {
|
|
126
|
+
const s = ms / 1000;
|
|
127
|
+
if (s < 100) {
|
|
128
|
+
return `${s.toFixed(1)}s`;
|
|
129
|
+
}
|
|
130
|
+
const m = Math.floor(s / 60);
|
|
131
|
+
return `${m}m${String(Math.floor(s - m * 60)).padStart(2, '0')}s`;
|
|
132
|
+
}
|
|
133
|
+
function span(ms) {
|
|
134
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : secs(ms);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Offset of a node's start from the beginning of its turn.
|
|
138
|
+
*
|
|
139
|
+
* Clamped at zero. Nothing can begin before the turn that contains it, so a
|
|
140
|
+
* negative number here is a clock that went backwards between two appends —
|
|
141
|
+
* and `t+-69.2s` is worse than useless to a reader trying to follow a sequence.
|
|
142
|
+
*/
|
|
143
|
+
function startedAt(e) {
|
|
144
|
+
return e.startMs === null || e.turnStart === null
|
|
145
|
+
? ''
|
|
146
|
+
: secs(Math.max(0, e.startMs - e.turnStart));
|
|
147
|
+
}
|
|
148
|
+
/** How long the work behind a node took. Blank when it is not worth a number. */
|
|
149
|
+
function took(e) {
|
|
150
|
+
if (e.startMs === null || e.endMs === null) {
|
|
151
|
+
return '';
|
|
152
|
+
}
|
|
153
|
+
const ms = e.endMs - e.startMs;
|
|
154
|
+
return ms < 10 ? '' : span(ms);
|
|
155
|
+
}
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Labels
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
/**
|
|
160
|
+
* Everything a model wrote passes through here before it reaches the diagram.
|
|
161
|
+
*
|
|
162
|
+
* This is a boundary, not a tidy-up: tool arguments and results are attacker
|
|
163
|
+
* input as far as the Mermaid parser is concerned. The character class is
|
|
164
|
+
* deliberately narrower than anything that could be a delimiter — no quotes,
|
|
165
|
+
* no brackets, no `#`, no `<`, no `>`, no `%`, no newlines. What survives
|
|
166
|
+
* cannot open a label, close one, start a comment or draw an edge. Widen the
|
|
167
|
+
* budget when a name needs room; never widen the class.
|
|
168
|
+
*/
|
|
169
|
+
const UNSAFE = /[^\w .:\-/+=()]/g;
|
|
170
|
+
const LABEL_MAX = 110;
|
|
171
|
+
export function safe(text, max = LABEL_MAX) {
|
|
172
|
+
return String(text).replace(UNSAFE, ' ').replace(/\s+/g, ' ').slice(0, max).trim();
|
|
173
|
+
}
|
|
174
|
+
function base(path) {
|
|
175
|
+
return path.split('/').pop() || path;
|
|
176
|
+
}
|
|
177
|
+
function preview(p) {
|
|
178
|
+
return p?.preview ?? '';
|
|
179
|
+
}
|
|
180
|
+
/** One short line saying what a node is, in the fewest identifiers that say it. */
|
|
181
|
+
function labelOf(n, opts) {
|
|
182
|
+
switch (n.type) {
|
|
183
|
+
case 'user_input':
|
|
184
|
+
return n.synthetic ? 'user input (synthetic)' : 'user input';
|
|
185
|
+
case 'system_prompt': {
|
|
186
|
+
const src = (n.sources ?? []).map((s) => base(s.path)).join(', ');
|
|
187
|
+
return safe(src ? `system prompt ${src}` : 'system prompt');
|
|
188
|
+
}
|
|
189
|
+
case 'load_skills':
|
|
190
|
+
return safe(`skills ${n.skills.map((s) => s.name).join(', ')}`);
|
|
191
|
+
case 'memory_recall':
|
|
192
|
+
return `recall ${n.nodes.length} nodes`;
|
|
193
|
+
case 'memory_op':
|
|
194
|
+
return `memory ${safe(n.op, 20)} ${n.nodes.length} nodes`;
|
|
195
|
+
case 'llm_call': {
|
|
196
|
+
const parts = [safe(`llm ${n.model}`, 60)];
|
|
197
|
+
if (opts.usage !== false) {
|
|
198
|
+
parts.push(`${format(n.usage.inputTokens)} in ${format(n.usage.outputTokens)} out`);
|
|
199
|
+
// Only when there was any: on a model that does not think, or
|
|
200
|
+
// one whose knobs were left off, the absence is the answer.
|
|
201
|
+
if (n.usage.reasoningTokens > 0) {
|
|
202
|
+
parts.push(`${format(n.usage.reasoningTokens)} thinking`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (n.toolCalls.length) {
|
|
206
|
+
parts.push(safe(`calls ${n.toolCalls.map((c) => c.name).join(', ')}`, 60));
|
|
207
|
+
}
|
|
208
|
+
return parts.join(' · ');
|
|
209
|
+
}
|
|
210
|
+
case 'tool_call': {
|
|
211
|
+
const { subject } = describeCall(n.name, preview(n.args));
|
|
212
|
+
return safe(subject ? `${n.name} ${subject}` : n.name);
|
|
213
|
+
}
|
|
214
|
+
case 'tool_result': {
|
|
215
|
+
const said = safe(summarise(n.name, preview(n.result)), 70);
|
|
216
|
+
const head = n.isError ? `ERROR ${n.name}` : n.name;
|
|
217
|
+
return `${safe(head, 40)} = ${said}`;
|
|
218
|
+
}
|
|
219
|
+
case 'handoff':
|
|
220
|
+
return safe(`handoff ${n.from} to ${n.to}`);
|
|
221
|
+
case 'fork':
|
|
222
|
+
// The context mode is what the branches started from; without it a
|
|
223
|
+
// reader cannot tell an informed branch from a cold one.
|
|
224
|
+
return safe(`fork (${n.contextMode} context) ${n.branches.map((b) => b.name).join(', ')}`);
|
|
225
|
+
case 'join':
|
|
226
|
+
return safe(`join ${n.branches.map((b) => `${b.name}=${b.status}`).join(', ')}`);
|
|
227
|
+
case 'compaction':
|
|
228
|
+
return safe(`compaction ${n.reason} (${n.covers.length} nodes hidden)`);
|
|
229
|
+
case 'final_output':
|
|
230
|
+
return 'final output';
|
|
231
|
+
default:
|
|
232
|
+
return safe(n.type);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** The whole node text: id, what it is, whose turn it was, and when. */
|
|
236
|
+
function textOf(e, previous, opts) {
|
|
237
|
+
const parts = [`${e.key} ${labelOf(e.node, opts)}`];
|
|
238
|
+
// The agent is only worth a column when it is news — on a run that never
|
|
239
|
+
// hands off it would be the same word on every line.
|
|
240
|
+
if (e.node.agent && e.node.agent !== previous?.node.agent) {
|
|
241
|
+
parts.push(`@${safe(e.node.agent, 40)}`);
|
|
242
|
+
}
|
|
243
|
+
if (opts.timing !== false) {
|
|
244
|
+
const at = startedAt(e);
|
|
245
|
+
const d = took(e);
|
|
246
|
+
if (at) {
|
|
247
|
+
parts.push(`t+${at}`);
|
|
248
|
+
}
|
|
249
|
+
if (d) {
|
|
250
|
+
parts.push(`took ${d}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (e.covered) {
|
|
254
|
+
parts.push(e.coveredBy ? `hidden by ${e.coveredBy}` : 'hidden by a compaction');
|
|
255
|
+
}
|
|
256
|
+
return parts.join(' · ');
|
|
257
|
+
}
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Emitting
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
const CLASSES = {
|
|
262
|
+
llm_call: 'llm',
|
|
263
|
+
tool_call: 'tool',
|
|
264
|
+
tool_result: 'tool',
|
|
265
|
+
compaction: 'comp',
|
|
266
|
+
fork: 'fork',
|
|
267
|
+
join: 'fork',
|
|
268
|
+
final_output: 'final',
|
|
269
|
+
handoff: 'hand',
|
|
270
|
+
};
|
|
271
|
+
function classOf(n) {
|
|
272
|
+
// A failed call is the one thing anyone looks for in a trace, so it is
|
|
273
|
+
// coloured by outcome rather than by type.
|
|
274
|
+
return n.type === 'tool_result' && n.isError ? 'err' : CLASSES[n.type];
|
|
275
|
+
}
|
|
276
|
+
function shape(key, n, text) {
|
|
277
|
+
const body = `"${text}"`;
|
|
278
|
+
if (n.type === 'fork' || n.type === 'join') {
|
|
279
|
+
return `${key}{{${body}}}`;
|
|
280
|
+
}
|
|
281
|
+
if (n.type === 'final_output') {
|
|
282
|
+
return `${key}([${body}])`;
|
|
283
|
+
}
|
|
284
|
+
if (n.type === 'user_input' || n.type === 'system_prompt') {
|
|
285
|
+
return `${key}[/${body}/]`;
|
|
286
|
+
}
|
|
287
|
+
return `${key}[${body}]`;
|
|
288
|
+
}
|
|
289
|
+
function declare(level, lines, opts) {
|
|
290
|
+
let previous;
|
|
291
|
+
for (const e of level) {
|
|
292
|
+
lines.push(` ${shape(e.key, e.node, textOf(e, previous, opts))}`);
|
|
293
|
+
if (e.node.type === 'join') {
|
|
294
|
+
for (const [i, br] of e.children.entries()) {
|
|
295
|
+
if (!br.entries.length) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const title = [
|
|
299
|
+
safe(`${br.name} ${br.agent} ${br.status}`, 60),
|
|
300
|
+
...(startedAt(br) ? [`t+${startedAt(br)}`] : []),
|
|
301
|
+
...(took(br) ? [`took ${took(br)}`] : []),
|
|
302
|
+
...(br.slowest ? ['slowest'] : []),
|
|
303
|
+
].join(' · ');
|
|
304
|
+
lines.push(` subgraph sg_${e.key}_${i}["${title}"]`);
|
|
305
|
+
lines.push(' direction TB');
|
|
306
|
+
declare(br.entries, lines, opts);
|
|
307
|
+
lines.push(' end');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
previous = e;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function collect(level, edges) {
|
|
314
|
+
const at = new Map(level.map((e, i) => [e, i]));
|
|
315
|
+
const calls = new Map();
|
|
316
|
+
const results = new Map();
|
|
317
|
+
for (const e of level) {
|
|
318
|
+
if (e.node.type === 'tool_call') {
|
|
319
|
+
calls.set(e.node.callId, e);
|
|
320
|
+
}
|
|
321
|
+
if (e.node.type === 'tool_result') {
|
|
322
|
+
results.set(e.node.callId, e);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// A pairing edge earns its place only when the two ends are apart: when
|
|
326
|
+
// they are neighbours the flow already draws it, and doubling every arrow
|
|
327
|
+
// would bury the batch of parallel calls that is the reason to look.
|
|
328
|
+
const apart = (a, b) => Math.abs((at.get(a) ?? 0) - (at.get(b) ?? 0)) > 1;
|
|
329
|
+
let previous;
|
|
330
|
+
for (const e of level) {
|
|
331
|
+
if (previous) {
|
|
332
|
+
edges.flow.push(` ${previous.key} --> ${e.key}`);
|
|
333
|
+
}
|
|
334
|
+
const cls = classOf(e.node);
|
|
335
|
+
const applied = [...(cls ? [cls] : []), ...(e.covered ? ['covered'] : [])];
|
|
336
|
+
if (applied.length) {
|
|
337
|
+
const list = applied.join(',');
|
|
338
|
+
edges.classes.set(list, [...(edges.classes.get(list) ?? []), e.key]);
|
|
339
|
+
}
|
|
340
|
+
if (e.node.type === 'llm_call') {
|
|
341
|
+
for (const c of e.node.toolCalls) {
|
|
342
|
+
const target = calls.get(c.callId) ?? results.get(c.callId);
|
|
343
|
+
if (target && apart(e, target)) {
|
|
344
|
+
edges.calls.push(` ${e.key} -.-> ${target.key}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (e.node.type === 'tool_call') {
|
|
349
|
+
const answer = results.get(e.node.callId);
|
|
350
|
+
if (answer && apart(e, answer)) {
|
|
351
|
+
edges.calls.push(` ${e.key} -.-> ${answer.key}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (e.node.type === 'join') {
|
|
355
|
+
const callId = e.node.callId;
|
|
356
|
+
const fork = level.find((x) => x.node.type === 'fork' && x.node.callId === callId);
|
|
357
|
+
for (const br of e.children) {
|
|
358
|
+
collect(br.entries, edges);
|
|
359
|
+
if (!br.entries.length) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (fork) {
|
|
363
|
+
edges.branches.push(` ${fork.key} -.-> ${br.entries[0].key}`);
|
|
364
|
+
}
|
|
365
|
+
edges.branches.push(` ${br.entries.at(-1)?.key} -.-> ${e.key}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
previous = e;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
// The header
|
|
373
|
+
//
|
|
374
|
+
// `%%` lines are comments: Mermaid drops them, a reader does not. They are
|
|
375
|
+
// where the run explains itself — what it was, who ran it, which branches
|
|
376
|
+
// existed and how often each tool was reached for. A count is what turns
|
|
377
|
+
// "this looks repetitive" into something a reader can act on.
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
function tally(entries) {
|
|
380
|
+
const counts = new Map();
|
|
381
|
+
for (const e of entries) {
|
|
382
|
+
if (e.node.type === 'tool_call') {
|
|
383
|
+
counts.set(e.node.name, (counts.get(e.node.name) ?? 0) + 1);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return [...counts.entries()]
|
|
387
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
388
|
+
.map(([name, n]) => `${safe(name, 40)} x${n}`);
|
|
389
|
+
}
|
|
390
|
+
const WRITES = new Set(['write_file', 'apply_patch', 'copy_file', 'move_file']);
|
|
391
|
+
/**
|
|
392
|
+
* What the run did with memory, zeros included. The `tools` row only lists
|
|
393
|
+
* calls that happened, and a commit that never happened is the commonest
|
|
394
|
+
* memory finding there is: absent from a list, a reader does not see it.
|
|
395
|
+
*/
|
|
396
|
+
function memoryRows(entries, bound) {
|
|
397
|
+
let recalls = 0;
|
|
398
|
+
let recalled = 0;
|
|
399
|
+
let commits = 0;
|
|
400
|
+
let committed = 0;
|
|
401
|
+
let files = 0;
|
|
402
|
+
let written = 0;
|
|
403
|
+
const calls = new Map();
|
|
404
|
+
for (const { node: n } of entries) {
|
|
405
|
+
if (n.type === 'memory_recall') {
|
|
406
|
+
recalls++;
|
|
407
|
+
recalled += n.nodes.length;
|
|
408
|
+
}
|
|
409
|
+
else if (n.type === 'memory_op' && n.op === 'commit') {
|
|
410
|
+
commits++;
|
|
411
|
+
committed += n.nodes.length;
|
|
412
|
+
files += n.files;
|
|
413
|
+
}
|
|
414
|
+
else if (n.type === 'tool_call') {
|
|
415
|
+
calls.set(n.name, (calls.get(n.name) ?? 0) + 1);
|
|
416
|
+
if (WRITES.has(n.name)) {
|
|
417
|
+
written++;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const used = (name) => calls.get(name) ?? 0;
|
|
422
|
+
const reads = used('memory_search') + used('memory_grep') + used('memory_load');
|
|
423
|
+
if (!bound && !recalls && !reads && !commits) {
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
const label = bound ? '' : 'memory';
|
|
427
|
+
const lines = [
|
|
428
|
+
row(label, `recall ${recalls} (${recalled} nodes) · search ${used('memory_search')} · ` +
|
|
429
|
+
`grep ${used('memory_grep')} · load ${used('memory_load')} · ` +
|
|
430
|
+
`commit ${commits} (${committed} nodes, ${files} files)`),
|
|
431
|
+
];
|
|
432
|
+
if (!commits && written) {
|
|
433
|
+
lines.push(row('', `nothing committed after ${written} workspace file writes`));
|
|
434
|
+
}
|
|
435
|
+
return lines;
|
|
436
|
+
}
|
|
437
|
+
function elapsed(entries) {
|
|
438
|
+
const stamps = entries.map((e) => e.endMs).filter((t) => t !== null);
|
|
439
|
+
return stamps.length < 2 ? '' : span(Math.max(...stamps) - Math.min(...stamps));
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* How much of the run was thinking, or nothing at all when none of it was.
|
|
443
|
+
*
|
|
444
|
+
* "Did this run reason?" is the commonest configuration question there is, and
|
|
445
|
+
* a reasoning knob that never reached the provider looks exactly like a healthy
|
|
446
|
+
* run. The trajectory has always known: an omitted row is the answer, and does
|
|
447
|
+
* not cost a token on a model that cannot think in the first place.
|
|
448
|
+
*/
|
|
449
|
+
function thinking(state, entries) {
|
|
450
|
+
const calls = entries.filter((e) => e.node.type === 'llm_call');
|
|
451
|
+
const thought = calls.filter((e) => e.node.type === 'llm_call' && e.node.thinking).length;
|
|
452
|
+
const tokens = totalUsage(state.trajectory).reasoningTokens;
|
|
453
|
+
if (!thought && !tokens) {
|
|
454
|
+
return '';
|
|
455
|
+
}
|
|
456
|
+
return `${thought} of ${calls.length} llm calls · ${format(tokens)} thinking tokens`;
|
|
457
|
+
}
|
|
458
|
+
function branchRoster(entries) {
|
|
459
|
+
const out = [];
|
|
460
|
+
for (const e of entries) {
|
|
461
|
+
if (e.node.type !== 'join') {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const of = e.node.branches
|
|
465
|
+
.map((b) => `${safe(b.name, 40)} (${safe(b.agent, 40)}, ${safe(b.status, 20)})`)
|
|
466
|
+
.join(', ');
|
|
467
|
+
out.push(`%% ${e.key} joins ${of}`);
|
|
468
|
+
}
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
471
|
+
function row(label, value) {
|
|
472
|
+
return `%% ${label.padEnd(9)} ${value}`;
|
|
473
|
+
}
|
|
474
|
+
function header(state, trace, opts) {
|
|
475
|
+
const { entries } = trace;
|
|
476
|
+
const kinds = new Map();
|
|
477
|
+
for (const e of entries) {
|
|
478
|
+
kinds.set(e.node.type, (kinds.get(e.node.type) ?? 0) + 1);
|
|
479
|
+
}
|
|
480
|
+
const usage = totalUsage(state.trajectory);
|
|
481
|
+
const agents = [...new Set(entries.map((e) => e.node.agent).filter(Boolean))];
|
|
482
|
+
const where = opts.dir ? ` --dir ${opts.dir}` : '';
|
|
483
|
+
const lines = [
|
|
484
|
+
'%% Zenera Neo run trajectory — every node of one run, in order.',
|
|
485
|
+
row('run', safe(opts.runId ?? state.runId, 80)),
|
|
486
|
+
// The three directories a reader needs to go further, spelt out rather
|
|
487
|
+
// than left to be reconstructed from the run id.
|
|
488
|
+
...(opts.dir ? [row('dir', safe(opts.dir, 200))] : []),
|
|
489
|
+
...(opts.workspace ? [row('workspace', safe(opts.workspace, 200))] : []),
|
|
490
|
+
...(opts.memory ? [row('memory', safe(opts.memory, 200))] : []),
|
|
491
|
+
...memoryRows(entries, Boolean(opts.memory)),
|
|
492
|
+
row('agent', `${safe(state.agentName, 40)} · started as ${safe(state.spec.startAgent, 40)}`),
|
|
493
|
+
row('phase', state.error ? `${state.phase} — ${safe(state.error, 200)}` : state.phase),
|
|
494
|
+
row('nodes', `${entries.length} · ${kinds.get('llm_call') ?? 0} llm · ` +
|
|
495
|
+
`${kinds.get('tool_call') ?? 0} tool calls · ${kinds.get('fork') ?? 0} forks`),
|
|
496
|
+
row('tokens', `${format(usage.inputTokens)} in · ${format(usage.outputTokens)} out`),
|
|
497
|
+
];
|
|
498
|
+
const thought = thinking(state, entries);
|
|
499
|
+
if (thought) {
|
|
500
|
+
lines.push(row('thinking', thought));
|
|
501
|
+
}
|
|
502
|
+
const took = elapsed(entries);
|
|
503
|
+
if (took) {
|
|
504
|
+
lines.push(row('elapsed', took));
|
|
505
|
+
}
|
|
506
|
+
if (agents.length > 1) {
|
|
507
|
+
lines.push(row('agents', agents.map((a) => safe(a, 40)).join(', ')));
|
|
508
|
+
}
|
|
509
|
+
const tools = tally(entries);
|
|
510
|
+
if (tools.length) {
|
|
511
|
+
lines.push(row('tools', tools.join(', ')));
|
|
512
|
+
}
|
|
513
|
+
const roster = branchRoster(entries);
|
|
514
|
+
if (roster.length) {
|
|
515
|
+
lines.push(row('branches', 'each subgraph below is one branch of a fork'), ...roster);
|
|
516
|
+
}
|
|
517
|
+
// Compacted nodes are numbered like any other, because they happened. What
|
|
518
|
+
// changed is only what the model could still see afterwards, and a reader
|
|
519
|
+
// comparing the diagram to the transcript needs to be told which it is.
|
|
520
|
+
const hidden = entries.filter((e) => e.covered).length;
|
|
521
|
+
if (hidden) {
|
|
522
|
+
lines.push(row('compacted', `${hidden} nodes are marked "hidden by" a later summary`), row('', 'they still ran; the model simply stopped seeing them'));
|
|
523
|
+
}
|
|
524
|
+
lines.push(
|
|
525
|
+
// Never a bare `%%`: Mermaid only strips a comment line with something
|
|
526
|
+
// after the marker, and leaves the rest to the parser, which rejects it.
|
|
527
|
+
'%% ---', row('reading', 'nN is a node id · t+ counts from the start of the turn'), row('', 'dotted edges are fork/join and calls answered out of order'), row('', 'nodes are declared in run order; every edge is in one block below'), row('detail', `zen inspect node n1 n2 n5..n9${where}`),
|
|
528
|
+
// Named here because this is where the question forms. A reader who has
|
|
529
|
+
// just found the node they distrust will not go looking for a verb.
|
|
530
|
+
row('why', `zen inspect ask n5 "why did you do that?"${where}`), '%% ---');
|
|
531
|
+
return lines;
|
|
532
|
+
}
|
|
533
|
+
export function traceMermaid(state, opts = {}) {
|
|
534
|
+
const trace = traceOf(state);
|
|
535
|
+
const styled = opts.style !== false;
|
|
536
|
+
const lines = header(state, trace, opts);
|
|
537
|
+
lines.push('flowchart TD');
|
|
538
|
+
if (styled) {
|
|
539
|
+
// Pale fills on a saturated stroke: this diagram is pasted into
|
|
540
|
+
// mermaid.live, a README or a PR, all of which are white.
|
|
541
|
+
lines.push(' classDef llm fill:#e7effc,stroke:#4a72b8,color:#16305c;');
|
|
542
|
+
lines.push(' classDef tool fill:#e4f4ed,stroke:#3f8c70,color:#124434;');
|
|
543
|
+
lines.push(' classDef comp fill:#fbf1da,stroke:#a8842f,color:#5c4410;');
|
|
544
|
+
lines.push(' classDef fork fill:#f2eafb,stroke:#8256b5,color:#3f2360;');
|
|
545
|
+
lines.push(' classDef final fill:#d6efe2,stroke:#2f8a63,color:#0d3d2a;');
|
|
546
|
+
lines.push(' classDef hand fill:#fbe9dc,stroke:#b06e3a,color:#5c3312;');
|
|
547
|
+
lines.push(' classDef err fill:#fde7e4,stroke:#c24a3e,color:#7a1f16;');
|
|
548
|
+
lines.push(' classDef covered opacity:0.55,stroke-dasharray:4 3;');
|
|
549
|
+
lines.push('');
|
|
550
|
+
}
|
|
551
|
+
declare(trace.root, lines, opts);
|
|
552
|
+
const edges = { flow: [], branches: [], calls: [], classes: new Map() };
|
|
553
|
+
collect(trace.root, edges);
|
|
554
|
+
const classes = styled
|
|
555
|
+
? [...edges.classes].map(([cls, keys]) => ` class ${keys.join(',')} ${cls};`)
|
|
556
|
+
: [];
|
|
557
|
+
const groups = [
|
|
558
|
+
['flow — the order things happened', edges.flow],
|
|
559
|
+
['branches — fork out, join back', edges.branches],
|
|
560
|
+
['calls — a call and the answer it waited for, when they are apart', edges.calls],
|
|
561
|
+
['classes', classes],
|
|
562
|
+
];
|
|
563
|
+
for (const [title, group] of groups) {
|
|
564
|
+
if (group.length) {
|
|
565
|
+
lines.push('', `%% ${title}`, ...group);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return `${lines.join('\n')}\n`;
|
|
569
|
+
}
|
|
570
|
+
/** The node list on its own, for `--json`. */
|
|
571
|
+
export function traceIndex(trace) {
|
|
572
|
+
return trace.entries.map((e) => ({
|
|
573
|
+
id: e.key,
|
|
574
|
+
nodeId: e.node.id,
|
|
575
|
+
kind: e.node.type,
|
|
576
|
+
agent: e.node.agent,
|
|
577
|
+
branch: e.branch,
|
|
578
|
+
ts: e.node.ts,
|
|
579
|
+
label: labelOf(e.node, {}),
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* The payloads a node carries, each under the name the trajectory gives it.
|
|
584
|
+
*
|
|
585
|
+
* `nodePayloads` already answers "what does this contribute to the context",
|
|
586
|
+
* but it answers it as an unnamed list. A reader asking for one node wants to
|
|
587
|
+
* know which blob is the request and which is the answer.
|
|
588
|
+
*/
|
|
589
|
+
function namedPayloads(n) {
|
|
590
|
+
const of = (name, payload) => payload ? [{ name, payload }] : [];
|
|
591
|
+
switch (n.type) {
|
|
592
|
+
case 'user_input':
|
|
593
|
+
return n.content.flatMap((part, i) => part.type === 'text'
|
|
594
|
+
? of(`content[${i}]`, part.text)
|
|
595
|
+
: [{ name: `${part.type}[${i}]`, literal: part.url }]);
|
|
596
|
+
case 'system_prompt':
|
|
597
|
+
return of('prompt', n.prompt);
|
|
598
|
+
case 'load_skills':
|
|
599
|
+
return of('content', n.content);
|
|
600
|
+
case 'memory_recall':
|
|
601
|
+
return of('recalled', n.content);
|
|
602
|
+
case 'llm_call':
|
|
603
|
+
return [
|
|
604
|
+
...of('request', n.request),
|
|
605
|
+
...of('thinking', n.thinking),
|
|
606
|
+
...of('text', n.text),
|
|
607
|
+
...n.toolCalls.flatMap((c) => of(`call ${c.name} (${c.callId})`, c.args)),
|
|
608
|
+
];
|
|
609
|
+
case 'tool_call':
|
|
610
|
+
return of('args', n.args);
|
|
611
|
+
case 'tool_result':
|
|
612
|
+
return of('result', n.result);
|
|
613
|
+
case 'fork':
|
|
614
|
+
return n.branches.flatMap((b) => of(`instructions ${b.name}`, b.instructions));
|
|
615
|
+
case 'join':
|
|
616
|
+
return n.branches.flatMap((b) => of(`output ${b.name}`, b.output));
|
|
617
|
+
case 'compaction':
|
|
618
|
+
return of('summary', n.summary);
|
|
619
|
+
case 'final_output':
|
|
620
|
+
return of('output', n.output);
|
|
621
|
+
default:
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function factsOf(n) {
|
|
626
|
+
switch (n.type) {
|
|
627
|
+
case 'llm_call':
|
|
628
|
+
return {
|
|
629
|
+
model: n.model,
|
|
630
|
+
stopReason: n.stopReason,
|
|
631
|
+
tokens: `${n.usage.inputTokens} in, ${n.usage.outputTokens} out` +
|
|
632
|
+
(n.usage.reasoningTokens ? `, ${n.usage.reasoningTokens} thinking` : ''),
|
|
633
|
+
...(n.request ? {} : { request: 'not recorded — rerun with request recording on' }),
|
|
634
|
+
};
|
|
635
|
+
case 'tool_call':
|
|
636
|
+
return { tool: n.name, callId: n.callId };
|
|
637
|
+
case 'tool_result':
|
|
638
|
+
return {
|
|
639
|
+
tool: n.name,
|
|
640
|
+
callId: n.callId,
|
|
641
|
+
outcome: n.isError ? 'error' : 'ok',
|
|
642
|
+
...(n.durationMs === undefined ? {} : { took: span(n.durationMs) }),
|
|
643
|
+
};
|
|
644
|
+
case 'handoff':
|
|
645
|
+
return { from: n.from, to: n.to, ...(n.reason ? { reason: n.reason } : {}) };
|
|
646
|
+
case 'compaction':
|
|
647
|
+
return { reason: n.reason, hidden: String(n.covers.length) };
|
|
648
|
+
case 'memory_op':
|
|
649
|
+
return { op: n.op, nodes: String(n.nodes.length), files: String(n.files) };
|
|
650
|
+
case 'memory_recall':
|
|
651
|
+
return { seeds: String(n.seeds.length), nodes: String(n.nodes.length) };
|
|
652
|
+
case 'join':
|
|
653
|
+
return Object.fromEntries(n.branches.map((b) => [b.name, b.error ?? b.status]));
|
|
654
|
+
default:
|
|
655
|
+
return {};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* `n5`, or `n5..n9`. A range is how a reader asks about a stretch, which is
|
|
660
|
+
* the shape of the question the diagram provokes ("what are n14 to n38 doing").
|
|
661
|
+
*/
|
|
662
|
+
export function parseNodeIds(words, known) {
|
|
663
|
+
const out = [];
|
|
664
|
+
const number = (word) => {
|
|
665
|
+
const m = /^n(\d+)$/.exec(word);
|
|
666
|
+
if (!m) {
|
|
667
|
+
throw new Error(`"${word}" is not a node id — they look like n7`);
|
|
668
|
+
}
|
|
669
|
+
return Number(m[1]);
|
|
670
|
+
};
|
|
671
|
+
for (const word of words) {
|
|
672
|
+
const range = word.split('..');
|
|
673
|
+
const ids = range.length === 2
|
|
674
|
+
? sequence(number(range[0]), number(range[1]))
|
|
675
|
+
: [`n${number(word)}`];
|
|
676
|
+
for (const id of ids) {
|
|
677
|
+
if (!known.has(id)) {
|
|
678
|
+
throw new Error(`${id} is not in this run — it has ${known.size} nodes`);
|
|
679
|
+
}
|
|
680
|
+
if (!out.includes(id)) {
|
|
681
|
+
out.push(id);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return out;
|
|
686
|
+
}
|
|
687
|
+
const MAX_RANGE = 200;
|
|
688
|
+
function sequence(from, to) {
|
|
689
|
+
if (to < from) {
|
|
690
|
+
throw new Error(`n${from}..n${to} runs backwards`);
|
|
691
|
+
}
|
|
692
|
+
if (to - from + 1 > MAX_RANGE) {
|
|
693
|
+
throw new Error(`n${from}..n${to} is over ${MAX_RANGE} nodes — ask for fewer`);
|
|
694
|
+
}
|
|
695
|
+
return Array.from({ length: to - from + 1 }, (_, i) => `n${from + i}`);
|
|
696
|
+
}
|
|
697
|
+
export async function nodeDetail(trace, ids, payloads) {
|
|
698
|
+
const entries = ids.map((id) => trace.byKey.get(id)).filter((e) => !!e);
|
|
699
|
+
const named = new Map(entries.map((e) => [e, namedPayloads(e.node)]));
|
|
700
|
+
const wanted = new Map([...named.values()]
|
|
701
|
+
.flatMap((parts) => parts.map((p) => p.payload))
|
|
702
|
+
.filter((p) => !!p)
|
|
703
|
+
.map((p) => [p.sha256, p]));
|
|
704
|
+
// Resolved one at a time so that one missing blob costs one part rather
|
|
705
|
+
// than the whole answer. A store can be pruned, copied without its blobs,
|
|
706
|
+
// or simply older than the run — and a preview is still worth reading.
|
|
707
|
+
const blobs = new Map(await Promise.all([...wanted.values()].map(async (p) => [
|
|
708
|
+
p.sha256,
|
|
709
|
+
await resolve(payloads, p),
|
|
710
|
+
])));
|
|
711
|
+
// Deliberately whole and unsanitized: this is the reason the diagram can be
|
|
712
|
+
// short. Having found the node, a reader needs what was actually said.
|
|
713
|
+
return entries.map((e) => ({
|
|
714
|
+
id: e.key,
|
|
715
|
+
nodeId: e.node.id,
|
|
716
|
+
kind: e.node.type,
|
|
717
|
+
agent: e.node.agent,
|
|
718
|
+
branch: e.branch,
|
|
719
|
+
ts: e.node.ts,
|
|
720
|
+
label: labelOf(e.node, {}),
|
|
721
|
+
facts: factsOf(e.node),
|
|
722
|
+
parts: (named.get(e) ?? []).map((part) => ({
|
|
723
|
+
name: part.name,
|
|
724
|
+
text: part.literal ?? blobs.get(part.payload?.sha256 ?? '') ?? '(unresolved)',
|
|
725
|
+
})),
|
|
726
|
+
}));
|
|
727
|
+
}
|
|
728
|
+
const GONE = '(the blob is not in this session’s store; only the preview survives)';
|
|
729
|
+
async function resolve(payloads, p) {
|
|
730
|
+
try {
|
|
731
|
+
return await payloads.get(p);
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
return p.preview ? `${p.preview}\n\n${GONE}` : GONE;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
//# sourceMappingURL=trace.js.map
|