@manyoars/oars 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -26
- package/dist/api.js +1 -1
- package/dist/bin.js +6 -29
- package/dist/cli.d.ts +6 -3
- package/dist/cli.js +370 -72
- package/dist/commands/keys.d.ts +31 -0
- package/dist/commands/keys.js +65 -0
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +3 -5
- package/dist/errors.d.ts +13 -13
- package/dist/errors.js +28 -11
- package/dist/gate/checks.d.ts +68 -0
- package/dist/gate/checks.js +584 -0
- package/dist/gate/command.d.ts +14 -0
- package/dist/gate/command.js +61 -0
- package/dist/gate/hook-fill-main.d.ts +1 -0
- package/dist/gate/hook-fill-main.js +16 -0
- package/dist/gate/hook-fill.d.ts +38 -0
- package/dist/gate/hook-fill.js +94 -0
- package/dist/gate/hook-main.d.ts +1 -0
- package/dist/gate/hook-main.js +35 -0
- package/dist/gate/hook-process.d.ts +14 -0
- package/dist/gate/hook-process.js +64 -0
- package/dist/gate/hook.d.ts +46 -0
- package/dist/gate/hook.js +256 -0
- package/dist/gate/install.d.ts +25 -0
- package/dist/gate/install.js +126 -0
- package/dist/gate/local-bin.d.ts +8 -0
- package/dist/gate/local-bin.js +36 -0
- package/dist/gate/notes.d.ts +19 -0
- package/dist/gate/notes.js +27 -0
- package/dist/gate/novelty.d.ts +35 -0
- package/dist/gate/novelty.js +99 -0
- package/dist/gate/policy.d.ts +25 -0
- package/dist/gate/policy.js +73 -0
- package/dist/gate/record.d.ts +120 -0
- package/dist/gate/record.js +125 -0
- package/dist/gate/registry-state.d.ts +10 -0
- package/dist/gate/registry-state.js +30 -0
- package/dist/gate/registry.d.ts +56 -0
- package/dist/gate/registry.js +94 -0
- package/dist/gate/replay.d.ts +20 -0
- package/dist/gate/replay.js +257 -0
- package/dist/gate/report.d.ts +50 -0
- package/dist/gate/report.js +89 -0
- package/dist/gate/scope.d.ts +7 -0
- package/dist/gate/scope.js +27 -0
- package/dist/gate/transcripts.d.ts +68 -0
- package/dist/gate/transcripts.js +232 -0
- package/dist/hook-bin.d.ts +1 -0
- package/dist/hook-bin.js +25 -0
- package/dist/issuer.d.ts +16 -0
- package/dist/issuer.js +55 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +64 -0
- package/dist/oidc.js +1 -1
- package/dist/sessions/actions.d.ts +35 -0
- package/dist/sessions/actions.js +351 -0
- package/dist/sessions/allow.d.ts +14 -0
- package/dist/sessions/allow.js +51 -0
- package/dist/sessions/archive.d.ts +11 -0
- package/dist/sessions/archive.js +38 -0
- package/dist/sessions/audit.d.ts +42 -0
- package/dist/sessions/audit.js +104 -0
- package/dist/sessions/baseline.d.ts +62 -0
- package/dist/sessions/baseline.js +188 -0
- package/dist/sessions/capture-runtime.d.ts +10 -0
- package/dist/sessions/capture-runtime.js +12 -0
- package/dist/sessions/cell-increments.d.ts +43 -0
- package/dist/sessions/cell-increments.js +209 -0
- package/dist/sessions/cells.d.ts +79 -0
- package/dist/sessions/cells.js +181 -0
- package/dist/sessions/codex.d.ts +53 -0
- package/dist/sessions/codex.js +679 -0
- package/dist/sessions/command.d.ts +117 -0
- package/dist/sessions/command.js +373 -0
- package/dist/sessions/copilot.d.ts +40 -0
- package/dist/sessions/copilot.js +432 -0
- package/dist/sessions/cursor.d.ts +24 -0
- package/dist/sessions/cursor.js +324 -0
- package/dist/sessions/derive.d.ts +57 -0
- package/dist/sessions/derive.js +320 -0
- package/dist/sessions/exclusion.d.ts +81 -0
- package/dist/sessions/exclusion.js +353 -0
- package/dist/sessions/extract.d.ts +179 -0
- package/dist/sessions/extract.js +1774 -0
- package/dist/sessions/fold.d.ts +17 -0
- package/dist/sessions/fold.js +71 -0
- package/dist/sessions/frame.d.ts +24 -0
- package/dist/sessions/frame.js +95 -0
- package/dist/sessions/gemini.d.ts +37 -0
- package/dist/sessions/gemini.js +613 -0
- package/dist/sessions/gold-arms.d.ts +67 -0
- package/dist/sessions/gold-arms.js +172 -0
- package/dist/sessions/gold.d.ts +197 -0
- package/dist/sessions/gold.js +463 -0
- package/dist/sessions/graph.d.ts +12 -0
- package/dist/sessions/graph.js +34 -0
- package/dist/sessions/grok.d.ts +36 -0
- package/dist/sessions/grok.js +421 -0
- package/dist/sessions/hook-calls.d.ts +3 -0
- package/dist/sessions/hook-calls.js +47 -0
- package/dist/sessions/hook-index.d.ts +53 -0
- package/dist/sessions/hook-index.js +157 -0
- package/dist/sessions/hooks.d.ts +13 -0
- package/dist/sessions/hooks.js +170 -0
- package/dist/sessions/identity.d.ts +22 -0
- package/dist/sessions/identity.js +101 -0
- package/dist/sessions/install.d.ts +54 -0
- package/dist/sessions/install.js +201 -0
- package/dist/sessions/lineage.d.ts +19 -0
- package/dist/sessions/lineage.js +118 -0
- package/dist/sessions/mcp.d.ts +6 -0
- package/dist/sessions/mcp.js +153 -0
- package/dist/sessions/merkle.d.ts +7 -0
- package/dist/sessions/merkle.js +28 -0
- package/dist/sessions/motif.d.ts +43 -0
- package/dist/sessions/motif.js +59 -0
- package/dist/sessions/package-resolve.d.ts +30 -0
- package/dist/sessions/package-resolve.js +183 -0
- package/dist/sessions/pending.d.ts +34 -0
- package/dist/sessions/pending.js +142 -0
- package/dist/sessions/pool-command.d.ts +44 -0
- package/dist/sessions/pool-command.js +542 -0
- package/dist/sessions/pool-delta.d.ts +54 -0
- package/dist/sessions/pool-delta.js +190 -0
- package/dist/sessions/pool-outbox.d.ts +63 -0
- package/dist/sessions/pool-outbox.js +260 -0
- package/dist/sessions/pool-print.d.ts +26 -0
- package/dist/sessions/pool-print.js +68 -0
- package/dist/sessions/pool-wire.d.ts +225 -0
- package/dist/sessions/pool-wire.js +620 -0
- package/dist/sessions/pool.d.ts +32 -0
- package/dist/sessions/pool.js +164 -0
- package/dist/sessions/projection.d.ts +75 -0
- package/dist/sessions/projection.js +165 -0
- package/dist/sessions/public-vocabulary.d.ts +73 -0
- package/dist/sessions/public-vocabulary.js +302 -0
- package/dist/sessions/query.d.ts +111 -0
- package/dist/sessions/query.js +246 -0
- package/dist/sessions/read-adoption.d.ts +2 -0
- package/dist/sessions/read-adoption.js +27 -0
- package/dist/sessions/read-call-shape.d.ts +2 -0
- package/dist/sessions/read-call-shape.js +25 -0
- package/dist/sessions/read-change.d.ts +2 -0
- package/dist/sessions/read-change.js +30 -0
- package/dist/sessions/read-cost.d.ts +4 -0
- package/dist/sessions/read-cost.js +37 -0
- package/dist/sessions/read-day-move.d.ts +3 -0
- package/dist/sessions/read-day-move.js +65 -0
- package/dist/sessions/read-fit.d.ts +7 -0
- package/dist/sessions/read-fit.js +41 -0
- package/dist/sessions/read-interaction.d.ts +5 -0
- package/dist/sessions/read-interaction.js +62 -0
- package/dist/sessions/read-lead-lag.d.ts +15 -0
- package/dist/sessions/read-lead-lag.js +81 -0
- package/dist/sessions/read-live.d.ts +6 -0
- package/dist/sessions/read-live.js +38 -0
- package/dist/sessions/read-method.d.ts +2 -0
- package/dist/sessions/read-method.js +29 -0
- package/dist/sessions/read-next-step.d.ts +2 -0
- package/dist/sessions/read-next-step.js +35 -0
- package/dist/sessions/read-order.d.ts +54 -0
- package/dist/sessions/read-order.js +62 -0
- package/dist/sessions/read-readable.d.ts +8 -0
- package/dist/sessions/read-readable.js +24 -0
- package/dist/sessions/read-recovery.d.ts +20 -0
- package/dist/sessions/read-recovery.js +97 -0
- package/dist/sessions/read-rows.d.ts +13 -0
- package/dist/sessions/read-rows.js +47 -0
- package/dist/sessions/read-substitution.d.ts +5 -0
- package/dist/sessions/read-substitution.js +51 -0
- package/dist/sessions/read-support.d.ts +48 -0
- package/dist/sessions/read-support.js +73 -0
- package/dist/sessions/read-tool-day.d.ts +2 -0
- package/dist/sessions/read-tool-day.js +26 -0
- package/dist/sessions/read-version-boundary.d.ts +31 -0
- package/dist/sessions/read-version-boundary.js +95 -0
- package/dist/sessions/read.d.ts +79 -0
- package/dist/sessions/read.js +84 -0
- package/dist/sessions/recall-consequence.d.ts +45 -0
- package/dist/sessions/recall-consequence.js +179 -0
- package/dist/sessions/recall-object.d.ts +137 -0
- package/dist/sessions/recall-object.js +77 -0
- package/dist/sessions/recall-pool.d.ts +19 -0
- package/dist/sessions/recall-pool.js +96 -0
- package/dist/sessions/recall-push.d.ts +26 -0
- package/dist/sessions/recall-push.js +95 -0
- package/dist/sessions/recall-reads.d.ts +117 -0
- package/dist/sessions/recall-reads.js +410 -0
- package/dist/sessions/recall-render.d.ts +19 -0
- package/dist/sessions/recall-render.js +168 -0
- package/dist/sessions/redact.d.ts +6 -0
- package/dist/sessions/redact.js +54 -0
- package/dist/sessions/repo-root.d.ts +10 -0
- package/dist/sessions/repo-root.js +101 -0
- package/dist/sessions/schema.d.ts +644 -0
- package/dist/sessions/schema.js +148 -0
- package/dist/sessions/session-shape.d.ts +73 -0
- package/dist/sessions/session-shape.js +342 -0
- package/dist/sessions/sources.d.ts +50 -0
- package/dist/sessions/sources.js +159 -0
- package/dist/sessions/steps.d.ts +105 -0
- package/dist/sessions/steps.js +250 -0
- package/dist/sessions/store.d.ts +297 -0
- package/dist/sessions/store.js +1478 -0
- package/dist/sessions/strata.d.ts +26 -0
- package/dist/sessions/strata.js +77 -0
- package/dist/sessions/survival.d.ts +25 -0
- package/dist/sessions/survival.js +230 -0
- package/dist/sessions/verify.d.ts +53 -0
- package/dist/sessions/verify.js +286 -0
- package/dist/sessions/vocab-load.d.ts +26 -0
- package/dist/sessions/vocab-load.js +61 -0
- package/dist/sessions/vscode-copilot.d.ts +38 -0
- package/dist/sessions/vscode-copilot.js +477 -0
- package/package.json +4 -5
- package/vocab/crates.json +1 -0
- package/vocab/go.json +1 -0
- package/vocab/harness.json +1 -0
- package/vocab/homebrew.json +1 -0
- package/vocab/manifest.json +1 -0
- package/vocab/maven.json +1 -0
- package/vocab/models.json +1 -0
- package/vocab/npm.json +1 -0
- package/vocab/programs.json +1 -0
- package/vocab/pypi.json +1 -0
- package/vocab/rubygems.json +1 -0
- package/dist/commands/deploy-input.d.ts +0 -35
- package/dist/commands/deploy-input.js +0 -107
- package/dist/commands/deploy.d.ts +0 -22
- package/dist/commands/deploy.js +0 -120
- package/dist/commands/status.d.ts +0 -7
- package/dist/commands/status.js +0 -18
- package/dist/commands/test-charge.d.ts +0 -11
- package/dist/commands/test-charge.js +0 -34
- package/dist/journal.d.ts +0 -26
- package/dist/journal.js +0 -58
- package/parity/deploy-plans/paid-api.json +0 -34
- package/parity/deploy-plans/per-task-agent.json +0 -34
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
// The outcome gold set: a random sample of prompts an operator judges blind, and a scorer that reads every signal against those
|
|
2
|
+
// judgments. It answers one question agreement among the signals cannot: does a local proxy predict human-judged success within an operator?
|
|
3
|
+
// Every arm stays separate: no composite, no per-person score. The file shows the request and the result; the signals and the agent's closing
|
|
4
|
+
// claim go to a sidecar beside it.
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { INTERRUPT_MARKER } from './derive.js';
|
|
9
|
+
import { checkSession } from './exclusion.js';
|
|
10
|
+
import { cut, EDITS, promptOpening, SHELL, shellTarget, shortName } from './extract.js';
|
|
11
|
+
import { PROMPT_HEAD_CHARS } from './schema.js';
|
|
12
|
+
import { SOURCES } from './sources.js';
|
|
13
|
+
import { paths } from './store.js';
|
|
14
|
+
export const isObj = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
|
+
const str = (value) => (typeof value === 'string' ? value : undefined);
|
|
16
|
+
const texts = (value) => str(value) ?? '';
|
|
17
|
+
// Capture's rules, kept in step by a test against `captureSession`, so a store `index` names the same turn here.
|
|
18
|
+
const firstTextBlock = (content) => {
|
|
19
|
+
if (typeof content === 'string')
|
|
20
|
+
return content;
|
|
21
|
+
if (!Array.isArray(content))
|
|
22
|
+
return '';
|
|
23
|
+
for (const block of content)
|
|
24
|
+
if (isObj(block) && block.type === 'text')
|
|
25
|
+
return texts(block.text);
|
|
26
|
+
return '';
|
|
27
|
+
};
|
|
28
|
+
const resultText = (block) => {
|
|
29
|
+
const { content } = block;
|
|
30
|
+
if (typeof content === 'string')
|
|
31
|
+
return content;
|
|
32
|
+
if (!Array.isArray(content))
|
|
33
|
+
return '';
|
|
34
|
+
return content.filter((part) => isObj(part) && part.type === 'text').map((part) => texts(part.text)).join('\n');
|
|
35
|
+
};
|
|
36
|
+
const failedResult = (block, text) => {
|
|
37
|
+
if (block.is_error === true)
|
|
38
|
+
return true;
|
|
39
|
+
const exit = /^Exit code (\d+)/.exec(text.trim());
|
|
40
|
+
return exit !== null && exit[1] !== '0';
|
|
41
|
+
};
|
|
42
|
+
export const readTurns = (text) => {
|
|
43
|
+
const turns = [];
|
|
44
|
+
// A resumed session repeats earlier lines: the first copy of a call carries its result, and a repeat is not fresh work.
|
|
45
|
+
const calls = new Map();
|
|
46
|
+
const lines = text.split('\n');
|
|
47
|
+
for (const [position, raw] of lines.entries()) {
|
|
48
|
+
let line;
|
|
49
|
+
try {
|
|
50
|
+
line = JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!isObj(line) || !isObj(line.message) || line.isSidechain === true)
|
|
56
|
+
continue;
|
|
57
|
+
const { content, role } = line.message;
|
|
58
|
+
const blocks = Array.isArray(content) ? content.filter(isObj) : [];
|
|
59
|
+
if (line.type === 'user' && role === 'user' && line.isMeta !== true) {
|
|
60
|
+
const written = firstTextBlock(content);
|
|
61
|
+
const opening = written === '' || blocks.some((block) => block.type === 'tool_result') ? undefined : promptOpening(written, lines, position);
|
|
62
|
+
if (opening !== undefined) {
|
|
63
|
+
turns.push({ index: turns.length, request: opening.request, calls: [], replies: [] });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const turn = turns.at(-1);
|
|
68
|
+
for (const block of blocks) {
|
|
69
|
+
if (block.type === 'tool_result') {
|
|
70
|
+
const answered = calls.get(texts(block.tool_use_id));
|
|
71
|
+
if (answered !== undefined && answered.ok === undefined) {
|
|
72
|
+
answered.result = resultText(block);
|
|
73
|
+
answered.ok = !failedResult(block, answered.result);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (turn === undefined || role !== 'assistant')
|
|
77
|
+
continue;
|
|
78
|
+
else if (block.type === 'text')
|
|
79
|
+
turn.replies.push(texts(block.text));
|
|
80
|
+
else if (block.type === 'tool_use') {
|
|
81
|
+
const id = texts(block.id);
|
|
82
|
+
if (id !== '' && calls.has(id))
|
|
83
|
+
continue;
|
|
84
|
+
const call = { name: texts(block.name), input: isObj(block.input) ? block.input : {}, cwd: str(line.cwd) ?? null, ok: undefined, result: '' };
|
|
85
|
+
calls.set(id, call);
|
|
86
|
+
turn.calls.push(call);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return turns;
|
|
91
|
+
};
|
|
92
|
+
const REQUEST_CHARS = 2000;
|
|
93
|
+
const SUBJECT_CHARS = 200;
|
|
94
|
+
const EXCERPT_CHARS = 300;
|
|
95
|
+
const ACTION_CHARS = 120;
|
|
96
|
+
const ACTIONS_SHOWN = 12;
|
|
97
|
+
const FILES_SHOWN = 12;
|
|
98
|
+
const PROSE = /\.(?:md|mdx|txt|rst)$/i;
|
|
99
|
+
const SUBAGENT_TOOLS = new Set(['Agent', 'Task']);
|
|
100
|
+
// `commits` sees only a first-line `git commit` (10% recall), so every segment of the command is read here.
|
|
101
|
+
const COMMIT_SEGMENT = /^(?:\S+=\S*\s+)*git(?:\s+-[cC]\s+(?:"[^"]*"|'[^']*'|\S+))*\s+commit\b/;
|
|
102
|
+
const RESULT_SUBJECT = /^\[[^\]\n]*\s[0-9a-f]{7,40}\]\s*(.*)$/m;
|
|
103
|
+
const RESULT_STAT = /^\s*(\d+ files? changed.*)$/m;
|
|
104
|
+
const HEREDOC_SUBJECT = /(?:<<-?\s*['"]?\w+['"]?\)?|@['"])\s*\r?\n([^\n]*)/;
|
|
105
|
+
const FLAG_SUBJECT = /\bcommit\b[^\n]*?\s-[a-zA-Z]*m\s+(?:"([^"\n]*)|'([^'\n]*))/;
|
|
106
|
+
const commandOf = (call) => texts(call.input.command);
|
|
107
|
+
const isCommitCall = (call) => SHELL.has(call.name) && call.ok === true && commandOf(call).split(/&&|\|\||;|\||\n/).some((segment) => COMMIT_SEGMENT.test(segment.trim()));
|
|
108
|
+
const commitOf = (call, redact) => {
|
|
109
|
+
const command = commandOf(call);
|
|
110
|
+
const flag = FLAG_SUBJECT.exec(command);
|
|
111
|
+
const subject = RESULT_SUBJECT.exec(call.result)?.[1] ?? HEREDOC_SUBJECT.exec(command)?.[1] ?? flag?.[1] ?? flag?.[2] ?? null;
|
|
112
|
+
return { subject: subject === null ? null : cut(redact(subject), SUBJECT_CHARS), stat: RESULT_STAT.exec(call.result)?.[1]?.trim() ?? null };
|
|
113
|
+
};
|
|
114
|
+
const lineCount = (text) => (text === '' ? 0 : text.split('\n').length);
|
|
115
|
+
/** Forward slashes, and relative to the call's folder when it sits inside it. */
|
|
116
|
+
const shownPath = (path, cwd) => {
|
|
117
|
+
const plain = path.replace(/\\/g, '/');
|
|
118
|
+
const base = cwd?.replace(/\\/g, '/');
|
|
119
|
+
return base !== undefined && plain.startsWith(`${base}/`) ? plain.slice(base.length + 1) : plain;
|
|
120
|
+
};
|
|
121
|
+
const editChange = (call) => {
|
|
122
|
+
const { input } = call;
|
|
123
|
+
if (call.name === 'Write')
|
|
124
|
+
return { added: lineCount(texts(input.content)), removed: 0, last: texts(input.content) };
|
|
125
|
+
const pieces = call.name === 'MultiEdit' && Array.isArray(input.edits) ? input.edits.filter(isObj) : [input];
|
|
126
|
+
return {
|
|
127
|
+
added: pieces.reduce((sum, piece) => sum + lineCount(texts(piece.new_string)), 0),
|
|
128
|
+
removed: pieces.reduce((sum, piece) => sum + lineCount(texts(piece.old_string)), 0),
|
|
129
|
+
last: texts(pieces.at(-1)?.new_string),
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
const editsOf = (calls, redact) => {
|
|
133
|
+
const byFile = new Map();
|
|
134
|
+
for (const call of calls) {
|
|
135
|
+
// A failed edit wrote nothing; one with no result may have written, as `isEdit` in derive.ts has it.
|
|
136
|
+
if (!EDITS.has(call.name) || call.ok === false)
|
|
137
|
+
continue;
|
|
138
|
+
const file = redact(shownPath(texts(call.input.file_path), call.cwd));
|
|
139
|
+
const change = editChange(call);
|
|
140
|
+
const row = byFile.get(file) ?? { file, added: 0, removed: 0, calls: 0, excerpt: null };
|
|
141
|
+
row.added += change.added;
|
|
142
|
+
row.removed += change.removed;
|
|
143
|
+
row.calls += 1;
|
|
144
|
+
row.excerpt = PROSE.test(file) ? null : cut(redact(change.last), EXCERPT_CHARS);
|
|
145
|
+
byFile.set(file, row);
|
|
146
|
+
}
|
|
147
|
+
return [...byFile.values()];
|
|
148
|
+
};
|
|
149
|
+
const actionLabel = (call) => {
|
|
150
|
+
const { input, name } = call;
|
|
151
|
+
if (SHELL.has(name))
|
|
152
|
+
return `${name} ${shellTarget(commandOf(call))}`;
|
|
153
|
+
const aim = str(input.file_path) === undefined ? (str(input.pattern) ?? str(input.url) ?? (name === 'WebSearch' ? str(input.query) : undefined)) : shownPath(texts(input.file_path), call.cwd);
|
|
154
|
+
return aim === undefined ? shortName(name) : `${shortName(name)} ${aim}`;
|
|
155
|
+
};
|
|
156
|
+
const actionsOf = (calls, redact) => {
|
|
157
|
+
const runs = [];
|
|
158
|
+
for (const call of calls) {
|
|
159
|
+
const label = cut(redact(actionLabel(call)), ACTION_CHARS);
|
|
160
|
+
const last = runs.at(-1);
|
|
161
|
+
if (last?.label === label)
|
|
162
|
+
last.times += 1;
|
|
163
|
+
else
|
|
164
|
+
runs.push({ label, times: 1 });
|
|
165
|
+
}
|
|
166
|
+
const lines = runs.map((run) => (run.times === 1 ? run.label : `${run.label} x${String(run.times)}`));
|
|
167
|
+
return lines.length <= ACTIONS_SHOWN ? lines : [...lines.slice(0, ACTIONS_SHOWN), `and ${String(lines.length - ACTIONS_SHOWN)} more`];
|
|
168
|
+
};
|
|
169
|
+
export const viewOf = (turns, index, redact) => {
|
|
170
|
+
const calls = turns[index]?.calls ?? [];
|
|
171
|
+
const earlier = turns[index - 1]?.request;
|
|
172
|
+
const edits = editsOf(calls, redact);
|
|
173
|
+
return {
|
|
174
|
+
request: cut(redact(turns[index]?.request ?? ''), REQUEST_CHARS),
|
|
175
|
+
earlier: earlier === undefined ? null : cut(redact(earlier), PROMPT_HEAD_CHARS),
|
|
176
|
+
commits: calls.filter(isCommitCall).map((call) => commitOf(call, redact)),
|
|
177
|
+
edits,
|
|
178
|
+
// A shell call after an edit is the `verify` signal, so an edit prompt lists none; a prompt with no edit reads `na` and shows nothing by listing them.
|
|
179
|
+
actions: edits.length > 0 ? [] : actionsOf(calls, redact),
|
|
180
|
+
subagents: calls.filter((call) => SUBAGENT_TOOLS.has(call.name)).length,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* The prompts that can be judged. A marker is not a request. A `no-tools` prompt has only its reply to show, and the reply is the closing claim the
|
|
185
|
+
* file must hide. A session from any other harness (every harness `SOURCES` names) has no Claude Code transcript to read. A prompt with no signals has nothing to score.
|
|
186
|
+
*/
|
|
187
|
+
export const frameOf = (prompts) => prompts.filter((prompt) => !prompt.head.startsWith(INTERRUPT_MARKER) && prompt.cls !== 'no-tools' && !SOURCES.some((source) => prompt.session.startsWith(`${source.name}-`)) && prompt.verify !== undefined && prompt.model !== '');
|
|
188
|
+
const SESSION_CAP = 2;
|
|
189
|
+
const hashOrder = (prompts, seed) => prompts
|
|
190
|
+
.map((prompt) => ({ prompt, key: createHash('sha256').update(`${seed}\n${prompt.session}\n${String(prompt.index)}`).digest('hex') }))
|
|
191
|
+
.sort((a, b) => (a.key < b.key ? -1 : 1))
|
|
192
|
+
.map((item) => item.prompt);
|
|
193
|
+
/**
|
|
194
|
+
* A seeded draw across class and model. Each next prompt comes from the cell with the largest `sqrt(size) / (taken + 1)`: rarer classes and models
|
|
195
|
+
* get more than their share, so each one can be read, and a common cell still gets the most. A cell that runs dry drops out, and the others fill the
|
|
196
|
+
* sample. The base rate it yields is the sample's, not the store's.
|
|
197
|
+
*/
|
|
198
|
+
export const drawSample = async (frame, size, seed, accept) => {
|
|
199
|
+
const cells = new Map();
|
|
200
|
+
for (const prompt of hashOrder(frame, seed)) {
|
|
201
|
+
const key = `${prompt.cls}\n${prompt.model}`;
|
|
202
|
+
const cell = cells.get(key);
|
|
203
|
+
if (cell === undefined)
|
|
204
|
+
cells.set(key, [prompt]);
|
|
205
|
+
else
|
|
206
|
+
cell.push(prompt);
|
|
207
|
+
}
|
|
208
|
+
const open = [...cells.entries()].sort(([a], [b]) => (a < b ? -1 : 1)).map(([, queue]) => ({ queue, weight: Math.sqrt(queue.length), taken: 0 }));
|
|
209
|
+
const perSession = new Map();
|
|
210
|
+
const picked = [];
|
|
211
|
+
while (picked.length < size && open.length > 0) {
|
|
212
|
+
const cell = open.reduce((best, item) => (item.weight / (item.taken + 1) > best.weight / (best.taken + 1) ? item : best));
|
|
213
|
+
const next = cell.queue.shift();
|
|
214
|
+
if (next === undefined) {
|
|
215
|
+
open.splice(open.indexOf(cell), 1);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
// One long session must not fill a cell: its prompts are not independent reads.
|
|
219
|
+
if ((perSession.get(next.session) ?? 0) >= SESSION_CAP)
|
|
220
|
+
continue;
|
|
221
|
+
if (!(await accept(next)))
|
|
222
|
+
continue;
|
|
223
|
+
perSession.set(next.session, (perSession.get(next.session) ?? 0) + 1);
|
|
224
|
+
picked.push(next);
|
|
225
|
+
cell.taken += 1;
|
|
226
|
+
}
|
|
227
|
+
return hashOrder(picked, `${seed}\norder`);
|
|
228
|
+
};
|
|
229
|
+
const quoted = (text) => text.split(/\r?\n/).map((line) => `> ${line}`.trimEnd());
|
|
230
|
+
const indented = (text) => text.split(/\r?\n/).map((line) => ` ${line}`.trimEnd());
|
|
231
|
+
/** `1 edit`, `2 edits`: a count agrees with its noun in every line the commands print. */
|
|
232
|
+
export const plural = (count, one) => `${String(count)} ${one}${count === 1 ? '' : 's'}`;
|
|
233
|
+
/** The request and the result as lines, every one prefixed: no text from a transcript can begin a line the parser reads. */
|
|
234
|
+
const shownLines = (view) => {
|
|
235
|
+
const result = [];
|
|
236
|
+
for (const commit of view.commits)
|
|
237
|
+
result.push(`- Commit: ${commit.subject ?? 'message not readable'}${commit.stat === null ? '' : ` (${commit.stat})`}`.replace(/\r?\n/g, ' '));
|
|
238
|
+
for (const edit of view.edits.slice(0, FILES_SHOWN)) {
|
|
239
|
+
result.push(`- ${edit.file}: +${String(edit.added)} -${String(edit.removed)} in ${plural(edit.calls, 'edit')}`.replace(/\r?\n/g, ' '));
|
|
240
|
+
if (edit.excerpt !== null && edit.excerpt !== '')
|
|
241
|
+
result.push(...indented(edit.excerpt));
|
|
242
|
+
}
|
|
243
|
+
if (view.edits.length > FILES_SHOWN)
|
|
244
|
+
result.push(`- and ${plural(view.edits.length - FILES_SHOWN, 'more file')}`);
|
|
245
|
+
for (const action of view.actions)
|
|
246
|
+
result.push(`- ${action}`.replace(/\r?\n/g, ' '));
|
|
247
|
+
if (view.subagents > 0)
|
|
248
|
+
result.push(`- ${plural(view.subagents, 'subagent call')} ran; their work is not shown`);
|
|
249
|
+
if (result.length === 0)
|
|
250
|
+
result.push('- No change or action in the main transcript.');
|
|
251
|
+
return [...(view.earlier === null ? [] : ['Earlier request:', ...quoted(view.earlier), '']), 'Request:', ...quoted(view.request), '', 'Result:', ...result];
|
|
252
|
+
};
|
|
253
|
+
/** What an evaluator is given, and only that: the same request and result the judge sees. */
|
|
254
|
+
export const renderShown = (view) => shownLines(view).join('\n');
|
|
255
|
+
const BLANK_JUDGMENT = 'yes|no';
|
|
256
|
+
export const renderGold = (items, options) => {
|
|
257
|
+
const batches = Math.ceil(items.length / options.batch);
|
|
258
|
+
const lines = [
|
|
259
|
+
`# Outcome gold set, ${options.date}`,
|
|
260
|
+
'',
|
|
261
|
+
'Did the work do what was asked? Judge from what is shown, then replace `yes|no` with `yes` or `no`. Leave a line as it is when you cannot tell.',
|
|
262
|
+
'You can stop after any batch; the score reads whatever is filled in:',
|
|
263
|
+
'',
|
|
264
|
+
' oars sessions gold score <this file>',
|
|
265
|
+
'',
|
|
266
|
+
"The agent's own closing words and every signal are kept out of this file. They are in the `.arms.json` file beside it: do not open that before judging.",
|
|
267
|
+
'Local only: never commit, never quote.',
|
|
268
|
+
];
|
|
269
|
+
items.forEach((item, position) => {
|
|
270
|
+
if (position % options.batch === 0)
|
|
271
|
+
lines.push('', `## Batch ${String(position / options.batch + 1)} of ${String(batches)}`);
|
|
272
|
+
lines.push('', `### ${item.id}`, '', ...shownLines(item.view), '', `Judgment: ${BLANK_JUDGMENT}`);
|
|
273
|
+
});
|
|
274
|
+
return `${lines.join('\n')}\n`;
|
|
275
|
+
};
|
|
276
|
+
const ANSWERS = new Map([['yes', true], ['y', true], ['no', false], ['n', false]]);
|
|
277
|
+
export const parseGold = (text) => {
|
|
278
|
+
const judged = new Map();
|
|
279
|
+
const unreadable = [];
|
|
280
|
+
let current;
|
|
281
|
+
for (const line of text.split(/\r?\n/)) {
|
|
282
|
+
const heading = /^### (G\d+)$/.exec(line);
|
|
283
|
+
if (heading !== null) {
|
|
284
|
+
current = heading[1];
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const judgment = /^Judgment:(.*)$/.exec(line);
|
|
288
|
+
if (judgment === null || current === undefined)
|
|
289
|
+
continue;
|
|
290
|
+
const answer = (judgment[1] ?? '').trim().toLowerCase();
|
|
291
|
+
const value = ANSWERS.get(answer);
|
|
292
|
+
if (value !== undefined)
|
|
293
|
+
judged.set(current, value);
|
|
294
|
+
else if (answer !== '' && answer !== BLANK_JUDGMENT)
|
|
295
|
+
unreadable.push(current);
|
|
296
|
+
}
|
|
297
|
+
return { judged, unreadable };
|
|
298
|
+
};
|
|
299
|
+
const CONFIDENCE_LEVELS = new Map([['low', 0], ['medium', 1], ['moderate', 1], ['high', 2]]);
|
|
300
|
+
const CONFIDENCE_VALUE = /^(low|medium|moderate|high)\b(?:[\s/-]+(?:to\s+)?(low|medium|moderate|high)\b)?/i;
|
|
301
|
+
/** The words these lines use, in order, with a range read as its middle. Anything else, such as a percentage, has no reading. */
|
|
302
|
+
export const confidenceLevel = (line) => {
|
|
303
|
+
const value = line?.slice(line.indexOf(':') + 1).replace(/^[\s*_]+/, '') ?? '';
|
|
304
|
+
const match = CONFIDENCE_VALUE.exec(value);
|
|
305
|
+
if (match === null)
|
|
306
|
+
return null;
|
|
307
|
+
const level = (word) => CONFIDENCE_LEVELS.get(word?.toLowerCase() ?? '');
|
|
308
|
+
const low = level(match[1]) ?? 0;
|
|
309
|
+
return ((low + (level(match[2]) ?? low)) / 2);
|
|
310
|
+
};
|
|
311
|
+
const answerRead = (answer) => (answer === 'yes' ? 1 : answer === 'no' ? 0 : null);
|
|
312
|
+
const VERIFY_ORDER = new Map([['passed', 2], ['none', 1], ['failed', 0]]);
|
|
313
|
+
/** The arms, in print order. Each is an observation read on its own, and none is a label. */
|
|
314
|
+
export const ARMS = [
|
|
315
|
+
{ arm: 'verify', read: (entry) => VERIFY_ORDER.get(entry.signals.verify) ?? null },
|
|
316
|
+
{ arm: 'stopped', read: (entry) => (entry.signals.stopped === null ? 1 : 0) },
|
|
317
|
+
{ arm: 'commits', read: (entry) => entry.signals.commits },
|
|
318
|
+
{ arm: 'redo', read: (entry) => (entry.signals.redo ? 0 : 1) },
|
|
319
|
+
{ arm: 'confidence', read: (entry) => confidenceLevel(entry.reads?.confidence) },
|
|
320
|
+
{ arm: 'claim', read: (entry) => answerRead(entry.answers?.claim) },
|
|
321
|
+
{ arm: 'evaluator', read: (entry) => answerRead(entry.answers?.evaluator) },
|
|
322
|
+
{ arm: 'later', read: (entry) => answerRead(entry.answers?.later) },
|
|
323
|
+
];
|
|
324
|
+
const mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
325
|
+
const sampleVariance = (values) => {
|
|
326
|
+
const center = mean(values);
|
|
327
|
+
return values.reduce((sum, value) => sum + (value - center) ** 2, 0) / (values.length - 1);
|
|
328
|
+
};
|
|
329
|
+
const Z_95 = 1.96;
|
|
330
|
+
const NO_VARIANCE = 1e-12;
|
|
331
|
+
/**
|
|
332
|
+
* AUROC with a DeLong 95% interval, which handles the heavy ties these signals have. A zero variance estimate (one value, perfect separation, or one
|
|
333
|
+
* row a side) gives no interval: it is a small sample, not certainty.
|
|
334
|
+
*/
|
|
335
|
+
export const auroc = (positives, negatives) => {
|
|
336
|
+
if (positives.length === 0 || negatives.length === 0)
|
|
337
|
+
return { auroc: null, low: null, high: null };
|
|
338
|
+
const beats = (positive, negative) => (positive > negative ? 1 : positive === negative ? 0.5 : 0);
|
|
339
|
+
const perPositive = positives.map((positive) => mean(negatives.map((negative) => beats(positive, negative))));
|
|
340
|
+
const perNegative = negatives.map((negative) => mean(positives.map((positive) => beats(positive, negative))));
|
|
341
|
+
const value = mean(perPositive);
|
|
342
|
+
if (positives.length < 2 || negatives.length < 2)
|
|
343
|
+
return { auroc: value, low: null, high: null };
|
|
344
|
+
const variance = sampleVariance(perPositive) / positives.length + sampleVariance(perNegative) / negatives.length;
|
|
345
|
+
if (variance < NO_VARIANCE)
|
|
346
|
+
return { auroc: value, low: null, high: null };
|
|
347
|
+
const half = Z_95 * Math.sqrt(variance);
|
|
348
|
+
return { auroc: value, low: Math.max(0, value - half), high: Math.min(1, value + half) };
|
|
349
|
+
};
|
|
350
|
+
const POOLED = 'all models';
|
|
351
|
+
const modelRow = (model, entries, judged) => {
|
|
352
|
+
const yes = entries.filter((entry) => judged.get(entry.id) === true).length;
|
|
353
|
+
const arms = ARMS.map(({ arm, read }) => {
|
|
354
|
+
const positives = [];
|
|
355
|
+
const negatives = [];
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
const value = read(entry);
|
|
358
|
+
if (value !== null)
|
|
359
|
+
(judged.get(entry.id) === true ? positives : negatives).push(value);
|
|
360
|
+
}
|
|
361
|
+
const covered = positives.length + negatives.length;
|
|
362
|
+
return { arm, covered, yes: positives.length, baseRate: covered === 0 ? null : positives.length / covered, ...auroc(positives, negatives) };
|
|
363
|
+
});
|
|
364
|
+
return { model, judged: entries.length, baseRate: yes / entries.length, arms };
|
|
365
|
+
};
|
|
366
|
+
export const scoreGold = (entries, judged) => {
|
|
367
|
+
const scored = entries.filter((entry) => judged.has(entry.id));
|
|
368
|
+
const models = [...new Set(scored.map((entry) => entry.model))].sort();
|
|
369
|
+
return {
|
|
370
|
+
total: entries.length,
|
|
371
|
+
judged: scored.length,
|
|
372
|
+
coverage: ARMS.map(({ arm, read }) => ({ arm, covered: entries.filter((entry) => read(entry) !== null).length })),
|
|
373
|
+
models: scored.length === 0 ? [] : [...models.map((model) => modelRow(model, scored.filter((entry) => entry.model === model), judged)), modelRow(POOLED, scored, judged)],
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
const percent = (value) => (value === null ? '-' : `${String(Math.round(value * 100))}%`);
|
|
377
|
+
const decimal = (value) => (value === null ? '-' : value.toFixed(2));
|
|
378
|
+
export const coverageLines = (score) => {
|
|
379
|
+
const width = Math.max(...score.coverage.map((row) => row.arm.length));
|
|
380
|
+
return ['Prompts each arm covers:', ...score.coverage.map((row) => ` ${row.arm.padEnd(width)} ${String(row.covered)} of ${String(score.total)}`)];
|
|
381
|
+
};
|
|
382
|
+
export const formatScore = (score, unreadable) => {
|
|
383
|
+
const width = Math.max(...score.coverage.map((row) => row.arm.length));
|
|
384
|
+
const lines = [
|
|
385
|
+
`Judged ${String(score.judged)} of ${String(score.total)}. Each arm is an observation, not a label, and no arm is combined with another.`,
|
|
386
|
+
...unreadable.map((id) => `Could not read the judgment of ${id}: write yes or no.`),
|
|
387
|
+
'',
|
|
388
|
+
...coverageLines(score),
|
|
389
|
+
];
|
|
390
|
+
for (const row of score.models) {
|
|
391
|
+
lines.push('', `${row.model}: ${String(row.judged)} judged, base rate ${percent(row.baseRate)}`, ` ${'arm'.padEnd(width)} covered yes base AUROC 95% interval`);
|
|
392
|
+
for (const arm of row.arms) {
|
|
393
|
+
const interval = arm.low === null || arm.high === null ? '-' : `${decimal(arm.low)} to ${decimal(arm.high)}`;
|
|
394
|
+
lines.push(` ${arm.arm.padEnd(width)} ${String(arm.covered).padStart(7)} ${String(arm.yes).padStart(3)} ${percent(arm.baseRate).padStart(4)} ${decimal(arm.auroc).padStart(5)} ${interval}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return lines.join('\n');
|
|
398
|
+
};
|
|
399
|
+
/** The sidecar beside a gold file. */
|
|
400
|
+
export const armsPath = (goldFile) => `${goldFile.replace(/\.md$/, '')}.arms.json`;
|
|
401
|
+
const readIfThere = (file) => readFile(file, 'utf8').catch(() => undefined);
|
|
402
|
+
/** The first root that holds the session, so the draw and the text arms cannot come to read different files. Undefined when none does. */
|
|
403
|
+
export const readTranscript = async (roots, project, session) => {
|
|
404
|
+
for (const root of roots) {
|
|
405
|
+
const text = await readIfThere(join(root, project, `${session}.jsonl`));
|
|
406
|
+
if (text !== undefined)
|
|
407
|
+
return text;
|
|
408
|
+
}
|
|
409
|
+
return undefined;
|
|
410
|
+
};
|
|
411
|
+
export const sampleGold = async (runtime, store, redact, options) => {
|
|
412
|
+
const roots = [runtime.projectsRoot, paths(runtime.storeDir).raw];
|
|
413
|
+
const projectOf = new Map(store.sessions.map((session) => [session.session, session.project]));
|
|
414
|
+
const excluded = new Set();
|
|
415
|
+
const checked = new Set();
|
|
416
|
+
const built = new Map();
|
|
417
|
+
let unreadable = 0;
|
|
418
|
+
const accept = async (prompt) => {
|
|
419
|
+
const project = projectOf.get(prompt.session) ?? '';
|
|
420
|
+
if (excluded.has(prompt.session))
|
|
421
|
+
return false;
|
|
422
|
+
if (!checked.has(prompt.session)) {
|
|
423
|
+
checked.add(prompt.session);
|
|
424
|
+
// The one exclusion rule (A38): a session that holds a term, has no raw to scan, or could not be read is out, and so is every session when the term file is empty.
|
|
425
|
+
if ((await checkSession(runtime, options.termsFile, project, prompt.session, options.extraTerms)).excluded) {
|
|
426
|
+
excluded.add(prompt.session);
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const text = await readTranscript(roots, project, prompt.session);
|
|
431
|
+
const turns = text === undefined ? [] : readTurns(text);
|
|
432
|
+
const turn = turns[prompt.index];
|
|
433
|
+
// The transcript grew or changed after the deposit if the head no longer matches, and the signals would then describe another turn.
|
|
434
|
+
if (turn === undefined || cut(redact(turn.request), PROMPT_HEAD_CHARS) !== prompt.head) {
|
|
435
|
+
unreadable += 1;
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
built.set(prompt, viewOf(turns, prompt.index, redact));
|
|
439
|
+
return true;
|
|
440
|
+
};
|
|
441
|
+
const frame = frameOf(store.prompts);
|
|
442
|
+
const picked = await drawSample(frame, options.size, options.seed, accept);
|
|
443
|
+
const width = Math.max(3, String(picked.length).length);
|
|
444
|
+
const views = [];
|
|
445
|
+
const entries = picked.map((prompt, position) => {
|
|
446
|
+
const id = `G${String(position + 1).padStart(width, '0')}`;
|
|
447
|
+
const view = built.get(prompt) ?? viewOf([], 0, redact);
|
|
448
|
+
views.push({ id, view });
|
|
449
|
+
return {
|
|
450
|
+
id,
|
|
451
|
+
session: prompt.session,
|
|
452
|
+
index: prompt.index,
|
|
453
|
+
project: projectOf.get(prompt.session) ?? '',
|
|
454
|
+
model: prompt.model,
|
|
455
|
+
cls: prompt.cls,
|
|
456
|
+
subagents: view.subagents,
|
|
457
|
+
signals: { verify: prompt.verify ?? 'na', stopped: prompt.stopped ?? null, commits: prompt.commits ?? 0, redo: prompt.redo ?? false },
|
|
458
|
+
shown: renderShown(view),
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
return { document: { date: options.date, seed: options.seed, batch: options.batch, frame: frame.length, excludedSessions: excluded.size, entries }, views, unreadable };
|
|
462
|
+
};
|
|
463
|
+
export const isGoldDocument = (value) => isObj(value) && Array.isArray(value.entries);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StepObservation, ToolStatus } from './schema.js';
|
|
2
|
+
/** One call's steps, with what the graph reads and does not keep. */
|
|
3
|
+
export interface LinkedCall {
|
|
4
|
+
steps: StepObservation[];
|
|
5
|
+
/** One per step. Masked identities can match by accident, so a retry compares these; they are never stored. */
|
|
6
|
+
rawKeys: readonly string[];
|
|
7
|
+
/** The line of work: the prompt, and whether the call ran on a sidechain. */
|
|
8
|
+
line: string;
|
|
9
|
+
status: ToolStatus;
|
|
10
|
+
}
|
|
11
|
+
/** Writes `next`, `after-error`, and `retried` on every step, and `spawned` on the transcript's first when `spawnedAt` knows the call that started it. */
|
|
12
|
+
export declare const linkSteps: (calls: readonly LinkedCall[], spawnedAt: number | null) => void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The edges of the steps of one transcript (recall spec Section 2.3). Every edge is held on the step it points to, naming the step it came
|
|
2
|
+
// from, so one transcript's capture writes all of its own edges and no earlier record is rewritten, including a subagent's first step, whose
|
|
3
|
+
// edge comes from a call in another transcript.
|
|
4
|
+
/** Writes `next`, `after-error`, and `retried` on every step, and `spawned` on the transcript's first when `spawnedAt` knows the call that started it. */
|
|
5
|
+
export const linkSteps = (calls, spawnedAt) => {
|
|
6
|
+
const lines = new Map();
|
|
7
|
+
for (const { steps, rawKeys, line, status } of calls) {
|
|
8
|
+
const held = lines.get(line) ?? { last: undefined, lastFailed: false, failed: new Map() };
|
|
9
|
+
lines.set(line, held);
|
|
10
|
+
for (const [seg, step] of steps.entries()) {
|
|
11
|
+
const before = seg === 0 ? held.last : steps[seg - 1];
|
|
12
|
+
if (before !== undefined)
|
|
13
|
+
step.edges.push({ kind: 'next', n: before.n, seg: before.seg });
|
|
14
|
+
if (seg === 0 && before !== undefined && held.lastFailed)
|
|
15
|
+
step.edges.push({ kind: 'after-error', n: before.n, seg: before.seg });
|
|
16
|
+
const failure = held.failed.get(rawKeys[seg] ?? '');
|
|
17
|
+
if (failure !== undefined)
|
|
18
|
+
step.edges.push({ kind: 'retried', n: failure.n, seg: failure.seg });
|
|
19
|
+
}
|
|
20
|
+
// After the whole call: a command repeated inside one call is not a retry of itself.
|
|
21
|
+
for (const [seg, step] of steps.entries()) {
|
|
22
|
+
const key = rawKeys[seg] ?? '';
|
|
23
|
+
if (step.status === 'error')
|
|
24
|
+
held.failed.set(key, step);
|
|
25
|
+
else if (step.status === 'ok')
|
|
26
|
+
held.failed.delete(key);
|
|
27
|
+
}
|
|
28
|
+
held.last = steps.at(-1);
|
|
29
|
+
held.lastFailed = status === 'error';
|
|
30
|
+
}
|
|
31
|
+
const first = calls[0]?.steps[0];
|
|
32
|
+
if (first !== undefined && spawnedAt !== null)
|
|
33
|
+
first.edges.unshift({ kind: 'spawned', n: spawnedAt, seg: 0 });
|
|
34
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** The Grok home for an environment; a test passes its own so it never reads the real one. */
|
|
2
|
+
export declare const grokRoot: (env: {
|
|
3
|
+
[name: string]: string | undefined;
|
|
4
|
+
}, home?: string) => string;
|
|
5
|
+
type Obj = {
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
};
|
|
8
|
+
/** A session found in the store, before its lines are read. */
|
|
9
|
+
export interface GrokSession {
|
|
10
|
+
/** The session folder's name, which is the session id and names the skeleton. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** `summary.json` `info.cwd`; null when it names none. */
|
|
13
|
+
folder: string | null;
|
|
14
|
+
/** The session's folder. */
|
|
15
|
+
dir: string;
|
|
16
|
+
size: number;
|
|
17
|
+
mtimeMs: number;
|
|
18
|
+
}
|
|
19
|
+
export interface GrokStore {
|
|
20
|
+
sessions: GrokSession[];
|
|
21
|
+
/** The session as transcript text for `captureSession`; throws when a file cannot be read or holds no parseable line. */
|
|
22
|
+
transcript: (session: GrokSession) => Promise<string>;
|
|
23
|
+
/** Folders skipped because their name is not a safe file name (it names a skeleton), counted so the skip is never silent. */
|
|
24
|
+
rejected: number;
|
|
25
|
+
/** Sessions that could not be inspected, reported once each and never fatal; the reason names no path. */
|
|
26
|
+
unreadable: {
|
|
27
|
+
file: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
}[];
|
|
30
|
+
}
|
|
31
|
+
/** The non-blank lines that are not JSON (a torn write), kept so `captureSession` counts them and the deposit line reports them. */
|
|
32
|
+
export declare const grokBadLines: (text: string) => string[];
|
|
33
|
+
/** One session as the lines `captureSession` reads. */
|
|
34
|
+
export declare const grokLines: (text: string, fallbackId: string, summary: Obj, usage: Obj | undefined) => Obj[];
|
|
35
|
+
export declare const openGrokStore: (root: string) => Promise<GrokStore | undefined>;
|
|
36
|
+
export {};
|