pi-reason-harness 1.0.1
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/LICENSE +21 -0
- package/README.md +401 -0
- package/extensions/pi-reason-harness/index.ts +171 -0
- package/extensions/pi-reason-harness/src/dashboard/index.ts +47 -0
- package/extensions/pi-reason-harness/src/lifecycle/index.ts +47 -0
- package/extensions/pi-reason-harness/src/state/index.ts +49 -0
- package/extensions/pi-reason-harness/src/tools/index.ts +88 -0
- package/extensions/pi-reason-harness/src/types/index.ts +23 -0
- package/extensions/pi-reason-harness/src/ui/index.ts +76 -0
- package/harness/cli.ts +472 -0
- package/harness/server.test.ts +2095 -0
- package/harness/server.ts +5900 -0
- package/package.json +50 -0
- package/skills/reason-harness/SKILL.md +178 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-reason-harness — Lifecycle helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
9
|
+
|
|
10
|
+
export function getDirs() {
|
|
11
|
+
const baseDir = join(process.cwd(), '.pi', 'reason-harness');
|
|
12
|
+
return { base: baseDir };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function writeSessionId(ctx: ExtensionContext): void {
|
|
16
|
+
try {
|
|
17
|
+
const dirs = getDirs();
|
|
18
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
19
|
+
if (sessionId) {
|
|
20
|
+
if (!fs.existsSync(dirs.base)) fs.mkdirSync(dirs.base, { recursive: true });
|
|
21
|
+
fs.writeFileSync(join(dirs.base, 'session-id'), sessionId, 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function installShellAlias(cliPath: string, projectRoot: string): void {
|
|
27
|
+
try {
|
|
28
|
+
const agentBinDir = join(homedir(), '.pi', 'agent', 'bin');
|
|
29
|
+
if (!fs.existsSync(agentBinDir)) {
|
|
30
|
+
fs.mkdirSync(agentBinDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
const linkPath = join(agentBinDir, 'pi-reason-harness');
|
|
33
|
+
|
|
34
|
+
const wrapperContent = `#!/bin/sh
|
|
35
|
+
cd "${projectRoot}" 2>/dev/null
|
|
36
|
+
exec npx tsx "${cliPath}" "$@"
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
let currentContent: string | null = null;
|
|
40
|
+
try {
|
|
41
|
+
currentContent = fs.readFileSync(linkPath, 'utf-8');
|
|
42
|
+
} catch {}
|
|
43
|
+
if (currentContent !== wrapperContent) {
|
|
44
|
+
fs.writeFileSync(linkPath, wrapperContent, { mode: 0o755 });
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-reason-harness — Runtime state management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ReasonHarnessRuntime } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
export function createRuntimeState(): ReasonHarnessRuntime {
|
|
8
|
+
return {
|
|
9
|
+
sessionName: null,
|
|
10
|
+
taskType: null,
|
|
11
|
+
status: 'idle',
|
|
12
|
+
iterationCount: 0,
|
|
13
|
+
bestScore: 0,
|
|
14
|
+
solved: false,
|
|
15
|
+
expertCount: 0,
|
|
16
|
+
models: [],
|
|
17
|
+
totalTokens: 0,
|
|
18
|
+
totalCost: 0,
|
|
19
|
+
adaptations: 0,
|
|
20
|
+
budget: {
|
|
21
|
+
costUsed: 0,
|
|
22
|
+
timeUsed: 0,
|
|
23
|
+
problemsSolved: 0,
|
|
24
|
+
problemsAttempted: 0,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RuntimeStore {
|
|
30
|
+
ensure(key: string): ReasonHarnessRuntime;
|
|
31
|
+
clear(key: string): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createRuntimeStore(): RuntimeStore {
|
|
35
|
+
const runtimes = new Map<string, ReasonHarnessRuntime>();
|
|
36
|
+
return {
|
|
37
|
+
ensure(key: string): ReasonHarnessRuntime {
|
|
38
|
+
let runtime = runtimes.get(key);
|
|
39
|
+
if (!runtime) {
|
|
40
|
+
runtime = createRuntimeState();
|
|
41
|
+
runtimes.set(key, runtime);
|
|
42
|
+
}
|
|
43
|
+
return runtime;
|
|
44
|
+
},
|
|
45
|
+
clear(key: string): void {
|
|
46
|
+
runtimes.delete(key);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-reason-harness — Registered tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import { Type } from '@sinclair/typebox';
|
|
7
|
+
|
|
8
|
+
export function registerTools(pi: ExtensionAPI): void {
|
|
9
|
+
pi.registerTool({
|
|
10
|
+
name: 'reason_harness_init',
|
|
11
|
+
label: 'Reason Harness Init',
|
|
12
|
+
description: 'Initialize a reasoning harness session for iterative solve-verify-feedback problem solving. Creates experts with verification strategies.',
|
|
13
|
+
parameters: Type.Object({
|
|
14
|
+
name: Type.String({ description: 'Session name' }),
|
|
15
|
+
type: Type.Union([
|
|
16
|
+
Type.Literal('code-reasoning'),
|
|
17
|
+
Type.Literal('knowledge-extraction'),
|
|
18
|
+
Type.Literal('hybrid'),
|
|
19
|
+
], { description: 'Task type' }),
|
|
20
|
+
models: Type.Array(Type.String(), { description: 'Models to use (provider/id format, e.g. "anthropic/claude-sonnet-4-5")', default: ['openai/gpt-4o'] }),
|
|
21
|
+
numExperts: Type.Number({ description: 'Number of parallel experts', default: 1 }),
|
|
22
|
+
verification: Type.Union([
|
|
23
|
+
Type.Literal('sandbox'),
|
|
24
|
+
Type.Literal('self-audit'),
|
|
25
|
+
Type.Literal('external'),
|
|
26
|
+
Type.Literal('none'),
|
|
27
|
+
], { description: 'Verification method', default: 'sandbox' }),
|
|
28
|
+
}),
|
|
29
|
+
execute: async (_toolCallId, params, _signal?, _onUpdate?) => {
|
|
30
|
+
const { execFile } = await import('node:child_process');
|
|
31
|
+
const payload = JSON.stringify({ action: 'init', ...params });
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
execFile('pi-reason-harness', [payload], { timeout: 10000 }, (err, stdout, stderr) => {
|
|
34
|
+
if (err) {
|
|
35
|
+
resolve({ content: [{ type: 'text' as const, text: `Error: ${stderr || err.message}` }], details: {} });
|
|
36
|
+
} else {
|
|
37
|
+
resolve({ content: [{ type: 'text' as const, text: stdout.trim() }], details: {} });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
pi.registerTool({
|
|
45
|
+
name: 'reason_harness_solve',
|
|
46
|
+
label: 'Reason Harness Solve',
|
|
47
|
+
description: 'Run the iterative solve-verify-feedback loop on a problem. Generates candidate solutions, verifies them, builds feedback from failures, and votes across parallel experts.',
|
|
48
|
+
parameters: Type.Object({
|
|
49
|
+
problem: Type.String({ description: 'The problem to solve' }),
|
|
50
|
+
trainInputs: Type.Array(Type.Any(), { description: 'Training input data for verification' }),
|
|
51
|
+
trainOutputs: Type.Array(Type.Any(), { description: 'Training output data (ground truth) for verification' }),
|
|
52
|
+
testInputs: Type.Array(Type.Any(), { description: 'Test input data' }),
|
|
53
|
+
}),
|
|
54
|
+
execute: async (_toolCallId, params, _signal?, _onUpdate?) => {
|
|
55
|
+
const { execFile } = await import('node:child_process');
|
|
56
|
+
const payload = JSON.stringify({ action: 'solve', ...params });
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
execFile('pi-reason-harness', [payload], { timeout: 600000 }, (err, stdout, stderr) => {
|
|
59
|
+
if (err) {
|
|
60
|
+
resolve({ content: [{ type: 'text' as const, text: `Error: ${stderr || err.message}` }], details: {} });
|
|
61
|
+
} else {
|
|
62
|
+
resolve({ content: [{ type: 'text' as const, text: stdout.trim() }], details: {} });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
pi.registerTool({
|
|
70
|
+
name: 'reason_harness_status',
|
|
71
|
+
label: 'Reason Harness Status',
|
|
72
|
+
description: 'Check the current reasoning harness session status, including iterations, scores, cost, and learned strategy adaptations.',
|
|
73
|
+
parameters: Type.Object({}),
|
|
74
|
+
execute: async (_toolCallId, _params, _signal?, _onUpdate?) => {
|
|
75
|
+
const { execFile } = await import('node:child_process');
|
|
76
|
+
const payload = JSON.stringify({ action: 'status' });
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
execFile('pi-reason-harness', [payload], { timeout: 10000 }, (err, stdout, stderr) => {
|
|
79
|
+
if (err) {
|
|
80
|
+
resolve({ content: [{ type: 'text' as const, text: `Error: ${stderr || err.message}` }], details: {} });
|
|
81
|
+
} else {
|
|
82
|
+
resolve({ content: [{ type: 'text' as const, text: stdout.trim() }], details: {} });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-reason-harness — Shared types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ReasonHarnessRuntime {
|
|
6
|
+
sessionName: string | null;
|
|
7
|
+
taskType: string | null;
|
|
8
|
+
status: string;
|
|
9
|
+
iterationCount: number;
|
|
10
|
+
bestScore: number;
|
|
11
|
+
solved: boolean;
|
|
12
|
+
expertCount: number;
|
|
13
|
+
models: string[];
|
|
14
|
+
totalTokens: number;
|
|
15
|
+
totalCost: number;
|
|
16
|
+
adaptations: number;
|
|
17
|
+
budget: {
|
|
18
|
+
costUsed: number;
|
|
19
|
+
timeUsed: number;
|
|
20
|
+
problemsSolved: number;
|
|
21
|
+
problemsAttempted: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-reason-harness — UI widgets
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import { Text, truncateToWidth } from '@earendil-works/pi-tui';
|
|
7
|
+
import type { ReasonHarnessRuntime } from '../types/index.js';
|
|
8
|
+
|
|
9
|
+
export function createWidgetUpdater(
|
|
10
|
+
getRuntime: (ctx: ExtensionContext) => ReasonHarnessRuntime
|
|
11
|
+
) {
|
|
12
|
+
return function updateWidget(extCtx: ExtensionContext): void {
|
|
13
|
+
if (!extCtx.hasUI) return;
|
|
14
|
+
const runtime = getRuntime(extCtx);
|
|
15
|
+
const width = process.stdout.columns || 120;
|
|
16
|
+
|
|
17
|
+
if (!runtime.sessionName) {
|
|
18
|
+
extCtx.ui.setWidget('reason-harness', undefined);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
extCtx.ui.setWidget('reason-harness', (_tui, theme) => {
|
|
23
|
+
const parts = [
|
|
24
|
+
theme.fg('accent', '🧠'),
|
|
25
|
+
theme.fg('text', ` ${runtime.sessionName}`),
|
|
26
|
+
theme.fg('dim', ` │ ${runtime.taskType}`),
|
|
27
|
+
theme.fg('dim', ' │ '),
|
|
28
|
+
theme.fg(
|
|
29
|
+
runtime.status === 'solving' ? 'warning' : runtime.solved ? 'success' : 'muted',
|
|
30
|
+
runtime.status
|
|
31
|
+
),
|
|
32
|
+
theme.fg('dim', ' │ '),
|
|
33
|
+
theme.fg('muted', `E:${runtime.expertCount}`),
|
|
34
|
+
theme.fg('dim', ' │ '),
|
|
35
|
+
theme.fg(runtime.solved ? 'success' : 'warning', `★ ${runtime.bestScore.toFixed(2)}`),
|
|
36
|
+
theme.fg('dim', ` │ ${runtime.iterationCount} iters`),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
if (runtime.totalTokens > 0) {
|
|
40
|
+
const tokStr =
|
|
41
|
+
runtime.totalTokens > 1000000
|
|
42
|
+
? `${(runtime.totalTokens / 1000000).toFixed(1)}M`
|
|
43
|
+
: runtime.totalTokens > 1000
|
|
44
|
+
? `${(runtime.totalTokens / 1000).toFixed(1)}K`
|
|
45
|
+
: `${runtime.totalTokens}`;
|
|
46
|
+
parts.push(theme.fg('dim', ` │ ${tokStr} tok`));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (runtime.totalCost > 0) {
|
|
50
|
+
const costStr =
|
|
51
|
+
runtime.totalCost < 0.01
|
|
52
|
+
? `$${runtime.totalCost.toFixed(4)}`
|
|
53
|
+
: `$${runtime.totalCost.toFixed(2)}`;
|
|
54
|
+
parts.push(theme.fg('dim', ` │ ${costStr}`));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (runtime.adaptations > 0) {
|
|
58
|
+
parts.push(theme.fg('accent', ` │ 🧠${runtime.adaptations}`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (runtime.solved) {
|
|
62
|
+
parts.push(theme.fg('success', ' ✓'));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
parts.push(theme.fg('dim', ' (ctrl+shift+r)'));
|
|
66
|
+
|
|
67
|
+
return new Text(truncateToWidth(parts.join(''), width), width);
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function clearSessionUi(extCtx: ExtensionContext): void {
|
|
73
|
+
if (extCtx.hasUI) {
|
|
74
|
+
extCtx.ui.setWidget('reason-harness', undefined);
|
|
75
|
+
}
|
|
76
|
+
}
|