@skill-harness/adapters 0.30.0 → 0.31.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 +10 -4
- package/dist/pi-json.js +92 -100
- package/package.json +2 -2
package/dist/pi-json.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExecutionTraceV1, ModelRef, RunMode } from "@skill-harness/core";
|
|
1
|
+
import type { ExecutionTraceV1, ModelRef, RunMode, ProcessObservation } from "@skill-harness/core";
|
|
2
2
|
/**
|
|
3
3
|
* Run `pi --mode json` and build an execution trace, **streaming**.
|
|
4
4
|
*
|
|
@@ -9,10 +9,10 @@ import type { ExecutionTraceV1, ModelRef, RunMode } from "@skill-harness/core";
|
|
|
9
9
|
* into a string (which is what the shared `exec()` helper does) would exhaust
|
|
10
10
|
* memory partway through a long wave, taking the whole run with it.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* The lifecycle is shared with `exec()`, but capture is streaming. Quadratic events are
|
|
13
13
|
* dropped as each line arrives, so the giant ones are never retained; the
|
|
14
|
-
* remainder
|
|
15
|
-
*
|
|
14
|
+
* remainder is bounded by both bytes and record count. Oversized lines or retained
|
|
15
|
+
* evidence terminate the owned process and report an infrastructure error.
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
18
|
* The two quadratic event types, matched at the head of the object where pi
|
|
@@ -37,6 +37,11 @@ export interface PiJsonRunOptions {
|
|
|
37
37
|
* as "inherit", so the control arm (which never sets this) is unaffected.
|
|
38
38
|
*/
|
|
39
39
|
env?: NodeJS.ProcessEnv;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
maxLineBytes?: number;
|
|
42
|
+
maxRetainedBytes?: number;
|
|
43
|
+
maxRecords?: number;
|
|
44
|
+
maxOutputBytes?: number;
|
|
40
45
|
}
|
|
41
46
|
export interface PiJsonRunResult {
|
|
42
47
|
trace: ExecutionTraceV1;
|
|
@@ -51,5 +56,6 @@ export interface PiJsonRunResult {
|
|
|
51
56
|
* verdict.
|
|
52
57
|
*/
|
|
53
58
|
providerFailure: string | null;
|
|
59
|
+
observation?: ProcessObservation;
|
|
54
60
|
}
|
|
55
61
|
export declare function runPiJson(opts: PiJsonRunOptions): Promise<PiJsonRunResult>;
|
package/dist/pi-json.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { StringDecoder } from "node:string_decoder";
|
|
3
|
-
import { parseTrace, providerFailureFromJsonLine } from "@skill-harness/core";
|
|
1
|
+
import { BoundedCapture, ProcessOutputLimitError, runProcess, parseTrace, providerFailureFromJsonLine } from "@skill-harness/core";
|
|
4
2
|
/**
|
|
5
3
|
* Run `pi --mode json` and build an execution trace, **streaming**.
|
|
6
4
|
*
|
|
@@ -11,10 +9,10 @@ import { parseTrace, providerFailureFromJsonLine } from "@skill-harness/core";
|
|
|
11
9
|
* into a string (which is what the shared `exec()` helper does) would exhaust
|
|
12
10
|
* memory partway through a long wave, taking the whole run with it.
|
|
13
11
|
*
|
|
14
|
-
*
|
|
12
|
+
* The lifecycle is shared with `exec()`, but capture is streaming. Quadratic events are
|
|
15
13
|
* dropped as each line arrives, so the giant ones are never retained; the
|
|
16
|
-
* remainder
|
|
17
|
-
*
|
|
14
|
+
* remainder is bounded by both bytes and record count. Oversized lines or retained
|
|
15
|
+
* evidence terminate the owned process and report an infrastructure error.
|
|
18
16
|
*/
|
|
19
17
|
/**
|
|
20
18
|
* The two quadratic event types, matched at the head of the object where pi
|
|
@@ -24,105 +22,99 @@ import { parseTrace, providerFailureFromJsonLine } from "@skill-harness/core";
|
|
|
24
22
|
export const SKIPPED_TYPE_RE = /^\s*\{\s*"type"\s*:\s*"(?:message_update|tool_execution_update)"/;
|
|
25
23
|
/** How much stderr to retain — enough to diagnose, bounded so a loop cannot blow up. */
|
|
26
24
|
const MAX_STDERR_CHARS = 8000;
|
|
27
|
-
export function runPiJson(opts) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (event.type === "message_update" && kept.at(-1) !== '{"type":"message_update"}') {
|
|
67
|
-
kept.push('{"type":"message_update"}');
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
kept.push("null"); // one parser-owned malformed-record error per bad line
|
|
72
|
-
}
|
|
25
|
+
export async function runPiJson(opts) {
|
|
26
|
+
const kept = [];
|
|
27
|
+
const stderr = new BoundedCapture(MAX_STDERR_CHARS);
|
|
28
|
+
const maxLine = opts.maxLineBytes ?? 8 * 1024 * 1024;
|
|
29
|
+
const maxRetained = opts.maxRetainedBytes ?? 16 * 1024 * 1024;
|
|
30
|
+
const maxRecords = opts.maxRecords ?? 50_000;
|
|
31
|
+
for (const limit of [maxLine, maxRetained, maxRecords])
|
|
32
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
33
|
+
throw new Error("Invalid Pi capture limit");
|
|
34
|
+
let retainedBytes = 0;
|
|
35
|
+
let providerFailure = null;
|
|
36
|
+
const keep = (line) => {
|
|
37
|
+
const size = Buffer.byteLength(line, "utf8");
|
|
38
|
+
if (retainedBytes + size > maxRetained || kept.length >= maxRecords)
|
|
39
|
+
throw new ProcessOutputLimitError("Pi retained JSON evidence limit exceeded");
|
|
40
|
+
kept.push(line);
|
|
41
|
+
retainedBytes += size;
|
|
42
|
+
};
|
|
43
|
+
const acceptLine = (line) => {
|
|
44
|
+
// Prefilter before the full parse: the events skipped here are both the
|
|
45
|
+
// overwhelming majority of lines and by far the largest.
|
|
46
|
+
//
|
|
47
|
+
// Anchored on the `type` field, NOT a substring of the whole line. A raw
|
|
48
|
+
// `line.includes('"message_update"')` also matched any event whose
|
|
49
|
+
// ARGUMENTS contained that text — so a `tool_execution_start` for, say,
|
|
50
|
+
// `grep '"message_update"' logs/` was dropped before parsing, and a
|
|
51
|
+
// dropped start means the call never enters the trace at all. A
|
|
52
|
+
// `forbid_calls` gate on that tool then passed for want of the evidence.
|
|
53
|
+
if (!line.trim())
|
|
54
|
+
return;
|
|
55
|
+
if (SKIPPED_TYPE_RE.test(line)) {
|
|
56
|
+
// Validate but never retain the quadratic payload. Otherwise a malformed
|
|
57
|
+
// update could disappear and make an incomplete capture look successful.
|
|
58
|
+
let event;
|
|
59
|
+
try {
|
|
60
|
+
event = JSON.parse(line);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
keep("null"); // one parser-owned malformed-record error per bad line
|
|
73
64
|
return;
|
|
74
65
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
66
|
+
// A late update invalidates an earlier final/settlement. Consecutive
|
|
67
|
+
// updates need only one small state marker, not their accumulated text.
|
|
68
|
+
if (event.type === "message_update" && kept.at(-1) !== '{"type":"message_update"}')
|
|
69
|
+
keep('{"type":"message_update"}');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
keep(line);
|
|
73
|
+
if (providerFailure === null)
|
|
74
|
+
providerFailure = providerFailureFromJsonLine(line);
|
|
75
|
+
};
|
|
76
|
+
// Frame bytes at LF, so split UTF-8 and legal Unicode separators stay intact.
|
|
77
|
+
// Enforce the bound before decoding or parsing an unterminated giant line.
|
|
78
|
+
let pending = new BoundedCapture(maxLine);
|
|
79
|
+
const append = (chunk) => {
|
|
80
|
+
pending.push(chunk);
|
|
81
|
+
if (pending.truncated)
|
|
82
|
+
throw new ProcessOutputLimitError("Pi JSON line byte limit exceeded");
|
|
83
|
+
};
|
|
84
|
+
const flush = () => {
|
|
85
|
+
if (pending.bytes)
|
|
86
|
+
acceptLine(pending.text());
|
|
87
|
+
pending = new BoundedCapture(maxLine);
|
|
88
|
+
};
|
|
89
|
+
const result = await runProcess("pi", opts.args, { cwd: opts.cwd, env: opts.env,
|
|
90
|
+
timeoutMs: opts.timeoutMs, signal: opts.signal, maxOutputBytes: opts.maxOutputBytes,
|
|
91
|
+
onStdout(chunk) {
|
|
85
92
|
let start = 0, end;
|
|
86
|
-
while ((end =
|
|
87
|
-
|
|
93
|
+
while ((end = chunk.indexOf(10, start)) >= 0) {
|
|
94
|
+
append(chunk.subarray(start, end));
|
|
95
|
+
flush();
|
|
88
96
|
start = end + 1;
|
|
89
97
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
child.on("close", (code) => {
|
|
110
|
-
if (settled)
|
|
111
|
-
return;
|
|
112
|
-
settled = true;
|
|
113
|
-
clearTimeout(timer);
|
|
114
|
-
const parsed = parseTrace(kept, {
|
|
115
|
-
piVersion: opts.piVersion,
|
|
116
|
-
subject: opts.subject,
|
|
117
|
-
scenarioId: opts.scenarioId,
|
|
118
|
-
mode: opts.mode,
|
|
119
|
-
rep: opts.rep,
|
|
120
|
-
turn: opts.turn,
|
|
121
|
-
changedPaths: opts.changedPaths,
|
|
122
|
-
homeDir: opts.homeDir,
|
|
123
|
-
});
|
|
124
|
-
resolve({ ...parsed, code, stderr: stderr.slice(0, MAX_STDERR_CHARS), providerFailure });
|
|
125
|
-
});
|
|
98
|
+
if (start < chunk.length)
|
|
99
|
+
append(chunk.subarray(start));
|
|
100
|
+
},
|
|
101
|
+
onStderr(chunk) { stderr.push(chunk); },
|
|
102
|
+
onStdoutEnd: flush, });
|
|
103
|
+
if (result.error) {
|
|
104
|
+
if (result.observation.reason === "timeout")
|
|
105
|
+
throw new Error(`pi --mode json timed out after ${opts.timeoutMs}ms`);
|
|
106
|
+
throw result.error;
|
|
107
|
+
}
|
|
108
|
+
const parsed = parseTrace(kept, {
|
|
109
|
+
piVersion: opts.piVersion,
|
|
110
|
+
subject: opts.subject,
|
|
111
|
+
scenarioId: opts.scenarioId,
|
|
112
|
+
mode: opts.mode,
|
|
113
|
+
rep: opts.rep,
|
|
114
|
+
turn: opts.turn,
|
|
115
|
+
changedPaths: opts.changedPaths,
|
|
116
|
+
homeDir: opts.homeDir,
|
|
126
117
|
});
|
|
118
|
+
return { ...parsed, code: result.code, stderr: stderr.text(), providerFailure, observation: result.observation };
|
|
127
119
|
}
|
|
128
120
|
//# sourceMappingURL=pi-json.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skill-harness/adapters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "skill-harness Pi runner and judge adapter (internal API)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"prepack": "node ../../scripts/release-pack.mjs --guard"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@skill-harness/core": "0.
|
|
45
|
+
"@skill-harness/core": "0.31.0"
|
|
46
46
|
}
|
|
47
47
|
}
|