@mjasnikovs/pi-task 0.13.22 → 0.13.24
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/task/phases.js
CHANGED
|
@@ -79,6 +79,16 @@ export async function phaseVerifyTooling(deps, research) {
|
|
|
79
79
|
return replaceToolingWithVerified(research, parsed.verified);
|
|
80
80
|
}
|
|
81
81
|
const DOCS_EXTENSION_PATH = new URL('../workers/docs-extension.js', import.meta.url).pathname;
|
|
82
|
+
/**
|
|
83
|
+
* In-process guards loaded into the TOOLING worker only: block a re-read of any
|
|
84
|
+
* file already read, and block any byte-identical grep/find/ls repeat, feeding
|
|
85
|
+
* the model "you already have this, answer now" instead of letting it re-run.
|
|
86
|
+
* TOOLING reads each file once and never needs an identical search twice in any
|
|
87
|
+
* healthy recorded run, so neither rule has a legitimate false positive here.
|
|
88
|
+
* See single-read-guard.ts.
|
|
89
|
+
*/
|
|
90
|
+
const SINGLE_READ_EXTENSION_PATH = new URL('../workers/single-read-extension.js', import.meta.url)
|
|
91
|
+
.pathname;
|
|
82
92
|
/**
|
|
83
93
|
* Task-file heading under which a research worker's validated output is cached.
|
|
84
94
|
* A resumed research phase reads these to skip workers that already succeeded,
|
|
@@ -216,7 +226,10 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
216
226
|
label: 'worker:tooling',
|
|
217
227
|
// Scope to the goal prose only — not the per-file edit checklist that
|
|
218
228
|
// makes a weak model spelunk source and loop. See scopedToolingGoal.
|
|
219
|
-
prompt: appendNoThink(promptHeader + RESEARCH_TOOLING_PROMPT(scopedToolingGoal(refined)))
|
|
229
|
+
prompt: appendNoThink(promptHeader + RESEARCH_TOOLING_PROMPT(scopedToolingGoal(refined))),
|
|
230
|
+
// Block re-reads in-process so a weak model that wants to re-read the
|
|
231
|
+
// same file is told to answer from what it has instead of looping.
|
|
232
|
+
extensions: [SINGLE_READ_EXTENSION_PATH]
|
|
220
233
|
}
|
|
221
234
|
];
|
|
222
235
|
// Run workers one at a time, persisting each worker's validated output the
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
/**
|
|
3
|
+
* Loaded via `-e` into the TOOLING research worker only. Two in-run thrash
|
|
4
|
+
* blocks, each returning a reason the model receives as an error tool result
|
|
5
|
+
* (agent-loop: block → createErrorToolResult) so the worker continues instead of
|
|
6
|
+
* looping:
|
|
7
|
+
* - read: blocks any re-read of a file already read this run (path-keyed).
|
|
8
|
+
* - grep/find/ls: blocks an identical repeat of the same call (args-keyed).
|
|
9
|
+
* See single-read-guard.ts for why this is scoped to TOOLING.
|
|
10
|
+
*/
|
|
11
|
+
export default function (pi: ExtensionAPI): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { RepeatedCallGuard, SingleReadGuard } from './single-read-guard.js';
|
|
3
|
+
/** Read-like tools whose byte-identical repeats are blocked (read is handled separately). */
|
|
4
|
+
const DEDUP_TOOLS = new Set(['grep', 'find', 'ls']);
|
|
5
|
+
/**
|
|
6
|
+
* Loaded via `-e` into the TOOLING research worker only. Two in-run thrash
|
|
7
|
+
* blocks, each returning a reason the model receives as an error tool result
|
|
8
|
+
* (agent-loop: block → createErrorToolResult) so the worker continues instead of
|
|
9
|
+
* looping:
|
|
10
|
+
* - read: blocks any re-read of a file already read this run (path-keyed).
|
|
11
|
+
* - grep/find/ls: blocks an identical repeat of the same call (args-keyed).
|
|
12
|
+
* See single-read-guard.ts for why this is scoped to TOOLING.
|
|
13
|
+
*/
|
|
14
|
+
export default function (pi) {
|
|
15
|
+
const reads = new SingleReadGuard();
|
|
16
|
+
const calls = new RepeatedCallGuard();
|
|
17
|
+
pi.on('tool_call', event => {
|
|
18
|
+
if (event.toolName === 'read') {
|
|
19
|
+
const path = event.input.path;
|
|
20
|
+
if (typeof path !== 'string')
|
|
21
|
+
return;
|
|
22
|
+
return reads.check(resolve(process.cwd(), path)) ?? undefined;
|
|
23
|
+
}
|
|
24
|
+
if (DEDUP_TOOLS.has(event.toolName)) {
|
|
25
|
+
return calls.check(event.toolName, event.input) ?? undefined;
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process thrash guards for the TOOLING research worker.
|
|
3
|
+
*
|
|
4
|
+
* Two guards, one mechanism — deny a wasteful repeat *inside the run* (the
|
|
5
|
+
* extension returns the block from a `tool_call` handler, pi feeds `reason` back
|
|
6
|
+
* as an error tool result, the worker continues). No kill, no restart: detect-
|
|
7
|
+
* and-kill only re-spawns a model that deterministically re-thrashes.
|
|
8
|
+
*
|
|
9
|
+
* - SingleReadGuard: "read each file once". Validated against every recorded
|
|
10
|
+
* mx5 run — a healthy TOOLING worker reads each file exactly once (max
|
|
11
|
+
* same-file reads = 1 across 7 tasks). TASK_0017 re-read one file up to 50×.
|
|
12
|
+
* Keyed on the resolved path so any re-read is blocked regardless of offset.
|
|
13
|
+
*
|
|
14
|
+
* - RepeatedCallGuard: "no identical search twice", for grep/find/ls. TASK_0017
|
|
15
|
+
* also looped on grep({pattern:"^\\s*}",path:".../index.ts"}) ×5 — a path the
|
|
16
|
+
* read guard never covered, so the call fell back to the ineffective detect→
|
|
17
|
+
* restart path. Keyed on (toolName, stableStringify(args)) — the *same* key
|
|
18
|
+
* the LoopDetector uses — so only byte-identical repeats trip; a legitimately
|
|
19
|
+
* different grep pattern on the same file still passes.
|
|
20
|
+
*
|
|
21
|
+
* Pure logic, no I/O — the extension does path resolution and tool routing.
|
|
22
|
+
*/
|
|
23
|
+
export interface ReadBlock {
|
|
24
|
+
block: true;
|
|
25
|
+
reason: string;
|
|
26
|
+
}
|
|
27
|
+
/** The error text the model receives in place of the re-read's contents. */
|
|
28
|
+
export declare function singleReadReason(path: string): string;
|
|
29
|
+
export declare class SingleReadGuard {
|
|
30
|
+
private readonly seen;
|
|
31
|
+
/**
|
|
32
|
+
* Record a read of `resolvedPath`. Returns a ReadBlock the first time a path
|
|
33
|
+
* is seen a second time (and every time after), else null on the first read.
|
|
34
|
+
* Callers pass an already-resolved/normalized path so `a.ts` and `./a.ts`
|
|
35
|
+
* dedupe to one entry.
|
|
36
|
+
*/
|
|
37
|
+
check(resolvedPath: string): ReadBlock | null;
|
|
38
|
+
}
|
|
39
|
+
/** The error text the model receives in place of a repeated grep/find/ls call. */
|
|
40
|
+
export declare function repeatedCallReason(toolName: string): string;
|
|
41
|
+
export declare class RepeatedCallGuard {
|
|
42
|
+
private readonly seen;
|
|
43
|
+
/**
|
|
44
|
+
* Record a `toolName` call with `args`. Returns a ReadBlock the second time
|
|
45
|
+
* the same (toolName, stable-stringified args) pair is seen (and every time
|
|
46
|
+
* after), else null on the first. Uses the LoopDetector's stableStringify so
|
|
47
|
+
* argument key-order never causes a miss; only byte-identical calls collapse.
|
|
48
|
+
*/
|
|
49
|
+
check(toolName: string, args: unknown): ReadBlock | null;
|
|
50
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process thrash guards for the TOOLING research worker.
|
|
3
|
+
*
|
|
4
|
+
* Two guards, one mechanism — deny a wasteful repeat *inside the run* (the
|
|
5
|
+
* extension returns the block from a `tool_call` handler, pi feeds `reason` back
|
|
6
|
+
* as an error tool result, the worker continues). No kill, no restart: detect-
|
|
7
|
+
* and-kill only re-spawns a model that deterministically re-thrashes.
|
|
8
|
+
*
|
|
9
|
+
* - SingleReadGuard: "read each file once". Validated against every recorded
|
|
10
|
+
* mx5 run — a healthy TOOLING worker reads each file exactly once (max
|
|
11
|
+
* same-file reads = 1 across 7 tasks). TASK_0017 re-read one file up to 50×.
|
|
12
|
+
* Keyed on the resolved path so any re-read is blocked regardless of offset.
|
|
13
|
+
*
|
|
14
|
+
* - RepeatedCallGuard: "no identical search twice", for grep/find/ls. TASK_0017
|
|
15
|
+
* also looped on grep({pattern:"^\\s*}",path:".../index.ts"}) ×5 — a path the
|
|
16
|
+
* read guard never covered, so the call fell back to the ineffective detect→
|
|
17
|
+
* restart path. Keyed on (toolName, stableStringify(args)) — the *same* key
|
|
18
|
+
* the LoopDetector uses — so only byte-identical repeats trip; a legitimately
|
|
19
|
+
* different grep pattern on the same file still passes.
|
|
20
|
+
*
|
|
21
|
+
* Pure logic, no I/O — the extension does path resolution and tool routing.
|
|
22
|
+
*/
|
|
23
|
+
import { stableStringify } from '../task/loop-detector.js';
|
|
24
|
+
/** The error text the model receives in place of the re-read's contents. */
|
|
25
|
+
export function singleReadReason(path) {
|
|
26
|
+
return (`You already read ${path} earlier in this run — its contents are in your context. `
|
|
27
|
+
+ `Re-reading the same file is blocked. Do not read it again: use what you have already `
|
|
28
|
+
+ `gathered and write your final answer now.`);
|
|
29
|
+
}
|
|
30
|
+
export class SingleReadGuard {
|
|
31
|
+
seen = new Set();
|
|
32
|
+
/**
|
|
33
|
+
* Record a read of `resolvedPath`. Returns a ReadBlock the first time a path
|
|
34
|
+
* is seen a second time (and every time after), else null on the first read.
|
|
35
|
+
* Callers pass an already-resolved/normalized path so `a.ts` and `./a.ts`
|
|
36
|
+
* dedupe to one entry.
|
|
37
|
+
*/
|
|
38
|
+
check(resolvedPath) {
|
|
39
|
+
if (this.seen.has(resolvedPath)) {
|
|
40
|
+
return { block: true, reason: singleReadReason(resolvedPath) };
|
|
41
|
+
}
|
|
42
|
+
this.seen.add(resolvedPath);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** The error text the model receives in place of a repeated grep/find/ls call. */
|
|
47
|
+
export function repeatedCallReason(toolName) {
|
|
48
|
+
return (`You already ran this exact ${toolName} call earlier in this run — its result is in your `
|
|
49
|
+
+ `context. Repeating the identical call is blocked. Use what you have already gathered, or `
|
|
50
|
+
+ `try a different angle if you still need more, then write your final answer.`);
|
|
51
|
+
}
|
|
52
|
+
export class RepeatedCallGuard {
|
|
53
|
+
seen = new Set();
|
|
54
|
+
/**
|
|
55
|
+
* Record a `toolName` call with `args`. Returns a ReadBlock the second time
|
|
56
|
+
* the same (toolName, stable-stringified args) pair is seen (and every time
|
|
57
|
+
* after), else null on the first. Uses the LoopDetector's stableStringify so
|
|
58
|
+
* argument key-order never causes a miss; only byte-identical calls collapse.
|
|
59
|
+
*/
|
|
60
|
+
check(toolName, args) {
|
|
61
|
+
const key = `${toolName}\x00${stableStringify(args)}`;
|
|
62
|
+
if (this.seen.has(key)) {
|
|
63
|
+
return { block: true, reason: repeatedCallReason(toolName) };
|
|
64
|
+
}
|
|
65
|
+
this.seen.add(key);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.24",
|
|
4
4
|
"description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|