@molecule/api-ai-tools 1.5.16 → 1.5.18
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 +22 -2
- package/dist/tools.d.ts +13 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +200 -33
- package/dist/utilities.d.ts +5 -1
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +15 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-10-
|
|
6
|
+
Generated: 2026-10-16T09:30:45.054Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-tools
|
|
@@ -488,6 +488,24 @@ function buildAgentPrompt(ctx: PromptContext): string
|
|
|
488
488
|
|
|
489
489
|
**Returns:** Fully assembled system prompt text for the coding agent.
|
|
490
490
|
|
|
491
|
+
#### `moleculeDistReadNote(path)`
|
|
492
|
+
|
|
493
|
+
The steer attached to a SUCCESSFUL read of a package's built output. The ENOENT
|
|
494
|
+
redirect below fires only when the read FAILS, so a model that knows the hoisted
|
|
495
|
+
layout reads `node_modules/@molecule/<pkg>/dist/*.js` happily and never learns the
|
|
496
|
+
documented API is one call away (X0 flash-m70: 24 such reads in 57 s, no note on
|
|
497
|
+
any of them). Carried as `distNote` rather than merged into `note` so a host that
|
|
498
|
+
has read_molecule_doc (molecule-dev's cadence note, which also names the functions)
|
|
499
|
+
can supersede it without string-matching.
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
function moleculeDistReadNote(path: string): string | null
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
- `path` — The resolved path that was read.
|
|
506
|
+
|
|
507
|
+
**Returns:** The note, or null when the path is not inside a package's `dist`.
|
|
508
|
+
|
|
491
509
|
#### `buildTools(backend, config)`
|
|
492
510
|
|
|
493
511
|
Build a complete set of AI agent tools bound to an execution backend.
|
|
@@ -1027,7 +1045,9 @@ const MAX_READ_RETURN_CHARS: number
|
|
|
1027
1045
|
|
|
1028
1046
|
#### `SMALL_FILE_WHOLE_CHARS`
|
|
1029
1047
|
|
|
1030
|
-
A file this small is returned whole
|
|
1048
|
+
A file this small is returned whole on a second window of it (paging it costs a call each).
|
|
1049
|
+
The same bound as {@link MAX_READ_RETURN_CHARS}: the windowed-read note promises that a
|
|
1050
|
+
file under it "fits in ONE call", and a smaller bound here broke that promise (X1 flash-m67).
|
|
1031
1051
|
|
|
1032
1052
|
```typescript
|
|
1033
1053
|
const SMALL_FILE_WHOLE_CHARS: number
|
package/dist/tools.d.ts
CHANGED
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { AITool } from '@molecule/api-ai';
|
|
10
10
|
import type { ExecutionBackend, ToolBuildConfig } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* The steer attached to a SUCCESSFUL read of a package's built output. The ENOENT
|
|
13
|
+
* redirect below fires only when the read FAILS, so a model that knows the hoisted
|
|
14
|
+
* layout reads `node_modules/@molecule/<pkg>/dist/*.js` happily and never learns the
|
|
15
|
+
* documented API is one call away (X0 flash-m70: 24 such reads in 57 s, no note on
|
|
16
|
+
* any of them). Carried as `distNote` rather than merged into `note` so a host that
|
|
17
|
+
* has read_molecule_doc (molecule-dev's cadence note, which also names the functions)
|
|
18
|
+
* can supersede it without string-matching.
|
|
19
|
+
*
|
|
20
|
+
* @param path - The resolved path that was read.
|
|
21
|
+
* @returns The note, or null when the path is not inside a package's `dist`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function moleculeDistReadNote(path: string): string | null;
|
|
11
24
|
/**
|
|
12
25
|
* Build a complete set of AI agent tools bound to an execution backend.
|
|
13
26
|
*
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AA0H9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA6CnE;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAutDxF"}
|
package/dist/tools.js
CHANGED
|
@@ -25,12 +25,118 @@ const waitedByTask = new Map();
|
|
|
25
25
|
const logBytesByTask = new Map();
|
|
26
26
|
/** The file a background command writes its own output to, when it redirects it. */
|
|
27
27
|
const outputFileByTask = new Map();
|
|
28
|
+
/**
|
|
29
|
+
* Task ids whose own-output file has already been looked for this process.
|
|
30
|
+
* {@link outputFileByTask} is empty for every task after a restart — it is
|
|
31
|
+
* process memory and the task is not — so without this the recovery below
|
|
32
|
+
* would re-read the task's script on every single wait.
|
|
33
|
+
*/
|
|
34
|
+
const outputFileProbed = new Set();
|
|
35
|
+
/** A millisecond span the way a model reads it: seconds under 90, else minutes. */
|
|
36
|
+
function durationText(ms) {
|
|
37
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
38
|
+
const mins = Math.floor(s / 60);
|
|
39
|
+
return s < 90 ? `${s} s` : s % 60 ? `${mins} min ${s % 60} s` : `${mins} min`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* When a background command started, read from its own id: the handle is minted
|
|
43
|
+
* as `mol-bg-${Date.now().toString(36)}-…`, so the start stamp is in the id and
|
|
44
|
+
* no new state has to be kept to have it.
|
|
45
|
+
*
|
|
46
|
+
* @param taskId - The handle a run_in_background command returned.
|
|
47
|
+
* @returns Epoch ms, or null when the id carries no usable stamp (a hand-written
|
|
48
|
+
* or synthetic id) — a wrong number in a note is worse than none.
|
|
49
|
+
*/
|
|
50
|
+
function taskStartedAt(taskId) {
|
|
51
|
+
const stamp = Number.parseInt(taskId.split('-')[2] ?? '', 36);
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
return Number.isFinite(stamp) && stamp > now - 24 * 3600_000 && stamp < now + 60_000
|
|
54
|
+
? stamp
|
|
55
|
+
: null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The progress a run reports in its own log: the last `[done/total]` counter a
|
|
59
|
+
* Playwright reporter writes (`[86/139] [chromium] › e2e/settings.spec.ts:…`).
|
|
60
|
+
*
|
|
61
|
+
* @param log - The log slice this wait read.
|
|
62
|
+
* @returns The counter, or null when the log carries none.
|
|
63
|
+
*/
|
|
64
|
+
function logProgress(log) {
|
|
65
|
+
const last = [...log.matchAll(/\[(\d+)\/(\d+)\]/g)].at(-1);
|
|
66
|
+
const done = Number(last?.[1]);
|
|
67
|
+
const total = Number(last?.[2]);
|
|
68
|
+
return last && total > 0 && done >= 0 && done <= total ? { done, total } : null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* What a wait knows about the command BESIDES "it has not exited": how long it
|
|
72
|
+
* has been running, whether its output is still growing, how far its own log
|
|
73
|
+
* says it has got, and how much longer that implies.
|
|
74
|
+
*
|
|
75
|
+
* X1 flash-m62 (2026-10-16) spent three 290 s waits on one 8.3-minute suite and
|
|
76
|
+
* every answer read only "Still running after 280s": the run could not tell a
|
|
77
|
+
* suite with 30 s left from one with 6 minutes left, so the wait cadence — not
|
|
78
|
+
* the suite — set the pace, and the run learned nothing for its 14 minutes of
|
|
79
|
+
* waiting. The elapsed time comes from the task id, the progress from the log
|
|
80
|
+
* this wait already read, and the time left is the rate so far, said as such.
|
|
81
|
+
*
|
|
82
|
+
* @param o - The wait's facts: `taskId`, the `slice` of the log it read,
|
|
83
|
+
* `stillWriting` (the log grew during this wait), `bytes` (the log's size, null
|
|
84
|
+
* when unreadable), and `ceilingMs` (this call's ceiling, for "one more wait").
|
|
85
|
+
* @returns A sentence to append to a running note, or '' when nothing is known.
|
|
86
|
+
*/
|
|
87
|
+
function waitProgressNote(o) {
|
|
88
|
+
const startedAt = taskStartedAt(o.taskId);
|
|
89
|
+
const elapsedMs = startedAt === null ? null : Math.max(0, Date.now() - startedAt);
|
|
90
|
+
const parts = [];
|
|
91
|
+
if (elapsedMs !== null)
|
|
92
|
+
parts.push(`it has been running ${durationText(elapsedMs)}`);
|
|
93
|
+
if (o.stillWriting)
|
|
94
|
+
parts.push(`its output is still growing (${o.bytes} bytes) — slow, not stuck`);
|
|
95
|
+
else if (o.bytes !== null)
|
|
96
|
+
parts.push(`its log has not grown during this wait (${o.bytes} bytes)`);
|
|
97
|
+
const counter = logProgress(o.slice);
|
|
98
|
+
if (counter) {
|
|
99
|
+
parts.push(`its log is at ${counter.done} of ${counter.total}`);
|
|
100
|
+
if (elapsedMs !== null && counter.done > 0 && counter.done < counter.total) {
|
|
101
|
+
const leftMs = Math.round((elapsedMs * (counter.total - counter.done)) / counter.done);
|
|
102
|
+
parts.push(leftMs <= o.ceilingMs
|
|
103
|
+
? `about ${durationText(leftMs)} left at that rate — one more wait covers it`
|
|
104
|
+
: `about ${durationText(leftMs)} left at that rate (${Math.ceil(leftMs / o.ceilingMs)} more waits)`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!parts.length)
|
|
108
|
+
return '';
|
|
109
|
+
const sentence = parts.join('; ');
|
|
110
|
+
return ` ${sentence[0].toUpperCase()}${sentence.slice(1)}.`;
|
|
111
|
+
}
|
|
28
112
|
import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, fusedLine, isEnvFilePath, isOutsideRoot, isValidGlob, MAX_BATCH_READ_BYTES, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_OUTPUT_SIZE, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, redactSecrets, redactSecretsInCode, resolvePath, RUNNING_FAILURE_LINES, runningOutputSliceCommand, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
|
|
29
113
|
/**
|
|
30
114
|
* How long exec_command waits for a command it backgrounded only because the
|
|
31
115
|
* command asked for more than the tool's ceiling, before handing back a task id.
|
|
32
116
|
*/
|
|
33
117
|
const OVER_CEILING_GRACE_MS = 45_000;
|
|
118
|
+
/**
|
|
119
|
+
* The steer attached to a SUCCESSFUL read of a package's built output. The ENOENT
|
|
120
|
+
* redirect below fires only when the read FAILS, so a model that knows the hoisted
|
|
121
|
+
* layout reads `node_modules/@molecule/<pkg>/dist/*.js` happily and never learns the
|
|
122
|
+
* documented API is one call away (X0 flash-m70: 24 such reads in 57 s, no note on
|
|
123
|
+
* any of them). Carried as `distNote` rather than merged into `note` so a host that
|
|
124
|
+
* has read_molecule_doc (molecule-dev's cadence note, which also names the functions)
|
|
125
|
+
* can supersede it without string-matching.
|
|
126
|
+
*
|
|
127
|
+
* @param path - The resolved path that was read.
|
|
128
|
+
* @returns The note, or null when the path is not inside a package's `dist`.
|
|
129
|
+
*/
|
|
130
|
+
export function moleculeDistReadNote(path) {
|
|
131
|
+
const pkg = /node_modules\/@molecule\/([a-z0-9-]+)\/dist\//.exec(path)?.[1];
|
|
132
|
+
if (!pkg)
|
|
133
|
+
return null;
|
|
134
|
+
return (`This is @molecule/${pkg}'s compiled dist, not its API: read_molecule_doc("@molecule/${pkg}") ` +
|
|
135
|
+
'returns its README.md — the functions, options and bond wiring — in one call.' +
|
|
136
|
+
(/\.d\.ts$/.test(path)
|
|
137
|
+
? ' The declarations above are generated from it.'
|
|
138
|
+
: ' For one signature, read the matching .d.ts file.'));
|
|
139
|
+
}
|
|
34
140
|
/**
|
|
35
141
|
* Build a complete set of AI agent tools bound to an execution backend.
|
|
36
142
|
*
|
|
@@ -135,6 +241,8 @@ export function buildTools(backend, config) {
|
|
|
135
241
|
}
|
|
136
242
|
/** Paths read with a window in this tool set, to tell paging from one targeted read. */
|
|
137
243
|
const windowedReads = new Set();
|
|
244
|
+
/** The text each path was last handed back WHOLE as, so that correction fires once per version. */
|
|
245
|
+
const wholeReturned = new Map();
|
|
138
246
|
/**
|
|
139
247
|
* Verify that `path` does not symlink outside the project root when guards are enabled.
|
|
140
248
|
*
|
|
@@ -365,6 +473,10 @@ export function buildTools(backend, config) {
|
|
|
365
473
|
const symlinkErr = await checkSymlink(path);
|
|
366
474
|
if (symlinkErr)
|
|
367
475
|
return { error: symlinkErr };
|
|
476
|
+
// Carried on every successful read of a package's built output, so no dist read
|
|
477
|
+
// goes out unsteered (see moleculeDistReadNote).
|
|
478
|
+
const distNote = moleculeDistReadNote(path);
|
|
479
|
+
const noted = (r) => distNote ? { ...r, distNote } : r;
|
|
368
480
|
try {
|
|
369
481
|
const content = await backend.readFile(path);
|
|
370
482
|
// A read that produced no usable text is verified against the file's real
|
|
@@ -376,12 +488,12 @@ export function buildTools(backend, config) {
|
|
|
376
488
|
const verdict = await classifyEmptyRead(path, 'read_file');
|
|
377
489
|
if ('error' in verdict)
|
|
378
490
|
return verdict;
|
|
379
|
-
return {
|
|
491
|
+
return noted({
|
|
380
492
|
path,
|
|
381
493
|
content: sanitizeFileContent(verdict.content, path),
|
|
382
494
|
...(verdict.content === '' ? { empty: true } : {}),
|
|
383
495
|
...(verdict.note ? { note: verdict.note } : {}),
|
|
384
|
-
};
|
|
496
|
+
});
|
|
385
497
|
}
|
|
386
498
|
if (content.length > MAX_READ_SIZE)
|
|
387
499
|
return {
|
|
@@ -395,21 +507,32 @@ export function buildTools(backend, config) {
|
|
|
395
507
|
: window;
|
|
396
508
|
// A SECOND window of the same small file: hand back all of it (one window is a targeted read). X1 flash-m50 paged files that fit in one
|
|
397
509
|
// call 13 times in a row, each window answered with "read it again with no offset/limit".
|
|
510
|
+
// The bound is the one that note states (SMALL_FILE_WHOLE_CHARS = MAX_READ_RETURN_CHARS): 24 KB left
|
|
511
|
+
// RealWorld's 24,935-byte openapi.yml paged nine times under "fits in ONE call" (flash-m67).
|
|
398
512
|
const small = sliceLines(content, window);
|
|
399
513
|
const pagedBefore = small?.truncated ? windowedReads.has(path) : false;
|
|
400
514
|
if (small?.truncated)
|
|
401
515
|
windowedReads.add(path);
|
|
402
|
-
|
|
403
|
-
|
|
516
|
+
// Once per version of the file: after the whole text went back, a later window is a
|
|
517
|
+
// targeted read and gets its window (X1 flash-m67: 25 firings, 8 byte-identical repeats).
|
|
518
|
+
const sentWhole = wholeReturned.get(path) === content;
|
|
519
|
+
if (small?.truncated &&
|
|
520
|
+
pagedBefore &&
|
|
521
|
+
!sentWhole &&
|
|
522
|
+
content.length <= SMALL_FILE_WHOLE_CHARS) {
|
|
523
|
+
wholeReturned.set(path, content);
|
|
524
|
+
return noted({
|
|
404
525
|
path,
|
|
405
526
|
content: sanitizeFileContent(content, path),
|
|
527
|
+
offset: 1,
|
|
528
|
+
lines: small.totalLines,
|
|
406
529
|
totalLines: small.totalLines,
|
|
407
530
|
note: `The whole file is returned (${Math.round(content.length / 1024)} KB, ${small.totalLines} lines): a file this small fits in one call, so it is not paged.`,
|
|
408
|
-
};
|
|
531
|
+
});
|
|
409
532
|
}
|
|
410
533
|
const windowed = sliceLines(content, effectiveWindow);
|
|
411
534
|
if (windowed) {
|
|
412
|
-
return {
|
|
535
|
+
return noted({
|
|
413
536
|
path,
|
|
414
537
|
content: sanitizeFileContent(windowed.text, path),
|
|
415
538
|
offset: windowed.offset,
|
|
@@ -422,18 +545,21 @@ export function buildTools(backend, config) {
|
|
|
422
545
|
// exact next window so the executor does not have to compute it.
|
|
423
546
|
...(windowed.truncated
|
|
424
547
|
? {
|
|
425
|
-
note:
|
|
426
|
-
? `Lines ${windowed.offset}-${windowed.offset + windowed.lines - 1} of ` +
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
548
|
+
note: sentWhole
|
|
549
|
+
? `Lines ${windowed.offset}-${windowed.offset + windowed.lines - 1} of ${windowed.totalLines}; ` +
|
|
550
|
+
'the whole file, unchanged, was returned earlier in this conversation.'
|
|
551
|
+
: content.length <= MAX_READ_RETURN_CHARS
|
|
552
|
+
? `Lines ${windowed.offset}-${windowed.offset + windowed.lines - 1} of ` +
|
|
553
|
+
`${windowed.totalLines}. The whole file is ${Math.round(content.length / 1024)} KB ` +
|
|
554
|
+
`and fits in ONE call — read it again with no offset/limit instead of paging.`
|
|
555
|
+
: `Lines ${windowed.offset}-${windowed.offset + windowed.lines - 1} of ` +
|
|
556
|
+
`${windowed.totalLines}. Next window: offset ${windowed.offset + windowed.lines}, ` +
|
|
557
|
+
`limit ${windowed.lines}.`,
|
|
432
558
|
}
|
|
433
559
|
: {}),
|
|
434
|
-
};
|
|
560
|
+
});
|
|
435
561
|
}
|
|
436
|
-
return { path, content: sanitizeFileContent(content, path) };
|
|
562
|
+
return noted({ path, content: sanitizeFileContent(content, path) });
|
|
437
563
|
}
|
|
438
564
|
catch (e) {
|
|
439
565
|
// Backends (e.g. the docker provider's `cat`) already prefix "Failed to read <path>: ";
|
|
@@ -1366,6 +1492,33 @@ export function buildTools(backend, config) {
|
|
|
1366
1492
|
// server-side costs no round trip and no context.
|
|
1367
1493
|
async wait_for_task(input) {
|
|
1368
1494
|
const taskId = typeof input.taskId === 'string' ? input.taskId.trim() : '';
|
|
1495
|
+
// Where a task writes its own output is process memory; the task is not.
|
|
1496
|
+
// An API process swapped mid-run (a release, a patch, a crash) leaves the
|
|
1497
|
+
// command running in the sandbox with nothing left that remembers the file
|
|
1498
|
+
// it redirects to — and then `wait_for_task` returns the task's near-empty
|
|
1499
|
+
// log as the command's whole result, which is the X1 flash-m48 loss all
|
|
1500
|
+
// over again (X1 flash-m63's +43:20 restart: the public walk's handle
|
|
1501
|
+
// outlived the process, its redirect target did not). The script the task
|
|
1502
|
+
// was started from is still on disk and carries the command, so read the
|
|
1503
|
+
// target back from there. The id is format-checked before any /tmp path is
|
|
1504
|
+
// built from it, exactly as the reads below are.
|
|
1505
|
+
if (!outputFileByTask.has(taskId) &&
|
|
1506
|
+
!outputFileProbed.has(taskId) &&
|
|
1507
|
+
/^mol-bg-[a-z0-9]+-[a-z0-9]+$/.test(taskId)) {
|
|
1508
|
+
outputFileProbed.add(taskId);
|
|
1509
|
+
try {
|
|
1510
|
+
const target = outputRedirectTarget(await backend.readFile(`/tmp/${taskId}.sh`));
|
|
1511
|
+
// A relative target was written against the command's own cwd, which
|
|
1512
|
+
// the handle does not carry; the project root is where a background
|
|
1513
|
+
// command runs unless the caller passed one.
|
|
1514
|
+
if (target)
|
|
1515
|
+
outputFileByTask.set(taskId, target.startsWith('/') ? target : `${root}/${target}`);
|
|
1516
|
+
}
|
|
1517
|
+
catch (_error) {
|
|
1518
|
+
// No script under that id: this process did not start the task, so
|
|
1519
|
+
// there is nothing to recover. The log stays the whole result.
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1369
1522
|
// A command that has been waited on for stuckAfterMs() in total without
|
|
1370
1523
|
// exiting is not going to: on one run the executor waited five times,
|
|
1371
1524
|
// 120 s each, on an e2e suite that could never start (no browser page
|
|
@@ -1447,16 +1600,44 @@ export function buildTools(backend, config) {
|
|
|
1447
1600
|
const lastSize = logBytesByTask.get(taskId);
|
|
1448
1601
|
if (grownTo !== null)
|
|
1449
1602
|
logBytesByTask.set(taskId, grownTo);
|
|
1603
|
+
// Evidence of liveness, read the same way by every answer below: a log
|
|
1604
|
+
// that grew during THIS wait means the command is working. On the first
|
|
1605
|
+
// wait any output at all counts (there is no earlier size to compare).
|
|
1450
1606
|
const stillWriting = grownTo !== null && (lastSize === undefined ? grownTo > 0 : grownTo > lastSize);
|
|
1607
|
+
// The log's own slice, read ONCE for whichever answer follows: it is what
|
|
1608
|
+
// the progress sentence is built from, and a wait that showed nothing was
|
|
1609
|
+
// waited blind (X1 flash-m50: 4m42s across two waits on an 11-minute run).
|
|
1610
|
+
// The slice leads with the failure lines, not the last bytes — a suite's
|
|
1611
|
+
// tail is its progress lines (X1 flash-m61: the last 3 000 bytes of a
|
|
1612
|
+
// 139-test walk were the skip list).
|
|
1613
|
+
const own = outputFileByTask.get(taskId);
|
|
1614
|
+
let soFar = '';
|
|
1615
|
+
try {
|
|
1616
|
+
const r = await backend.run(runningOutputSliceCommand(log, own), { timeout: 5_000 });
|
|
1617
|
+
soFar = r.stdout ?? '';
|
|
1618
|
+
}
|
|
1619
|
+
catch (_error) {
|
|
1620
|
+
// No slice: the notes still say how to peek, and the task id still
|
|
1621
|
+
// carries how long the command has been running.
|
|
1622
|
+
}
|
|
1623
|
+
const progress = waitProgressNote({
|
|
1624
|
+
taskId,
|
|
1625
|
+
slice: soFar,
|
|
1626
|
+
stillWriting,
|
|
1627
|
+
bytes: grownTo,
|
|
1628
|
+
ceilingMs: ceiling,
|
|
1629
|
+
});
|
|
1630
|
+
const sliceField = soFar.trim() ? { outputSoFar: sanitizeOutput(soFar) } : {};
|
|
1451
1631
|
if (stillWriting && waitedBefore + waitedMs >= stuckAfterMs()) {
|
|
1452
1632
|
waitedByTask.set(taskId, 0);
|
|
1453
1633
|
return {
|
|
1454
1634
|
taskId,
|
|
1455
1635
|
status: 'running',
|
|
1456
1636
|
waitedMs,
|
|
1637
|
+
...sliceField,
|
|
1457
1638
|
note: `Still running after ${Math.round((waitedBefore + waitedMs) / 1000)}s of waiting, and its ` +
|
|
1458
|
-
`output is still growing (${grownTo} bytes) — slow, not stuck
|
|
1459
|
-
`keep waiting; peek with exec_command: tail ${log}.`,
|
|
1639
|
+
`output is still growing (${grownTo} bytes) — slow, not stuck.${progress} Call wait_for_task ` +
|
|
1640
|
+
`again to keep waiting; peek with exec_command: tail ${log}.`,
|
|
1460
1641
|
};
|
|
1461
1642
|
}
|
|
1462
1643
|
if (waitedBefore + waitedMs >= stuckAfterMs()) {
|
|
@@ -1470,26 +1651,12 @@ export function buildTools(backend, config) {
|
|
|
1470
1651
|
`kill $(cat /tmp/${taskId}.pid) 2>/dev/null; pkill -f ${taskId}.sh) and move on; do not wait on it again.`,
|
|
1471
1652
|
};
|
|
1472
1653
|
}
|
|
1473
|
-
// The output so far rides the result: a wait that shows nothing was waited blind
|
|
1474
|
-
// (X1 flash-m50: 4m42s across two waits on an 11-minute run). The slice leads with the
|
|
1475
|
-
// failure lines, not the last bytes — a suite's tail is its progress lines
|
|
1476
|
-
// (X1 flash-m61: the last 3 000 bytes of a 139-test walk were the skip list).
|
|
1477
|
-
const own = outputFileByTask.get(taskId);
|
|
1478
|
-
let soFar;
|
|
1479
|
-
try {
|
|
1480
|
-
const r = await backend.run(runningOutputSliceCommand(log, own), { timeout: 5_000 });
|
|
1481
|
-
soFar = r.stdout ?? '';
|
|
1482
|
-
}
|
|
1483
|
-
catch (_error) {
|
|
1484
|
-
// No slice: the note still says how to peek.
|
|
1485
|
-
soFar = '';
|
|
1486
|
-
}
|
|
1487
1654
|
return {
|
|
1488
1655
|
taskId,
|
|
1489
1656
|
status: 'running',
|
|
1490
1657
|
waitedMs,
|
|
1491
|
-
...
|
|
1492
|
-
note: `Still running after ${Math.round(waitedMs / 1000)}s
|
|
1658
|
+
...sliceField,
|
|
1659
|
+
note: `Still running after ${Math.round(waitedMs / 1000)}s.${progress} Call wait_for_task again to ` +
|
|
1493
1660
|
`keep waiting — its result carries the log output. ` +
|
|
1494
1661
|
(soFar.trim()
|
|
1495
1662
|
? `outputSoFar is the failure lines so far (up to ${RUNNING_FAILURE_LINES}) and then the end of ` +
|
package/dist/utilities.d.ts
CHANGED
|
@@ -267,7 +267,11 @@ export declare const MAX_BATCH_READ_BYTES: number;
|
|
|
267
267
|
* needs.
|
|
268
268
|
*/
|
|
269
269
|
export declare const MAX_READ_RETURN_CHARS: number;
|
|
270
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* A file this small is returned whole on a second window of it (paging it costs a call each).
|
|
272
|
+
* The same bound as {@link MAX_READ_RETURN_CHARS}: the windowed-read note promises that a
|
|
273
|
+
* file under it "fits in ONE call", and a smaller bound here broke that promise (X1 flash-m67).
|
|
274
|
+
*/
|
|
271
275
|
export declare const SMALL_FILE_WHOLE_CHARS: number;
|
|
272
276
|
/** Max search results. */
|
|
273
277
|
export declare const MAX_SEARCH_RESULTS = 50;
|
package/dist/utilities.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAS5C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,MAAM,KAAG,MAEE,CAAA;AAuGhD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAErD;AAKD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAsCzD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGnD;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,MAAM,CAUpF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAElE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWnE;AAQD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG;IAC3D,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB,
|
|
1
|
+
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
|
package/dist/utilities.js
CHANGED
|
@@ -345,6 +345,15 @@ export function streamingBackgroundCommand(command) {
|
|
|
345
345
|
changes.push('Added `--reporter=list` so each test result is written to the log as it finishes ' +
|
|
346
346
|
'(the config reporter may print nothing until the end).');
|
|
347
347
|
}
|
|
348
|
+
// A diagnosis or a re-run (capped, last-failed, grepped, or named files) walks each failure
|
|
349
|
+
// once: the suite's own `retries` doubled every failure of X1 flash-m71's first whole run,
|
|
350
|
+
// which had dropped the `--retries=0` it used one command earlier. The uncapped proof run
|
|
351
|
+
// keeps the suite's retries.
|
|
352
|
+
const iterating = /--max-failures\b|--last-failed\b|(?:^|\s)(?:-g|--grep)\b|\S+\.(?:spec|test)\.[cm]?[jt]sx?(?::\d+)?\b/.test(lastStep);
|
|
353
|
+
if (finalPlaywrightRun && iterating && !/--retries\b/.test(lastStep)) {
|
|
354
|
+
out = `${out.trimEnd()} --retries=0`;
|
|
355
|
+
changes.push('Added `--retries=0`: a diagnosis or re-run walks each failure once; keep the suite’s retries for the proof run.');
|
|
356
|
+
}
|
|
348
357
|
return { command: out, changes };
|
|
349
358
|
}
|
|
350
359
|
// ── Command blocking ──────────────────────────────────────────────────────────
|
|
@@ -577,8 +586,12 @@ export const MAX_BATCH_READ_BYTES = 200 * 1024;
|
|
|
577
586
|
* needs.
|
|
578
587
|
*/
|
|
579
588
|
export const MAX_READ_RETURN_CHARS = 80 * 1024;
|
|
580
|
-
/**
|
|
581
|
-
|
|
589
|
+
/**
|
|
590
|
+
* A file this small is returned whole on a second window of it (paging it costs a call each).
|
|
591
|
+
* The same bound as {@link MAX_READ_RETURN_CHARS}: the windowed-read note promises that a
|
|
592
|
+
* file under it "fits in ONE call", and a smaller bound here broke that promise (X1 flash-m67).
|
|
593
|
+
*/
|
|
594
|
+
export const SMALL_FILE_WHOLE_CHARS = MAX_READ_RETURN_CHARS;
|
|
582
595
|
/** Max search results. */
|
|
583
596
|
export const MAX_SEARCH_RESULTS = 50;
|
|
584
597
|
/** Max find results. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-ai-tools",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.18",
|
|
4
4
|
"description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@molecule/api-ai": "^1.0.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@molecule/api-ai": "1.3.
|
|
34
|
+
"@molecule/api-ai": "1.3.1",
|
|
35
35
|
"@types/node": "26.1.2",
|
|
36
36
|
"esbuild": "0.28.2",
|
|
37
37
|
"typescript": "6.0.3",
|