@openlines/opl-pi-sht 0.1.14
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 +191 -0
- package/configs/opl-browser.json.sample +8 -0
- package/configs/opl-footer.json +48 -0
- package/configs/opl-footer.json.sample +48 -0
- package/configs/opl-input.json +22 -0
- package/configs/opl-input.json.sample +22 -0
- package/configs/opl-modes.json +291 -0
- package/configs/opl-modes.json.sample +285 -0
- package/configs/opl-simplebench.json +8 -0
- package/configs/opl-simplebench.json.sample +8 -0
- package/configs/opl-todo.json +12 -0
- package/configs/opl-todo.json.sample +12 -0
- package/configs/opl-webaccess.json +31 -0
- package/configs/opl-webaccess.json.sample +31 -0
- package/extensions/opl-browser/README.md +83 -0
- package/extensions/opl-browser/browser.ts +185 -0
- package/extensions/opl-browser/config.ts +37 -0
- package/extensions/opl-browser/index.ts +89 -0
- package/extensions/opl-browser/package-lock.json +60 -0
- package/extensions/opl-browser/package.json +10 -0
- package/extensions/opl-ctxtrim/README.md +78 -0
- package/extensions/opl-ctxtrim/index.ts +151 -0
- package/extensions/opl-footer/README.md +193 -0
- package/extensions/opl-footer/config.ts +91 -0
- package/extensions/opl-footer/git-status.ts +170 -0
- package/extensions/opl-footer/icons.ts +49 -0
- package/extensions/opl-footer/index.ts +368 -0
- package/extensions/opl-footer/segments/caveman.ts +25 -0
- package/extensions/opl-footer/segments/chat-mode.ts +24 -0
- package/extensions/opl-footer/segments/context.ts +92 -0
- package/extensions/opl-footer/segments/cost.ts +25 -0
- package/extensions/opl-footer/segments/git.ts +47 -0
- package/extensions/opl-footer/segments/helpers.ts +18 -0
- package/extensions/opl-footer/segments/index.ts +51 -0
- package/extensions/opl-footer/segments/mode-switcher.ts +24 -0
- package/extensions/opl-footer/segments/model.ts +22 -0
- package/extensions/opl-footer/segments/path.ts +34 -0
- package/extensions/opl-footer/segments/pi.ts +11 -0
- package/extensions/opl-footer/segments/plan-mode.ts +24 -0
- package/extensions/opl-footer/segments/separator.ts +9 -0
- package/extensions/opl-footer/segments/session-stats.ts +82 -0
- package/extensions/opl-footer/segments/thinking.ts +51 -0
- package/extensions/opl-footer/segments/tokens.ts +66 -0
- package/extensions/opl-footer/theme.ts +113 -0
- package/extensions/opl-footer/types.ts +172 -0
- package/extensions/opl-init/README.md +23 -0
- package/extensions/opl-init/index.ts +451 -0
- package/extensions/opl-input/README.md +83 -0
- package/extensions/opl-input/config.ts +137 -0
- package/extensions/opl-input/index.ts +284 -0
- package/extensions/opl-input/mode-style.ts +38 -0
- package/extensions/opl-input/utils.ts +378 -0
- package/extensions/opl-modes/README.md +132 -0
- package/extensions/opl-modes/config.ts +473 -0
- package/extensions/opl-modes/index.ts +1081 -0
- package/extensions/opl-modes/menus.ts +60 -0
- package/extensions/opl-modes/state.ts +152 -0
- package/extensions/opl-modes/types.ts +164 -0
- package/extensions/opl-modes/utils.ts +138 -0
- package/extensions/opl-questionnaire/README.md +26 -0
- package/extensions/opl-questionnaire/index.ts +445 -0
- package/extensions/opl-questionnaire/types.ts +55 -0
- package/extensions/opl-simplebench/README.md +242 -0
- package/extensions/opl-simplebench/artifact.ts +41 -0
- package/extensions/opl-simplebench/benchmark.ts +873 -0
- package/extensions/opl-simplebench/coding.ts +282 -0
- package/extensions/opl-simplebench/index.ts +203 -0
- package/extensions/opl-simplebench/llama-server.ts +290 -0
- package/extensions/opl-simplebench/metrics.ts +73 -0
- package/extensions/opl-simplebench/report.ts +46 -0
- package/extensions/opl-simplebench/research.ts +200 -0
- package/extensions/opl-simplebench/scoring.ts +48 -0
- package/extensions/opl-simplebench/tests.ts +39 -0
- package/extensions/opl-simplebench/types.ts +6 -0
- package/extensions/opl-simplebench/util/config.ts +170 -0
- package/extensions/opl-simplebench/util/debug.ts +32 -0
- package/extensions/opl-simplebench/util/format.ts +402 -0
- package/extensions/opl-simplebench/util/providers.ts +841 -0
- package/extensions/opl-todo/README.md +43 -0
- package/extensions/opl-todo/config.ts +71 -0
- package/extensions/opl-todo/index.ts +415 -0
- package/extensions/opl-webaccess/README.md +47 -0
- package/extensions/opl-webaccess/config.ts +55 -0
- package/extensions/opl-webaccess/extract.ts +83 -0
- package/extensions/opl-webaccess/index.ts +232 -0
- package/extensions/opl-webaccess/package-lock.json +345 -0
- package/extensions/opl-webaccess/package.json +18 -0
- package/extensions/opl-webaccess/pdf.ts +8 -0
- package/extensions/opl-webaccess/providers/ddgs.ts +72 -0
- package/extensions/opl-webaccess/providers/exa.ts +74 -0
- package/extensions/opl-webaccess/providers/gemini.ts +60 -0
- package/extensions/opl-webaccess/providers/searxng.ts +86 -0
- package/extensions/opl-webaccess/providers/tavily.ts +60 -0
- package/extensions/opl-webaccess/search.ts +36 -0
- package/extensions/opl-webaccess/storage.ts +51 -0
- package/extensions/opl-webaccess/turndown.d.ts +1 -0
- package/extensions/opl-webaccess/types.ts +26 -0
- package/extensions/opl-webaccess/utils.ts +28 -0
- package/package.json +55 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { buildToolResultMessages, type ChatFn, type ChatMessage } from "./util/config";
|
|
6
|
+
import { mergeRequestMetrics, metricsFromChat } from "./metrics";
|
|
7
|
+
import type { RequestMetrics } from "./types";
|
|
8
|
+
|
|
9
|
+
export interface CodingTaskFixture {
|
|
10
|
+
id: string;
|
|
11
|
+
prompt: string;
|
|
12
|
+
files: Record<string, string>;
|
|
13
|
+
allowedFiles: string[];
|
|
14
|
+
verify: (root: string, hidden: boolean) => string;
|
|
15
|
+
/** Inline single-shot variant: full code in prompt, no tools, one turn. */
|
|
16
|
+
inlinePrompt?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CodingTaskResult {
|
|
20
|
+
id: string;
|
|
21
|
+
passed: boolean;
|
|
22
|
+
publicPassed: boolean;
|
|
23
|
+
hiddenPassed: boolean;
|
|
24
|
+
verifiedAfterEdit: boolean;
|
|
25
|
+
unrelatedFiles: string[];
|
|
26
|
+
toolCalls: number;
|
|
27
|
+
turns: number;
|
|
28
|
+
wallTimeMs: number;
|
|
29
|
+
outputTokens: number | null;
|
|
30
|
+
error: string | null;
|
|
31
|
+
metrics: RequestMetrics;
|
|
32
|
+
/** STRONG = solved in 1 turn; MODERATE = 2–3 turns; WEAK = 4–5 turns; FAIL = not solved. */
|
|
33
|
+
efficiency: "STRONG" | "MODERATE" | "WEAK" | "FAIL";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const assertCode = (root: string, moduleName: string, expression: string) => `import * as m from ${JSON.stringify(path.join(root, moduleName))};\nif (!(await (${expression}))) throw new Error('verification failed');`;
|
|
37
|
+
|
|
38
|
+
export const CODING_LITE_TASKS: CodingTaskFixture[] = [
|
|
39
|
+
{
|
|
40
|
+
id: "fix-off-by-one",
|
|
41
|
+
prompt: "Fix the inclusive range bug in src/sum.mjs. Keep the exported API. Run the available tests after editing.",
|
|
42
|
+
files: { "src/sum.mjs": "export function sumInclusive(start, end) { let total = 0; for (let value = start; value < end; value += 1) total += value; return total; }\n" },
|
|
43
|
+
allowedFiles: ["src/sum.mjs"],
|
|
44
|
+
verify: (root, hidden) => hidden ? assertCode(root, "src/sum.mjs", "m.sumInclusive(1, 4) === 10 && m.sumInclusive(5, 5) === 5") : assertCode(root, "src/sum.mjs", "m.sumInclusive(1, 4) === 10"),
|
|
45
|
+
inlinePrompt: "Fix the inclusive range bug below. sumInclusive(1, 4) must return 10; sumInclusive(5, 5) must return 5.\n\nFile: src/sum.mjs\n```js\nexport function sumInclusive(start, end) { let total = 0; for (let value = start; value < end; value += 1) total += value; return total; }\n```\n\nReply with ONLY the corrected file content.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "validate-config",
|
|
49
|
+
prompt: "Implement strict config parsing in src/config.mjs. Accept a valid numeric port, default the host to localhost, and reject ports outside 1..65535. Run the available tests after editing.",
|
|
50
|
+
files: { "src/config.mjs": "export function parseConfig(input = {}) { return { host: input.host || 'localhost', port: Number(input.port || 3000) }; }\n" },
|
|
51
|
+
allowedFiles: ["src/config.mjs"],
|
|
52
|
+
verify: (root, hidden) => hidden ? assertCode(root, "src/config.mjs", "(() => { try { m.parseConfig({ port: 0 }); return false; } catch {} return m.parseConfig({ port: 65535 }).port === 65535; })()") : assertCode(root, "src/config.mjs", "m.parseConfig({ port: 8080 }).port === 8080 && m.parseConfig({}).host === 'localhost'"),
|
|
53
|
+
inlinePrompt: "Implement strict config parsing: accept a valid numeric port (1–65535), default host to 'localhost', throw on out-of-range ports.\n\nFile: src/config.mjs\n```js\nexport function parseConfig(input = {}) { return { host: input.host || 'localhost', port: Number(input.port || 3000) }; }\n```\n\nReply with ONLY the corrected file content.",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "diagnose-cross-file",
|
|
57
|
+
prompt: "Find and fix the cross-file normalization regression. Usernames should be trimmed before lookup. Keep the public API and run tests after editing.",
|
|
58
|
+
files: { "src/lookup.mjs": "import { normalizeUser } from './normalize.mjs';\nexport function findUser(users, name) { return users.find(user => user.name === normalizeUser(name)); }\n", "src/normalize.mjs": "export function normalizeUser(value) { return String(value).toLowerCase(); }\n" },
|
|
59
|
+
allowedFiles: ["src/normalize.mjs"],
|
|
60
|
+
verify: (root, hidden) => hidden ? assertCode(root, "src/lookup.mjs", "m.findUser([{ name: 'alice' }], ' ALICE ')?.name === 'alice'") : assertCode(root, "src/lookup.mjs", "m.findUser([{ name: 'alice' }], 'ALICE')?.name === 'alice'"),
|
|
61
|
+
inlinePrompt: "Usernames must be trimmed before lookup. Fix src/normalize.mjs only — do not change src/lookup.mjs.\n\nFile: src/lookup.mjs (read-only)\n```js\nimport { normalizeUser } from './normalize.mjs';\nexport function findUser(users, name) { return users.find(user => user.name === normalizeUser(name)); }\n```\n\nFile: src/normalize.mjs\n```js\nexport function normalizeUser(value) { return String(value).toLowerCase(); }\n```\n\nReply with ONLY the corrected src/normalize.mjs content.",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "safe-refactor",
|
|
65
|
+
prompt: "Make formatLabels skip entries that are empty after trimming. For [' a ', '', 'b'], the exact result must be 'A, B'; preserve the existing output for non-empty values. You may refactor src/format.mjs, but do not change the exported function name. Run tests after editing.",
|
|
66
|
+
files: { "src/format.mjs": "export function formatLabels(values) { return values.map(value => String(value).trim().toUpperCase()).join(', '); }\n" },
|
|
67
|
+
allowedFiles: ["src/format.mjs"],
|
|
68
|
+
verify: (root, hidden) => hidden ? assertCode(root, "src/format.mjs", "m.formatLabels([' a ', '', 'b']) === 'A, B'") : assertCode(root, "src/format.mjs", "m.formatLabels([' a ', '', 'b']) === 'A, B' && m.formatLabels([' a ', 'b']) === 'A, B'"),
|
|
69
|
+
inlinePrompt: "Make formatLabels skip entries that are empty after trimming. For [' a ', '', 'b'], the exact result must be 'A, B'; keep the exported name and all other output unchanged.\n\nFile: src/format.mjs\n```js\nexport function formatLabels(values) { return values.map(value => String(value).trim().toUpperCase()).join(', '); }\n```\n\nReply with ONLY the corrected file content.",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "cli-flag",
|
|
73
|
+
prompt: "Add a --upper flag to src/cli.mjs. The supported invocations are `cli --upper alice` and `cli alice`. Without the flag, print the supplied name unchanged; with it, print uppercase text. Preserve the current usage error and run tests after editing.",
|
|
74
|
+
files: { "src/cli.mjs": "const args = process.argv.slice(2);\nif (args.length === 0) { console.error('usage: cli <name>'); process.exit(2); }\nconsole.log(args[0]);\n" },
|
|
75
|
+
allowedFiles: ["src/cli.mjs"],
|
|
76
|
+
verify: (root, hidden) => hidden ? `const { spawnSync } = await import('node:child_process'); const r1 = spawnSync(process.execPath, ['src/cli.mjs', '--upper', 'alice'], { cwd: ${JSON.stringify(root)}, encoding: 'utf8' }); const r2 = spawnSync(process.execPath, ['src/cli.mjs', 'alice'], { cwd: ${JSON.stringify(root)}, encoding: 'utf8' }); if (r1.status !== 0 || r1.stdout.trim() !== 'ALICE') throw new Error('verification failed'); if (r2.status !== 0 || r2.stdout.trim() !== 'alice') throw new Error('verification failed');` : `const { spawnSync } = await import('node:child_process'); const r = spawnSync(process.execPath, ['src/cli.mjs', '--upper', 'alice'], { cwd: ${JSON.stringify(root)}, encoding: 'utf8' }); if (r.status !== 0 || r.stdout.trim() !== 'ALICE') throw new Error('verification failed');`,
|
|
77
|
+
inlinePrompt: "Add a --upper flag. The supported invocations are `cli --upper alice` and `cli alice`. Without the flag, print the name unchanged; with it, print uppercase. Preserve the usage error on empty args.\n\nFile: src/cli.mjs\n```js\nconst args = process.argv.slice(2);\nif (args.length === 0) { console.error('usage: cli <name>'); process.exit(2); }\nconsole.log(args[0]);\n```\n\nReply with ONLY the corrected file content."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "verify-after-edit",
|
|
81
|
+
prompt: "Fix retryOnce in src/retry.mjs so it retries once after a failure and returns the successful value. Use the test tool after your edit to verify the fix.",
|
|
82
|
+
files: { "src/retry.mjs": "export async function retryOnce(operation) { return await operation(); }\n" },
|
|
83
|
+
allowedFiles: ["src/retry.mjs"],
|
|
84
|
+
verify: (root, hidden) => hidden ? assertCode(root, "src/retry.mjs", "(async () => { let attempts = 0; const value = await m.retryOnce(async () => { attempts += 1; if (attempts === 1) throw new Error('retry'); return 42; }); return value === 42 && attempts === 2; })()") : assertCode(root, "src/retry.mjs", "(async () => (await m.retryOnce(async () => 42)) === 42)()"),
|
|
85
|
+
inlinePrompt: "Fix retryOnce so it retries once after a failure and returns the successful value.\n\nFile: src/retry.mjs\n```js\nexport async function retryOnce(operation) { return await operation(); }\n```\n\nReply with ONLY the corrected file content.",
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
export function resolveCodingPath(root: string, relativePath: string): string {
|
|
90
|
+
if (path.isAbsolute(relativePath)) throw new Error("absolute paths are outside the coding task");
|
|
91
|
+
const taskRoot = path.resolve(root);
|
|
92
|
+
const target = path.resolve(taskRoot, relativePath);
|
|
93
|
+
if (target !== taskRoot && !target.startsWith(`${taskRoot}${path.sep}`)) throw new Error("path is outside the coding task");
|
|
94
|
+
return target;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createCodingTaskDir(task: CodingTaskFixture): string {
|
|
98
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), `simplebench-${task.id}-`));
|
|
99
|
+
for (const [relativePath, content] of Object.entries(task.files)) {
|
|
100
|
+
const target = resolveCodingPath(root, relativePath);
|
|
101
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
102
|
+
fs.writeFileSync(target, content);
|
|
103
|
+
}
|
|
104
|
+
return root;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function runCodingVerifier(task: CodingTaskFixture, root: string, mode: "public" | "hidden") {
|
|
108
|
+
const result = spawnSync(process.execPath, ["--eval", task.verify(root, mode === "hidden")], { cwd: root, encoding: "utf8", timeout: 10_000 });
|
|
109
|
+
const rawOutput = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
110
|
+
return { passed: result.status === 0, output: result.status === 0 ? rawOutput : truncateVerifierError(rawOutput) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Keep only the thrown error line; the eval stack embeds the full script and tmp path. */
|
|
114
|
+
function truncateVerifierError(raw: string): string {
|
|
115
|
+
const marker = raw.lastIndexOf("Error: ");
|
|
116
|
+
if (marker < 0) return raw.slice(-200);
|
|
117
|
+
const end = raw.indexOf("\n", marker);
|
|
118
|
+
return end === -1 ? raw.slice(marker) : raw.slice(marker, end);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function safeJson(value: unknown): { value: any; error?: string } {
|
|
122
|
+
try { return { value: typeof value === "string" ? JSON.parse(value) : value ?? {} }; }
|
|
123
|
+
catch { return { value: {}, error: "invalid JSON arguments" }; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const CODING_TOOLS = [
|
|
127
|
+
{ type: "function", function: { name: "list_files", description: "List files in the task repository", parameters: { type: "object", properties: { path: { type: "string" } } } } },
|
|
128
|
+
{ type: "function", function: { name: "search_files", description: "Search text files in the task repository", parameters: { type: "object", properties: { query: { type: "string" }, path: { type: "string" } }, required: ["query"] } } },
|
|
129
|
+
{ type: "function", function: { name: "read_file", description: "Read a task repository file", parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } } },
|
|
130
|
+
{ type: "function", function: { name: "write_file", description: "Write a task repository file", parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } } },
|
|
131
|
+
{ type: "function", function: { name: "run_tests", description: "Run the public tests for this task", parameters: { type: "object", properties: {} } } },
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
function codingToolError(message: string, requestedPath?: string): string {
|
|
135
|
+
return JSON.stringify({ error: message, ...(requestedPath ? { path: requestedPath } : {}) });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function executeCodingTool(root: string, task: CodingTaskFixture, name: string, args: any): string {
|
|
139
|
+
if (name === "list_files") {
|
|
140
|
+
const base = resolveCodingPath(root, args.path || ".");
|
|
141
|
+
if (!fs.existsSync(base)) return codingToolError("path not found", args.path || ".");
|
|
142
|
+
if (!fs.statSync(base).isDirectory()) return codingToolError("path is not a directory", args.path || ".");
|
|
143
|
+
const files: string[] = [];
|
|
144
|
+
const visit = (dir: string) => { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const target = path.join(dir, entry.name); if (entry.isDirectory()) visit(target); else files.push(path.relative(root, target)); } };
|
|
145
|
+
visit(base);
|
|
146
|
+
return JSON.stringify(files);
|
|
147
|
+
}
|
|
148
|
+
if (name === "search_files") {
|
|
149
|
+
const base = resolveCodingPath(root, args.path || ".");
|
|
150
|
+
if (!fs.existsSync(base)) return codingToolError("path not found", args.path || ".");
|
|
151
|
+
if (!fs.statSync(base).isDirectory()) return codingToolError("path is not a directory", args.path || ".");
|
|
152
|
+
const hits: string[] = [];
|
|
153
|
+
const visit = (dir: string) => { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const target = path.join(dir, entry.name); if (entry.isDirectory()) visit(target); else if (fs.readFileSync(target, "utf8").includes(String(args.query))) hits.push(path.relative(root, target)); } };
|
|
154
|
+
visit(base);
|
|
155
|
+
return JSON.stringify(hits);
|
|
156
|
+
}
|
|
157
|
+
if (name === "read_file") {
|
|
158
|
+
const target = resolveCodingPath(root, args.path);
|
|
159
|
+
if (!fs.existsSync(target)) return codingToolError("file not found", args.path);
|
|
160
|
+
if (!fs.statSync(target).isFile()) return codingToolError("path is not a file", args.path);
|
|
161
|
+
return fs.readFileSync(target, "utf8");
|
|
162
|
+
}
|
|
163
|
+
if (name === "write_file") { const target = resolveCodingPath(root, args.path); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, String(args.content)); return "written"; }
|
|
164
|
+
if (name === "run_tests") return JSON.stringify(runCodingVerifier(task, root, "public"));
|
|
165
|
+
throw new Error(`unknown coding tool: ${name}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function runSingleShotCodingTask(chatFn: ChatFn, model: string, task: CodingTaskFixture): Promise<CodingTaskResult> {
|
|
169
|
+
const root = createCodingTaskDir(task);
|
|
170
|
+
const started = Date.now();
|
|
171
|
+
try {
|
|
172
|
+
const response = await chatFn(model, [
|
|
173
|
+
{ role: "system", content: "You are fixing a coding bug. Reply with ONLY the corrected file content — no explanation, no markdown fences." },
|
|
174
|
+
{ role: "user", content: task.inlinePrompt! },
|
|
175
|
+
]);
|
|
176
|
+
const raw = response.content.trim();
|
|
177
|
+
const fenced = raw.match(/```(?:[a-z]*)\n([\s\S]*?)```/);
|
|
178
|
+
const code = fenced ? fenced[1].trim() : raw;
|
|
179
|
+
const target = resolveCodingPath(root, task.allowedFiles[0]);
|
|
180
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
181
|
+
fs.writeFileSync(target, code + "\n");
|
|
182
|
+
const publicResult = runCodingVerifier(task, root, "public");
|
|
183
|
+
const hiddenResult = runCodingVerifier(task, root, "hidden");
|
|
184
|
+
const passed = hiddenResult.passed;
|
|
185
|
+
return {
|
|
186
|
+
id: task.id, passed,
|
|
187
|
+
publicPassed: publicResult.passed, hiddenPassed: hiddenResult.passed,
|
|
188
|
+
verifiedAfterEdit: true, unrelatedFiles: [], toolCalls: 0, turns: 1,
|
|
189
|
+
wallTimeMs: Date.now() - started, outputTokens: null,
|
|
190
|
+
error: passed ? null : hiddenResult.output || "single-shot verification failed",
|
|
191
|
+
metrics: metricsFromChat(response),
|
|
192
|
+
efficiency: passed ? "STRONG" : "FAIL",
|
|
193
|
+
};
|
|
194
|
+
} catch (e: any) {
|
|
195
|
+
return {
|
|
196
|
+
id: task.id, passed: false,
|
|
197
|
+
publicPassed: false, hiddenPassed: false,
|
|
198
|
+
verifiedAfterEdit: false, unrelatedFiles: [], toolCalls: 0, turns: 1,
|
|
199
|
+
wallTimeMs: Date.now() - started, outputTokens: null,
|
|
200
|
+
error: e?.message || String(e),
|
|
201
|
+
metrics: mergeRequestMetrics([]),
|
|
202
|
+
efficiency: "FAIL",
|
|
203
|
+
};
|
|
204
|
+
} finally {
|
|
205
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function runCodingTask(chatFn: ChatFn, model: string, task: CodingTaskFixture, options: { maxTurns?: number; singleShot?: boolean; onProgress?: (message: string) => void } = {}): Promise<CodingTaskResult> {
|
|
210
|
+
if (options.singleShot && task.inlinePrompt) return runSingleShotCodingTask(chatFn, model, task);
|
|
211
|
+
const root = createCodingTaskDir(task);
|
|
212
|
+
const started = Date.now();
|
|
213
|
+
let turns = 0;
|
|
214
|
+
let toolCalls = 0;
|
|
215
|
+
let verifiedAfterEdit = false;
|
|
216
|
+
let error: string | null = null;
|
|
217
|
+
const requestMetrics: RequestMetrics[] = [];
|
|
218
|
+
const messages: ChatMessage[] = [{ role: "system", content: "You are completing a coding task in a disposable repository. Use the available tools. Do not assume a change worked until you run_tests after editing." }, { role: "user", content: task.prompt }];
|
|
219
|
+
try {
|
|
220
|
+
try {
|
|
221
|
+
while (turns < (options.maxTurns ?? 5)) {
|
|
222
|
+
turns += 1;
|
|
223
|
+
options.onProgress?.(`${task.id}: agent turn ${turns}/${options.maxTurns ?? 5}...`);
|
|
224
|
+
const response = await chatFn(model, messages, { tools: CODING_TOOLS });
|
|
225
|
+
requestMetrics.push(metricsFromChat(response));
|
|
226
|
+
const calls = response.toolCalls ?? [];
|
|
227
|
+
if (!calls.length) break;
|
|
228
|
+
const results: string[] = [];
|
|
229
|
+
for (const call of calls) {
|
|
230
|
+
const fn = call.function ?? call;
|
|
231
|
+
const name = fn.name;
|
|
232
|
+
const parsed = safeJson(fn.arguments);
|
|
233
|
+
const args = parsed.value;
|
|
234
|
+
toolCalls += 1;
|
|
235
|
+
if (name === "write_file") verifiedAfterEdit = false;
|
|
236
|
+
let result: string;
|
|
237
|
+
try { result = parsed.error ? codingToolError(parsed.error) : executeCodingTool(root, task, name, args); }
|
|
238
|
+
catch (e: any) { result = codingToolError(e?.message || String(e)); }
|
|
239
|
+
if (name === "run_tests") {
|
|
240
|
+
try { verifiedAfterEdit = JSON.parse(result).passed === true; }
|
|
241
|
+
catch { verifiedAfterEdit = false; }
|
|
242
|
+
}
|
|
243
|
+
results.push(`TOOL_RESULT ${name}: ${result}`);
|
|
244
|
+
options.onProgress?.(`${task.id}: ${name} (${toolCalls})`);
|
|
245
|
+
}
|
|
246
|
+
messages.push(...buildToolResultMessages(response.content, calls, results));
|
|
247
|
+
}
|
|
248
|
+
} catch (e: any) { error = e?.message || String(e); }
|
|
249
|
+
const publicResult = runCodingVerifier(task, root, "public");
|
|
250
|
+
const hiddenResult = runCodingVerifier(task, root, "hidden");
|
|
251
|
+
const changed = listChangedFiles(root, task.files);
|
|
252
|
+
const unrelatedFiles = changed.filter(file => !task.allowedFiles.includes(file));
|
|
253
|
+
const metrics = mergeRequestMetrics(requestMetrics);
|
|
254
|
+
// Pass is correctness-only: hidden tests green and only allowed files touched.
|
|
255
|
+
// verifiedAfterEdit (self-verification habit) is reported but not required — a
|
|
256
|
+
// concise model that stops after writing correct code must not fail.
|
|
257
|
+
const passed = hiddenResult.passed && unrelatedFiles.length === 0;
|
|
258
|
+
const efficiency: CodingTaskResult["efficiency"] =
|
|
259
|
+
!passed ? "FAIL"
|
|
260
|
+
: turns <= 2 ? "STRONG"
|
|
261
|
+
: turns <= 4 ? "MODERATE"
|
|
262
|
+
: "WEAK";
|
|
263
|
+
return { id: task.id, passed, publicPassed: publicResult.passed, hiddenPassed: hiddenResult.passed, verifiedAfterEdit, unrelatedFiles, toolCalls, turns, wallTimeMs: Date.now() - started, outputTokens: metrics.outputTokens, error: error ?? (!hiddenResult.passed ? hiddenResult.output || "hidden verification failed" : null), metrics, efficiency };
|
|
264
|
+
} finally {
|
|
265
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function listChangedFiles(root: string, initialFiles: Record<string, string>): string[] {
|
|
270
|
+
const changed: string[] = [];
|
|
271
|
+
const current: string[] = [];
|
|
272
|
+
const visit = (dir: string) => { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const target = path.join(dir, entry.name); if (entry.isDirectory()) visit(target); else current.push(path.relative(root, target)); } };
|
|
273
|
+
visit(root);
|
|
274
|
+
for (const relativePath of current) {
|
|
275
|
+
const target = resolveCodingPath(root, relativePath);
|
|
276
|
+
if (!(relativePath in initialFiles) || fs.readFileSync(target, "utf8") !== initialFiles[relativePath]) changed.push(relativePath);
|
|
277
|
+
}
|
|
278
|
+
for (const relativePath of Object.keys(initialFiles)) if (!fs.existsSync(resolveCodingPath(root, relativePath))) changed.push(relativePath);
|
|
279
|
+
return changed;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export { CODING_TOOLS };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { ExtensionAPI, AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { debugLog } from "./util/debug";
|
|
3
|
+
import { detectProvider } from "./util/providers";
|
|
4
|
+
import { TOOL_SUPPORT_CACHE_PATH } from "./util/config";
|
|
5
|
+
import type { SimplebenchOptions } from "./types";
|
|
6
|
+
import { createBenchmark } from "./benchmark";
|
|
7
|
+
|
|
8
|
+
export function parseCommandArgs(args: string): SimplebenchOptions {
|
|
9
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
10
|
+
const tagToken = tokens.find(token => token.startsWith("--tag="));
|
|
11
|
+
let tag: string | undefined;
|
|
12
|
+
if (tagToken) {
|
|
13
|
+
tag = tagToken.slice("--tag=".length);
|
|
14
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(tag)) throw new Error(`--tag must be a single word (letters, digits, dot, dash, underscore): got "${tag}"`);
|
|
15
|
+
}
|
|
16
|
+
return { model: tokens.find(token => !token.startsWith("--")), allModels: tokens.includes("--all"), writeArtifact: !tokens.includes("--no-artifact"), thinkingMax: tokens.includes("--thinking-max"), codingLite: tokens.includes("--coding-lite"), testAll: tokens.includes("--test-all"), researchLive: tokens.includes("--research-live"), llamaServer: tokens.includes("--llama-server"), llamagputop: tokens.includes("--llamagputop"), ...(tag ? { tag } : {}) };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function (pi: ExtensionAPI) {
|
|
20
|
+
const { getOllamaModels, testModel } = createBenchmark();
|
|
21
|
+
// ── Register /simplebench command ─────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
pi.registerCommand("simplebench", {
|
|
24
|
+
description: "Benchmark a model with auditable closed-answer, instruction, and tool-use tests.",
|
|
25
|
+
detailedHelp: "\n\n🔍 Simplebench Extension\n\nThis extension tests AI models across multiple dimensions:\n• Closed-answer contract: 20 deterministic final-line answers (logic, math, spatial, commonsense)\n• Tool Usage: Ability to use available tools effectively\n• Instruction Following: How well the model follows complex JSON instructions\n• Coding Lite: Six isolated, execution-backed coding tasks\n\n📋 Usage Examples:\n /simplebench # Test current model\n /simplebench <model> # Test a specific model\n /simplebench --all # Test all Ollama models\n /simplebench <model> --coding-lite # Run only coding tasks\n /simplebench <model> --test-all # Run baseline, coding, and grounded research\n /simplebench <model> --research-live # Add live search integration smoke test\n /simplebench --all --test-all # Run complete suite for every Ollama model\n /simplebench <model> --thinking-max # Request max reasoning\n /simplebench <model> --llama-server # Capture configured /props and /metrics\n /simplebench <model> --llamagputop # Capture configured llama.cpp stats\n /simplebench <model> --tag=coldrun # Label the run: benchmark.tag plus artifact filename prefix\n /simplebench --help # Show this help\n /simplebench --clear-cache # Clear tool support cache\n\nCoding tasks run in disposable directories and never access the user repository.\n",
|
|
26
|
+
getArgumentCompletions: async (prefix) => {
|
|
27
|
+
try {
|
|
28
|
+
const models = await getOllamaModels();
|
|
29
|
+
return models.map(m => ({ label: m, description: `Test ${m}` }))
|
|
30
|
+
.filter(m => m.label.startsWith(prefix));
|
|
31
|
+
} catch (err) { debugLog("simplebench", "failed to get model completions", err); return []; }
|
|
32
|
+
},
|
|
33
|
+
handler: async (args, ctx) => {
|
|
34
|
+
if (!ctx.hasUI) {
|
|
35
|
+
ctx.ui.notify("simplebench requires TUI mode", "error");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let parsedArgs: SimplebenchOptions;
|
|
40
|
+
try {
|
|
41
|
+
parsedArgs = parseCommandArgs(args);
|
|
42
|
+
} catch (e: any) {
|
|
43
|
+
ctx.ui.notify(e?.message || String(e), "error");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const arg = args.trim();
|
|
47
|
+
|
|
48
|
+
if (arg === "--help") {
|
|
49
|
+
ctx.ui.notify(
|
|
50
|
+
"🔍 Simplebench Extension\n\n" +
|
|
51
|
+
"📋 Usage:\n" +
|
|
52
|
+
" /simplebench [model] [--no-artifact] [--thinking-max] [--llama-server] - Test current or specific model\n" +
|
|
53
|
+
" /simplebench [model] --coding-lite - Run coding tasks only\n" +
|
|
54
|
+
" /simplebench [model] --test-all - Run baseline, coding, and deterministic grounded research\n" +
|
|
55
|
+
" /simplebench [model] --research-live - Add live-search integration smoke test\n" +
|
|
56
|
+
" /simplebench [model] --tag=<word> - Label the run (single word; added to benchmark.tag and the artifact name)\n" +
|
|
57
|
+
" /simplebench --all --test-all - Run complete suite for all Ollama models\n" +
|
|
58
|
+
" /simplebench --clear-cache - Clear tool support cache\n",
|
|
59
|
+
"info"
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (arg === "--clear-cache") {
|
|
65
|
+
try {
|
|
66
|
+
const fs = require("node:fs");
|
|
67
|
+
if (fs.existsSync(TOOL_SUPPORT_CACHE_PATH)) {
|
|
68
|
+
fs.unlinkSync(TOOL_SUPPORT_CACHE_PATH);
|
|
69
|
+
ctx.ui.notify("Tool support cache cleared successfully", "info");
|
|
70
|
+
} else {
|
|
71
|
+
ctx.ui.notify("No cache file found to clear", "info");
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
ctx.ui.notify("Could not clear cache", "error");
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (parsedArgs.allModels) {
|
|
80
|
+
const providerInfo = detectProvider(ctx);
|
|
81
|
+
if (providerInfo.kind !== "ollama") {
|
|
82
|
+
ctx.ui.notify(`--all is only supported for Ollama models. Current provider: ${providerInfo.name} (${providerInfo.kind})`, "error");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
ctx.ui.notify("Testing all models — this will take a while...", "info");
|
|
87
|
+
let models: string[];
|
|
88
|
+
try {
|
|
89
|
+
models = await getOllamaModels();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
debugLog("simplebench", "failed to list Ollama models for --all", err);
|
|
92
|
+
ctx.ui.notify("Could not list Ollama models", "error");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (models.length === 0) {
|
|
97
|
+
ctx.ui.notify("No models found in Ollama", "error");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const model of models) {
|
|
102
|
+
ctx.ui.notify(`Testing ${model}...`, "info");
|
|
103
|
+
try {
|
|
104
|
+
const report = await testModel(model, ctx, parsedArgs);
|
|
105
|
+
pi.sendMessage({
|
|
106
|
+
customType: "simplebench-report",
|
|
107
|
+
content: report,
|
|
108
|
+
display: { type: "content", content: report },
|
|
109
|
+
details: { model, timestamp: new Date().toISOString() },
|
|
110
|
+
});
|
|
111
|
+
} catch (e: any) {
|
|
112
|
+
ctx.ui.notify(`Failed to test ${model}: ${e.message}`, "error");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
ctx.ui.notify(`Done testing ${models.length} models`, "info");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const model = parsedArgs.model || ctx.model?.id;
|
|
120
|
+
if (!model) {
|
|
121
|
+
ctx.ui.notify("No model specified and no model currently selected", "error");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
ctx.ui.notify(`Testing ${model}...`, "info");
|
|
126
|
+
try {
|
|
127
|
+
const report = await testModel(model, ctx, parsedArgs);
|
|
128
|
+
pi.sendMessage({
|
|
129
|
+
customType: "simplebench-report",
|
|
130
|
+
content: report,
|
|
131
|
+
display: { type: "content", content: report },
|
|
132
|
+
details: { model, timestamp: new Date().toISOString() },
|
|
133
|
+
});
|
|
134
|
+
} catch (e: any) {
|
|
135
|
+
let errorMessage = "Model test failed";
|
|
136
|
+
if (e.message) {
|
|
137
|
+
errorMessage += `: ${e.message}`;
|
|
138
|
+
}
|
|
139
|
+
ctx.ui.notify(errorMessage, "error");
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ── Register simplebench tool (LLM-callable) ─────────────────────────
|
|
145
|
+
|
|
146
|
+
pi.registerTool({
|
|
147
|
+
name: "simplebench",
|
|
148
|
+
label: "Simplebench",
|
|
149
|
+
description: "Benchmark a model's reasoning, instruction following, and tool-call generation. Writes an audit JSON artifact by default.",
|
|
150
|
+
promptSnippet: "simplebench - benchmark a model with an optional JSON artifact",
|
|
151
|
+
promptGuidelines: [
|
|
152
|
+
"When the user asks to test or evaluate a model, call simplebench with the model name.",
|
|
153
|
+
],
|
|
154
|
+
parameters: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
model: { type: "string", description: "Model name to test. If omitted, tests the current model." },
|
|
158
|
+
no_artifact: { type: "boolean", description: "If true, do not write the detailed JSON audit artifact to the current working directory." },
|
|
159
|
+
thinking_max: { type: "boolean", description: "Request maximum reasoning on an OpenAI-compatible provider or a direct Bedrock model that advertises max thinking. Omit to use provider defaults." },
|
|
160
|
+
coding_lite: { type: "boolean", description: "Run only the six execution-backed coding tasks in disposable directories." },
|
|
161
|
+
test_all: { type: "boolean", description: "Run the existing baseline, coding-lite, and deterministic grounded research tests." },
|
|
162
|
+
research_live: { type: "boolean", description: "Run configured live-search research as an integration smoke test; it does not affect recommendation." },
|
|
163
|
+
llama_server: { type: "boolean", description: "Capture /props and /metrics from configured llamaServerUrl. Inference routing is unchanged." },
|
|
164
|
+
llamagputop: { type: "boolean", description: "Capture configured llamagputopUrl /stats. The declared endpoint is authoritative; no Pi model match is required." },
|
|
165
|
+
tag: { type: "string", description: "Optional single-word label (letters, digits, dot, dash, underscore). Stored under benchmark.tag and prefixed onto the artifact file or bundle name." },
|
|
166
|
+
},
|
|
167
|
+
} as any,
|
|
168
|
+
execute: async (_toolCallId, _params, _signal, _onUpdate, ctx) => {
|
|
169
|
+
const params = _params as any;
|
|
170
|
+
if (params?.tag !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(String(params.tag))) {
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: "text", text: "--tag must be a single word (letters, digits, dot, dash, underscore)" }],
|
|
173
|
+
isError: true,
|
|
174
|
+
} as AgentToolResult;
|
|
175
|
+
}
|
|
176
|
+
const options: SimplebenchOptions = { model: params?.model as string | undefined, allModels: false, writeArtifact: params?.no_artifact !== true, thinkingMax: params?.thinking_max === true, codingLite: params?.coding_lite === true, testAll: params?.test_all === true, researchLive: params?.research_live === true, llamaServer: params?.llama_server === true, llamagputop: params?.llamagputop === true, ...(params?.tag ? { tag: String(params.tag) } : {}) };
|
|
177
|
+
const model = options.model || ctx.model?.id;
|
|
178
|
+
if (!model) {
|
|
179
|
+
return {
|
|
180
|
+
content: [{ type: "text", text: "No model currently selected to test." }],
|
|
181
|
+
isError: true,
|
|
182
|
+
} as AgentToolResult;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const report = await testModel(model, ctx, options);
|
|
186
|
+
return {
|
|
187
|
+
content: [{ type: "text", text: report }],
|
|
188
|
+
isError: false,
|
|
189
|
+
} as AgentToolResult;
|
|
190
|
+
} catch (e: any) {
|
|
191
|
+
let errorMessage = "Model test failed";
|
|
192
|
+
if (e.message) {
|
|
193
|
+
errorMessage += `: ${e.message}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
content: [{ type: "text", text: errorMessage }],
|
|
198
|
+
isError: true,
|
|
199
|
+
} as AgentToolResult;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|