@mjasnikovs/pi-task 0.13.22 → 0.13.23
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,15 @@ 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 guard loaded into the TOOLING worker only: blocks a second read of
|
|
84
|
+
* any file it already read, feeding the model "you already read this, answer
|
|
85
|
+
* now" instead of letting it re-read. TOOLING reads each file exactly once in
|
|
86
|
+
* every healthy recorded run; re-reading is purely the thrash signature, so a
|
|
87
|
+
* read-once rule has no legitimate false positive here. See single-read-guard.ts.
|
|
88
|
+
*/
|
|
89
|
+
const SINGLE_READ_EXTENSION_PATH = new URL('../workers/single-read-extension.js', import.meta.url)
|
|
90
|
+
.pathname;
|
|
82
91
|
/**
|
|
83
92
|
* Task-file heading under which a research worker's validated output is cached.
|
|
84
93
|
* A resumed research phase reads these to skip workers that already succeeded,
|
|
@@ -216,7 +225,10 @@ export async function phaseResearch(deps, refined, researchDeps = {}) {
|
|
|
216
225
|
label: 'worker:tooling',
|
|
217
226
|
// Scope to the goal prose only — not the per-file edit checklist that
|
|
218
227
|
// makes a weak model spelunk source and loop. See scopedToolingGoal.
|
|
219
|
-
prompt: appendNoThink(promptHeader + RESEARCH_TOOLING_PROMPT(scopedToolingGoal(refined)))
|
|
228
|
+
prompt: appendNoThink(promptHeader + RESEARCH_TOOLING_PROMPT(scopedToolingGoal(refined))),
|
|
229
|
+
// Block re-reads in-process so a weak model that wants to re-read the
|
|
230
|
+
// same file is told to answer from what it has instead of looping.
|
|
231
|
+
extensions: [SINGLE_READ_EXTENSION_PATH]
|
|
220
232
|
}
|
|
221
233
|
];
|
|
222
234
|
// Run workers one at a time, persisting each worker's validated output the
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
/**
|
|
3
|
+
* Loaded via `-e` into the TOOLING research worker only. Blocks a second read of
|
|
4
|
+
* any file the worker has already read this run, returning a reason the model
|
|
5
|
+
* receives as an error tool result (agent-loop: block → createErrorToolResult).
|
|
6
|
+
* The worker then continues from what it has instead of re-reading. See
|
|
7
|
+
* SingleReadGuard for why this is scoped to TOOLING.
|
|
8
|
+
*/
|
|
9
|
+
export default function (pi: ExtensionAPI): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { SingleReadGuard } from './single-read-guard.js';
|
|
3
|
+
/**
|
|
4
|
+
* Loaded via `-e` into the TOOLING research worker only. Blocks a second read of
|
|
5
|
+
* any file the worker has already read this run, returning a reason the model
|
|
6
|
+
* receives as an error tool result (agent-loop: block → createErrorToolResult).
|
|
7
|
+
* The worker then continues from what it has instead of re-reading. See
|
|
8
|
+
* SingleReadGuard for why this is scoped to TOOLING.
|
|
9
|
+
*/
|
|
10
|
+
export default function (pi) {
|
|
11
|
+
const guard = new SingleReadGuard();
|
|
12
|
+
pi.on('tool_call', event => {
|
|
13
|
+
if (event.toolName !== 'read')
|
|
14
|
+
return;
|
|
15
|
+
const path = event.input.path;
|
|
16
|
+
if (typeof path !== 'string')
|
|
17
|
+
return;
|
|
18
|
+
return guard.check(resolve(process.cwd(), path)) ?? undefined;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process "read each file once" guard for the TOOLING research worker.
|
|
3
|
+
*
|
|
4
|
+
* Validated against every recorded mx5 run: a healthy TOOLING worker reads each
|
|
5
|
+
* file exactly once (max same-file reads = 1 across 7 tasks). The failure case
|
|
6
|
+
* (TASK_0017) re-read the same file up to 50× — the model already had the answer
|
|
7
|
+
* (package.json) but kept oscillating read→write-fragment→read until it looped.
|
|
8
|
+
*
|
|
9
|
+
* Rather than detect-and-kill (which only restarts a model that re-thrashes), we
|
|
10
|
+
* deny the re-read *inside the run*: the extension wiring returns this guard's
|
|
11
|
+
* block result from a `tool_call` handler, so pi feeds `reason` back to the model
|
|
12
|
+
* as an error tool result and the worker continues without the re-read. No kill,
|
|
13
|
+
* no restart. Pure logic, no I/O — the extension does path resolution.
|
|
14
|
+
*/
|
|
15
|
+
export interface ReadBlock {
|
|
16
|
+
block: true;
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
19
|
+
/** The error text the model receives in place of the re-read's contents. */
|
|
20
|
+
export declare function singleReadReason(path: string): string;
|
|
21
|
+
export declare class SingleReadGuard {
|
|
22
|
+
private readonly seen;
|
|
23
|
+
/**
|
|
24
|
+
* Record a read of `resolvedPath`. Returns a ReadBlock the first time a path
|
|
25
|
+
* is seen a second time (and every time after), else null on the first read.
|
|
26
|
+
* Callers pass an already-resolved/normalized path so `a.ts` and `./a.ts`
|
|
27
|
+
* dedupe to one entry.
|
|
28
|
+
*/
|
|
29
|
+
check(resolvedPath: string): ReadBlock | null;
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process "read each file once" guard for the TOOLING research worker.
|
|
3
|
+
*
|
|
4
|
+
* Validated against every recorded mx5 run: a healthy TOOLING worker reads each
|
|
5
|
+
* file exactly once (max same-file reads = 1 across 7 tasks). The failure case
|
|
6
|
+
* (TASK_0017) re-read the same file up to 50× — the model already had the answer
|
|
7
|
+
* (package.json) but kept oscillating read→write-fragment→read until it looped.
|
|
8
|
+
*
|
|
9
|
+
* Rather than detect-and-kill (which only restarts a model that re-thrashes), we
|
|
10
|
+
* deny the re-read *inside the run*: the extension wiring returns this guard's
|
|
11
|
+
* block result from a `tool_call` handler, so pi feeds `reason` back to the model
|
|
12
|
+
* as an error tool result and the worker continues without the re-read. No kill,
|
|
13
|
+
* no restart. Pure logic, no I/O — the extension does path resolution.
|
|
14
|
+
*/
|
|
15
|
+
/** The error text the model receives in place of the re-read's contents. */
|
|
16
|
+
export function singleReadReason(path) {
|
|
17
|
+
return (`You already read ${path} earlier in this run — its contents are in your context. `
|
|
18
|
+
+ `Re-reading the same file is blocked. Do not read it again: use what you have already `
|
|
19
|
+
+ `gathered and write your final answer now.`);
|
|
20
|
+
}
|
|
21
|
+
export class SingleReadGuard {
|
|
22
|
+
seen = new Set();
|
|
23
|
+
/**
|
|
24
|
+
* Record a read of `resolvedPath`. Returns a ReadBlock the first time a path
|
|
25
|
+
* is seen a second time (and every time after), else null on the first read.
|
|
26
|
+
* Callers pass an already-resolved/normalized path so `a.ts` and `./a.ts`
|
|
27
|
+
* dedupe to one entry.
|
|
28
|
+
*/
|
|
29
|
+
check(resolvedPath) {
|
|
30
|
+
if (this.seen.has(resolvedPath)) {
|
|
31
|
+
return { block: true, reason: singleReadReason(resolvedPath) };
|
|
32
|
+
}
|
|
33
|
+
this.seen.add(resolvedPath);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.23",
|
|
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",
|