@openlines/opl-pi-sht 0.2.11 → 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 +92 -82
- package/configs/opl-browser.json.sample +3 -2
- package/configs/opl-footer.json.sample +3 -2
- package/configs/opl-guardian.json.sample +8 -2
- package/configs/opl-modes.json.sample +5 -0
- package/configs/opl-webaccess.json.sample +7 -1
- package/extensions/opl-browser/README.md +81 -55
- package/extensions/opl-browser/browser.ts +244 -50
- package/extensions/opl-browser/config.ts +15 -7
- package/extensions/opl-browser/index.ts +5 -4
- package/extensions/opl-browser/package.json +4 -4
- package/extensions/opl-browser/validate.ts +313 -60
- package/extensions/opl-configurator/README.md +40 -0
- package/extensions/opl-configurator/configurator.ts +148 -0
- package/extensions/opl-configurator/index.ts +12 -0
- package/extensions/opl-ctxtrim/README.md +45 -14
- package/extensions/opl-ctxtrim/index.ts +33 -13
- package/extensions/opl-footer/README.md +32 -7
- package/extensions/opl-footer/config.ts +11 -10
- package/extensions/opl-footer/configure.ts +157 -115
- package/extensions/opl-footer/git-status.ts +58 -1
- package/extensions/opl-footer/icons.ts +3 -0
- package/extensions/opl-footer/index.ts +287 -109
- package/extensions/opl-footer/segments/caveman.ts +9 -1
- package/extensions/opl-footer/segments/chat-mode.ts +8 -1
- package/extensions/opl-footer/segments/compactions.ts +13 -0
- package/extensions/opl-footer/segments/context.ts +6 -5
- package/extensions/opl-footer/segments/index.ts +2 -0
- package/extensions/opl-footer/segments/mode-switcher.ts +14 -1
- package/extensions/opl-footer/segments/path.ts +1 -1
- package/extensions/opl-footer/segments/plan-mode.ts +8 -1
- package/extensions/opl-footer/theme.ts +5 -2
- package/extensions/opl-footer/types.ts +4 -0
- package/extensions/opl-guardian/README.md +17 -5
- package/extensions/opl-guardian/config.ts +72 -6
- package/extensions/opl-guardian/guardian.ts +42 -10
- package/extensions/opl-guardian/index.ts +52 -17
- package/extensions/opl-guardian/policies.ts +52 -16
- package/extensions/opl-init/README.md +12 -4
- package/extensions/opl-init/index.ts +228 -27
- package/extensions/opl-input/README.md +17 -3
- package/extensions/opl-input/config.ts +59 -5
- package/extensions/opl-input/configure.ts +144 -0
- package/extensions/opl-input/index.ts +38 -30
- package/extensions/opl-input/mode-style.ts +6 -2
- package/extensions/opl-modes/README.md +16 -4
- package/extensions/opl-modes/config.ts +62 -8
- package/extensions/opl-modes/index.ts +48 -17
- package/extensions/opl-modes/state.ts +50 -2
- package/extensions/opl-modes/types.ts +6 -0
- package/extensions/opl-modes/utils.ts +18 -8
- package/extensions/opl-questionnaire/index.ts +10 -4
- package/extensions/opl-questionnaire/types.ts +4 -0
- package/extensions/opl-simplebench/README.md +21 -15
- package/extensions/opl-simplebench/artifact.ts +7 -4
- package/extensions/opl-simplebench/benchmark.ts +96 -35
- package/extensions/opl-simplebench/coding.ts +9 -4
- package/extensions/opl-simplebench/index.ts +19 -19
- package/extensions/opl-simplebench/llama-server.ts +3 -2
- package/extensions/opl-simplebench/report.ts +0 -6
- package/extensions/opl-simplebench/research.ts +5 -4
- package/extensions/opl-simplebench/scoring.ts +4 -2
- package/extensions/opl-simplebench/util/config.ts +15 -8
- package/extensions/opl-simplebench/util/exec-env.ts +36 -0
- package/extensions/opl-simplebench/util/format.ts +2 -2
- package/extensions/opl-simplebench/util/providers.ts +23 -194
- package/extensions/opl-todo/README.md +5 -5
- package/extensions/opl-todo/config.ts +7 -2
- package/extensions/opl-todo/index.ts +50 -9
- package/extensions/opl-webaccess/README.md +31 -5
- package/extensions/opl-webaccess/config.ts +36 -7
- package/extensions/opl-webaccess/extract.ts +37 -47
- package/extensions/opl-webaccess/http.ts +259 -0
- package/extensions/opl-webaccess/index.ts +12 -6
- package/extensions/opl-webaccess/package.json +4 -9
- package/extensions/opl-webaccess/pdf.ts +4 -4
- package/extensions/opl-webaccess/search.ts +19 -8
- package/extensions/opl-webaccess/storage.ts +134 -2
- package/extensions/opl-webaccess/types.ts +4 -0
- package/extensions/opl-webaccess/utils.ts +221 -33
- package/images/ss-init.png +0 -0
- package/images/ss-input-footer.png +0 -0
- package/images/ss-input-pet.png +0 -0
- package/images/ss-mode-custom.png +0 -0
- package/images/ss-questionnaire0.png +0 -0
- package/images/ss-questionnaire1.png +0 -0
- package/images/ss-simplebench.png +0 -0
- package/images/ss-todo.png +0 -0
- package/package.json +20 -12
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
3
5
|
import { section, ok, fail, warn, info, msHuman, truncate, sanitizeForReport } from "./util/format";
|
|
4
6
|
import { getOllamaBaseUrl, detectProvider } from "./util/providers";
|
|
5
7
|
import { debugLog } from "./util/debug";
|
|
@@ -60,6 +62,92 @@ export function openAiThinkingOptions(thinkingMax: boolean): Record<string, stri
|
|
|
60
62
|
return thinkingMax ? { reasoning_effort: "max" } : {};
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
const execFileAsync = promisify(execFile);
|
|
66
|
+
|
|
67
|
+
/** Credentials for the Bedrock Converse API. */
|
|
68
|
+
export interface AwsCredentials {
|
|
69
|
+
accessKeyId: string;
|
|
70
|
+
secretAccessKey: string;
|
|
71
|
+
sessionToken?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface AwsCliCommand { file: string; args: string[] }
|
|
75
|
+
export type AwsCliRunner = (command: AwsCliCommand) => Promise<{ stdout: string }>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A profile name reaches the AWS CLI as one argv element, never as part of a
|
|
79
|
+
* shell string. Anything that is not a plain profile name is refused before a
|
|
80
|
+
* process is spawned: metacharacters would only matter if the value were
|
|
81
|
+
* interpolated into a shell command, and a leading dash the CLI would read as
|
|
82
|
+
* another option.
|
|
83
|
+
*/
|
|
84
|
+
const AWS_PROFILE_RE = /^[A-Za-z0-9][A-Za-z0-9_.:@+-]{0,127}$/;
|
|
85
|
+
|
|
86
|
+
export function isValidAwsProfileName(profile: string): boolean {
|
|
87
|
+
return AWS_PROFILE_RE.test(profile);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function awsCredentialsCommand(profile: string): AwsCliCommand {
|
|
91
|
+
return { file: "aws", args: ["configure", "export-credentials", "--profile", profile] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const defaultAwsCliRunner: AwsCliRunner = async (command) => {
|
|
95
|
+
const { stdout } = await execFileAsync(command.file, command.args, { timeout: 15_000, encoding: "utf8" });
|
|
96
|
+
return { stdout };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function parseAwsCredentials(stdout: string): AwsCredentials | null {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(stdout);
|
|
102
|
+
if (parsed?.AccessKeyId && parsed?.SecretAccessKey) {
|
|
103
|
+
return { accessKeyId: parsed.AccessKeyId, secretAccessKey: parsed.SecretAccessKey, sessionToken: parsed.SessionToken };
|
|
104
|
+
}
|
|
105
|
+
} catch { /* not JSON: missing CLI, unresolvable profile, or a stray warning line */ }
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let _awsCredsCache: AwsCredentials | null | undefined;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolve AWS credentials: environment first, then the AWS CLI
|
|
113
|
+
* (`aws configure export-credentials`), which handles static keys, assume-role
|
|
114
|
+
* chains (source_profile + role_arn), and SSO. The CLI runs from argv and off
|
|
115
|
+
* the event loop, so a slow credential chain cannot stall the TUI. Injecting
|
|
116
|
+
* `env` or `runner` bypasses the per-process cache, which keeps tests from
|
|
117
|
+
* poisoning the real path.
|
|
118
|
+
* # ponytail: requires AWS CLI v2; cached per process
|
|
119
|
+
*/
|
|
120
|
+
export async function resolveAwsCredentials(
|
|
121
|
+
source: { env?: Record<string, string | undefined>; runner?: AwsCliRunner } = {},
|
|
122
|
+
): Promise<AwsCredentials | null> {
|
|
123
|
+
const env = source.env ?? process.env;
|
|
124
|
+
const runner = source.runner ?? defaultAwsCliRunner;
|
|
125
|
+
const cached = source.env === undefined && source.runner === undefined;
|
|
126
|
+
if (cached && _awsCredsCache !== undefined) return _awsCredsCache;
|
|
127
|
+
|
|
128
|
+
const lookup = async (): Promise<AwsCredentials | null> => {
|
|
129
|
+
if (env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY) {
|
|
130
|
+
return { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY, sessionToken: env.AWS_SESSION_TOKEN };
|
|
131
|
+
}
|
|
132
|
+
const profile = env.AWS_PROFILE || "default";
|
|
133
|
+
if (!isValidAwsProfileName(profile)) return null;
|
|
134
|
+
try {
|
|
135
|
+
return parseAwsCredentials((await runner(awsCredentialsCommand(profile))).stdout);
|
|
136
|
+
} catch {
|
|
137
|
+
return null; /* aws CLI missing or profile unresolvable */
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const creds = await lookup();
|
|
142
|
+
if (cached) _awsCredsCache = creds;
|
|
143
|
+
return creds;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Drop the per-process credential cache (tests, credential rotation). */
|
|
147
|
+
export function resetAwsCredentialsCacheForTest(): void {
|
|
148
|
+
_awsCredsCache = undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
63
151
|
export function createBenchmark() {
|
|
64
152
|
// Use effective config (user overrides merged with defaults)
|
|
65
153
|
const effectiveConfig = getEffectiveConfig();
|
|
@@ -174,34 +262,6 @@ function makeOpenAiChatFn(baseUrl: string, apiKey?: string, thinkingMax = false)
|
|
|
174
262
|
};
|
|
175
263
|
}
|
|
176
264
|
|
|
177
|
-
/**
|
|
178
|
-
* Resolve AWS credentials: env vars first, then the AWS CLI
|
|
179
|
-
* (`aws configure export-credentials`), which handles static keys,
|
|
180
|
-
* assume-role chains (source_profile + role_arn), and SSO.
|
|
181
|
-
* # ponytail: requires AWS CLI v2; cached per process
|
|
182
|
-
*/
|
|
183
|
-
let _awsCredsCache: { accessKeyId: string; secretAccessKey: string; sessionToken?: string } | null | undefined;
|
|
184
|
-
function resolveAwsCredentials(): { accessKeyId: string; secretAccessKey: string; sessionToken?: string } | null {
|
|
185
|
-
if (_awsCredsCache !== undefined) return _awsCredsCache;
|
|
186
|
-
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
|
187
|
-
_awsCredsCache = { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, sessionToken: process.env.AWS_SESSION_TOKEN };
|
|
188
|
-
return _awsCredsCache;
|
|
189
|
-
}
|
|
190
|
-
try {
|
|
191
|
-
const prof = process.env.AWS_PROFILE || "default";
|
|
192
|
-
const out = require("node:child_process")
|
|
193
|
-
.execSync(`aws configure export-credentials --profile ${prof}`, { timeout: 15000 })
|
|
194
|
-
.toString();
|
|
195
|
-
const c = JSON.parse(out);
|
|
196
|
-
if (c.AccessKeyId && c.SecretAccessKey) {
|
|
197
|
-
_awsCredsCache = { accessKeyId: c.AccessKeyId, secretAccessKey: c.SecretAccessKey, sessionToken: c.SessionToken };
|
|
198
|
-
return _awsCredsCache;
|
|
199
|
-
}
|
|
200
|
-
} catch { /* aws CLI missing or profile unresolvable */ }
|
|
201
|
-
_awsCredsCache = null;
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
265
|
/** Minimal SigV4 signer for a POST to the Bedrock Converse API. */
|
|
206
266
|
function sigv4Headers(
|
|
207
267
|
creds: { accessKeyId: string; secretAccessKey: string; sessionToken?: string },
|
|
@@ -241,7 +301,7 @@ function sigv4Headers(
|
|
|
241
301
|
*/
|
|
242
302
|
function makeBedrockChatFn(providerInfo: { region?: string }): ChatFn {
|
|
243
303
|
return async (model, messages, options) => {
|
|
244
|
-
const creds = resolveAwsCredentials();
|
|
304
|
+
const creds = await resolveAwsCredentials();
|
|
245
305
|
if (!creds) throw new Error("AWS credentials not found — set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or ~/.aws/credentials");
|
|
246
306
|
const region = providerInfo.region || process.env.AWS_REGION || "us-east-1";
|
|
247
307
|
const host = `bedrock-runtime.${region}.amazonaws.com`;
|
|
@@ -570,8 +630,7 @@ type ProgressFn = (msg: string) => void;
|
|
|
570
630
|
async function testReasoningExtended(chatFn: ChatFn, model: string, onProgress?: ProgressFn): Promise<{ score: string; scores: string[]; answers: string[]; results: ReasoningTestResult[] }> {
|
|
571
631
|
const results: ReasoningTestResult[] = [];
|
|
572
632
|
const total = REASONING_TESTS.length;
|
|
573
|
-
for (
|
|
574
|
-
const test = REASONING_TESTS[i];
|
|
633
|
+
for (const [i, test] of REASONING_TESTS.entries()) {
|
|
575
634
|
onProgress?.(`[1/3] Reasoning ${i + 1}/${total}: ${test.name} (${test.category})...`);
|
|
576
635
|
try {
|
|
577
636
|
const requestedAt = new Date().toISOString();
|
|
@@ -646,8 +705,7 @@ async function testToolUsageExtended(chatFn: ChatFn, model: string, useToolResul
|
|
|
646
705
|
|
|
647
706
|
async function testCodingLite(chatFn: ChatFn, model: string, onProgress?: (message: string) => void): Promise<{ results: Awaited<ReturnType<typeof runCodingTask>>[]; passed: number; total: number }> {
|
|
648
707
|
const results = [] as Awaited<ReturnType<typeof runCodingTask>>[];
|
|
649
|
-
for (
|
|
650
|
-
const task = CODING_LITE_TASKS[index];
|
|
708
|
+
for (const [index, task] of CODING_LITE_TASKS.entries()) {
|
|
651
709
|
const prefix = `[${index + 1}/${CODING_LITE_TASKS.length}] coding-lite: [${task.id}]`;
|
|
652
710
|
onProgress?.(`${prefix} starting...`);
|
|
653
711
|
const result = await runCodingTask(chatFn, model, task, {
|
|
@@ -696,6 +754,9 @@ function getCurrentModel(ctx: any): string | undefined {
|
|
|
696
754
|
|
|
697
755
|
async function testModelExtended(model: string, ctx?: any, options: SimplebenchOptions = { allModels: false, writeArtifact: true }): Promise<string> {
|
|
698
756
|
const lines: string[] = [];
|
|
757
|
+
// The artifact lands in the session directory when Pi invoked the tool. A bare
|
|
758
|
+
// programmatic run has no session, so the harness directory is all there is.
|
|
759
|
+
const artifactDir = ctx?.cwd ?? process.cwd();
|
|
699
760
|
const totalStart = Date.now();
|
|
700
761
|
const providerInfo = ctx ? detectProvider(ctx) : { kind: "ollama" as const, name: "ollama" };
|
|
701
762
|
const resolvedModel = resolveBenchmarkModel(ctx, model);
|
|
@@ -784,7 +845,7 @@ async function testModelExtended(model: string, ctx?: any, options: SimplebenchO
|
|
|
784
845
|
let artifactPath: string | null = null;
|
|
785
846
|
const serverStats = await captureServerStats();
|
|
786
847
|
if (options.writeArtifact) {
|
|
787
|
-
try { artifactPath = writeArtifact({ schemaVersion: 1, benchmark: { name: "opl-simplebench", suite, ...(options.tag ? { tag: options.tag } : {}), model, provider: providerInfo.name, providerKind: providerInfo.kind, thinking: { ...thinking, modelMetadataSource: resolvedModel.source }, startedAt: new Date(totalStart).toISOString(), finishedAt: new Date().toISOString(), wallTimeMs: totalMs, artifactEnabled: true }, tests: codingTestRecords(codingSummary), summary: { coding: { passed: codingSummary.passed, total: codingSummary.total, efficiency: { strong: codingSummary.results.filter(r => r.efficiency === "STRONG").length, moderate: codingSummary.results.filter(r => r.efficiency === "MODERATE").length, weak: codingSummary.results.filter(r => r.efficiency === "WEAK").length, fail: codingSummary.results.filter(r => r.efficiency === "FAIL").length } }, ...(serverStats ? { serverStats } : {}) } }); }
|
|
848
|
+
try { artifactPath = writeArtifact({ schemaVersion: 1, benchmark: { name: "opl-simplebench", suite, ...(options.tag ? { tag: options.tag } : {}), model, provider: providerInfo.name, providerKind: providerInfo.kind, thinking: { ...thinking, modelMetadataSource: resolvedModel.source }, startedAt: new Date(totalStart).toISOString(), finishedAt: new Date().toISOString(), wallTimeMs: totalMs, artifactEnabled: true }, tests: codingTestRecords(codingSummary), summary: { coding: { passed: codingSummary.passed, total: codingSummary.total, efficiency: { strong: codingSummary.results.filter(r => r.efficiency === "STRONG").length, moderate: codingSummary.results.filter(r => r.efficiency === "MODERATE").length, weak: codingSummary.results.filter(r => r.efficiency === "WEAK").length, fail: codingSummary.results.filter(r => r.efficiency === "FAIL").length } }, ...(serverStats ? { serverStats } : {}) } }, artifactDir); }
|
|
788
849
|
catch (e: any) { lines.push(warn(`Artifact could not be written: ${e?.message || e}`)); }
|
|
789
850
|
}
|
|
790
851
|
const effCounts = { strong: codingSummary.results.filter(r => r.efficiency === "STRONG").length, moderate: codingSummary.results.filter(r => r.efficiency === "MODERATE").length, weak: codingSummary.results.filter(r => r.efficiency === "WEAK").length };
|
|
@@ -855,7 +916,7 @@ async function testModelExtended(model: string, ctx?: any, options: SimplebenchO
|
|
|
855
916
|
if (options.writeArtifact) {
|
|
856
917
|
try {
|
|
857
918
|
const artifact = { schemaVersion: 1 as const, benchmark: { name: "opl-simplebench" as const, suite, ...(options.tag ? { tag: options.tag } : {}), model, provider: providerInfo.name, providerKind: providerInfo.kind, thinking: { ...thinking, modelMetadataSource: resolvedModel.source }, startedAt: new Date(totalStart).toISOString(), finishedAt: new Date().toISOString(), wallTimeMs: totalMs, artifactEnabled: true }, tests: artifactTests, summary: { reasoning: { score: reasoning.score, passed: reasoning.results.filter(r => r.pass).length, total: reasoning.results.length }, instructions: instructions.score, tools: tools.score, ...(codingSummary ? { coding: { passed: codingSummary.passed, total: codingSummary.total, efficiency: { strong: codingSummary.results.filter(r => r.efficiency === "STRONG").length, moderate: codingSummary.results.filter(r => r.efficiency === "MODERATE").length, weak: codingSummary.results.filter(r => r.efficiency === "WEAK").length, fail: codingSummary.results.filter(r => r.efficiency === "FAIL").length } } } : {}), ...(researchGrounded ? { researchGrounded: researchGrounded.score } : {}), ...(researchArtifact ? { researchLive: researchArtifact.score } : {}), metrics: aggregate, ...(serverStats ? { serverStats } : {}) } };
|
|
858
|
-
artifactPath = researchGrounded ? writeArtifactBundle(artifact, researchGrounded.files) : writeArtifact(artifact);
|
|
919
|
+
artifactPath = researchGrounded ? writeArtifactBundle(artifact, researchGrounded.files, artifactDir) : writeArtifact(artifact, artifactDir);
|
|
859
920
|
} catch (e: any) { lines.push(warn(`Artifact could not be written: ${e?.message || e}`)); }
|
|
860
921
|
}
|
|
861
922
|
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { scrubbedEnv } from "./util/exec-env";
|
|
5
6
|
import { buildToolResultMessages, type ChatFn, type ChatMessage } from "./util/config";
|
|
6
7
|
import { mergeRequestMetrics, metricsFromChat } from "./metrics";
|
|
7
8
|
import type { RequestMetrics } from "./types";
|
|
@@ -29,7 +30,7 @@ export interface CodingTaskResult {
|
|
|
29
30
|
outputTokens: number | null;
|
|
30
31
|
error: string | null;
|
|
31
32
|
metrics: RequestMetrics;
|
|
32
|
-
/** STRONG =
|
|
33
|
+
/** Efficiency grade: STRONG = 1-2 turns, MODERATE = 3-4, WEAK = the 5-turn budget, FAIL = not solved. */
|
|
33
34
|
efficiency: "STRONG" | "MODERATE" | "WEAK" | "FAIL";
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -105,7 +106,9 @@ export function createCodingTaskDir(task: CodingTaskFixture): string {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
export function runCodingVerifier(task: CodingTaskFixture, root: string, mode: "public" | "hidden") {
|
|
108
|
-
|
|
109
|
+
// The verifier evaluates model-authored code, so it runs with an allowlisted
|
|
110
|
+
// environment (see util/exec-env.ts). Anything it spawns inherits that.
|
|
111
|
+
const result = spawnSync(process.execPath, ["--eval", task.verify(root, mode === "hidden")], { cwd: root, encoding: "utf8", timeout: 10_000, env: scrubbedEnv() });
|
|
109
112
|
const rawOutput = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
110
113
|
return { passed: result.status === 0, output: result.status === 0 ? rawOutput : truncateVerifierError(rawOutput) };
|
|
111
114
|
}
|
|
@@ -175,8 +178,10 @@ async function runSingleShotCodingTask(chatFn: ChatFn, model: string, task: Codi
|
|
|
175
178
|
]);
|
|
176
179
|
const raw = response.content.trim();
|
|
177
180
|
const fenced = raw.match(/```(?:[a-z]*)\n([\s\S]*?)```/);
|
|
178
|
-
const code = fenced ? fenced[1].trim() : raw;
|
|
179
|
-
const
|
|
181
|
+
const code = fenced ? (fenced[1] ?? "").trim() : raw;
|
|
182
|
+
const firstAllowed = task.allowedFiles[0];
|
|
183
|
+
if (!firstAllowed) throw new Error(`coding task ${task.id} declares no allowed file`);
|
|
184
|
+
const target = resolveCodingPath(root, firstAllowed);
|
|
180
185
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
181
186
|
fs.writeFileSync(target, code + "\n");
|
|
182
187
|
const publicResult = runCodingVerifier(task, root, "public");
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI, AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { Type } from "typebox";
|
|
2
4
|
import { debugLog } from "./util/debug";
|
|
3
5
|
import { detectProvider } from "./util/providers";
|
|
4
|
-
import { readTestConfig,
|
|
6
|
+
import { readTestConfig, toolSupportCachePath, type ModelTestUserConfig, type RunSequenceProfile } from "./util/config";
|
|
5
7
|
import type { SimplebenchOptions } from "./types";
|
|
6
8
|
import { createBenchmark } from "./benchmark";
|
|
7
9
|
|
|
@@ -107,9 +109,8 @@ pi.registerCommand("simplebench", {
|
|
|
107
109
|
|
|
108
110
|
if (arg === "--clear-cache") {
|
|
109
111
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
fs.unlinkSync(TOOL_SUPPORT_CACHE_PATH);
|
|
112
|
+
if (existsSync(toolSupportCachePath())) {
|
|
113
|
+
unlinkSync(toolSupportCachePath());
|
|
113
114
|
ctx.ui.notify("Tool support cache cleared successfully", "info");
|
|
114
115
|
} else {
|
|
115
116
|
ctx.ui.notify("No cache file found to clear", "info");
|
|
@@ -141,7 +142,9 @@ pi.registerCommand("simplebench", {
|
|
|
141
142
|
ctx.ui.notify(`Cooling down ${resolved.pauseMs / 1000}s before iteration ${i + 1}/${resolved.runs.length}...`, "info");
|
|
142
143
|
await new Promise(resolve => setTimeout(resolve, resolved.pauseMs));
|
|
143
144
|
}
|
|
144
|
-
const
|
|
145
|
+
const run = resolved.runs[i];
|
|
146
|
+
if (!run) continue;
|
|
147
|
+
const { entry, options } = run;
|
|
145
148
|
const model = options.model || sequenceModel;
|
|
146
149
|
ctx.ui.notify(`Iteration ${i + 1}/${resolved.runs.length} (${entry}) on ${model}...`, "info");
|
|
147
150
|
try {
|
|
@@ -236,20 +239,17 @@ pi.registerTool({
|
|
|
236
239
|
promptGuidelines: [
|
|
237
240
|
"When the user asks to test or evaluate a model, call simplebench with the model name.",
|
|
238
241
|
],
|
|
239
|
-
parameters: {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
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." },
|
|
251
|
-
},
|
|
252
|
-
} as any,
|
|
242
|
+
parameters: Type.Object({
|
|
243
|
+
model: Type.Optional(Type.String({ description: "Model name to test. If omitted, tests the current model." })),
|
|
244
|
+
no_artifact: Type.Optional(Type.Boolean({ description: "If true, do not write the detailed JSON audit artifact to the session directory." })),
|
|
245
|
+
thinking_max: Type.Optional(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." })),
|
|
246
|
+
coding_lite: Type.Optional(Type.Boolean({ description: "Run only the six execution-backed coding tasks in disposable directories." })),
|
|
247
|
+
test_all: Type.Optional(Type.Boolean({ description: "Run the existing baseline, coding-lite, and deterministic grounded research tests." })),
|
|
248
|
+
research_live: Type.Optional(Type.Boolean({ description: "Run configured live-search research as an integration smoke test; it does not affect recommendation." })),
|
|
249
|
+
llama_server: Type.Optional(Type.Boolean({ description: "Capture /props and /metrics from configured llamaServerUrl. Inference routing is unchanged." })),
|
|
250
|
+
llamagputop: Type.Optional(Type.Boolean({ description: "Capture configured llamagputopUrl /stats. The declared endpoint is authoritative; no Pi model match is required." })),
|
|
251
|
+
tag: Type.Optional(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." })),
|
|
252
|
+
}),
|
|
253
253
|
execute: async (_toolCallId, _params, _signal, _onUpdate, ctx) => {
|
|
254
254
|
const params = _params as any;
|
|
255
255
|
if (params?.tag !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(String(params.tag))) {
|
|
@@ -115,14 +115,15 @@ export function parsePrometheusMetrics(text: string): Record<string, number> {
|
|
|
115
115
|
if (!match) continue;
|
|
116
116
|
const value = Number(match[2]);
|
|
117
117
|
// llama.cpp namespaces every metric as `llamacpp:<name>`; key on the bare name.
|
|
118
|
-
|
|
118
|
+
const name = match[1];
|
|
119
|
+
if (name && Number.isFinite(value)) out[name.replace(/^llamacpp:/, "")] = value;
|
|
119
120
|
}
|
|
120
121
|
return out;
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
export function diffPrometheusMetrics(before: Record<string, number>, after: Record<string, number>): Record<string, number> {
|
|
124
125
|
const out: Record<string, number> = {};
|
|
125
|
-
for (const key of Object.keys(after)) out[key] = after[key] - (before[key] ?? 0);
|
|
126
|
+
for (const key of Object.keys(after)) out[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
126
127
|
return out;
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -10,12 +10,6 @@ export function formatInstructionScore(instructions: { pass: boolean; score: str
|
|
|
10
10
|
export function formatTestSummary(tests: Array<{ name: string; pass: boolean; score: string }>, totalMs: number): string[] {
|
|
11
11
|
return [section("SUMMARY"), ...tests.map(t => t.pass ? ok(`${t.name}: ${t.score}`) : fail(`${t.name}: ${t.score}`)), info(`Total time: ${msHuman(totalMs)}`), info(`Score: ${tests.filter(t => t.pass).length}/${tests.length} tests passed`)];
|
|
12
12
|
}
|
|
13
|
-
/** Legacy-compatible category recommendation. New runner uses recommendation() above. */
|
|
14
|
-
export function formatRecommendation(model: string, passed: number, total: number): string[] {
|
|
15
|
-
const label = passed === total ? "STRONG" : passed >= total - 1 ? "GOOD" : passed >= total - 2 ? "USABLE" : "WEAK";
|
|
16
|
-
return [section("RECOMMENDATION"), label === "WEAK" ? fail(`${model} is ${label}`) : ok(`${model} is ${label}`)];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
13
|
export function recommendation(reasoning: string, instructionsPass: boolean, toolsPass: boolean, coding?: { passed: number; total: number; efficiency?: { strong: number; moderate: number; weak: number; fail: number } }) {
|
|
20
14
|
const reasoningPass = reasoning === "STRONG" || reasoning === "MODERATE";
|
|
21
15
|
// Efficiency-weighted coding pass: STRONG=1.0, MODERATE=0.7, WEAK=0.4, FAIL=0
|
|
@@ -95,15 +95,16 @@ export function verifyGroundedResearch(markdown: string, fixture: GroundedResear
|
|
|
95
95
|
const findings = markdown.match(/##\s+Findings\s*\n([\s\S]*?)(?=\n##\s|$)/i)?.[1] ?? "";
|
|
96
96
|
const sourceSection = markdown.match(/##\s+Sources\s*\n([\s\S]*?)(?=\n##\s|$)/i)?.[1] ?? "";
|
|
97
97
|
const citedClaims = [...findings.matchAll(/^\s*[-*]\s+(.+?)\s+\[([A-Za-z0-9]+)\]\s*$/gm)];
|
|
98
|
-
const
|
|
98
|
+
const citationIds = citedClaims.map((match) => match[2] ?? "");
|
|
99
|
+
const unknownCitations = citationIds.filter((id) => !knownSources.has(id));
|
|
99
100
|
const missingClaims: string[] = [];
|
|
100
101
|
const wrongCitations: string[] = [];
|
|
101
102
|
for (const claim of fixture.claims) {
|
|
102
|
-
const match = citedClaims.find(item => normalizeClaim(item[1]) === normalizeClaim(claim.text));
|
|
103
|
+
const match = citedClaims.find((item) => normalizeClaim(item[1] ?? "") === normalizeClaim(claim.text));
|
|
103
104
|
if (!match) missingClaims.push(claim.text);
|
|
104
|
-
else if (match[2] !== claim.sourceId) wrongCitations.push(claim.text);
|
|
105
|
+
else if ((match[2] ?? "") !== claim.sourceId) wrongCitations.push(claim.text);
|
|
105
106
|
}
|
|
106
|
-
for (const id of new Set(
|
|
107
|
+
for (const id of new Set(citationIds.filter((id) => knownSources.has(id)))) {
|
|
107
108
|
const source = knownSources.get(id)!;
|
|
108
109
|
if (!new RegExp(`\\[${id}\\]\\(${source.url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\)`).test(sourceSection)) wrongCitations.push(id);
|
|
109
110
|
}
|
|
@@ -6,7 +6,7 @@ const NUMERIC_ANSWER = /^\d+$/;
|
|
|
6
6
|
|
|
7
7
|
function lastNumericToken(line: string): string | null {
|
|
8
8
|
const tokens = line.match(/\d+/g);
|
|
9
|
-
return tokens
|
|
9
|
+
return tokens?.[tokens.length - 1] ?? null;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function extractAnswer(response: string, expected?: string): { answer: string; method: string } {
|
|
@@ -16,7 +16,9 @@ export function extractAnswer(response: string, expected?: string): { answer: st
|
|
|
16
16
|
// bare-number remnants that thinking templates leak after the real final line.
|
|
17
17
|
if (expected && NUMERIC_ANSWER.test(expected)) {
|
|
18
18
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
19
|
-
const
|
|
19
|
+
const line = lines[i];
|
|
20
|
+
if (line === undefined) continue;
|
|
21
|
+
const token = lastNumericToken(line);
|
|
20
22
|
if (token === expected) {
|
|
21
23
|
return { answer: token, method: i === lines.length - 1 ? "final-line" : "expected-last-numeric" };
|
|
22
24
|
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
|
-
import * as os from "node:os";
|
|
12
11
|
import * as path from "node:path";
|
|
12
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
|
|
14
14
|
// ============================================================================
|
|
15
15
|
// Configuration Constants
|
|
@@ -59,9 +59,15 @@ export const CONFIG = {
|
|
|
59
59
|
// User Configuration Overrides
|
|
60
60
|
// ============================================================================
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
export
|
|
64
|
-
|
|
62
|
+
/** Resolved per call so a custom `PI_AGENT_DIR` (pi's own agent dir) is honoured. */
|
|
63
|
+
export function configPath(): string {
|
|
64
|
+
return path.join(getAgentDir(), "configs", "opl-simplebench.json");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Pre-rename location, still read when the current path is absent. */
|
|
68
|
+
function legacyConfigPath(): string {
|
|
69
|
+
return path.join(getAgentDir(), "simplebench-config.json");
|
|
70
|
+
}
|
|
65
71
|
|
|
66
72
|
/** Shape of the user configuration file. */
|
|
67
73
|
export interface ModelTestUserConfig {
|
|
@@ -107,8 +113,8 @@ export interface RunSequenceProfile {
|
|
|
107
113
|
*/
|
|
108
114
|
export function readTestConfig(): ModelTestUserConfig {
|
|
109
115
|
try {
|
|
110
|
-
const
|
|
111
|
-
if (fs.existsSync(
|
|
116
|
+
const file = fs.existsSync(configPath()) ? configPath() : legacyConfigPath();
|
|
117
|
+
if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, "utf-8")) as ModelTestUserConfig;
|
|
112
118
|
} catch { /* config read/parse failure is non-critical — defaults are used */ }
|
|
113
119
|
return {};
|
|
114
120
|
}
|
|
@@ -161,8 +167,9 @@ export const WEATHER_TOOL_DEFINITION = {
|
|
|
161
167
|
// Tool Support Cache (path only — backing file is no longer written)
|
|
162
168
|
// ============================================================================
|
|
163
169
|
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
export function toolSupportCachePath(): string {
|
|
171
|
+
return path.join(getAgentDir(), "cache", "tool_support.json");
|
|
172
|
+
}
|
|
166
173
|
|
|
167
174
|
// ============================================================================
|
|
168
175
|
// ChatFn Abstraction
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment for processes that run model-authored code.
|
|
3
|
+
*
|
|
4
|
+
* `coding_lite` and `test_all` write the model's answer to disk and execute it,
|
|
5
|
+
* so whatever the model wrote runs with this process's parent environment. The
|
|
6
|
+
* harness is normally started from a shell that holds provider keys, cloud
|
|
7
|
+
* credentials and agent paths, and none of those belong in front of generated
|
|
8
|
+
* code. This is therefore an allowlist, not a denylist: anything not listed here
|
|
9
|
+
* is absent from the child, including every `*_API_KEY`, `AWS_*`, `GH_*` and
|
|
10
|
+
* `PI_*` variable.
|
|
11
|
+
*
|
|
12
|
+
* `NODE_OPTIONS` and `NODE_PATH` are deliberately never forwarded: both can make
|
|
13
|
+
* Node load attacker-chosen files.
|
|
14
|
+
*/
|
|
15
|
+
export const INHERITED_KEYS = [
|
|
16
|
+
// POSIX: node resolution, temp files, locale and timezone.
|
|
17
|
+
"PATH", "HOME", "TMPDIR", "TEMP", "TMP", "LANG", "LC_ALL", "TZ", "SHELL", "USER", "LOGNAME",
|
|
18
|
+
// TLS trust the child may legitimately need.
|
|
19
|
+
"SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS",
|
|
20
|
+
// Terminal capabilities only; no secrets live here.
|
|
21
|
+
"TERM", "NO_COLOR", "FORCE_COLOR", "COLORTERM",
|
|
22
|
+
// Windows: spawning and node resolution fail without these.
|
|
23
|
+
"SystemRoot", "WINDIR", "COMSPEC", "PATHEXT", "USERPROFILE", "HOMEDRIVE", "HOMEPATH",
|
|
24
|
+
"APPDATA", "LOCALAPPDATA",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** Build the environment for a child that executes untrusted, model-authored code. */
|
|
28
|
+
export function scrubbedEnv(base: NodeJS.ProcessEnv = process.env): Record<string, string> {
|
|
29
|
+
const out: Record<string, string> = {};
|
|
30
|
+
for (const key of INHERITED_KEYS) {
|
|
31
|
+
const value = base[key];
|
|
32
|
+
// NUL-containing values are rejected by spawn(); drop them instead of throwing.
|
|
33
|
+
if (typeof value === "string" && value.length > 0 && !value.includes("\0")) out[key] = value;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
@@ -258,7 +258,7 @@ export function sanitizeForReport(s: string, maxLines = 40): string {
|
|
|
258
258
|
// Use strict detection: require a closing tag AND a common HTML opening tag to avoid
|
|
259
259
|
// false positives on code that mentions HTML-like syntax (e.g., "use <Item> from 'react'")
|
|
260
260
|
if (/<!DOCTYPE\b|<html[\s>]/i.test(cleaned) || (/<[a-z][\s\S]*>/i.test(cleaned) && cleaned.includes("</") && /<(?:div|span|p|head|body|html|table|form|script)\b/i.test(cleaned))) {
|
|
261
|
-
const firstLine = cleaned.split("\n")[0];
|
|
261
|
+
const firstLine = cleaned.split("\n")[0] ?? "";
|
|
262
262
|
return truncate(firstLine, 200) + "\n ℹ️ (HTML response truncated)";
|
|
263
263
|
}
|
|
264
264
|
|
|
@@ -365,7 +365,7 @@ function parseParamCount(s: string): number | undefined {
|
|
|
365
365
|
const match = str.match(/^([\d.]+)\s*([bmt]?|a(?:pple)?)$/);
|
|
366
366
|
if (!match) return undefined;
|
|
367
367
|
|
|
368
|
-
const num = parseFloat(match[1]);
|
|
368
|
+
const num = parseFloat(match[1] ?? "");
|
|
369
369
|
if (isNaN(num) || num <= 0) return undefined;
|
|
370
370
|
|
|
371
371
|
const suffix = match[2];
|