@openlines/opl-pi-sht 0.2.12 → 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 -84
- 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 +31 -6
- 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 +209 -105
- 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/package.json +19 -12
|
@@ -52,6 +52,7 @@ function commandSegments(command: string): string[] {
|
|
|
52
52
|
for (let i = 0; i < command.length; i++) {
|
|
53
53
|
const ch = command[i];
|
|
54
54
|
const top = stack[stack.length - 1];
|
|
55
|
+
if (!top) break; // stack drained: the rest of the command is literal text
|
|
55
56
|
// Single quotes are literal: nothing expands, only the closing ' matters.
|
|
56
57
|
if (top.quote === "'") {
|
|
57
58
|
if (ch === "'") top.quote = "";
|
|
@@ -165,9 +166,9 @@ export function extractPlanText(message: string): string | null {
|
|
|
165
166
|
|
|
166
167
|
// ─── Plan File I/O ────────────────────────────────────────────────────────────
|
|
167
168
|
|
|
168
|
-
/** Ensure
|
|
169
|
-
export function ensurePlanDir(): string {
|
|
170
|
-
const dir = join(
|
|
169
|
+
/** Ensure <sessionCwd>/.pi/plans/ exists, return its absolute path. */
|
|
170
|
+
export function ensurePlanDir(cwd: string): string {
|
|
171
|
+
const dir = join(cwd, PLAN_DIR);
|
|
171
172
|
mkdirSync(dir, { recursive: true });
|
|
172
173
|
return dir;
|
|
173
174
|
}
|
|
@@ -181,7 +182,12 @@ export function titleFromFilename(filename: string): string {
|
|
|
181
182
|
// Detect timestamp pattern: YYYY-MM-DD-HH-MM
|
|
182
183
|
const tsMatch = stem.match(/^(\d{4})-(\d{2})-(\d{2})-(\d{2})-(\d{2})$/);
|
|
183
184
|
if (tsMatch) {
|
|
184
|
-
|
|
185
|
+
// Five digit groups by the shape test above; the explicit checks are what let the compiler see
|
|
186
|
+
// that, rather than a cast.
|
|
187
|
+
const [y, m, d, h, min] = tsMatch.slice(1);
|
|
188
|
+
if (y === undefined || m === undefined || d === undefined || h === undefined || min === undefined) {
|
|
189
|
+
return stem.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
190
|
+
}
|
|
185
191
|
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
186
192
|
const month = months[parseInt(m, 10) - 1] ?? m;
|
|
187
193
|
return `${month} ${parseInt(d, 10)}, ${y} ${h}:${min}`;
|
|
@@ -203,8 +209,8 @@ export function sanitizePlanName(name: string): string | null {
|
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
/** List available plan files in .pi/plans/ with titles from # Plan: heading. */
|
|
206
|
-
export function listPlanFiles(): PlanFileSummary[] {
|
|
207
|
-
const dir = join(
|
|
212
|
+
export function listPlanFiles(cwd: string): PlanFileSummary[] {
|
|
213
|
+
const dir = join(cwd, PLAN_DIR);
|
|
208
214
|
if (!existsSync(dir)) return [];
|
|
209
215
|
|
|
210
216
|
const files = readdirSync(dir)
|
|
@@ -214,7 +220,7 @@ export function listPlanFiles(): PlanFileSummary[] {
|
|
|
214
220
|
return files.map((filename) => {
|
|
215
221
|
const content = readFileSync(join(dir, filename), "utf-8");
|
|
216
222
|
const titleMatch = content.match(/^# Plan:\s*(.+)$/m);
|
|
217
|
-
const title = titleMatch ? titleMatch[1].trim() : titleFromFilename(filename);
|
|
223
|
+
const title = titleMatch?.[1] ? titleMatch[1].trim() : titleFromFilename(filename);
|
|
218
224
|
return { name: filename, title };
|
|
219
225
|
});
|
|
220
226
|
}
|
|
@@ -265,4 +271,8 @@ export function applyLabelColor(theme: Theme, color: string, text: string): stri
|
|
|
265
271
|
} catch {
|
|
266
272
|
return text;
|
|
267
273
|
}
|
|
268
|
-
}
|
|
274
|
+
}
|
|
275
|
+
/** Absolute path of a plan file inside the session directory. */
|
|
276
|
+
export function planFilePath(cwd: string, filename: string): string {
|
|
277
|
+
return join(cwd, PLAN_DIR, filename);
|
|
278
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
type Answer,
|
|
24
24
|
type Question,
|
|
25
|
+
type QuestionCheck,
|
|
25
26
|
QuestionnaireParams,
|
|
26
27
|
type QuestionnaireResult,
|
|
27
28
|
type RenderOption,
|
|
@@ -42,7 +43,7 @@ export function errorResult(
|
|
|
42
43
|
/** Validate the question list. Returns the first error message, or null when valid.
|
|
43
44
|
* Blank ids are rejected (answers are keyed by id and "" would produce an empty key),
|
|
44
45
|
* as are duplicate ids and questions with nothing selectable. */
|
|
45
|
-
export function validateQuestions(questions:
|
|
46
|
+
export function validateQuestions(questions: readonly QuestionCheck[]): string | null {
|
|
46
47
|
const seenIds = new Set<string>();
|
|
47
48
|
for (const q of questions) {
|
|
48
49
|
if (q.id.trim() === "") {
|
|
@@ -303,8 +304,10 @@ export default function questionnaire(pi: ExtensionAPI) {
|
|
|
303
304
|
const tabs: string[] = ["← "];
|
|
304
305
|
for (let i = 0; i < questions.length; i++) {
|
|
305
306
|
const isActive = i === currentTab;
|
|
306
|
-
const
|
|
307
|
-
|
|
307
|
+
const question = questions[i];
|
|
308
|
+
if (!question) continue;
|
|
309
|
+
const isAnswered = answers.has(question.id);
|
|
310
|
+
const lbl = question.label;
|
|
308
311
|
const box = isAnswered ? "■" : "□";
|
|
309
312
|
const color = isAnswered ? "success" : "muted";
|
|
310
313
|
const text = ` ${box} ${lbl} `;
|
|
@@ -326,6 +329,7 @@ export default function questionnaire(pi: ExtensionAPI) {
|
|
|
326
329
|
function renderOptions() {
|
|
327
330
|
for (let i = 0; i < opts.length; i++) {
|
|
328
331
|
const opt = opts[i];
|
|
332
|
+
if (!opt) continue; // a hole in the option list renders nothing rather than throwing mid-frame
|
|
329
333
|
const selected = i === optionIndex;
|
|
330
334
|
const isOther = opt.isOther === true;
|
|
331
335
|
const prefix = selected ? theme.fg("accent", "> ") : " ";
|
|
@@ -435,7 +439,9 @@ export default function questionnaire(pi: ExtensionAPI) {
|
|
|
435
439
|
},
|
|
436
440
|
|
|
437
441
|
renderResult(result, _options, theme, _context) {
|
|
438
|
-
|
|
442
|
+
// `isError` on tool results is newer than the declared floor: pi 0.87.x does not carry
|
|
443
|
+
// it in its types, and an older host leaves it undefined, which reads as "not an error".
|
|
444
|
+
if ((result as { isError?: boolean }).isError) {
|
|
439
445
|
const text = result.content[0];
|
|
440
446
|
return new Text(theme.fg("error", text?.type === "text" ? text.text : ""), 0, 0);
|
|
441
447
|
}
|
|
@@ -18,6 +18,10 @@ export interface Question {
|
|
|
18
18
|
allowOther: boolean;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** The fields `validateQuestions` reads: the same shape on raw tool params (where `label`
|
|
22
|
+
* still has to be defaulted) and on normalized questions. */
|
|
23
|
+
export type QuestionCheck = Pick<Question, "id" | "options"> & Partial<Pick<Question, "allowOther">>;
|
|
24
|
+
|
|
21
25
|
export interface Answer {
|
|
22
26
|
id: string;
|
|
23
27
|
value: string;
|
|
@@ -32,7 +32,7 @@ simplebench({ model: "global.openai.gpt-5.6-terra", coding_lite: true })
|
|
|
32
32
|
simplebench({ model: "global.openai.gpt-5.6-terra", test_all: true })
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
`--no-artifact` and `no_artifact: true` suppress the JSON file. Default runs use the provider's sampling and reasoning defaults and write one JSON report per model to
|
|
35
|
+
`--no-artifact` and `no_artifact: true` suppress the JSON file. Default runs use the provider's sampling and reasoning defaults and write one JSON report per model to the current Pi session directory (`ExtensionContext.cwd`). Without an active session context, artifacts fall back to the harness working directory. `--thinking-max` and `thinking_max: true` request `reasoning_effort: "max"` for OpenAI-compatible providers, or Pi's model-aware Bedrock max-thinking path for direct Bedrock models that advertise `reasoning: true` and `thinkingLevelMap.max`. Other providers reject that mode rather than silently using defaults.
|
|
36
36
|
|
|
37
37
|
## Extension features
|
|
38
38
|
|
|
@@ -172,7 +172,7 @@ The stats endpoint is metadata-only and does not change inference routing. Its s
|
|
|
172
172
|
|
|
173
173
|
## Artifacts and privacy
|
|
174
174
|
|
|
175
|
-
Baseline and coding-lite artifacts
|
|
175
|
+
Baseline and coding-lite artifacts use the current Pi session directory (`ExtensionContext.cwd`), passed down explicitly; without a Pi session, they fall back to the harness working directory. `--test-all` writes a bundle:
|
|
176
176
|
|
|
177
177
|
```text
|
|
178
178
|
simplebench--test-all-<sanitized-model>-<thinking>-<UTC-timestamp>/
|
|
@@ -211,6 +211,8 @@ The model receives weather and calculation tools. Simplebench validates expected
|
|
|
211
211
|
|
|
212
212
|
Coding-lite is the execution-backed coding-agent suite. It runs model-directed edits in temporary directories. The model can list, search, read, write, and run the fixture's public tests, but cannot execute arbitrary commands, access the real repository, read hidden verification code, or use the network. The host runs hidden verification after the model stops.
|
|
213
213
|
|
|
214
|
+
**The verifier executes model-authored code.** Verification runs `node --eval` over the model's files, so that process is started with an allowlisted environment (`util/exec-env.ts`): `PATH`, `HOME`, temp-directory and locale variables, TLS trust paths, terminal capability flags, and the Windows equivalents. Everything else is absent — every `*_API_KEY`, `AWS_*`, `GH_*`, `PI_*`, and any other secret your shell happens to export — and `NODE_OPTIONS`/`NODE_PATH` are never forwarded because both can make Node load attacker-chosen files. Anything the model's code spawns inherits that environment, so a nested `node src/cli.mjs` sees the same allowlist. This limits what generated code can *read out of the process*; it is not sandboxing. The child still runs with your user's filesystem permissions and network reach, and a `HOME` it can see points at files it could read by path anyway. Use `--no-artifact` and a dedicated shell if that is not acceptable for your environment.
|
|
215
|
+
|
|
214
216
|
The agent loop defaults to **5 turns** per task (reduced from 12). This tighter budget forces the model to converge on a solution quickly and makes turn count a meaningful quality signal. Results are scored on three dimensions:
|
|
215
217
|
|
|
216
218
|
- **STRONG** — solved in 1–2 turns (first or second attempt correct)
|
|
@@ -230,17 +232,17 @@ Use `--coding-lite` for coding tasks only. Use `--test-all` to run the existing
|
|
|
230
232
|
|
|
231
233
|
### Recommendation policy
|
|
232
234
|
|
|
233
|
-
|
|
235
|
+
Four categories can be in play: the closed-answer contract, instruction following, tool usage, and (with `--coding-lite`) coding. A category counts as passed when its own rule succeeds - the closed-answer contract counts as passed at `STRONG` **or** `MODERATE`, and coding counts as passed on its efficiency-weighted average. The label then depends on how many passed, and on how many categories actually ran:
|
|
234
236
|
|
|
235
|
-
|
|
|
236
|
-
|
|
237
|
-
|
|
|
238
|
-
|
|
|
239
|
-
|
|
|
240
|
-
|
|
|
241
|
-
|
|
|
237
|
+
| Categories passed | Baseline (3 categories) | With coding (4 categories) |
|
|
238
|
+
|---|---|---|
|
|
239
|
+
| All | STRONG if the contract is `STRONG` and, when coding ran, every task is `STRONG`; GOOD otherwise | |
|
|
240
|
+
| All but one | GOOD | GOOD |
|
|
241
|
+
| Two | USABLE | LIMITED |
|
|
242
|
+
| One | LIMITED | LIMITED |
|
|
243
|
+
| None | WEAK | WEAK |
|
|
242
244
|
|
|
243
|
-
|
|
245
|
+
`WEAK` therefore requires that **nothing** passed; one passing category is `LIMITED`. A run that omits coding can report `USABLE` where the same results with coding report `LIMITED`, because the denominator changes. For runs with no clean category counts (an error or a cancelled run), `report.ts` falls back to the average `tests[].score`: `>= 0.8` `GOOD`, `>= 0.5` `USABLE`, `>= 0.2` `LIMITED`, otherwise `WEAK`. The coding category is scored efficiency-weighted (STRONG=1.0, MODERATE=0.7, WEAK=0.4, FAIL=0) and passes when that weighted average is >= 0.5 across all tasks.
|
|
244
246
|
|
|
245
247
|
## Provider authentication
|
|
246
248
|
|
|
@@ -260,6 +262,8 @@ Simplebench derives the region from the selected model's Bedrock base URL, signs
|
|
|
260
262
|
aws configure export-credentials --profile "$AWS_PROFILE"
|
|
261
263
|
```
|
|
262
264
|
|
|
265
|
+
The CLI is invoked with an argument vector, never through a shell, so the profile value is never interpolated into a command string; and it runs asynchronously, so a slow SSO or assume-role chain cannot stall the TUI. A profile name that is not a plain token (`^[A-Za-z0-9][A-Za-z0-9_.:@+-]{0,127}$`) is refused before any process is spawned — `AWS_PROFILE` set by repo tooling such as `.envrc` or a Makefile is not trusted to reach the CLI. Set `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` (plus `AWS_SESSION_TOKEN`) to bypass the CLI entirely.
|
|
266
|
+
|
|
263
267
|
AWS CLI v2 is therefore required when the selected profile uses SSO or an assume-role chain.
|
|
264
268
|
|
|
265
269
|
## Troubleshooting
|
|
@@ -270,7 +274,7 @@ AWS CLI v2 is therefore required when the selected profile uses SSO or an assume
|
|
|
270
274
|
| OpenRouter returns 401 | Set `OPENROUTER_API_KEY`, configure the provider key, or authenticate Pi so `auth.json` has an API key. |
|
|
271
275
|
| Bedrock says credentials are unavailable | Run `aws configure export-credentials --profile "$AWS_PROFILE"` and resolve profile login/role issues first. |
|
|
272
276
|
| Bedrock rejects `--thinking-max` | The selected model does not advertise max thinking in Pi metadata, or the provider rejected its model-specific reasoning request. |
|
|
273
|
-
| Artifact is absent | Check the report for `--no-artifact` or an artifact-write warning; the file
|
|
277
|
+
| Artifact is absent | Check the report for `--no-artifact` or an artifact-write warning; the file lands in the session directory, not where the shell was standing. |
|
|
274
278
|
| Token or TTFT values are null | The provider response did not expose authoritative usage/timing values. |
|
|
275
279
|
|
|
276
280
|
## Architecture
|
|
@@ -279,13 +283,15 @@ AWS CLI v2 is therefore required when the selected profile uses SSO or an assume
|
|
|
279
283
|
index.ts Pi registration, command parsing, tool schema
|
|
280
284
|
benchmark.ts provider adapters and benchmark orchestration
|
|
281
285
|
llama-server.ts direct llama-server /props and /metrics capture
|
|
286
|
+
coding.ts the six execution-backed coding tasks and their agent loop
|
|
287
|
+
research.ts grounded research fixtures and the deterministic verifier
|
|
282
288
|
tests.ts benchmark fixtures
|
|
283
289
|
scoring.ts extraction and scoring
|
|
284
290
|
metrics.ts request/usage aggregation
|
|
285
|
-
artifact.ts
|
|
291
|
+
artifact.ts session-directory JSON artifact writer
|
|
286
292
|
report.ts terminal output and recommendation policy
|
|
287
293
|
types.ts shared benchmark types
|
|
288
|
-
util/ local configuration, formatting, debugging, and provider helpers
|
|
294
|
+
util/ local configuration, formatting, debugging, exec-environment scrubbing, and provider helpers
|
|
289
295
|
```
|
|
290
296
|
|
|
291
|
-
|
|
297
|
+
Everything lives under `extensions/opl-simplebench/` because `install.sh` installs one extension directory at a time; the only environment boundary model-authored code gets is `util/exec-env.ts`, which is where a new child process must go through.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
// The output directory is always passed in: artifacts land in the session directory
|
|
5
|
+
// (`ExtensionContext.cwd`), not wherever the harness was started.
|
|
3
6
|
import type { RunArtifact } from "./types";
|
|
4
7
|
|
|
5
8
|
export function artifactFileName(model: string, suite: "baseline" | "coding-lite" | "test-all" = "baseline", thinking: "default" | "max" = "default", date = new Date(), tag?: string): string {
|
|
@@ -9,10 +12,10 @@ export function artifactFileName(model: string, suite: "baseline" | "coding-lite
|
|
|
9
12
|
return `simplebench${tag ? `-${tag}-` : "--"}${suiteName}-${safeModel}-${thinking}-${stamp}.json`;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
export function writeArtifactBundle(artifact: RunArtifact, files: Record<string, string
|
|
15
|
+
export function writeArtifactBundle(artifact: RunArtifact, files: Record<string, string>, cwd: string): string {
|
|
13
16
|
const name = artifactFileName(artifact.benchmark.model, artifact.benchmark.suite || "baseline", artifact.benchmark.thinking.requested, new Date(), artifact.benchmark.tag).replace(/\.json$/, "");
|
|
14
17
|
for (let suffix = 0; ; suffix += 1) {
|
|
15
|
-
const output = path.resolve(
|
|
18
|
+
const output = path.resolve(cwd, suffix ? `${name}-${suffix}` : name);
|
|
16
19
|
try {
|
|
17
20
|
fs.mkdirSync(output);
|
|
18
21
|
fs.writeFileSync(path.join(output, "result.json"), JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
@@ -24,13 +27,13 @@ export function writeArtifactBundle(artifact: RunArtifact, files: Record<string,
|
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
export function writeArtifact(artifact: RunArtifact): string {
|
|
30
|
+
export function writeArtifact(artifact: RunArtifact, cwd: string): string {
|
|
28
31
|
const name = artifactFileName(artifact.benchmark.model, artifact.benchmark.suite || "baseline", artifact.benchmark.thinking.requested, new Date(), artifact.benchmark.tag);
|
|
29
32
|
const extension = path.extname(name);
|
|
30
33
|
const base = name.slice(0, -extension.length);
|
|
31
34
|
const contents = JSON.stringify(artifact, null, 2) + "\n";
|
|
32
35
|
for (let suffix = 0; ; suffix += 1) {
|
|
33
|
-
const output = path.resolve(
|
|
36
|
+
const output = path.resolve(cwd, suffix ? `${base}-${suffix}${extension}` : name);
|
|
34
37
|
try {
|
|
35
38
|
fs.writeFileSync(output, contents, { encoding: "utf8", flag: "wx" });
|
|
36
39
|
return output;
|
|
@@ -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
|
}
|