@mjasnikovs/pi-task 0.18.14 → 0.18.16

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.
Files changed (55) hide show
  1. package/README.md +2 -2
  2. package/dist/task/accept-debt.d.ts +28 -1
  3. package/dist/task/accept-debt.js +61 -3
  4. package/dist/task/auto-io.d.ts +4 -2
  5. package/dist/task/auto-io.js +6 -3
  6. package/dist/task/auto-orchestrator.d.ts +1 -0
  7. package/dist/task/auto-orchestrator.js +135 -19
  8. package/dist/task/auto-prompts.d.ts +5 -5
  9. package/dist/task/auto-prompts.js +9 -2
  10. package/dist/task/contracts.d.ts +8 -0
  11. package/dist/task/contracts.js +4 -2
  12. package/dist/task/decompose-fidelity.d.ts +47 -0
  13. package/dist/task/decompose-fidelity.js +132 -0
  14. package/dist/task/final-gate-fix.d.ts +22 -3
  15. package/dist/task/final-gate-fix.js +72 -7
  16. package/dist/task/final-gate.d.ts +48 -1
  17. package/dist/task/final-gate.js +182 -34
  18. package/dist/task/gate-deps.d.ts +7 -0
  19. package/dist/task/gate-deps.js +37 -1
  20. package/dist/task/launch-contract.d.ts +36 -1
  21. package/dist/task/launch-contract.js +80 -2
  22. package/dist/task/phases.d.ts +13 -1
  23. package/dist/task/phases.js +50 -11
  24. package/dist/task/prompts.js +2 -0
  25. package/dist/task/render-check.d.ts +32 -0
  26. package/dist/task/render-check.js +186 -0
  27. package/dist/task/requirements.d.ts +88 -0
  28. package/dist/task/requirements.js +331 -0
  29. package/dist/task/verify-reconcile.d.ts +36 -0
  30. package/dist/task/verify-reconcile.js +203 -0
  31. package/dist/task/write-guard.d.ts +52 -0
  32. package/dist/task/write-guard.js +112 -0
  33. package/package.json +1 -1
  34. package/dist/task/_ab.d.ts +0 -1
  35. package/dist/task/_ab.js +0 -68
  36. package/dist/task/task-file.d.ts +0 -14
  37. package/dist/task/task-file.js +0 -15
  38. package/dist/think-test/cli.d.ts +0 -1
  39. package/dist/think-test/cli.js +0 -98
  40. package/dist/think-test/client.d.ts +0 -26
  41. package/dist/think-test/client.js +0 -37
  42. package/dist/think-test/compressor.d.ts +0 -5
  43. package/dist/think-test/compressor.js +0 -25
  44. package/dist/think-test/judge.d.ts +0 -4
  45. package/dist/think-test/judge.js +0 -11
  46. package/dist/think-test/score.d.ts +0 -8
  47. package/dist/think-test/score.js +0 -22
  48. package/dist/think-test/serialize.d.ts +0 -19
  49. package/dist/think-test/serialize.js +0 -41
  50. package/dist/think-test/transcript.d.ts +0 -7
  51. package/dist/think-test/transcript.js +0 -41
  52. package/dist/think-test/transform.d.ts +0 -6
  53. package/dist/think-test/transform.js +0 -24
  54. package/dist/think-test/types.d.ts +0 -45
  55. package/dist/think-test/types.js +0 -1
@@ -0,0 +1,112 @@
1
+ /**
2
+ * write-guard — deterministic tree-change accounting for WRITE-CAPABLE gate
3
+ * children (mx5 run 11).
4
+ *
5
+ * The failure class: the final-gate autofix child (read,edit,bash) was added after
6
+ * the run-8 guard generation and inherited NONE of the guards the other
7
+ * write-capable passes carry — no diff capture, no frozen-path deny, no probe
8
+ * scans, free `rm`. Run 11 it deleted `src/client/pages/admin.tsx` (TASK_0008's
9
+ * verified deliverable) to satisfy a recorded debt claim, and the deletion was
10
+ * invisible: nothing even logged what the pass changed.
11
+ *
12
+ * This module is the pure half of the guard stack: parse `git status --porcelain`
13
+ * into a change summary (the diff-capture log line every write-capable child now
14
+ * gets at the gate-deps seam), and classify tracked-file DELETIONS. A fix pass
15
+ * exists to repair the assembled repository, not to shrink it: every tracked file
16
+ * is a committed task's deliverable, so deleting one is rejected outright — with
17
+ * one allowance, a RELOCATION (the same file name reappears as an added file
18
+ * elsewhere, e.g. moving a test the runner was never meant to pick up out of its
19
+ * glob — the legitimate fix shape from run 7). Pure text/path analysis; no git
20
+ * execution, no stack assumptions.
21
+ */
22
+ /** Porcelain v1 line: `XY <path>` or `XY <orig> -> <new>` (rename/copy). */
23
+ function splitEntry(raw) {
24
+ if (raw.length < 4)
25
+ return null;
26
+ const x = raw[0];
27
+ const y = raw[1];
28
+ const file = raw.slice(3).trim();
29
+ if (file.length === 0)
30
+ return null;
31
+ let from = file;
32
+ let to = file;
33
+ const arrow = file.indexOf(' -> ');
34
+ if (arrow !== -1) {
35
+ from = file.slice(0, arrow).trim();
36
+ to = file.slice(arrow + 4).trim();
37
+ }
38
+ const unquote = (s) => s.startsWith('"') && s.endsWith('"') && s.length >= 2 ? s.slice(1, -1) : s;
39
+ return { x, y, from: unquote(from), to: unquote(to) };
40
+ }
41
+ /**
42
+ * Parse `git status --porcelain` output into the change summary. A rename entry
43
+ * contributes its source to `deleted` and its target to `added` (unstaged child
44
+ * edits show the same reality as separate ` D old` + `?? new` lines, so both
45
+ * shapes classify identically). Deterministic and pure so it is unit-tested
46
+ * without a repo.
47
+ */
48
+ export function parseTreeChanges(porcelain) {
49
+ const modified = new Set();
50
+ const deleted = new Set();
51
+ const added = new Set();
52
+ for (const raw of porcelain.split('\n')) {
53
+ const e = splitEntry(raw);
54
+ if (!e)
55
+ continue;
56
+ const status = `${e.x}${e.y}`;
57
+ if (status === '??') {
58
+ added.add(e.to);
59
+ continue;
60
+ }
61
+ if (e.x === 'R' || e.y === 'R' || e.x === 'C') {
62
+ deleted.add(e.from);
63
+ added.add(e.to);
64
+ continue;
65
+ }
66
+ if (e.x === 'D' || e.y === 'D') {
67
+ deleted.add(e.from);
68
+ continue;
69
+ }
70
+ if (e.x === 'A') {
71
+ added.add(e.to);
72
+ continue;
73
+ }
74
+ modified.add(e.to);
75
+ }
76
+ return { modified: [...modified], deleted: [...deleted], added: [...added] };
77
+ }
78
+ const basename = (p) => {
79
+ const i = p.lastIndexOf('/');
80
+ return i === -1 ? p : p.slice(i + 1);
81
+ };
82
+ /**
83
+ * The deletions a fix pass may NOT make: every deleted tracked file whose name does
84
+ * not reappear among the added files (a relocation keeps the file, under the same
85
+ * name, somewhere in the tree). Anything returned here rejects the whole fix
86
+ * attempt — run 11's `rm src/client/pages/admin.tsx` had no corresponding add and
87
+ * destroyed a sibling task's verified deliverable.
88
+ */
89
+ export function findForbiddenDeletions(changes) {
90
+ if (changes.deleted.length === 0)
91
+ return [];
92
+ const addedNames = new Set(changes.added.map(basename));
93
+ return changes.deleted.filter(p => !addedNames.has(basename(p)));
94
+ }
95
+ /**
96
+ * One-line summary for the gate debug log — the diff capture every write-capable
97
+ * child gets so "what did this pass change" is answerable from artifacts (the
98
+ * run-11 `rm` left no trace outside the bash stream).
99
+ */
100
+ export function formatTreeChanges(changes) {
101
+ if (changes.modified.length === 0
102
+ && changes.deleted.length === 0
103
+ && changes.added.length === 0) {
104
+ return '(no tree changes)';
105
+ }
106
+ const part = (label, list) => list.length > 0 ? [`${label} [${list.join(', ')}]`] : [];
107
+ return [
108
+ ...part('MODIFIED', changes.modified),
109
+ ...part('NEW', changes.added),
110
+ ...part('DELETED', changes.deleted)
111
+ ].join(' ');
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.18.14",
3
+ "version": "0.18.16",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1 +0,0 @@
1
- export {};
package/dist/task/_ab.js DELETED
@@ -1,68 +0,0 @@
1
- /* Live A/B: orientation OFF vs ON, real pi + local model, real mx5 repo. */
2
- import { readFile } from 'node:fs/promises';
3
- import { resolve } from 'node:path';
4
- import { runWorker } from '../workers/pi-worker-core.js';
5
- import { getFileInventory } from './file-inventory.js';
6
- import { buildOrientation } from './orientation.js';
7
- import { appendNoThink, RESEARCH_FILES_PROMPT, RESEARCH_CONTEXT_PROMPT, RESEARCH_APIS_PROMPT, RESEARCH_TOOLING_PROMPT } from './prompts.js';
8
- import { scopedToolingGoal } from './phases.js';
9
- const DOCS_EXT = new URL('../workers/docs-extension.js', import.meta.url).pathname;
10
- const SINGLE_READ_EXT = new URL('../workers/single-read-extension.js', import.meta.url).pathname;
11
- const CWD = '/home/edgars/hub/mx5';
12
- process.env.PI_BIN ??= '/home/edgars/.local/share/mise/installs/node/26.2.0/bin/pi';
13
- const refined = await readFile('/tmp/refined.txt', 'utf8');
14
- const inventoryRaw = await getFileInventory(CWD);
15
- const invPaths = inventoryRaw.split('\n').filter(l => l.trim());
16
- const inventoryHeader = `PROJECT FILE INVENTORY\n${inventoryRaw}\n\n`;
17
- const orientation = await buildOrientation(invPaths, async (p) => {
18
- try {
19
- return await readFile(resolve(CWD, p), 'utf8');
20
- }
21
- catch {
22
- return null;
23
- }
24
- });
25
- console.error(`orientation: ${orientation.supplied.size} files, ${(orientation.block.length / 1024).toFixed(1)}KB`);
26
- const suppliedRel = new Set([...orientation.supplied].map(p => resolve(CWD, p)));
27
- const workers = {
28
- FILES: { build: RESEARCH_FILES_PROMPT, tools: undefined, ext: [] },
29
- APIS: {
30
- build: RESEARCH_APIS_PROMPT,
31
- tools: 'read,grep,find,ls,pi-worker-docs',
32
- ext: [DOCS_EXT]
33
- },
34
- CONTEXT: { build: RESEARCH_CONTEXT_PROMPT, tools: 'read,grep', ext: [] },
35
- TOOLING: {
36
- build: (r) => RESEARCH_TOOLING_PROMPT(scopedToolingGoal(r)),
37
- tools: undefined,
38
- ext: [SINGLE_READ_EXT]
39
- }
40
- };
41
- async function run(worker, on) {
42
- const header = (on ? orientation.block : '') + inventoryHeader;
43
- const prompt = appendNoThink(header + workers[worker].build(refined));
44
- const reads = [];
45
- const r = await runWorker({
46
- prompt,
47
- cwd: CWD,
48
- ...(workers[worker].tools ? { tools: workers[worker].tools } : {}),
49
- ...(workers[worker].ext.length ? { extensions: workers[worker].ext } : {}),
50
- onLine: line => {
51
- const m = /read: (\S+)/.exec(line);
52
- if (m)
53
- reads.push(resolve(CWD, m[1]));
54
- }
55
- });
56
- const reReadSupplied = reads.filter(p => suppliedRel.has(p)).length;
57
- const label = `${worker} ${on ? 'ON ' : 'OFF'}`;
58
- console.log(`${label} exit=${r.exitCode} loop=${r.loopHit ? 'Y' : 'n'} time=${r.loopHit || r.timedOut ? '(runaway)' : ''}` +
59
- ` workMs=${r.workMs} reads=${reads.length} reads_of_core=${reReadSupplied} answerChars=${r.text.trim().length}`);
60
- return { reads: reads.length, reReadSupplied, workMs: r.workMs, exit: r.exitCode };
61
- }
62
- for (const w of ['FILES', 'APIS', 'CONTEXT', 'TOOLING']) {
63
- const off = await run(w, false);
64
- const on = await run(w, true);
65
- const saved = off.reads - on.reads;
66
- console.log(` -> ${w}: reads ${off.reads}->${on.reads} (${saved >= 0 ? '-' : '+'}${Math.abs(saved)}), ` +
67
- `workMs ${off.workMs}->${on.workMs}, core re-read while ON: ${on.reReadSupplied}\n`);
68
- }
@@ -1,14 +0,0 @@
1
- /**
2
- * Task file — barrel re-export for backward compatibility.
3
- *
4
- * All existing import sites continue to work unchanged.
5
- *
6
- * @deprecated Import from the specific modules:
7
- * - Types & constants: task-types.ts
8
- * - Parsing & formatting: task-parsers.ts
9
- * - File I/O: task-io.ts
10
- */
11
- export type { TaskState, PhaseName, TaskFrontMatter } from './task-types.js';
12
- export { PHASE_ORDER, PHASE_INDEX, TASKS_DIR_NAME, RESUMABLE_STATES } from './task-types.js';
13
- export { emitFrontMatter, parseFrontMatter, sectionRegex, extractSection, normaliseTaskId } from './task-parsers.js';
14
- export { tasksDir, taskFilePath, ensureTasksDir, allocateTaskId, readTaskFile, writeTaskFile, updateTaskFrontMatter, readSection, setTaskSection } from './task-io.js';
@@ -1,15 +0,0 @@
1
- /**
2
- * Task file — barrel re-export for backward compatibility.
3
- *
4
- * All existing import sites continue to work unchanged.
5
- *
6
- * @deprecated Import from the specific modules:
7
- * - Types & constants: task-types.ts
8
- * - Parsing & formatting: task-parsers.ts
9
- * - File I/O: task-io.ts
10
- */
11
- export { PHASE_ORDER, PHASE_INDEX, TASKS_DIR_NAME, RESUMABLE_STATES } from './task-types.js';
12
- // Parsing & formatting
13
- export { emitFrontMatter, parseFrontMatter, sectionRegex, extractSection, normaliseTaskId } from './task-parsers.js';
14
- // File I/O
15
- export { tasksDir, taskFilePath, ensureTasksDir, allocateTaskId, readTaskFile, writeTaskFile, updateTaskFrontMatter, readSection, setTaskSection } from './task-io.js';
@@ -1 +0,0 @@
1
- export {};
@@ -1,98 +0,0 @@
1
- // src/think-test/cli.ts
2
- import { readFileSync } from 'node:fs';
3
- import { parseTranscript, decisionPoints } from './transcript.js';
4
- import { applyMode } from './transform.js';
5
- import { toOpenAiMessages } from './serialize.js';
6
- import { createHttpClient } from './client.js';
7
- import { buildCompressionMap } from './compressor.js';
8
- import { scoreSamples, aggregate } from './score.js';
9
- const ENDPOINT = process.env.PI_THINKTEST_ENDPOINT ?? 'http://localhost:8080/v1/chat/completions';
10
- const MODES = ['full', 'none', 'compressed'];
11
- function flag(name, def) {
12
- const hit = process.argv.find(a => a.startsWith(`--${name}=`));
13
- if (!hit)
14
- return def;
15
- const value = Number(hit.split('=')[1]);
16
- if (!Number.isFinite(value))
17
- throw new Error(`--${name} must be a number`);
18
- return value;
19
- }
20
- function readFixtureTools() {
21
- const url = new URL('./__fixtures__/tools.json', import.meta.url);
22
- let raw;
23
- try {
24
- raw = readFileSync(url, 'utf8');
25
- }
26
- catch {
27
- throw new Error('missing src/think-test/__fixtures__/tools.json — capture it first (see plan Task 6). '
28
- + 'Without the tools schema the model cannot emit tool_calls and the test is meaningless.');
29
- }
30
- const tools = JSON.parse(raw);
31
- if (!Array.isArray(tools) || tools.length === 0) {
32
- throw new Error('tools.json must be a non-empty JSON array of tool schemas');
33
- }
34
- return tools;
35
- }
36
- async function main() {
37
- const sessionPath = process.argv[2];
38
- if (!sessionPath) {
39
- console.error('usage: bun run think-test <session.jsonl> [--n=5] [--limit=0]');
40
- process.exit(1);
41
- }
42
- const n = flag('n', 5);
43
- const limit = flag('limit', 0); // 0 = all turns
44
- const tools = readFixtureTools();
45
- let transcript;
46
- try {
47
- transcript = readFileSync(sessionPath, 'utf8');
48
- }
49
- catch {
50
- throw new Error(`cannot read session file: ${sessionPath}`);
51
- }
52
- const messages = parseTranscript(transcript);
53
- let points = decisionPoints(messages).filter(p => p.prior.length > 0);
54
- if (points.length === 0) {
55
- throw new Error('no decision points with prior context found in transcript');
56
- }
57
- if (limit > 0)
58
- points = points.slice(0, limit);
59
- const sampler = createHttpClient(fetch, ENDPOINT, tools);
60
- const textClient = createHttpClient(fetch, ENDPOINT, []); // compression + judging: no tools, want text
61
- // Only compress thinking the selected points reference (deduped inside
62
- // buildCompressionMap). With --limit this avoids compressing the whole
63
- // transcript; a full run covers every turn anyway.
64
- const referenced = points.flatMap(p => p.prior);
65
- console.error(`compressing prior thinking referenced by ${points.length} points…`);
66
- const compressed = await buildCompressionMap(referenced, textClient);
67
- // Dump the compressions for manual inspection (guards against a weak prompt).
68
- console.error('--- compression samples (first 3) ---');
69
- let shown = 0;
70
- for (const [orig, comp] of compressed) {
71
- if (shown++ >= 3)
72
- break;
73
- console.error(`[${orig.length}→${comp.length}] ${comp.slice(0, 200)}`);
74
- }
75
- const perTurn = { full: [], none: [], compressed: [] };
76
- for (const [i, pt] of points.entries()) {
77
- for (const mode of MODES) {
78
- const body = toOpenAiMessages(applyMode(pt.prior, mode, compressed));
79
- const samples = [];
80
- for (let s = 0; s < n; s++) {
81
- samples.push(await sampler.complete(body, { temperature: 1 }));
82
- }
83
- const rate = await scoreSamples(pt.recordedTool, pt.recordedArgs, samples, textClient);
84
- perTurn[mode].push(rate);
85
- }
86
- console.error(`turn ${i + 1}/${points.length} (${pt.recordedTool}) `
87
- + MODES.map(m => `${m}=${perTurn[m][i].toFixed(2)}`).join(' '));
88
- }
89
- console.log('\n=== aggregate agreement vs recorded action ===');
90
- for (const mode of MODES) {
91
- console.log(`${mode.padEnd(11)} ${aggregate(perTurn[mode]).toFixed(3)}`);
92
- }
93
- console.log('\nread: full=ceiling, none=floor, compressed=verdict');
94
- }
95
- main().catch((err) => {
96
- console.error(err instanceof Error ? err.message : String(err));
97
- process.exit(1);
98
- });
@@ -1,26 +0,0 @@
1
- import type { Action } from './types.js';
2
- import type { OpenAiMessage } from './serialize.js';
3
- export interface CompletionOpts {
4
- temperature: number;
5
- }
6
- export interface ModelClient {
7
- complete(messages: OpenAiMessage[], opts: CompletionOpts): Promise<Action>;
8
- }
9
- export interface RawChoice {
10
- message: {
11
- content?: string | null;
12
- tool_calls?: {
13
- function: {
14
- name: string;
15
- arguments: string;
16
- };
17
- }[];
18
- };
19
- }
20
- /** Normalize one completion choice into an Action: a tool call (with parsed
21
- * args, `{}` on malformed JSON) or, absent any tool call, the text. */
22
- export declare function parseChoice(choice: RawChoice): Action;
23
- /** HTTP client for the local llama-server. `tools` is the captured schema array
24
- * — without it the model cannot emit tool_calls and the test is meaningless.
25
- * `fetchFn`/`endpoint` are injectable for tests. */
26
- export declare function createHttpClient(fetchFn: typeof fetch, endpoint: string, tools: unknown[]): ModelClient;
@@ -1,37 +0,0 @@
1
- /** Normalize one completion choice into an Action: a tool call (with parsed
2
- * args, `{}` on malformed JSON) or, absent any tool call, the text. */
3
- export function parseChoice(choice) {
4
- const tc = choice.message.tool_calls?.[0];
5
- if (tc) {
6
- let args;
7
- try {
8
- args = JSON.parse(tc.function.arguments);
9
- }
10
- catch {
11
- args = {};
12
- }
13
- return { tool: tc.function.name, args };
14
- }
15
- return { text: choice.message.content ?? '' };
16
- }
17
- /** HTTP client for the local llama-server. `tools` is the captured schema array
18
- * — without it the model cannot emit tool_calls and the test is meaningless.
19
- * `fetchFn`/`endpoint` are injectable for tests. */
20
- export function createHttpClient(fetchFn, endpoint, tools) {
21
- return {
22
- async complete(messages, opts) {
23
- const res = await fetchFn(endpoint, {
24
- method: 'POST',
25
- headers: { 'content-type': 'application/json' },
26
- body: JSON.stringify({ messages, tools, temperature: opts.temperature })
27
- });
28
- if (!res.ok)
29
- throw new Error(`model HTTP ${res.status}`);
30
- const json = (await res.json());
31
- const choice = json.choices?.[0];
32
- if (!choice)
33
- throw new Error('model returned no choices');
34
- return parseChoice(choice);
35
- }
36
- };
37
- }
@@ -1,5 +0,0 @@
1
- import type { SessionMsg } from './types.js';
2
- import type { ModelClient } from './client.js';
3
- /** Compress every distinct thinking block once (compress-once semantics, even
4
- * here in the test) and return a map from original text → compressed text. */
5
- export declare function buildCompressionMap(messages: SessionMsg[], client: ModelClient): Promise<Map<string, string>>;
@@ -1,25 +0,0 @@
1
- const COMPRESS_PROMPT = (thinking) => `Compress the following reasoning trace. Keep every decision, conclusion, `
2
- + `constraint, and fact the author will rely on later. Drop restated questions, `
3
- + `false starts, self-talk, and verbosity. Output only the compressed reasoning, `
4
- + `no preamble.\n\n---\n${thinking}\n---\n\n/no_think`;
5
- /** Compress every distinct thinking block once (compress-once semantics, even
6
- * here in the test) and return a map from original text → compressed text. */
7
- export async function buildCompressionMap(messages, client) {
8
- const unique = new Set();
9
- for (const m of messages) {
10
- if (m.role !== 'assistant')
11
- continue;
12
- for (const c of m.content) {
13
- if (c.type === 'thinking')
14
- unique.add(c.thinking);
15
- }
16
- }
17
- const map = new Map();
18
- for (const original of unique) {
19
- const action = await client.complete([{ role: 'user', content: COMPRESS_PROMPT(original) }], {
20
- temperature: 0
21
- });
22
- map.set(original, (action.text ?? '').trim());
23
- }
24
- return map;
25
- }
@@ -1,4 +0,0 @@
1
- import type { ModelClient } from './client.js';
2
- /** Ask the model whether two arg objects are equivalent in intent. Deterministic
3
- * (temperature 0). Only meaningful when the tool name already matched. */
4
- export declare function judgeArgs(tool: string, a: Record<string, unknown>, b: Record<string, unknown>, client: ModelClient): Promise<boolean>;
@@ -1,11 +0,0 @@
1
- const JUDGE_PROMPT = (tool, a, b) => `Two calls to the tool "${tool}" were made. Are their arguments equivalent `
2
- + `in intent (same target/effect), ignoring cosmetic differences? Answer with `
3
- + `YES or NO only.\n\nA: ${JSON.stringify(a)}\nB: ${JSON.stringify(b)}\n\n/no_think`;
4
- /** Ask the model whether two arg objects are equivalent in intent. Deterministic
5
- * (temperature 0). Only meaningful when the tool name already matched. */
6
- export async function judgeArgs(tool, a, b, client) {
7
- const action = await client.complete([{ role: 'user', content: JUDGE_PROMPT(tool, a, b) }], {
8
- temperature: 0
9
- });
10
- return /\byes\b/i.test(action.text ?? '');
11
- }
@@ -1,8 +0,0 @@
1
- import type { Action } from './types.js';
2
- import type { ModelClient } from './client.js';
3
- /** Agreement rate for one decision point's samples against the recorded action:
4
- * fraction of samples whose tool name matches AND whose args the judge deems
5
- * equivalent. The judge is only consulted on a tool-name match. */
6
- export declare function scoreSamples(recordedTool: string, recordedArgs: Record<string, unknown>, samples: Action[], judge: ModelClient): Promise<number>;
7
- /** Mean of per-turn agreement rates; 0 for an empty list. */
8
- export declare function aggregate(perTurnRates: number[]): number;
@@ -1,22 +0,0 @@
1
- import { judgeArgs } from './judge.js';
2
- /** Agreement rate for one decision point's samples against the recorded action:
3
- * fraction of samples whose tool name matches AND whose args the judge deems
4
- * equivalent. The judge is only consulted on a tool-name match. */
5
- export async function scoreSamples(recordedTool, recordedArgs, samples, judge) {
6
- if (samples.length === 0)
7
- return 0;
8
- let hits = 0;
9
- for (const s of samples) {
10
- if (s.tool !== recordedTool)
11
- continue;
12
- if (await judgeArgs(recordedTool, recordedArgs, s.args ?? {}, judge))
13
- hits++;
14
- }
15
- return hits / samples.length;
16
- }
17
- /** Mean of per-turn agreement rates; 0 for an empty list. */
18
- export function aggregate(perTurnRates) {
19
- if (perTurnRates.length === 0)
20
- return 0;
21
- return perTurnRates.reduce((a, b) => a + b, 0) / perTurnRates.length;
22
- }
@@ -1,19 +0,0 @@
1
- import type { SessionMsg } from './types.js';
2
- export interface OpenAiMessage {
3
- role: 'user' | 'assistant' | 'tool';
4
- content: string;
5
- reasoning_content?: string;
6
- tool_call_id?: string;
7
- tool_calls?: {
8
- id: string;
9
- type: 'function';
10
- function: {
11
- name: string;
12
- arguments: string;
13
- };
14
- }[];
15
- }
16
- /** Convert session messages to the OpenAI chat-completions wire format used by
17
- * llama-server, replicating pi's `reasoning_content` carry-back so prior
18
- * thinking is preserved exactly as in a real request. */
19
- export declare function toOpenAiMessages(messages: SessionMsg[]): OpenAiMessage[];
@@ -1,41 +0,0 @@
1
- function joinText(blocks) {
2
- return blocks
3
- .filter((b) => b.type === 'text')
4
- .map(b => b.text)
5
- .join('');
6
- }
7
- /** Convert session messages to the OpenAI chat-completions wire format used by
8
- * llama-server, replicating pi's `reasoning_content` carry-back so prior
9
- * thinking is preserved exactly as in a real request. */
10
- export function toOpenAiMessages(messages) {
11
- const out = [];
12
- for (const m of messages) {
13
- if (m.role === 'user') {
14
- const content = typeof m.content === 'string' ? m.content : joinText(m.content);
15
- out.push({ role: 'user', content });
16
- }
17
- else if (m.role === 'assistant') {
18
- const content = joinText(m.content);
19
- const thinking = m.content
20
- .filter((c) => c.type === 'thinking')
21
- .map(c => c.thinking)
22
- .join('\n');
23
- const toolCalls = m.content.filter((c) => c.type === 'toolCall');
24
- const msg = { role: 'assistant', content };
25
- if (thinking.length > 0)
26
- msg.reasoning_content = thinking;
27
- if (toolCalls.length > 0) {
28
- msg.tool_calls = toolCalls.map(tc => ({
29
- id: tc.id,
30
- type: 'function',
31
- function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
32
- }));
33
- }
34
- out.push(msg);
35
- }
36
- else {
37
- out.push({ role: 'tool', tool_call_id: m.toolCallId, content: joinText(m.content) });
38
- }
39
- }
40
- return out;
41
- }
@@ -1,7 +0,0 @@
1
- import type { SessionMsg, DecisionPoint } from './types.js';
2
- /** Parse a pi session `.jsonl` into the ordered message list, dropping
3
- * non-message rows (session header, model_change, blanks, malformed lines). */
4
- export declare function parseTranscript(jsonl: string): SessionMsg[];
5
- /** Every assistant turn whose content includes a toolCall becomes a scoring
6
- * unit: the prior context plus the recorded tool name + args. */
7
- export declare function decisionPoints(messages: SessionMsg[]): DecisionPoint[];
@@ -1,41 +0,0 @@
1
- /** Parse a pi session `.jsonl` into the ordered message list, dropping
2
- * non-message rows (session header, model_change, blanks, malformed lines). */
3
- export function parseTranscript(jsonl) {
4
- const out = [];
5
- for (const line of jsonl.split('\n')) {
6
- const trimmed = line.trim();
7
- if (!trimmed)
8
- continue;
9
- let obj;
10
- try {
11
- obj = JSON.parse(trimmed);
12
- }
13
- catch {
14
- continue;
15
- }
16
- const rec = obj;
17
- if (rec.type !== 'message' || !rec.message)
18
- continue;
19
- out.push(rec.message);
20
- }
21
- return out;
22
- }
23
- /** Every assistant turn whose content includes a toolCall becomes a scoring
24
- * unit: the prior context plus the recorded tool name + args. */
25
- export function decisionPoints(messages) {
26
- const points = [];
27
- messages.forEach((m, index) => {
28
- if (m.role !== 'assistant')
29
- return;
30
- const tc = m.content.find((c) => c.type === 'toolCall');
31
- if (!tc)
32
- return;
33
- points.push({
34
- index,
35
- prior: messages.slice(0, index),
36
- recordedTool: tc.name,
37
- recordedArgs: tc.arguments
38
- });
39
- });
40
- return points;
41
- }
@@ -1,6 +0,0 @@
1
- import type { SessionMsg, ThinkMode } from './types.js';
2
- /** Produce the message list to send for a given arm. `full` is identity;
3
- * `none` removes thinking; `compressed` swaps each thinking block's text for
4
- * its precomputed summary (keyed by the original text), leaving it verbatim on
5
- * a cache miss. */
6
- export declare function applyMode(messages: SessionMsg[], mode: ThinkMode, compressed: Map<string, string>): SessionMsg[];
@@ -1,24 +0,0 @@
1
- function stripThinking(messages) {
2
- return messages.map(m => m.role === 'assistant' ? { ...m, content: m.content.filter(c => c.type !== 'thinking') } : m);
3
- }
4
- function compressThinking(messages, compressed) {
5
- return messages.map(m => m.role === 'assistant' ?
6
- {
7
- ...m,
8
- content: m.content.map(c => c.type === 'thinking' ?
9
- { ...c, thinking: compressed.get(c.thinking) ?? c.thinking }
10
- : c)
11
- }
12
- : m);
13
- }
14
- /** Produce the message list to send for a given arm. `full` is identity;
15
- * `none` removes thinking; `compressed` swaps each thinking block's text for
16
- * its precomputed summary (keyed by the original text), leaving it verbatim on
17
- * a cache miss. */
18
- export function applyMode(messages, mode, compressed) {
19
- if (mode === 'full')
20
- return messages;
21
- if (mode === 'none')
22
- return stripThinking(messages);
23
- return compressThinking(messages, compressed);
24
- }