@mjasnikovs/pi-task 0.38.25 → 0.38.27
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/workers/pi-worker.js +6 -5
- package/dist/workers/shared.d.ts +18 -5
- package/dist/workers/shared.js +0 -0
- package/dist/workers/worker-failure.d.ts +26 -0
- package/dist/workers/worker-failure.js +54 -0
- package/dist/workers/worker-profiles.d.ts +1 -1
- package/dist/workers/worker-profiles.js +31 -12
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Text } from '@earendil-works/pi-tui';
|
|
10
10
|
import { Type } from '@sinclair/typebox';
|
|
11
|
+
import { getConfig } from '../config/config.js';
|
|
11
12
|
import { groupThinkingArgs } from '../config/reasoning-args.js';
|
|
12
13
|
import { runWorker } from './pi-worker-core.js';
|
|
13
14
|
import { childFailureReason, formatChildFailure, makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
|
|
@@ -47,12 +48,12 @@ export function registerPiWorker(pi) {
|
|
|
47
48
|
prompt: params.prompt,
|
|
48
49
|
cwd: ctx.cwd,
|
|
49
50
|
signal,
|
|
50
|
-
// `adhoc` is every guard at its default — which is what this call
|
|
51
|
-
// site already got by naming none of them. Named now so it is a
|
|
52
|
-
// decision, and so the one asymmetry it carries (a FIXED 240s cap
|
|
53
|
-
// where a research worker gets 240s without progress) is written
|
|
54
|
-
// down. See the `adhoc` row in workers/worker-profiles.ts.
|
|
55
51
|
profile: 'adhoc',
|
|
52
|
+
// The user's own `stuck reply retry` is what bounds this worker
|
|
53
|
+
// now — it kills on SILENCE, never on slowness. It is an INPUT and
|
|
54
|
+
// not policy for the same reason the gate's two ceilings are: the
|
|
55
|
+
// number is the user's, the decision to arm it is the profile's.
|
|
56
|
+
policyInputs: { streamInactivityMs: getConfig().streamInactivityMs },
|
|
56
57
|
thinking: groupThinkingArgs('research')
|
|
57
58
|
});
|
|
58
59
|
const details = { exitCode: result.exitCode };
|
package/dist/workers/shared.d.ts
CHANGED
|
@@ -17,12 +17,25 @@ export interface ChildOutcome {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* The one place worker child-failure is turned into a user-facing message.
|
|
20
|
-
* Returns `null` when the child succeeded (caller proceeds to format output)
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* Returns `null` when the child succeeded (caller proceeds to format output).
|
|
21
|
+
*
|
|
22
|
+
* It ASKS THE LADDER now (`classifyWorkerFailure` -> `describeWorkerFailure`)
|
|
23
|
+
* rather than re-deriving the answer from two fields. It used to be
|
|
24
|
+
* `if (aborted) return abortedMessage`, and every kill path also sets `aborted`,
|
|
25
|
+
* so a wall-clock kill, a hung command, a dead backend, a loop kill and a user
|
|
26
|
+
* ESC all produced the same four words — while `childFailureReason`, one line
|
|
27
|
+
* later at the only caller that had the richer result, computed the real cause
|
|
28
|
+
* and sent it to a debug trail nobody reads. That is the same shape as the bug
|
|
29
|
+
* `worker-failure.ts`'s own header records, one layer out: a second author of a
|
|
30
|
+
* taxonomy the module exists to own.
|
|
31
|
+
*
|
|
32
|
+
* A caller with only a `ChildOutcome` is unaffected. It carries no kill flags,
|
|
33
|
+
* so the ladder falls through to `aborted`/`exit` and returns exactly what this
|
|
34
|
+
* function returned before — which `shared.test.ts` pins.
|
|
24
35
|
*/
|
|
25
|
-
export declare function formatChildFailure(child: ChildOutcome
|
|
36
|
+
export declare function formatChildFailure(child: ChildOutcome | (WorkerFailureInput & {
|
|
37
|
+
stderr?: string;
|
|
38
|
+
}), abortedMessage: string): string | null;
|
|
26
39
|
/**
|
|
27
40
|
* What a worker tool PRODUCED — an answer, or a statement that it has none.
|
|
28
41
|
*
|
package/dist/workers/shared.js
CHANGED
|
Binary file
|
|
@@ -122,4 +122,30 @@ export declare const FAILURE_RULES: ReadonlyArray<{
|
|
|
122
122
|
* which is not the same as "it answered": the text may still be empty, and that
|
|
123
123
|
* judgement belongs to the caller.
|
|
124
124
|
*/
|
|
125
|
+
/**
|
|
126
|
+
* What a worker failure SAYS to the caller that asked for the work.
|
|
127
|
+
*
|
|
128
|
+
* WHY IT EXISTS. The ladder above already names the cause exactly, with its
|
|
129
|
+
* detail — which tool hung, how long the stream was idle, which exit code. None
|
|
130
|
+
* of that reached a human. `formatChildFailure` was handed a `ChildOutcome`
|
|
131
|
+
* (`{aborted, exitCode, stderr}`) and answered `if (aborted) return
|
|
132
|
+
* abortedMessage`, so a 240s wall-clock kill, a hung `bash`, a dead model
|
|
133
|
+
* backend, a loop kill and a user pressing ESC all printed the SAME four words.
|
|
134
|
+
* The discriminating value was computed a line later by `childFailureReason` and
|
|
135
|
+
* put in the debug trail, which a user reading a tool result never sees.
|
|
136
|
+
*
|
|
137
|
+
* That is not only unhelpful; it is why the `pi-worker` tool's 240s cap has no
|
|
138
|
+
* base rate. 53 recorded invocations across eight repos carry 14 failures, and
|
|
139
|
+
* NOTHING in the transcript says which of them ran out of time — the honest
|
|
140
|
+
* bound recoverable from timestamps alone is "somewhere between 0 and 8".
|
|
141
|
+
*
|
|
142
|
+
* A switch, not a table: `WorkerFailure` is a discriminated union carrying a
|
|
143
|
+
* different payload per arm, so the exhaustiveness check is the compiler's and a
|
|
144
|
+
* ninth arm cannot be added without a message.
|
|
145
|
+
*/
|
|
146
|
+
export declare function describeWorkerFailure(f: WorkerFailure,
|
|
147
|
+
/** What a genuine user cancel says. The caller's wording — only this arm is theirs. */
|
|
148
|
+
abortedMessage: string,
|
|
149
|
+
/** stderr for the `exit` arm; ignored by every other. */
|
|
150
|
+
stderr?: string): string;
|
|
125
151
|
export declare function classifyWorkerFailure(r: WorkerFailureInput): WorkerFailure | undefined;
|
|
@@ -83,6 +83,60 @@ export const FAILURE_RULES = [
|
|
|
83
83
|
* which is not the same as "it answered": the text may still be empty, and that
|
|
84
84
|
* judgement belongs to the caller.
|
|
85
85
|
*/
|
|
86
|
+
/**
|
|
87
|
+
* What a worker failure SAYS to the caller that asked for the work.
|
|
88
|
+
*
|
|
89
|
+
* WHY IT EXISTS. The ladder above already names the cause exactly, with its
|
|
90
|
+
* detail — which tool hung, how long the stream was idle, which exit code. None
|
|
91
|
+
* of that reached a human. `formatChildFailure` was handed a `ChildOutcome`
|
|
92
|
+
* (`{aborted, exitCode, stderr}`) and answered `if (aborted) return
|
|
93
|
+
* abortedMessage`, so a 240s wall-clock kill, a hung `bash`, a dead model
|
|
94
|
+
* backend, a loop kill and a user pressing ESC all printed the SAME four words.
|
|
95
|
+
* The discriminating value was computed a line later by `childFailureReason` and
|
|
96
|
+
* put in the debug trail, which a user reading a tool result never sees.
|
|
97
|
+
*
|
|
98
|
+
* That is not only unhelpful; it is why the `pi-worker` tool's 240s cap has no
|
|
99
|
+
* base rate. 53 recorded invocations across eight repos carry 14 failures, and
|
|
100
|
+
* NOTHING in the transcript says which of them ran out of time — the honest
|
|
101
|
+
* bound recoverable from timestamps alone is "somewhere between 0 and 8".
|
|
102
|
+
*
|
|
103
|
+
* A switch, not a table: `WorkerFailure` is a discriminated union carrying a
|
|
104
|
+
* different payload per arm, so the exhaustiveness check is the compiler's and a
|
|
105
|
+
* ninth arm cannot be added without a message.
|
|
106
|
+
*/
|
|
107
|
+
export function describeWorkerFailure(f,
|
|
108
|
+
/** What a genuine user cancel says. The caller's wording — only this arm is theirs. */
|
|
109
|
+
abortedMessage,
|
|
110
|
+
/** stderr for the `exit` arm; ignored by every other. */
|
|
111
|
+
stderr = '') {
|
|
112
|
+
switch (f.kind) {
|
|
113
|
+
case 'stalled':
|
|
114
|
+
return ('Worker stopped: it produced no output and the model backend was '
|
|
115
|
+
+ 'unreachable. This is an infrastructure failure, not a bad question — '
|
|
116
|
+
+ 'retrying the same request once the backend is back is reasonable.');
|
|
117
|
+
case 'command-timeout':
|
|
118
|
+
return (`Worker killed: \`${f.toolName}\` ran past its `
|
|
119
|
+
+ `${Math.round(f.timeoutMs / 1000)}s per-command limit and was still `
|
|
120
|
+
+ 'running. A command that does not terminate on its own (a dev server, a '
|
|
121
|
+
+ 'watcher) has to be bounded by the command itself.');
|
|
122
|
+
case 'stream-stall':
|
|
123
|
+
return (`Worker killed: no output for ${Math.round(f.idleMs / 1000)}s while the `
|
|
124
|
+
+ 'model backend was still reachable — a hung stream, not a slow one.');
|
|
125
|
+
case 'worker-timeout':
|
|
126
|
+
return ('Worker ran out of time before answering, on every attempt, and returned '
|
|
127
|
+
+ 'nothing. The question was too broad for one worker: narrow it to one '
|
|
128
|
+
+ 'directory or one question, or split it across several workers.');
|
|
129
|
+
case 'loop':
|
|
130
|
+
return ('Worker killed: it repeated the same tool call without making progress. '
|
|
131
|
+
+ 'Nothing it had already read was answering the question as asked.');
|
|
132
|
+
case 'leaked-tool-call':
|
|
133
|
+
return 'Worker produced a malformed tool call instead of an answer.';
|
|
134
|
+
case 'aborted':
|
|
135
|
+
return abortedMessage;
|
|
136
|
+
case 'exit':
|
|
137
|
+
return `Worker exited ${f.code}.\n${stderr.trim().slice(-500) || '(no stderr)'}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
86
140
|
export function classifyWorkerFailure(r) {
|
|
87
141
|
for (const rule of FAILURE_RULES) {
|
|
88
142
|
const hit = rule.match(r);
|
|
@@ -295,7 +295,7 @@ export declare const WORKER_PROFILES: {
|
|
|
295
295
|
readonly adhoc: {
|
|
296
296
|
readonly id: "adhoc";
|
|
297
297
|
readonly why: string;
|
|
298
|
-
readonly resolve: () => {
|
|
298
|
+
readonly resolve: (inputs: WorkerPolicyInputs) => {
|
|
299
299
|
guards: WorkerGuards;
|
|
300
300
|
carryForward: false;
|
|
301
301
|
};
|
|
@@ -175,18 +175,37 @@ export const WORKER_PROFILES = {
|
|
|
175
175
|
},
|
|
176
176
|
adhoc: {
|
|
177
177
|
id: 'adhoc',
|
|
178
|
-
why: 'The model-dispatched `pi-worker` tool
|
|
179
|
-
+ '
|
|
180
|
-
+ '
|
|
181
|
-
+ '
|
|
182
|
-
+ '
|
|
183
|
-
+
|
|
184
|
-
+ '
|
|
185
|
-
+ '
|
|
186
|
-
+ '
|
|
187
|
-
+ '
|
|
188
|
-
+ '
|
|
189
|
-
|
|
178
|
+
why: 'The model-dispatched `pi-worker` tool: a read-only child with '
|
|
179
|
+
+ '`read,grep,find,ls` and nothing else, asked a question the MODEL wrote. '
|
|
180
|
+
+ 'It has NO WALL CLOCK, and that is a decision with evidence behind it. '
|
|
181
|
+
+ 'It used to run a FIXED 240s cap — total elapsed, not idle — inherited by '
|
|
182
|
+
+ 'naming no guards at all. A fixed wall clock on a read-only worker makes '
|
|
183
|
+
+ "answer quality a function of the user's hardware: the same prompt on a "
|
|
184
|
+
+ 'slower local model loses its work and degrades, which is the argument '
|
|
185
|
+
+ 'research-fanout-budget.ts already records against every wall-clock lever '
|
|
186
|
+
+ '("no constant fixes that"). It is worse than that — the constant was '
|
|
187
|
+
+ 'sized against "~25-130s on the local backend", and MEASURED on 37 '
|
|
188
|
+
+ 'replayed real prompts after only a MODEL swap on the SAME machine: '
|
|
189
|
+
+ 'median 76s, p90 371s, max 442s, with 14/37 above the 130s it was '
|
|
190
|
+
+ 'calibrated to and 8/37 (22%) past the cap outright. A bound that has to '
|
|
191
|
+
+ 'be re-measured whenever the model changes is the wrong bound. '
|
|
192
|
+
+ "What replaces it is `stream-stall`, armed from the user's own "
|
|
193
|
+
+ '`stuck reply retry` setting: it kills on SILENCE, never on slowness — '
|
|
194
|
+
+ '"one token every 30s is a working local model and must never be killed; '
|
|
195
|
+
+ 'zero events for the whole window is a hang" — so it needs no calibration '
|
|
196
|
+
+ 'and no new setting. Real thrash is still caught by the two runaway '
|
|
197
|
+
+ 'detectors and the dead-backend probe, all of which stay on.',
|
|
198
|
+
resolve: inputs => {
|
|
199
|
+
const guards = baseGuards();
|
|
200
|
+
// No wall clock. See `why`: a fixed elapsed-time cap on a read-only
|
|
201
|
+
// worker is a hardware test, not a work test.
|
|
202
|
+
guards['worker-timeout'] = { timeoutMs: 0, progressCeilingMs: null, fanout: null };
|
|
203
|
+
// The silence bound that replaces it — the user's own setting, not a
|
|
204
|
+
// new one. 0 when the caller hands none, which is off, exactly as
|
|
205
|
+
// before: a harness must not silently acquire a guard.
|
|
206
|
+
guards['stream-stall'] = inputs.streamInactivityMs ?? 0;
|
|
207
|
+
return { guards, carryForward: false };
|
|
208
|
+
}
|
|
190
209
|
// `as const satisfies`, not an annotation — the same reason RESTART_ORDER
|
|
191
210
|
// gives: an annotation widens each row back to `WorkerProfile`, and the
|
|
192
211
|
// `why` strings and literal ids stop being visible to a reader or a test.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.27",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|