@molecule/api-ai-tools 1.6.4 → 1.6.6
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 +520 -3
- package/dist/command-position.d.ts +27 -1
- package/dist/command-position.d.ts.map +1 -1
- package/dist/command-position.js +109 -28
- package/dist/cut-run.d.ts +124 -0
- package/dist/cut-run.d.ts.map +1 -0
- package/dist/cut-run.js +267 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/suite-overlap.d.ts +132 -0
- package/dist/suite-overlap.d.ts.map +1 -0
- package/dist/suite-overlap.js +208 -0
- package/dist/tools.d.ts +45 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +333 -27
- package/dist/utilities.d.ts +113 -2
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +171 -12
- package/package.json +1 -1
package/dist/command-position.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* (`head -30 run-api-tests-hurl.sh`, `grep -n playwright e2e/README.md`,
|
|
23
23
|
* `git show HEAD:run-api-tests.sh`), and a runner named in a heredoc body — a file a command
|
|
24
24
|
* WRITES is the caller's business, so a command with a heredoc goes through the caller's own
|
|
25
|
-
* stripper first (`withoutHeredocs
|
|
25
|
+
* stripper first (`withoutHeredocs`, in `./suite-overlap.js`).
|
|
26
26
|
*
|
|
27
27
|
* @module
|
|
28
28
|
*/
|
|
@@ -92,6 +92,22 @@ const WRAPPER_VALUE_FLAGS = {
|
|
|
92
92
|
const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
93
93
|
/** A wrapper's own duration — `timeout 280 …`, `timeout 1m …` — which is not a flag. */
|
|
94
94
|
const DURATION = /^\d+(?:\.\d+)?[smhd]?$/;
|
|
95
|
+
/** The seconds a `timeout` duration word names (`280`, `1m`, `1.5h`), or null when it is none. */
|
|
96
|
+
const DURATION_UNITS = { s: 1, m: 60, h: 3600, d: 86_400 };
|
|
97
|
+
/**
|
|
98
|
+
* The seconds a duration word stands for.
|
|
99
|
+
*
|
|
100
|
+
* @param word - A duration (`280`, `20s`, `1m`).
|
|
101
|
+
* @returns The seconds, or null when the word is not a duration.
|
|
102
|
+
*/
|
|
103
|
+
export function durationSeconds(word) {
|
|
104
|
+
if (!DURATION.test(word))
|
|
105
|
+
return null;
|
|
106
|
+
const [, number = '', unit = 's'] = /^(\d+(?:\.\d+)?)([smhd]?)$/.exec(word) ?? [];
|
|
107
|
+
const scale = DURATION_UNITS[unit] ?? 1;
|
|
108
|
+
const seconds = Number(number) * scale;
|
|
109
|
+
return Number.isFinite(seconds) ? seconds : null;
|
|
110
|
+
}
|
|
95
111
|
/**
|
|
96
112
|
* A word without its shell quotes.
|
|
97
113
|
*
|
|
@@ -155,14 +171,16 @@ function nestedCommand(words, from) {
|
|
|
155
171
|
return null;
|
|
156
172
|
}
|
|
157
173
|
/**
|
|
158
|
-
* Where the program starts in one segment's words.
|
|
174
|
+
* Where the program starts in one segment's words, and the bound the segment puts on itself.
|
|
159
175
|
*
|
|
160
176
|
* @param words - The segment's words, quotes kept.
|
|
161
177
|
* @returns The index of the word in command position (one past the end when the segment runs
|
|
162
|
-
* nothing),
|
|
178
|
+
* nothing), the command string of a `sh -c '<command>'` this segment turns out to be, and the
|
|
179
|
+
* seconds its own `timeout` wrapper allows that command (`timeout 280 hurl …` → 280) or null.
|
|
163
180
|
*/
|
|
164
181
|
function commandStart(words) {
|
|
165
182
|
let at = 0;
|
|
183
|
+
let timeoutSeconds = null;
|
|
166
184
|
for (let guard = 0; guard < MAX_WRAPPERS && at < words.length; guard++) {
|
|
167
185
|
const word = words[at] ?? '';
|
|
168
186
|
if (ENV_ASSIGNMENT.test(word)) {
|
|
@@ -178,7 +196,7 @@ function commandStart(words) {
|
|
|
178
196
|
if (SHELLS.has(name)) {
|
|
179
197
|
const nested = nestedCommand(words, at + 1);
|
|
180
198
|
if (nested !== null)
|
|
181
|
-
return { at, nested };
|
|
199
|
+
return { at, nested, timeoutSeconds };
|
|
182
200
|
at += 1 + flagWords(words, at + 1, name);
|
|
183
201
|
continue;
|
|
184
202
|
}
|
|
@@ -192,12 +210,14 @@ function commandStart(words) {
|
|
|
192
210
|
continue;
|
|
193
211
|
}
|
|
194
212
|
if (!LAUNCHERS.has(name))
|
|
195
|
-
return { at, nested: null };
|
|
213
|
+
return { at, nested: null, timeoutSeconds };
|
|
196
214
|
at += 1 + flagWords(words, at + 1, name);
|
|
197
|
-
if (name === 'timeout' && DURATION.test(words[at] ?? ''))
|
|
215
|
+
if (name === 'timeout' && DURATION.test(words[at] ?? '')) {
|
|
216
|
+
timeoutSeconds = durationSeconds(words[at] ?? '');
|
|
198
217
|
at += 1;
|
|
218
|
+
}
|
|
199
219
|
}
|
|
200
|
-
return { at, nested: null };
|
|
220
|
+
return { at, nested: null, timeoutSeconds };
|
|
201
221
|
}
|
|
202
222
|
/**
|
|
203
223
|
* A command's statements and pipeline stages, split where the shell splits them — inside quotes
|
|
@@ -206,50 +226,84 @@ function commandStart(words) {
|
|
|
206
226
|
* A single `&` separates only when it backgrounds a command: the one in a redirect (`2>&1`,
|
|
207
227
|
* `&>log`) is part of the word it sits in.
|
|
208
228
|
*
|
|
229
|
+
* The nestings are a STACK, not a (quote, depth) pair, because they interleave: a command
|
|
230
|
+
* substitution opened inside a double quote can itself hold double quotes, and each of those can
|
|
231
|
+
* hold another substitution. `DIR="$(cd "$(dirname "$0")" && pwd)"` is exactly that shape — it
|
|
232
|
+
* opens `"`, `$(`, `"`, `$(`, `"` in order — and every runner script of the RealWorld kind has it.
|
|
233
|
+
* A flat walk read that first inner `"` as the CLOSE of the outer one, so the `)` that ends the
|
|
234
|
+
* outer substitution landed inside a quote that was never opened, the depth was left standing at
|
|
235
|
+
* 1, and every newline in the rest of the script was swallowed: the script's own `hurl --test`
|
|
236
|
+
* never reached command position, and X1 xr-molecule-flash-m89 saw seven passing Hurl runs the
|
|
237
|
+
* platform could not see (the runner is read from the tree's own script TEXT —
|
|
238
|
+
* `molecule-dev/api/src/ai/supplied-tree-guard.ts` — so a script that does not classify records
|
|
239
|
+
* no runner, and every run of it reads as nothing).
|
|
240
|
+
*
|
|
209
241
|
* @param command - The shell command.
|
|
210
242
|
* @returns Each segment's text, in order, with empty ones dropped.
|
|
211
243
|
*/
|
|
212
244
|
function splitSegments(command) {
|
|
213
245
|
const parts = [];
|
|
214
246
|
let buffer = '';
|
|
215
|
-
|
|
216
|
-
|
|
247
|
+
/** The nestings this position sits inside, outermost first. */
|
|
248
|
+
const stack = [];
|
|
217
249
|
for (let i = 0; i < command.length; i++) {
|
|
218
250
|
const char = command[i] ?? '';
|
|
219
|
-
|
|
251
|
+
const next = command[i + 1];
|
|
252
|
+
const top = stack[stack.length - 1];
|
|
253
|
+
// A single-quoted string expands nothing at all: no escape, no substitution, and no quote
|
|
254
|
+
// but its own ends it.
|
|
255
|
+
if (top === 'single') {
|
|
220
256
|
buffer += char;
|
|
221
|
-
if (char === '
|
|
222
|
-
|
|
223
|
-
i += 1;
|
|
224
|
-
}
|
|
225
|
-
else if (char === quote)
|
|
226
|
-
quote = null;
|
|
257
|
+
if (char === "'")
|
|
258
|
+
stack.pop();
|
|
227
259
|
continue;
|
|
228
260
|
}
|
|
229
|
-
|
|
230
|
-
|
|
261
|
+
// A command substitution, wherever a shell opens one — inside double quotes too, and never
|
|
262
|
+
// inside single quotes (handled above). The `(` is consumed here so the depth is counted once.
|
|
263
|
+
if (char === '$' && next === '(') {
|
|
264
|
+
stack.push('sub');
|
|
265
|
+
buffer += '$(';
|
|
266
|
+
i += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
// Inside double quotes: a `"` closes them, a backslash escapes the next character (so
|
|
270
|
+
// `"\$("` opens nothing), and everything else is literal — a `'`, a `|`, a newline included.
|
|
271
|
+
if (top === 'double') {
|
|
272
|
+
if (char === '"')
|
|
273
|
+
stack.pop();
|
|
274
|
+
else if (char === '\\' && next !== undefined) {
|
|
275
|
+
buffer += char + next;
|
|
276
|
+
i += 1;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
231
279
|
buffer += char;
|
|
232
280
|
continue;
|
|
233
281
|
}
|
|
234
|
-
if (char === '
|
|
235
|
-
|
|
236
|
-
|
|
282
|
+
if (char === '"') {
|
|
283
|
+
stack.push('double');
|
|
284
|
+
buffer += char;
|
|
237
285
|
continue;
|
|
238
286
|
}
|
|
239
|
-
if (char === '
|
|
240
|
-
|
|
287
|
+
if (char === "'") {
|
|
288
|
+
stack.push('single');
|
|
241
289
|
buffer += char;
|
|
242
290
|
continue;
|
|
243
291
|
}
|
|
244
|
-
|
|
292
|
+
// Inside a substitution or a subshell of one: its own parentheses nest, its `|`/`;`/`&&`
|
|
293
|
+
// belong to the nested command, and its newlines are that command's too.
|
|
294
|
+
if (top === 'sub') {
|
|
245
295
|
if (char === '(')
|
|
246
|
-
|
|
296
|
+
stack.push('sub');
|
|
247
297
|
else if (char === ')')
|
|
248
|
-
|
|
298
|
+
stack.pop();
|
|
249
299
|
buffer += char;
|
|
250
300
|
continue;
|
|
251
301
|
}
|
|
252
|
-
|
|
302
|
+
if (char === '\\' && next !== undefined) {
|
|
303
|
+
buffer += char + next;
|
|
304
|
+
i += 1;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
253
307
|
if ((char === '&' && next === '&') || (char === '|' && next === '|')) {
|
|
254
308
|
parts.push(buffer);
|
|
255
309
|
buffer = '';
|
|
@@ -301,6 +355,28 @@ function segmentWords(text) {
|
|
|
301
355
|
export function commandSegments(command) {
|
|
302
356
|
return segmentsOf(command, 0);
|
|
303
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* The bound a command puts on its own run: the largest `timeout <N>` any of its segments runs
|
|
360
|
+
* under (`timeout 280 hurl --test …` → 280), or null when it carries none.
|
|
361
|
+
*
|
|
362
|
+
* This is the bound `man timeout` enforces by KILLING the command — so a run that used it did not
|
|
363
|
+
* finish, whatever the pipeline's own exit status says. It is read here, from the same parse every
|
|
364
|
+
* other command reader uses, rather than matched as text: `timeout` in an argument
|
|
365
|
+
* (`grep -n 'timeout 280' notes.md`) bounds nothing, and `-k 5` is the grace before the kill, not
|
|
366
|
+
* the bound ({@link WRAPPER_VALUE_FLAGS}).
|
|
367
|
+
*
|
|
368
|
+
* @param command - The shell command.
|
|
369
|
+
* @returns The seconds the command allows itself, or null.
|
|
370
|
+
*/
|
|
371
|
+
export function commandTimeoutSeconds(command) {
|
|
372
|
+
let bound = null;
|
|
373
|
+
for (const segment of segmentsOf(command, 0)) {
|
|
374
|
+
if (segment.timeoutSeconds === null)
|
|
375
|
+
continue;
|
|
376
|
+
bound = bound === null ? segment.timeoutSeconds : Math.max(bound, segment.timeoutSeconds);
|
|
377
|
+
}
|
|
378
|
+
return bound;
|
|
379
|
+
}
|
|
304
380
|
/**
|
|
305
381
|
* {@link commandSegments}'s recursion, so a `sh -c '<command>'` is opened as far as
|
|
306
382
|
* {@link MAX_NESTING} allows.
|
|
@@ -328,7 +404,12 @@ function segmentsOf(command, depth) {
|
|
|
328
404
|
continue;
|
|
329
405
|
const program = programName(word);
|
|
330
406
|
const args = words.slice(start.at + 1);
|
|
331
|
-
out.push({
|
|
407
|
+
out.push({
|
|
408
|
+
program,
|
|
409
|
+
args,
|
|
410
|
+
text: [program, ...args].join(' '),
|
|
411
|
+
timeoutSeconds: start.timeoutSeconds,
|
|
412
|
+
});
|
|
332
413
|
}
|
|
333
414
|
return out;
|
|
334
415
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A command that a budget stopped before it finished, and the state its result carries so no
|
|
3
|
+
* reader can mistake it for a run that ended.
|
|
4
|
+
*
|
|
5
|
+
* X1 xr-molecule-flash-m93 (2026-10-16, +12:37) sent the supplied RealWorld suite as
|
|
6
|
+
* `timeout 280 hurl --test --continue-on-error --jobs 1 … hurl/*.hurl 2>&1 | tail -60` and got
|
|
7
|
+
* back `exitCode: 0`, four finished files' lines and nothing else. Two lies in one result: the
|
|
8
|
+
* `timeout 280` killed the run at 280 s, and the `| tail` made the pipeline's status the status —
|
|
9
|
+
* so the executor read a suite that stopped at its fifth file as a suite that ran and nearly
|
|
10
|
+
* passed, fixed one message, and immediately sent the same 280-second command again (the run's
|
|
11
|
+
* third, ~14 minutes of a 60-minute cap across three of them).
|
|
12
|
+
*
|
|
13
|
+
* So the tool never reports a stopped run as `exitCode: 0`. Such a result carries
|
|
14
|
+
* `status: 'cut_at_budget'` and an exit code that is `null` whenever the number the pipeline
|
|
15
|
+
* reported belongs to the filter rather than to the run, and every reader of a run's verdict reads
|
|
16
|
+
* the state through {@link commandRunState} — a cut run is NOT RUN, never a pass.
|
|
17
|
+
*
|
|
18
|
+
* Two things stop a run, and both are read from the command and the result, never inferred:
|
|
19
|
+
*
|
|
20
|
+
* 1. **Its own bound** — a `timeout <N>` the command carries ({@link commandTimeoutSeconds}). The
|
|
21
|
+
* run used it, so the run was killed at it; with no closing tally in the output, the run did
|
|
22
|
+
* not finish. This is m93's shape.
|
|
23
|
+
* 2. **This tool's budget** — the backend runs the command under `timeout` (see
|
|
24
|
+
* `backends/sandbox.ts`), which reports 124 whatever the pipeline did, and returns everything
|
|
25
|
+
* printed until then. The budget is the tool's own, so its number belongs to the tool.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* The state a result carries when a budget stopped its command: a run that did not finish is not a
|
|
31
|
+
* run that passed, and no reader may treat it as one.
|
|
32
|
+
*/
|
|
33
|
+
export declare const CUT_AT_BUDGET = "cut_at_budget";
|
|
34
|
+
/** What a command's result records: a run that ended, or one a budget stopped. */
|
|
35
|
+
export type CommandRunState = 'ran' | typeof CUT_AT_BUDGET;
|
|
36
|
+
/**
|
|
37
|
+
* The state a command's result records — THE reader every verdict about a run goes through.
|
|
38
|
+
*
|
|
39
|
+
* A result that carries no state is a run that ended: the state is written where the run is
|
|
40
|
+
* stopped ({@link commandCutRun}), so a result without it was never stopped by a budget this
|
|
41
|
+
* platform set or can see. Results produced before this state existed read the same way, which is
|
|
42
|
+
* what keeps a past turn's tally readable.
|
|
43
|
+
*
|
|
44
|
+
* @param result - The exec_command / wait_for_task result.
|
|
45
|
+
* @returns The state the result records.
|
|
46
|
+
*/
|
|
47
|
+
export declare function commandRunState(result: unknown): CommandRunState;
|
|
48
|
+
/**
|
|
49
|
+
* Whether text carries a runner's closing summary ({@link SUITE_TALLY}).
|
|
50
|
+
*
|
|
51
|
+
* @param text - The run's stdout and stderr.
|
|
52
|
+
* @returns True when the runner's own end-of-run summary is there.
|
|
53
|
+
*/
|
|
54
|
+
export declare function suiteTally(text: string): boolean;
|
|
55
|
+
/** What stopped a run. */
|
|
56
|
+
export type CutBy = 'tool-budget' | 'command-timeout';
|
|
57
|
+
/** A run a budget stopped, as its result reports it. */
|
|
58
|
+
export interface CutRun {
|
|
59
|
+
/** The bound the run was allowed, in seconds. */
|
|
60
|
+
seconds: number;
|
|
61
|
+
/** Whether this tool's budget stopped it or the `timeout` the command carried. */
|
|
62
|
+
stoppedBy: CutBy;
|
|
63
|
+
/**
|
|
64
|
+
* The exit code the run's own process reported, or null when the code in the result belongs to
|
|
65
|
+
* the pipeline's filter (`… | tail`) rather than to the run — the case that read as success.
|
|
66
|
+
*/
|
|
67
|
+
exitCode: number | null;
|
|
68
|
+
/** How many files or tests the runner's own output shows finishing, or null when it shows none. */
|
|
69
|
+
ran: number | null;
|
|
70
|
+
/** How many the run was walking, when the runner printed it (Playwright's `[n/K]`), or null. */
|
|
71
|
+
total: number | null;
|
|
72
|
+
/** The noun the runner counts in. */
|
|
73
|
+
noun: 'files' | 'tests';
|
|
74
|
+
/** The last thing the run printed — the line a reader looks at first. */
|
|
75
|
+
doing: string | null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The run a command's result records as stopped by a budget, or null when the result is a run that
|
|
79
|
+
* ended (or says nothing about a run).
|
|
80
|
+
*
|
|
81
|
+
* Evidence, in order — never a guess:
|
|
82
|
+
*
|
|
83
|
+
* - `timeout`'s own 124 (the code the backend's wrapper returns whenever the TOOL's budget fired,
|
|
84
|
+
* whatever the pipeline ended with) — and, when the clock says the command's own smaller bound
|
|
85
|
+
* ran out first and less than the budget, that bound instead.
|
|
86
|
+
* - the command carried its own `timeout <N>`, N is inside this tool's budget (a larger one never
|
|
87
|
+
* fires — the tool's does), the run used N, and no runner printed a closing summary: the run was
|
|
88
|
+
* killed at its own bound. Its exit code, when the pipeline handed one back, is the filter's.
|
|
89
|
+
* - the run used this tool's whole budget behind a filter that withheld its output, and printed no
|
|
90
|
+
* closing summary: the budget stopped it, and no code in the result is the run's.
|
|
91
|
+
*
|
|
92
|
+
* @param command - The command the executor sent.
|
|
93
|
+
* @param result - Its result.
|
|
94
|
+
* @param elapsedMs - How long the command ran, in ms.
|
|
95
|
+
* @param budgetSeconds - This tool's foreground budget, in seconds (0 when it has none).
|
|
96
|
+
* @returns The stopped run, or null.
|
|
97
|
+
*/
|
|
98
|
+
export declare function commandCutRun(command: string, result: unknown, elapsedMs: number, budgetSeconds?: number): CutRun | null;
|
|
99
|
+
/**
|
|
100
|
+
* The facts a result records about the run a budget stopped it in — what a reader needs to say
|
|
101
|
+
* how far the run got WITHOUT re-reading its log or re-deriving the counts.
|
|
102
|
+
*
|
|
103
|
+
* The numbers are read from the runner's own output where it printed them (Hurl's per-file lines,
|
|
104
|
+
* Playwright's `[n/K]`), so a caller that can also see the suite itself — the file count of the
|
|
105
|
+
* `*.hurl` directory the run walked — fills in the `total` this could not know, and both then say
|
|
106
|
+
* the same sentence ({@link cutRunSentence}).
|
|
107
|
+
*
|
|
108
|
+
* @param result - The exec_command / wait_for_task result.
|
|
109
|
+
* @returns The facts the result carries, or null when it records no stopped run.
|
|
110
|
+
*/
|
|
111
|
+
export declare function cutRunFacts(result: unknown): CutRun | null;
|
|
112
|
+
/**
|
|
113
|
+
* The first line of a stopped run's result: how long it was allowed, what stopped it, how far it
|
|
114
|
+
* got, and the last thing it printed.
|
|
115
|
+
*
|
|
116
|
+
* Written to be quotable by the model in one sentence and to leave no room for "nearly passing":
|
|
117
|
+
* the counts come from the runner's own output ({@link runProgress}), and a `total` the caller
|
|
118
|
+
* read elsewhere (the suite's own file count) fills in the "M of K" it can then state.
|
|
119
|
+
*
|
|
120
|
+
* @param run - The stopped run.
|
|
121
|
+
* @returns The sentence.
|
|
122
|
+
*/
|
|
123
|
+
export declare function cutRunSentence(run: CutRun): string;
|
|
124
|
+
//# sourceMappingURL=cut-run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cut-run.d.ts","sourceRoot":"","sources":["../src/cut-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAKH;;;GAGG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAE5C,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,aAAa,CAAA;AAE1D;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAGhE;AA0BD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,0BAA0B;AAC1B,MAAM,MAAM,KAAK,GAAG,aAAa,GAAG,iBAAiB,CAAA;AAErD,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,EAAE,KAAK,CAAA;IAChB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mGAAmG;IACnG,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,gGAAgG;IAChG,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qCAAqC;IACrC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,yEAAyE;IACzE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AA2CD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,aAAa,SAAI,GAChB,MAAM,GAAG,IAAI,CA2Df;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAgB1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAkBlD"}
|
package/dist/cut-run.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A command that a budget stopped before it finished, and the state its result carries so no
|
|
3
|
+
* reader can mistake it for a run that ended.
|
|
4
|
+
*
|
|
5
|
+
* X1 xr-molecule-flash-m93 (2026-10-16, +12:37) sent the supplied RealWorld suite as
|
|
6
|
+
* `timeout 280 hurl --test --continue-on-error --jobs 1 … hurl/*.hurl 2>&1 | tail -60` and got
|
|
7
|
+
* back `exitCode: 0`, four finished files' lines and nothing else. Two lies in one result: the
|
|
8
|
+
* `timeout 280` killed the run at 280 s, and the `| tail` made the pipeline's status the status —
|
|
9
|
+
* so the executor read a suite that stopped at its fifth file as a suite that ran and nearly
|
|
10
|
+
* passed, fixed one message, and immediately sent the same 280-second command again (the run's
|
|
11
|
+
* third, ~14 minutes of a 60-minute cap across three of them).
|
|
12
|
+
*
|
|
13
|
+
* So the tool never reports a stopped run as `exitCode: 0`. Such a result carries
|
|
14
|
+
* `status: 'cut_at_budget'` and an exit code that is `null` whenever the number the pipeline
|
|
15
|
+
* reported belongs to the filter rather than to the run, and every reader of a run's verdict reads
|
|
16
|
+
* the state through {@link commandRunState} — a cut run is NOT RUN, never a pass.
|
|
17
|
+
*
|
|
18
|
+
* Two things stop a run, and both are read from the command and the result, never inferred:
|
|
19
|
+
*
|
|
20
|
+
* 1. **Its own bound** — a `timeout <N>` the command carries ({@link commandTimeoutSeconds}). The
|
|
21
|
+
* run used it, so the run was killed at it; with no closing tally in the output, the run did
|
|
22
|
+
* not finish. This is m93's shape.
|
|
23
|
+
* 2. **This tool's budget** — the backend runs the command under `timeout` (see
|
|
24
|
+
* `backends/sandbox.ts`), which reports 124 whatever the pipeline did, and returns everything
|
|
25
|
+
* printed until then. The budget is the tool's own, so its number belongs to the tool.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
import { commandTimeoutSeconds } from './command-position.js';
|
|
30
|
+
import { isSuiteRunCommand } from './utilities.js';
|
|
31
|
+
/**
|
|
32
|
+
* The state a result carries when a budget stopped its command: a run that did not finish is not a
|
|
33
|
+
* run that passed, and no reader may treat it as one.
|
|
34
|
+
*/
|
|
35
|
+
export const CUT_AT_BUDGET = 'cut_at_budget';
|
|
36
|
+
/**
|
|
37
|
+
* The state a command's result records — THE reader every verdict about a run goes through.
|
|
38
|
+
*
|
|
39
|
+
* A result that carries no state is a run that ended: the state is written where the run is
|
|
40
|
+
* stopped ({@link commandCutRun}), so a result without it was never stopped by a budget this
|
|
41
|
+
* platform set or can see. Results produced before this state existed read the same way, which is
|
|
42
|
+
* what keeps a past turn's tally readable.
|
|
43
|
+
*
|
|
44
|
+
* @param result - The exec_command / wait_for_task result.
|
|
45
|
+
* @returns The state the result records.
|
|
46
|
+
*/
|
|
47
|
+
export function commandRunState(result) {
|
|
48
|
+
if (result === null || typeof result !== 'object')
|
|
49
|
+
return 'ran';
|
|
50
|
+
return result.status === CUT_AT_BUDGET ? CUT_AT_BUDGET : 'ran';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The gap between a bound and the elapsed time a run killed at it reports. Zero, and deliberately
|
|
54
|
+
* not a tolerance: a command killed at its bound reports AT LEAST the bound (the shell's own
|
|
55
|
+
* bookkeeping runs after the kill, and this tool's round trip is on top), so any slack would only
|
|
56
|
+
* make a SHORT bound fire on a run that finished inside it (`timeout 5 curl …` answering in 3 s).
|
|
57
|
+
* A run that finished near its bound carried its runner's closing summary — {@link suiteTally}.
|
|
58
|
+
*
|
|
59
|
+
* @param boundSeconds - The bound the run was allowed.
|
|
60
|
+
* @returns The elapsed ms a run stopped at that bound reports at least.
|
|
61
|
+
*/
|
|
62
|
+
function atBoundMs(boundSeconds) {
|
|
63
|
+
return boundSeconds * 1000;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A runner's closing summary, printed only by a run that walked its whole suite: Hurl's
|
|
67
|
+
* `Executed files:` block and the `N passed` / `N failed` / `N flaky` line Playwright and Vitest
|
|
68
|
+
* print when they finish.
|
|
69
|
+
*
|
|
70
|
+
* This is what separates "stopped at the bound" from "used the bound and finished": a suite that
|
|
71
|
+
* printed its summary ran to its end, and is read as the run it was.
|
|
72
|
+
*/
|
|
73
|
+
const SUITE_TALLY = /\bExecuted files:\s*\d+|\b\d+\s+(?:passed|failed|flaky|skipped)\b/;
|
|
74
|
+
/**
|
|
75
|
+
* Whether text carries a runner's closing summary ({@link SUITE_TALLY}).
|
|
76
|
+
*
|
|
77
|
+
* @param text - The run's stdout and stderr.
|
|
78
|
+
* @returns True when the runner's own end-of-run summary is there.
|
|
79
|
+
*/
|
|
80
|
+
export function suiteTally(text) {
|
|
81
|
+
return SUITE_TALLY.test(text);
|
|
82
|
+
}
|
|
83
|
+
/** The last thing a run printed: its last non-blank line, as one line and short enough to quote. */
|
|
84
|
+
function lastPrinted(text) {
|
|
85
|
+
const lines = text.split('\n');
|
|
86
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
87
|
+
const line = (lines[i] ?? '').trim();
|
|
88
|
+
if (line)
|
|
89
|
+
return line.length > 180 ? `${line.slice(0, 177)}…` : line;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/** Hurl's per-file line: `Success hurl/articles.hurl (17 request(s) in 318 ms)`. */
|
|
94
|
+
const HURL_FILE_LINE = /^(?:Success|Failure)\s+(\S+)/gm;
|
|
95
|
+
/** Playwright's progress line — `[5/13] specs/auth.spec.ts:10:5 › signs up`. */
|
|
96
|
+
const PROGRESS_LINE = /^\s*\[(\d+)\/(\d+)\]/gm;
|
|
97
|
+
/**
|
|
98
|
+
* What the runner's own output says the run got through.
|
|
99
|
+
*
|
|
100
|
+
* Hurl prints a line per finished file and no total unless it finished; Playwright's line reporter
|
|
101
|
+
* prints `[n/K]` with both numbers. Neither is inferred from the command — a run that printed
|
|
102
|
+
* nothing says nothing, which is what `null` means here.
|
|
103
|
+
*
|
|
104
|
+
* @param text - The run's stdout and stderr.
|
|
105
|
+
* @param suite - Whether the command runs a suite.
|
|
106
|
+
* @returns How many finished, how many the run was walking, and the noun.
|
|
107
|
+
*/
|
|
108
|
+
function runProgress(text, suite) {
|
|
109
|
+
const files = new Set();
|
|
110
|
+
for (const m of text.matchAll(HURL_FILE_LINE))
|
|
111
|
+
files.add(m[1] ?? '');
|
|
112
|
+
if (files.size > 0)
|
|
113
|
+
return { ran: files.size, total: null, noun: 'files' };
|
|
114
|
+
let ran = null;
|
|
115
|
+
let total = null;
|
|
116
|
+
for (const m of text.matchAll(PROGRESS_LINE)) {
|
|
117
|
+
ran = Number(m[1]);
|
|
118
|
+
total = Number(m[2]);
|
|
119
|
+
}
|
|
120
|
+
if (ran !== null)
|
|
121
|
+
return { ran, total, noun: 'tests' };
|
|
122
|
+
return { ran: null, total: null, noun: suite ? 'tests' : 'files' };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The run a command's result records as stopped by a budget, or null when the result is a run that
|
|
126
|
+
* ended (or says nothing about a run).
|
|
127
|
+
*
|
|
128
|
+
* Evidence, in order — never a guess:
|
|
129
|
+
*
|
|
130
|
+
* - `timeout`'s own 124 (the code the backend's wrapper returns whenever the TOOL's budget fired,
|
|
131
|
+
* whatever the pipeline ended with) — and, when the clock says the command's own smaller bound
|
|
132
|
+
* ran out first and less than the budget, that bound instead.
|
|
133
|
+
* - the command carried its own `timeout <N>`, N is inside this tool's budget (a larger one never
|
|
134
|
+
* fires — the tool's does), the run used N, and no runner printed a closing summary: the run was
|
|
135
|
+
* killed at its own bound. Its exit code, when the pipeline handed one back, is the filter's.
|
|
136
|
+
* - the run used this tool's whole budget behind a filter that withheld its output, and printed no
|
|
137
|
+
* closing summary: the budget stopped it, and no code in the result is the run's.
|
|
138
|
+
*
|
|
139
|
+
* @param command - The command the executor sent.
|
|
140
|
+
* @param result - Its result.
|
|
141
|
+
* @param elapsedMs - How long the command ran, in ms.
|
|
142
|
+
* @param budgetSeconds - This tool's foreground budget, in seconds (0 when it has none).
|
|
143
|
+
* @returns The stopped run, or null.
|
|
144
|
+
*/
|
|
145
|
+
export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0) {
|
|
146
|
+
if (typeof command !== 'string' || !command.trim())
|
|
147
|
+
return null;
|
|
148
|
+
if (result === null || typeof result !== 'object')
|
|
149
|
+
return null;
|
|
150
|
+
if (!Number.isFinite(elapsedMs))
|
|
151
|
+
return null;
|
|
152
|
+
const r = result;
|
|
153
|
+
const exitCode = typeof r.exitCode === 'number' ? r.exitCode : null;
|
|
154
|
+
const text = `${String(r.stdout ?? '')}\n${String(r.stderr ?? '')}`;
|
|
155
|
+
// A suite that printed its summary ran to its end, whatever it was wrapped in.
|
|
156
|
+
if (suiteTally(text))
|
|
157
|
+
return null;
|
|
158
|
+
const suite = isSuiteRunCommand(command);
|
|
159
|
+
const withheld = /\|\s*(?:tail|head|grep|egrep|fgrep|sort|uniq|wc|jq)\b/.test(command);
|
|
160
|
+
const progress = runProgress(text, suite);
|
|
161
|
+
const doing = lastPrinted(text);
|
|
162
|
+
const declared = commandTimeoutSeconds(command);
|
|
163
|
+
// A command whose own bound is inside this tool's budget is stopped by ITS bound: a larger one
|
|
164
|
+
// never gets the chance — the backend's `timeout` fires first (see the module doc). With no
|
|
165
|
+
// tool budget at all (a reader re-reading a background run) the command's own bound is the only
|
|
166
|
+
// one there is.
|
|
167
|
+
const ownBoundStops = budgetSeconds === 0 || (declared !== null && declared < budgetSeconds);
|
|
168
|
+
// `timeout`'s own 124 — the code the backend's wrapper returns when the TOOL's budget fires,
|
|
169
|
+
// whatever the pipeline ended with (`backends/sandbox.ts`). Which of the two bounds fired is the
|
|
170
|
+
// clock's to say: a run killed at the tool's budget reports at least the budget, and one killed
|
|
171
|
+
// at its own bound reports at least that bound and less than the budget.
|
|
172
|
+
if (budgetSeconds > 0 && exitCode === 124) {
|
|
173
|
+
const ownFired = declared !== null &&
|
|
174
|
+
ownBoundStops &&
|
|
175
|
+
elapsedMs >= atBoundMs(declared) &&
|
|
176
|
+
elapsedMs < atBoundMs(budgetSeconds);
|
|
177
|
+
return {
|
|
178
|
+
seconds: ownFired ? declared : budgetSeconds,
|
|
179
|
+
stoppedBy: ownFired ? 'command-timeout' : 'tool-budget',
|
|
180
|
+
exitCode,
|
|
181
|
+
...progress,
|
|
182
|
+
doing,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (declared !== null && ownBoundStops && elapsedMs >= atBoundMs(declared)) {
|
|
186
|
+
return {
|
|
187
|
+
seconds: declared,
|
|
188
|
+
stoppedBy: 'command-timeout',
|
|
189
|
+
// A code the filter produced is not the run's: `… | tail` handed back 0 forever (m93).
|
|
190
|
+
exitCode: withheld || exitCode === 0 ? null : exitCode,
|
|
191
|
+
...progress,
|
|
192
|
+
doing,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// The tool's budget with its code masked: the run used the whole budget and the pipeline left
|
|
196
|
+
// nothing readable of it — no code of its own, and no runner's closing summary.
|
|
197
|
+
if (budgetSeconds > 0 && withheld && elapsedMs >= atBoundMs(budgetSeconds)) {
|
|
198
|
+
return {
|
|
199
|
+
seconds: budgetSeconds,
|
|
200
|
+
stoppedBy: 'tool-budget',
|
|
201
|
+
exitCode: exitCode === 0 ? null : exitCode,
|
|
202
|
+
...progress,
|
|
203
|
+
doing,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The facts a result records about the run a budget stopped it in — what a reader needs to say
|
|
210
|
+
* how far the run got WITHOUT re-reading its log or re-deriving the counts.
|
|
211
|
+
*
|
|
212
|
+
* The numbers are read from the runner's own output where it printed them (Hurl's per-file lines,
|
|
213
|
+
* Playwright's `[n/K]`), so a caller that can also see the suite itself — the file count of the
|
|
214
|
+
* `*.hurl` directory the run walked — fills in the `total` this could not know, and both then say
|
|
215
|
+
* the same sentence ({@link cutRunSentence}).
|
|
216
|
+
*
|
|
217
|
+
* @param result - The exec_command / wait_for_task result.
|
|
218
|
+
* @returns The facts the result carries, or null when it records no stopped run.
|
|
219
|
+
*/
|
|
220
|
+
export function cutRunFacts(result) {
|
|
221
|
+
if (result === null || typeof result !== 'object')
|
|
222
|
+
return null;
|
|
223
|
+
if (commandRunState(result) !== CUT_AT_BUDGET)
|
|
224
|
+
return null;
|
|
225
|
+
const cut = result.cutRun;
|
|
226
|
+
if (cut === null || typeof cut !== 'object')
|
|
227
|
+
return null;
|
|
228
|
+
const c = cut;
|
|
229
|
+
if (typeof c.seconds !== 'number' || typeof c.stoppedBy !== 'string')
|
|
230
|
+
return null;
|
|
231
|
+
return {
|
|
232
|
+
seconds: c.seconds,
|
|
233
|
+
stoppedBy: c.stoppedBy,
|
|
234
|
+
exitCode: typeof c.exitCode === 'number' ? c.exitCode : null,
|
|
235
|
+
ran: typeof c.ran === 'number' ? c.ran : null,
|
|
236
|
+
total: typeof c.total === 'number' ? c.total : null,
|
|
237
|
+
noun: c.noun === 'tests' ? 'tests' : 'files',
|
|
238
|
+
doing: typeof c.doing === 'string' && c.doing ? c.doing : null,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The first line of a stopped run's result: how long it was allowed, what stopped it, how far it
|
|
243
|
+
* got, and the last thing it printed.
|
|
244
|
+
*
|
|
245
|
+
* Written to be quotable by the model in one sentence and to leave no room for "nearly passing":
|
|
246
|
+
* the counts come from the runner's own output ({@link runProgress}), and a `total` the caller
|
|
247
|
+
* read elsewhere (the suite's own file count) fills in the "M of K" it can then state.
|
|
248
|
+
*
|
|
249
|
+
* @param run - The stopped run.
|
|
250
|
+
* @returns The sentence.
|
|
251
|
+
*/
|
|
252
|
+
export function cutRunSentence(run) {
|
|
253
|
+
const by = run.stoppedBy === 'tool-budget' ? "this tool's limit" : `its own \`timeout ${run.seconds}\``;
|
|
254
|
+
const lead = `This run was stopped after ${run.seconds}s (${by})`;
|
|
255
|
+
const count = run.ran !== null && run.total !== null
|
|
256
|
+
? `: ${run.ran} of ${run.total} ${run.noun} ran, the rest did not run`
|
|
257
|
+
: run.ran !== null
|
|
258
|
+
? `: ${run.ran} ${run.noun} ran and the rest did not run`
|
|
259
|
+
: ' before it finished';
|
|
260
|
+
const doing = run.doing ? `; the last thing it was doing: ${run.doing}` : '';
|
|
261
|
+
// Why the result carries no code: without this the model reads the missing field as an
|
|
262
|
+
// omission, and the number it used to read there was the pipeline's own 0.
|
|
263
|
+
const code = run.exitCode === null
|
|
264
|
+
? ' It has no exit code of its own to report — the one this pipeline produced belongs to the filter, not to the run.'
|
|
265
|
+
: '';
|
|
266
|
+
return `${lead}${count}${doing}.${code}`;
|
|
267
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -51,6 +51,18 @@
|
|
|
51
51
|
* `newlineNote` hint beside an unrelated-looking parse error, which an executor
|
|
52
52
|
* repairs by rewriting the fused line's text and fuses the next one. The
|
|
53
53
|
* `newlineNote` alone is still returned when the file parses.
|
|
54
|
+
* - **A replacement that only changes a line break at its boundary is refused,
|
|
55
|
+
* not applied.** When `new_string` is `old_string` with its trailing newline
|
|
56
|
+
* added or removed and nothing else, the edit can only move text across lines —
|
|
57
|
+
* joining the next line onto the replaced text, or pushing the rest of the line
|
|
58
|
+
* down — which is what a weak model does by accident when it repeats a line it
|
|
59
|
+
* means to keep (flash-m28, m55, m62, m89, five times in the m89 run alone). The
|
|
60
|
+
* replacement is skipped, the rest of the batch is applied, and the result
|
|
61
|
+
* carries `skippedNote`: one line naming the replacement, what applying it would
|
|
62
|
+
* have done, and how to say it on purpose (include the next line in `old_string`).
|
|
63
|
+
* The file's own next character decides: text on the same line is a join or a
|
|
64
|
+
* split and is refused; a blank line after it, or the end of the file, means no
|
|
65
|
+
* text crosses lines and the edit is applied as written.
|
|
54
66
|
* - **A parameter under another name is not a broken tool.** Every tool's input
|
|
55
67
|
* is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
|
|
56
68
|
* snake_case), and a handler that throws returns an error naming the missing
|
|
@@ -121,9 +133,11 @@ export * from './backends/local.js';
|
|
|
121
133
|
export * from './backends/sandbox.js';
|
|
122
134
|
export * from './browser-guard.js';
|
|
123
135
|
export * from './command-position.js';
|
|
136
|
+
export * from './cut-run.js';
|
|
124
137
|
export * from './input-normalizer.js';
|
|
125
138
|
export * from './no-test-run.js';
|
|
126
139
|
export * from './schemas.js';
|
|
140
|
+
export * from './suite-overlap.js';
|
|
127
141
|
export * from './system-prompt.js';
|
|
128
142
|
export * from './tools.js';
|
|
129
143
|
export * from './types.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkIG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
|