@skill-harness/adapters 0.10.0 → 0.11.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/dist/pi-json.d.ts +12 -0
- package/dist/pi-json.js +6 -2
- package/dist/pi.js +60 -10
- package/package.json +2 -2
package/dist/pi-json.d.ts
CHANGED
|
@@ -32,6 +32,11 @@ export interface PiJsonRunOptions {
|
|
|
32
32
|
turn: number;
|
|
33
33
|
changedPaths?: string[];
|
|
34
34
|
homeDir?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Extra env for the subject process, from the arm. `spawn` treats `undefined`
|
|
37
|
+
* as "inherit", so the control arm (which never sets this) is unaffected.
|
|
38
|
+
*/
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
35
40
|
}
|
|
36
41
|
export interface PiJsonRunResult {
|
|
37
42
|
trace: ExecutionTraceV1;
|
|
@@ -39,5 +44,12 @@ export interface PiJsonRunResult {
|
|
|
39
44
|
malformedLines: number;
|
|
40
45
|
code: number | null;
|
|
41
46
|
stderr: string;
|
|
47
|
+
/**
|
|
48
|
+
* Set when a line on the stream carried a provider-side failure diagnostic
|
|
49
|
+
* (auth, transport) rather than the model answering badly. `runStructured`
|
|
50
|
+
* collects this across turns; `run.ts` turns it into ERROR — never a model
|
|
51
|
+
* verdict.
|
|
52
|
+
*/
|
|
53
|
+
providerFailure: string | null;
|
|
42
54
|
}
|
|
43
55
|
export declare function runPiJson(opts: PiJsonRunOptions): Promise<PiJsonRunResult>;
|
package/dist/pi-json.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
|
-
import { parseTrace } from "@skill-harness/core";
|
|
3
|
+
import { parseTrace, providerFailureFromJsonLine } from "@skill-harness/core";
|
|
4
4
|
/**
|
|
5
5
|
* Run `pi --mode json` and build an execution trace, **streaming**.
|
|
6
6
|
*
|
|
@@ -28,12 +28,14 @@ export function runPiJson(opts) {
|
|
|
28
28
|
return new Promise((resolve, reject) => {
|
|
29
29
|
const child = spawn("pi", opts.args, {
|
|
30
30
|
cwd: opts.cwd,
|
|
31
|
+
env: opts.env,
|
|
31
32
|
// stdin from /dev/null: pi hangs waiting on it otherwise, and a hang in a
|
|
32
33
|
// wave is indistinguishable from a slow model until the timeout fires.
|
|
33
34
|
stdio: ["ignore", "pipe", "pipe"],
|
|
34
35
|
});
|
|
35
36
|
const kept = [];
|
|
36
37
|
let stderr = "";
|
|
38
|
+
let providerFailure = null;
|
|
37
39
|
let settled = false;
|
|
38
40
|
const timer = setTimeout(() => {
|
|
39
41
|
if (settled)
|
|
@@ -58,6 +60,8 @@ export function runPiJson(opts) {
|
|
|
58
60
|
if (SKIPPED_TYPE_RE.test(line))
|
|
59
61
|
return;
|
|
60
62
|
kept.push(line);
|
|
63
|
+
if (providerFailure === null)
|
|
64
|
+
providerFailure = providerFailureFromJsonLine(line);
|
|
61
65
|
});
|
|
62
66
|
child.stderr.on("data", (chunk) => {
|
|
63
67
|
if (stderr.length < MAX_STDERR_CHARS)
|
|
@@ -85,7 +89,7 @@ export function runPiJson(opts) {
|
|
|
85
89
|
changedPaths: opts.changedPaths,
|
|
86
90
|
homeDir: opts.homeDir,
|
|
87
91
|
});
|
|
88
|
-
resolve({ ...parsed, code, stderr: stderr.slice(0, MAX_STDERR_CHARS) });
|
|
92
|
+
resolve({ ...parsed, code, stderr: stderr.slice(0, MAX_STDERR_CHARS), providerFailure });
|
|
89
93
|
});
|
|
90
94
|
});
|
|
91
95
|
}
|
package/dist/pi.js
CHANGED
|
@@ -3,8 +3,24 @@ import { tmpdir, homedir } from "node:os";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { runPiJson } from "./pi-json.js";
|
|
5
5
|
import { collectTrajectorySources, normalizePiTraces, resequence } from "./trajectory.js";
|
|
6
|
-
import { exec, onPath, envNum, traceSha256 } from "@skill-harness/core";
|
|
6
|
+
import { exec, onPath, envNum, traceSha256, withProviderFailure } from "@skill-harness/core";
|
|
7
7
|
const PI_TIMEOUT_MS = envNum("PI_TIMEOUT_MS", 300_000);
|
|
8
|
+
/**
|
|
9
|
+
* stderr fragments that mean the provider refused the request, so the run measured
|
|
10
|
+
* nothing about the model. Substring matching on a message pi passes through from
|
|
11
|
+
* the provider — deliberately narrow: a stderr line we cannot classify stays an
|
|
12
|
+
* ordinary non-zero exit, because calling a real model failure "infrastructure"
|
|
13
|
+
* would hide a regression.
|
|
14
|
+
*/
|
|
15
|
+
const PROVIDER_STDERR_SIGNATURES = [
|
|
16
|
+
"invalidated oauth token",
|
|
17
|
+
"invalid_api_key",
|
|
18
|
+
"insufficient_quota",
|
|
19
|
+
];
|
|
20
|
+
function providerStderr(stderr) {
|
|
21
|
+
const hay = stderr.toLowerCase();
|
|
22
|
+
return PROVIDER_STDERR_SIGNATURES.some((sig) => hay.includes(sig)) ? stderr.trim() : null;
|
|
23
|
+
}
|
|
8
24
|
/**
|
|
9
25
|
* Refuse to hand pi a skill dir it will silently ignore.
|
|
10
26
|
*
|
|
@@ -127,26 +143,46 @@ export const piAdapter = {
|
|
|
127
143
|
: skillFlags(req.mode, req.skillDir);
|
|
128
144
|
const total = req.turns.length;
|
|
129
145
|
const parts = [];
|
|
146
|
+
// The arm's env, merged over the harness's own — undefined (not `process.env`)
|
|
147
|
+
// when there is none, so `exec`'s `env: opts.env ?? process.env` inherits
|
|
148
|
+
// normally and the control arm is unaffected.
|
|
149
|
+
const env = req.armEnv ? { ...process.env, ...req.armEnv } : undefined;
|
|
150
|
+
// Collected across turns and written by `withProviderFailure` into the
|
|
151
|
+
// transcript PREAMBLE at the end, never inline after an assistant turn: the
|
|
152
|
+
// preamble is the only region of the transcript the model provably cannot
|
|
153
|
+
// reach, and a marker anywhere else is forgeable by a model that types the
|
|
154
|
+
// words (which would convert a FAIL into ERROR and mute the judge forever).
|
|
155
|
+
let providerFailure = null;
|
|
130
156
|
if (total === 1) {
|
|
131
157
|
const args = [...flags, ...common, "--no-session", "-p", req.turns[0]];
|
|
132
|
-
const r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS });
|
|
158
|
+
const r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS, env });
|
|
133
159
|
parts.push(header(1, 1, req.turns[0]));
|
|
134
160
|
parts.push(`<<< ASSISTANT:\n${r.stdout.trim()}\n`);
|
|
135
|
-
if (r.code !== 0)
|
|
136
|
-
|
|
137
|
-
|
|
161
|
+
if (r.code !== 0) {
|
|
162
|
+
providerFailure = providerStderr(r.stderr);
|
|
163
|
+
if (!providerFailure)
|
|
164
|
+
parts.push(`[pi exited ${r.code}]\n${r.stderr.trim()}\n`);
|
|
165
|
+
}
|
|
166
|
+
return withProviderFailure(parts.join("\n"), providerFailure);
|
|
138
167
|
}
|
|
139
168
|
const session = mkdtempSync(join(tmpdir(), "sc-pi-session-"));
|
|
140
169
|
for (let i = 0; i < total; i++) {
|
|
141
170
|
const turnFlags = i === 0 ? ["--session-dir", session] : ["--session-dir", session, "-c"];
|
|
142
171
|
const args = [...flags, ...common, ...turnFlags, "-p", req.turns[i]];
|
|
143
|
-
const r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS });
|
|
172
|
+
const r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS, env });
|
|
144
173
|
parts.push(header(i + 1, total, req.turns[i]));
|
|
145
174
|
parts.push(`<<< ASSISTANT:\n${r.stdout.trim()}\n`);
|
|
146
|
-
if (r.code !== 0)
|
|
147
|
-
|
|
175
|
+
if (r.code !== 0) {
|
|
176
|
+
const provider = providerStderr(r.stderr);
|
|
177
|
+
// First failure wins, same as the structured path: the turns after an
|
|
178
|
+
// outage are downstream of it, not independent evidence.
|
|
179
|
+
if (provider && providerFailure === null)
|
|
180
|
+
providerFailure = provider;
|
|
181
|
+
if (!provider)
|
|
182
|
+
parts.push(`[pi exited ${r.code} on turn ${i + 1}]\n${r.stderr.trim()}\n`);
|
|
183
|
+
}
|
|
148
184
|
}
|
|
149
|
-
return parts.join("\n");
|
|
185
|
+
return withProviderFailure(parts.join("\n"), providerFailure);
|
|
150
186
|
},
|
|
151
187
|
/**
|
|
152
188
|
* Structured run: same flags, same turn loop, plus `--mode json` and a trace
|
|
@@ -178,6 +214,10 @@ export const piAdapter = {
|
|
|
178
214
|
const traces = [];
|
|
179
215
|
const parts = [];
|
|
180
216
|
const session = total === 1 ? null : mkdtempSync(join(tmpdir(), "sc-pi-session-"));
|
|
217
|
+
let providerFailure = null;
|
|
218
|
+
// Same merge as `run()`: undefined when the arm carries no env, so `spawn`
|
|
219
|
+
// (which treats `undefined` as "inherit") leaves the control arm untouched.
|
|
220
|
+
const env = req.armEnv ? { ...process.env, ...req.armEnv } : undefined;
|
|
181
221
|
for (let i = 0; i < total; i++) {
|
|
182
222
|
const turnFlags = session === null
|
|
183
223
|
? ["--no-session"]
|
|
@@ -196,6 +236,7 @@ export const piAdapter = {
|
|
|
196
236
|
rep: req.rep ?? 0,
|
|
197
237
|
turn: i,
|
|
198
238
|
homeDir: homedir(),
|
|
239
|
+
env,
|
|
199
240
|
});
|
|
200
241
|
// A stream with no terminal events at all is not evidence of a clean run.
|
|
201
242
|
// Fail loudly here rather than let an empty trace satisfy a `forbid_calls`
|
|
@@ -210,6 +251,14 @@ export const piAdapter = {
|
|
|
210
251
|
r.trace.capture_errors = [`pi JSONL contained ${r.malformedLines} malformed line(s); absence-based trace assertions are unsafe`];
|
|
211
252
|
r.trace.trace_sha256 = traceSha256(r.trace);
|
|
212
253
|
}
|
|
254
|
+
// Recorded here, written into the transcript preamble by
|
|
255
|
+
// `withProviderFailure` below — not just returned on `providerFailure`: the
|
|
256
|
+
// artifact on disk is the only thing a later `grade`/`regrade` call ever
|
|
257
|
+
// reads (see `judgeOneRep` in core/regrade.ts), and the structured path
|
|
258
|
+
// exits 0 while carrying the evidence, so a field a re-judge never sees
|
|
259
|
+
// leaves it unrecoverable from the saved transcript.
|
|
260
|
+
if (providerFailure === null && r.providerFailure)
|
|
261
|
+
providerFailure = r.providerFailure;
|
|
213
262
|
traces.push(r.trace);
|
|
214
263
|
parts.push(header(i + 1, total, req.turns[i]));
|
|
215
264
|
parts.push(`<<< ASSISTANT:\n${r.trace.final_text.trim()}\n`);
|
|
@@ -235,10 +284,11 @@ export const piAdapter = {
|
|
|
235
284
|
}
|
|
236
285
|
const eventErrors = [...native.errors, ...chronologyErrors];
|
|
237
286
|
return {
|
|
238
|
-
transcript: parts.join("\n"),
|
|
287
|
+
transcript: withProviderFailure(parts.join("\n"), providerFailure),
|
|
239
288
|
traces,
|
|
240
289
|
events: resequence(combined),
|
|
241
290
|
...(eventErrors.length ? { eventErrors } : {}),
|
|
291
|
+
...(providerFailure ? { providerFailure } : {}),
|
|
242
292
|
};
|
|
243
293
|
},
|
|
244
294
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skill-harness/adapters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "skill-harness harness adapters — pi runner + claude-code judge routing (internal API)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,6 +40,6 @@
|
|
|
40
40
|
"prepack": "cp ../../LICENSE ./LICENSE"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@skill-harness/core": "0.
|
|
43
|
+
"@skill-harness/core": "0.11.0"
|
|
44
44
|
}
|
|
45
45
|
}
|