@mjasnikovs/pi-task 0.39.0 → 0.39.2
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 +2 -2
- package/dist/config/group-args.d.ts +24 -9
- package/dist/config/group-args.js +38 -28
- package/dist/config/register.d.ts +0 -9
- package/dist/config/register.js +17 -62
- package/dist/shared/child-process.d.ts +34 -32
- package/dist/shared/child-process.js +44 -58
- package/dist/shared/command-watchdog.d.ts +12 -4
- package/dist/shared/command-watchdog.js +6 -7
- package/dist/shared/connection-error.d.ts +7 -0
- package/dist/shared/connection-error.js +65 -0
- package/dist/shared/model-endpoint.d.ts +12 -24
- package/dist/shared/model-endpoint.js +32 -82
- package/dist/shared/model-resolve.d.ts +105 -0
- package/dist/shared/model-resolve.js +97 -0
- package/dist/shared/reasoning-capability.d.ts +20 -0
- package/dist/shared/reasoning-capability.js +32 -1
- package/dist/shared/stall-probe.d.ts +51 -0
- package/dist/shared/stall-probe.js +79 -0
- package/dist/task/child-runner.d.ts +76 -278
- package/dist/task/child-runner.js +186 -722
- package/dist/task/context-usage.js +2 -7
- package/dist/task/failure-classifier.js +53 -81
- package/dist/task/gate-child.js +1 -1
- package/dist/task/impl-widget.d.ts +2 -0
- package/dist/task/impl-widget.js +4 -0
- package/dist/task/implementation-hold.d.ts +11 -0
- package/dist/task/implementation-hold.js +20 -0
- package/dist/task/implementation-scope.d.ts +24 -0
- package/dist/task/implementation-scope.js +34 -0
- package/dist/task/loop-detector.d.ts +13 -5
- package/dist/task/loop-detector.js +11 -5
- package/dist/task/model-hold-stash.js +4 -14
- package/dist/task/orchestrator.d.ts +1 -8
- package/dist/task/orchestrator.js +11 -34
- package/dist/task/phases.js +2 -2
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +1 -1
- package/dist/workers/model-warning.d.ts +4 -16
- package/dist/workers/model-warning.js +14 -70
- package/dist/workers/pi-worker-core.d.ts +65 -20
- package/dist/workers/pi-worker-core.js +109 -50
- package/dist/workers/reasoning-warning.js +2 -24
- package/dist/workers/worker-failure.d.ts +2 -0
- package/dist/workers/worker-failure.js +2 -1
- package/dist/workers/worker-kill.d.ts +30 -11
- package/dist/workers/worker-kill.js +68 -20
- package/dist/workers/worker-profiles.d.ts +20 -0
- package/dist/workers/worker-profiles.js +22 -9
- package/package.json +1 -1
|
@@ -17,15 +17,21 @@
|
|
|
17
17
|
* row type would need an escape hatch per row. What the orderings gain here is that
|
|
18
18
|
* neither can name a cause with no row, nor silently omit one.
|
|
19
19
|
*
|
|
20
|
-
* Not every cause appears in both ladders, and that asymmetry is real.
|
|
21
|
-
*
|
|
22
|
-
* `reported` and those eight are exactly FAILURE_ORDER. `connection-error`
|
|
23
|
-
*
|
|
24
|
-
* as a `modelError
|
|
25
|
-
*
|
|
20
|
+
* Not every cause appears in both ladders, and that asymmetry is real. Eight of
|
|
21
|
+
* the ten are `restartable` and those eight are exactly RESTART_ORDER; eight are
|
|
22
|
+
* `reported` and those eight are exactly FAILURE_ORDER. `connection-error` and
|
|
23
|
+
* `empty-answer` restart without ever being reported as a kill — the first
|
|
24
|
+
* reaches the caller as a `modelError`, the second as empty text, and whether
|
|
25
|
+
* either counts as a failure is the consumer's policy. `aborted` and `exit` end
|
|
26
|
+
* an attempt outright, and no hint would help.
|
|
27
|
+
*
|
|
28
|
+
* Whether a restartable cause actually restarts is the PROFILE's decision
|
|
29
|
+
* (worker-profiles.ts): `stalled` and `empty-answer` each carry a switch there,
|
|
30
|
+
* because the phase children re-spawn on both and the research workers on
|
|
31
|
+
* neither.
|
|
26
32
|
*/
|
|
27
33
|
/** Every way a worker attempt can end other than by answering. */
|
|
28
|
-
export type WorkerKillId = 'stalled' | 'command-timeout' | 'stream-stall' | 'worker-timeout' | 'connection-error' | 'loop' | 'leaked-tool-call' | 'aborted' | 'exit';
|
|
34
|
+
export type WorkerKillId = 'stalled' | 'command-timeout' | 'stream-stall' | 'worker-timeout' | 'connection-error' | 'loop' | 'leaked-tool-call' | 'empty-answer' | 'aborted' | 'exit';
|
|
29
35
|
export interface WorkerKill {
|
|
30
36
|
id: WorkerKillId;
|
|
31
37
|
/**
|
|
@@ -54,20 +60,33 @@ export interface WorkerKill {
|
|
|
54
60
|
restartable: boolean;
|
|
55
61
|
/** Does this cause reach a consumer as a `WorkerFailure`? */
|
|
56
62
|
reported: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Must a best-effort `catch` in the phase pipeline rethrow this?
|
|
65
|
+
*
|
|
66
|
+
* A child that merely answered badly should degrade — that is what those
|
|
67
|
+
* catches are for. A dead backend is different in kind: the run is over
|
|
68
|
+
* either way, and swallowing it ships a half-built spec while every later
|
|
69
|
+
* phase dies against the same dead server. A property of the cause, so a
|
|
70
|
+
* catch asks the roster rather than keeping its own list.
|
|
71
|
+
*/
|
|
72
|
+
fatal: boolean;
|
|
57
73
|
}
|
|
58
74
|
export declare const WORKER_KILLS: readonly WorkerKill[];
|
|
59
75
|
/** Look one cause up. `undefined` only for an id with no row, which the suite forbids. */
|
|
60
76
|
export declare function workerKill(id: WorkerKillId): WorkerKill | undefined;
|
|
77
|
+
/** Takes any string: the phase failure union carries kinds that are not kills. */
|
|
78
|
+
export declare function isFatalKill(kind: string): boolean;
|
|
61
79
|
/**
|
|
62
80
|
* The restart ladder's precedence, as ids. `RESTART_RULES` must be exactly this,
|
|
63
81
|
* in this order.
|
|
64
82
|
*
|
|
65
83
|
* `loop` leads: its hint names the offending call, which is the most useful thing
|
|
66
|
-
* to tell a re-spawn. The two watchdogs
|
|
67
|
-
* is the narrower diagnosis, and they cannot be
|
|
68
|
-
* kill leaves the worker's own timeout flag false.
|
|
84
|
+
* to tell a re-spawn. The two watchdogs and the dead-backend probe come before
|
|
85
|
+
* the wall clock because each is the narrower diagnosis, and they cannot be
|
|
86
|
+
* confused with it — a guard kill leaves the worker's own timeout flag false.
|
|
87
|
+
* `empty-answer` is last: like a leaked call it exists only on a clean run.
|
|
69
88
|
*/
|
|
70
|
-
export declare const RESTART_ORDER: readonly ["loop", "command-timeout", "stream-stall", "worker-timeout", "connection-error", "leaked-tool-call"];
|
|
89
|
+
export declare const RESTART_ORDER: readonly ["loop", "command-timeout", "stream-stall", "stalled", "worker-timeout", "connection-error", "leaked-tool-call", "empty-answer"];
|
|
71
90
|
/**
|
|
72
91
|
* The failure ladder's precedence, as ids. `FAILURE_RULES` must be exactly this,
|
|
73
92
|
* in this order.
|
|
@@ -17,80 +17,128 @@
|
|
|
17
17
|
* row type would need an escape hatch per row. What the orderings gain here is that
|
|
18
18
|
* neither can name a cause with no row, nor silently omit one.
|
|
19
19
|
*
|
|
20
|
-
* Not every cause appears in both ladders, and that asymmetry is real.
|
|
21
|
-
*
|
|
22
|
-
* `reported` and those eight are exactly FAILURE_ORDER. `connection-error`
|
|
23
|
-
*
|
|
24
|
-
* as a `modelError
|
|
25
|
-
*
|
|
20
|
+
* Not every cause appears in both ladders, and that asymmetry is real. Eight of
|
|
21
|
+
* the ten are `restartable` and those eight are exactly RESTART_ORDER; eight are
|
|
22
|
+
* `reported` and those eight are exactly FAILURE_ORDER. `connection-error` and
|
|
23
|
+
* `empty-answer` restart without ever being reported as a kill — the first
|
|
24
|
+
* reaches the caller as a `modelError`, the second as empty text, and whether
|
|
25
|
+
* either counts as a failure is the consumer's policy. `aborted` and `exit` end
|
|
26
|
+
* an attempt outright, and no hint would help.
|
|
27
|
+
*
|
|
28
|
+
* Whether a restartable cause actually restarts is the PROFILE's decision
|
|
29
|
+
* (worker-profiles.ts): `stalled` and `empty-answer` each carry a switch there,
|
|
30
|
+
* because the phase children re-spawn on both and the research workers on
|
|
31
|
+
* neither.
|
|
26
32
|
*/
|
|
27
33
|
export const WORKER_KILLS = [
|
|
28
34
|
{
|
|
29
35
|
id: 'stalled',
|
|
30
36
|
resultField: 'stalled',
|
|
31
37
|
carryForward: false,
|
|
32
|
-
restartable:
|
|
33
|
-
reported: true
|
|
38
|
+
restartable: true,
|
|
39
|
+
reported: true,
|
|
40
|
+
fatal: true
|
|
34
41
|
},
|
|
35
42
|
{
|
|
36
43
|
id: 'command-timeout',
|
|
37
44
|
resultField: 'commandTimedOut',
|
|
38
45
|
carryForward: true,
|
|
39
46
|
restartable: true,
|
|
40
|
-
reported: true
|
|
47
|
+
reported: true,
|
|
48
|
+
fatal: false
|
|
41
49
|
},
|
|
42
50
|
{
|
|
43
51
|
id: 'stream-stall',
|
|
44
52
|
resultField: 'streamStalled',
|
|
45
53
|
carryForward: true,
|
|
46
54
|
restartable: true,
|
|
47
|
-
reported: true
|
|
55
|
+
reported: true,
|
|
56
|
+
fatal: false
|
|
48
57
|
},
|
|
49
58
|
{
|
|
50
59
|
id: 'worker-timeout',
|
|
51
60
|
resultField: 'timedOut',
|
|
52
61
|
carryForward: true,
|
|
53
62
|
restartable: true,
|
|
54
|
-
reported: true
|
|
63
|
+
reported: true,
|
|
64
|
+
fatal: false
|
|
55
65
|
},
|
|
56
66
|
{
|
|
57
67
|
id: 'connection-error',
|
|
58
68
|
resultField: null,
|
|
59
69
|
carryForward: true,
|
|
60
70
|
restartable: true,
|
|
61
|
-
reported: false
|
|
71
|
+
reported: false,
|
|
72
|
+
fatal: false
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'loop',
|
|
76
|
+
resultField: 'loopHit',
|
|
77
|
+
carryForward: false,
|
|
78
|
+
restartable: true,
|
|
79
|
+
reported: true,
|
|
80
|
+
fatal: false
|
|
62
81
|
},
|
|
63
|
-
{ id: 'loop', resultField: 'loopHit', carryForward: false, restartable: true, reported: true },
|
|
64
82
|
{
|
|
65
83
|
id: 'leaked-tool-call',
|
|
66
84
|
resultField: 'leakedToolCall',
|
|
67
85
|
carryForward: false,
|
|
68
86
|
restartable: true,
|
|
69
|
-
reported: true
|
|
87
|
+
reported: true,
|
|
88
|
+
fatal: false
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'empty-answer',
|
|
92
|
+
resultField: null,
|
|
93
|
+
carryForward: false,
|
|
94
|
+
restartable: true,
|
|
95
|
+
reported: false,
|
|
96
|
+
fatal: false
|
|
70
97
|
},
|
|
71
|
-
{
|
|
72
|
-
|
|
98
|
+
{
|
|
99
|
+
id: 'aborted',
|
|
100
|
+
resultField: null,
|
|
101
|
+
carryForward: false,
|
|
102
|
+
restartable: false,
|
|
103
|
+
reported: true,
|
|
104
|
+
fatal: false
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'exit',
|
|
108
|
+
resultField: null,
|
|
109
|
+
carryForward: false,
|
|
110
|
+
restartable: false,
|
|
111
|
+
reported: true,
|
|
112
|
+
fatal: false
|
|
113
|
+
}
|
|
73
114
|
];
|
|
74
115
|
/** Look one cause up. `undefined` only for an id with no row, which the suite forbids. */
|
|
75
116
|
export function workerKill(id) {
|
|
76
117
|
return WORKER_KILLS.find(k => k.id === id);
|
|
77
118
|
}
|
|
119
|
+
/** Takes any string: the phase failure union carries kinds that are not kills. */
|
|
120
|
+
export function isFatalKill(kind) {
|
|
121
|
+
return WORKER_KILLS.some(k => k.id === kind && k.fatal);
|
|
122
|
+
}
|
|
78
123
|
/**
|
|
79
124
|
* The restart ladder's precedence, as ids. `RESTART_RULES` must be exactly this,
|
|
80
125
|
* in this order.
|
|
81
126
|
*
|
|
82
127
|
* `loop` leads: its hint names the offending call, which is the most useful thing
|
|
83
|
-
* to tell a re-spawn. The two watchdogs
|
|
84
|
-
* is the narrower diagnosis, and they cannot be
|
|
85
|
-
* kill leaves the worker's own timeout flag false.
|
|
128
|
+
* to tell a re-spawn. The two watchdogs and the dead-backend probe come before
|
|
129
|
+
* the wall clock because each is the narrower diagnosis, and they cannot be
|
|
130
|
+
* confused with it — a guard kill leaves the worker's own timeout flag false.
|
|
131
|
+
* `empty-answer` is last: like a leaked call it exists only on a clean run.
|
|
86
132
|
*/
|
|
87
133
|
export const RESTART_ORDER = [
|
|
88
134
|
'loop',
|
|
89
135
|
'command-timeout',
|
|
90
136
|
'stream-stall',
|
|
137
|
+
'stalled',
|
|
91
138
|
'worker-timeout',
|
|
92
139
|
'connection-error',
|
|
93
|
-
'leaked-tool-call'
|
|
140
|
+
'leaked-tool-call',
|
|
141
|
+
'empty-answer'
|
|
94
142
|
// `as const satisfies`, not an annotation: `WorkerRestartReason` is
|
|
95
143
|
// `(typeof RESTART_ORDER)[number]`, and a `readonly WorkerKillId[]`
|
|
96
144
|
// annotation collapses that to the whole `WorkerKillId` union — which would
|
|
@@ -89,6 +89,12 @@ export interface StalledGuard {
|
|
|
89
89
|
* harnesses inject a real probe through the override.
|
|
90
90
|
*/
|
|
91
91
|
probe: (() => Promise<boolean>) | null;
|
|
92
|
+
/**
|
|
93
|
+
* Re-spawn on a dead-backend kill, within the shared restart budget, so the
|
|
94
|
+
* verdict is earned on every attempt rather than on one probe sample. Off
|
|
95
|
+
* for a worker whose caller would rather hear "unreachable" at once.
|
|
96
|
+
*/
|
|
97
|
+
restart: boolean;
|
|
92
98
|
}
|
|
93
99
|
/**
|
|
94
100
|
* The whole-worker deadline. All three fields move the SAME timer, which is why
|
|
@@ -198,6 +204,14 @@ interface WorkerGuardShapes {
|
|
|
198
204
|
'connection-error': number;
|
|
199
205
|
loop: LoopGuard;
|
|
200
206
|
'leaked-tool-call': null;
|
|
207
|
+
/**
|
|
208
|
+
* Re-spawn on an empty completion — exit 0, no text, no reported error —
|
|
209
|
+
* within the shared restart budget. For a phase child that is almost always
|
|
210
|
+
* a provider error swallowed inside `--mode json`, not a repeatable mistake.
|
|
211
|
+
* Off for the research workers, which decide what an empty section means
|
|
212
|
+
* themselves (research-worker.ts).
|
|
213
|
+
*/
|
|
214
|
+
'empty-answer': boolean;
|
|
201
215
|
aborted: null;
|
|
202
216
|
exit: null;
|
|
203
217
|
}
|
|
@@ -252,6 +266,12 @@ export interface WorkerPolicyInputs {
|
|
|
252
266
|
commandTimeoutMs?: number;
|
|
253
267
|
/** gate, phase: `config.streamInactivityMs`. */
|
|
254
268
|
streamInactivityMs?: number;
|
|
269
|
+
/**
|
|
270
|
+
* phase: a wall clock on ONE spawn, for a caller that genuinely wants a hard
|
|
271
|
+
* stop. Nothing sets it in production — see the phase row's `why` — and
|
|
272
|
+
* tests inject a short one.
|
|
273
|
+
*/
|
|
274
|
+
timeoutMs?: number;
|
|
255
275
|
/** research: only `worker:apis` fans out, so only it can be scaled. */
|
|
256
276
|
fanoutBounded?: boolean;
|
|
257
277
|
/** research: the env reader the fanout and progress-ceiling levers use.
|
|
@@ -102,7 +102,7 @@ export const DEFAULT_LOOP_PROGRESS = {
|
|
|
102
102
|
*/
|
|
103
103
|
function baseGuards() {
|
|
104
104
|
return {
|
|
105
|
-
stalled: { afterMs: STALL_AFTER_MS, probe: null },
|
|
105
|
+
stalled: { afterMs: STALL_AFTER_MS, probe: null, restart: false },
|
|
106
106
|
'command-timeout': 0,
|
|
107
107
|
'stream-stall': 0,
|
|
108
108
|
'worker-timeout': {
|
|
@@ -113,6 +113,7 @@ function baseGuards() {
|
|
|
113
113
|
'connection-error': MAX_LOOP_RESTARTS,
|
|
114
114
|
loop: { detector: { ...DEFAULT_LOOP_DETECTOR }, progress: { ...DEFAULT_LOOP_PROGRESS } },
|
|
115
115
|
'leaked-tool-call': null,
|
|
116
|
+
'empty-answer': false,
|
|
116
117
|
aborted: null,
|
|
117
118
|
exit: null
|
|
118
119
|
};
|
|
@@ -213,21 +214,33 @@ export const WORKER_PROFILES = {
|
|
|
213
214
|
+ 'watchdog SUSPENDS for the duration of a tool call, the dead-backend '
|
|
214
215
|
+ 'probe reads a reachable endpoint as alive, and both runaway detectors '
|
|
215
216
|
+ 'wait on a result that never arrives. Only a user ESC could end it. '
|
|
216
|
-
+ 'The wall clock
|
|
217
|
-
+ "
|
|
218
|
-
+ '
|
|
219
|
-
+ '
|
|
220
|
-
+ '
|
|
217
|
+
+ 'The wall clock is OFF. A wall clock on a model child measures the '
|
|
218
|
+
+ "MODEL'S SPEED, not its health: the same planning child that answers "
|
|
219
|
+
+ 'in seconds on one backend takes minutes on another, so any cap loose '
|
|
220
|
+
+ 'enough to be safe catches nothing and any cap tight enough to catch a '
|
|
221
|
+
+ 'runaway kills healthy work. The runaway it would catch — forward-paging '
|
|
222
|
+
+ "through a whole context window — is the StallDetector's, which bounds "
|
|
223
|
+
+ 'non-progress and churn rather than elapsed time. '
|
|
224
|
+
+ 'Two departures the research workers do not share. A dead-backend kill '
|
|
225
|
+
+ 'is RETRIED: one probe sample cannot tell a blip from a death, three '
|
|
226
|
+
+ 'failed probes cost the stall window three times, and one wrong verdict '
|
|
227
|
+
+ 'ends the whole run. And an EMPTY completion is retried: inside '
|
|
228
|
+
+ '`--mode json` a swallowed provider error arrives as exit 0 with no '
|
|
229
|
+
+ 'text, and a fresh spawn almost always answers.',
|
|
221
230
|
resolve: inputs => {
|
|
222
231
|
const guards = baseGuards();
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
guards['worker-timeout'] = {
|
|
233
|
+
timeoutMs: inputs.timeoutMs ?? 0,
|
|
234
|
+
progressCeilingMs: null,
|
|
235
|
+
fanout: null
|
|
236
|
+
};
|
|
226
237
|
// Both ceilings are the user's own settings, as they are for gate: the
|
|
227
238
|
// number is theirs, the decision to arm it is this row's. 0 from a
|
|
228
239
|
// caller that hands none, so a harness cannot silently acquire a guard.
|
|
229
240
|
guards['command-timeout'] = inputs.commandTimeoutMs ?? 0;
|
|
230
241
|
guards['stream-stall'] = inputs.streamInactivityMs ?? 0;
|
|
242
|
+
guards.stalled = { afterMs: STALL_AFTER_MS, probe: null, restart: true };
|
|
243
|
+
guards['empty-answer'] = true;
|
|
231
244
|
return { guards, carryForward: false };
|
|
232
245
|
}
|
|
233
246
|
// `as const satisfies`, not an annotation — the same reason RESTART_ORDER
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.2",
|
|
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",
|