@mjasnikovs/pi-task 0.38.2 → 0.38.4
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/config/config.d.ts +7 -0
- package/dist/config/config.js +10 -4
- package/dist/config/register.d.ts +37 -0
- package/dist/config/register.js +89 -114
- package/dist/remote/events.js +0 -3
- package/dist/remote/register.js +12 -3
- package/dist/task/auto-orchestrator.js +119 -94
- package/dist/task/command-run.d.ts +104 -0
- package/dist/task/command-run.js +138 -0
- package/dist/task/coverage-loop.d.ts +45 -0
- package/dist/task/critique-probes.d.ts +82 -0
- package/dist/task/critique-probes.js +156 -0
- package/dist/task/deep-render-check.d.ts +30 -0
- package/dist/task/deep-render-check.js +19 -11
- package/dist/task/enforce-guidelines.d.ts +14 -17
- package/dist/task/enforce-guidelines.js +44 -31
- package/dist/task/final-gate.d.ts +8 -10
- package/dist/task/final-gate.js +36 -74
- package/dist/task/gate-child.d.ts +104 -0
- package/dist/task/gate-child.js +177 -0
- package/dist/task/gate-deps.d.ts +13 -0
- package/dist/task/gate-deps.js +72 -208
- package/dist/task/orchestrator.js +13 -22
- package/dist/task/phases.js +109 -182
- package/dist/task/plan-session.d.ts +4 -22
- package/dist/task/plan-session.js +4 -33
- package/dist/task/question-dialog.d.ts +71 -0
- package/dist/task/question-dialog.js +89 -0
- package/dist/task/terminal-outcome.d.ts +67 -0
- package/dist/task/terminal-outcome.js +76 -0
- package/dist/task/type-only-answer.js +2 -3
- package/dist/workers/abstention.d.ts +71 -0
- package/dist/workers/abstention.js +108 -0
- package/dist/workers/docs-chunk.d.ts +74 -0
- package/dist/workers/docs-chunk.js +143 -0
- package/dist/workers/docs-core.d.ts +10 -1
- package/dist/workers/docs-core.js +22 -19
- package/dist/workers/docs-index.js +2 -69
- package/dist/workers/docs-project.d.ts +15 -1
- package/dist/workers/docs-project.js +27 -66
- package/dist/workers/fetch-core.d.ts +1 -1
- package/dist/workers/fetch-core.js +2 -1
- package/dist/workers/pi-worker-core.js +157 -86
- package/dist/workers/pi-worker-docs.js +5 -10
- package/dist/workers/pi-worker-fetch.js +8 -1
- package/dist/workers/typeonly-log.js +2 -10
- package/dist/workers/worker-failure.d.ts +91 -0
- package/dist/workers/worker-failure.js +82 -0
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { Type } from '@sinclair/typebox';
|
|
|
3
3
|
import { Text } from '@earendil-works/pi-tui';
|
|
4
4
|
import { openCache as defaultOpenCache } from './docs-cache.js';
|
|
5
5
|
import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
|
|
6
|
-
import { docsRaw, formatResultText, buildPrompt, buildVersionBanner } from './docs-core.js';
|
|
6
|
+
import { docsRaw, formatResultText, packageHeader, buildPrompt, buildVersionBanner } from './docs-core.js';
|
|
7
7
|
import { formatNpmVersionSection } from './npm-version.js';
|
|
8
8
|
import { runFocusedExtraction } from './focused-extractor.js';
|
|
9
9
|
import { makeWorkerTool } from './shared.js';
|
|
@@ -12,6 +12,7 @@ import { logDocsAnswer } from './typeonly-log.js';
|
|
|
12
12
|
import { normalizeQuery } from './research-cache.js';
|
|
13
13
|
import { projectDocsRaw, buildProjectPrompt } from './docs-project.js';
|
|
14
14
|
import { projectDocsBudget, projectDocsBudgetExhausted } from '../task/research-fanout-budget.js';
|
|
15
|
+
import { isAbstention } from './abstention.js';
|
|
15
16
|
const RENDER_QUERY_MAX = 100;
|
|
16
17
|
const Params = Type.Object({
|
|
17
18
|
module: Type.String({
|
|
@@ -189,13 +190,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
189
190
|
if (!extraction.ok)
|
|
190
191
|
return docsFailureResult(extraction, baseDetails, '');
|
|
191
192
|
const verified = extraction.excerptVerified;
|
|
192
|
-
const text = formatResultText({
|
|
193
|
-
name: projectName,
|
|
194
|
-
version: 'local',
|
|
195
|
-
root: ctx.cwd,
|
|
196
|
-
entryDts: null,
|
|
197
|
-
readme: null
|
|
198
|
-
}, extraction, verified);
|
|
193
|
+
const text = formatResultText(`Per ${projectName} (project source):`, extraction, verified);
|
|
199
194
|
// SAME instrumentation channel as the package path below, extended to the
|
|
200
195
|
// project-source branch because that branch is the MAJORITY of what
|
|
201
196
|
// worker:apis asks — 13 of 17 docs calls in run 15's fatal task, 7 of 12 in
|
|
@@ -302,7 +297,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
302
297
|
return docsFailureResult(extraction, baseDetails, versionBanner + npmHeader);
|
|
303
298
|
}
|
|
304
299
|
const verified = extraction.excerptVerified;
|
|
305
|
-
const body = formatResultText(pkg, extraction, verified);
|
|
300
|
+
const body = formatResultText(packageHeader(pkg), extraction, verified);
|
|
306
301
|
// F-2: a TYPE-ONLY answer is the dangerous failure. "unclear from this package"
|
|
307
302
|
// is honest and already escalates; a signature is a well-formed, confident,
|
|
308
303
|
// on-topic answer that names the very parameter asked about, so the worker
|
|
@@ -404,6 +399,6 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
404
399
|
cacheable: (d, text) => d.childExitCode === 0
|
|
405
400
|
&& d.typeOnly !== true
|
|
406
401
|
&& d.excerptVerified !== false
|
|
407
|
-
&&
|
|
402
|
+
&& !isAbstention(text)
|
|
408
403
|
});
|
|
409
404
|
}
|
|
@@ -4,6 +4,7 @@ import { FetchAndCleanError } from './html-clean.js';
|
|
|
4
4
|
import { fetchFocused, formatResultText } from './fetch-core.js';
|
|
5
5
|
import { makeWorkerTool } from './shared.js';
|
|
6
6
|
import { normalizeQuery } from './research-cache.js';
|
|
7
|
+
import { isAbstention } from './abstention.js';
|
|
7
8
|
const RENDER_QUERY_MAX = 100;
|
|
8
9
|
const Params = Type.Object({
|
|
9
10
|
url: Type.String({ description: 'URL to fetch. Must be http or https.' }),
|
|
@@ -92,6 +93,12 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
92
93
|
cacheKey: params => `${params.url.trim()}::${normalizeQuery(params.query)}`,
|
|
93
94
|
// Only a completed fetch (child exited 0) is a real answer; invalid-URL,
|
|
94
95
|
// fetch failures, and aborts omit childExitCode:0 and fall through.
|
|
95
|
-
|
|
96
|
+
// F-2(e), on the fetch channel. A child that ran fine and answered
|
|
97
|
+
// "unclear from this page" exits 0, so caching on process health alone
|
|
98
|
+
// memoised the NON-ANSWER and re-served it to every later sibling task —
|
|
99
|
+
// the same dead-end-paid-many-times shape pi-worker-docs already closed
|
|
100
|
+
// for packages, with escalation unable to re-fire because the miss never
|
|
101
|
+
// recurred. One predicate now covers every corpus (workers/abstention.ts).
|
|
102
|
+
cacheable: (d, text) => d.childExitCode === 0 && !isAbstention(text)
|
|
96
103
|
});
|
|
97
104
|
}
|
|
@@ -37,17 +37,9 @@
|
|
|
37
37
|
* reconstruct later.
|
|
38
38
|
*/
|
|
39
39
|
import * as fs from 'node:fs';
|
|
40
|
+
import { isAbstention } from './abstention.js';
|
|
40
41
|
/** Env var naming the JSONL sink. Unset (or empty) ⇒ instrumentation is entirely off. */
|
|
41
42
|
export const TYPEONLY_LOG_ENV = 'PI_TASK_TYPEONLY_LOG';
|
|
42
|
-
/**
|
|
43
|
-
* The honest non-answer, in BOTH wordings the tool can emit. A package lookup is told to
|
|
44
|
-
* write "unclear from this package" (docs-core.ts:622); a project-source lookup is told
|
|
45
|
-
* "unclear from this project" (docs-project.ts:310). Matching only the first silently scored
|
|
46
|
-
* every project-source abstention as a valid answer — and project-source is the MAJORITY of
|
|
47
|
-
* what worker:apis asks (13 of 17 calls in run 15's fatal task), so that one missing word
|
|
48
|
-
* would have put the wrong denominator under the whole termination diagnostic.
|
|
49
|
-
*/
|
|
50
|
-
const UNCLEAR = /unclear from this (package|project)/i;
|
|
51
43
|
/**
|
|
52
44
|
* Append one record to the JSONL sink named by `PI_TASK_TYPEONLY_LOG`, if set.
|
|
53
45
|
*
|
|
@@ -61,7 +53,7 @@ export function logDocsAnswer(rec, getEnv = k => process.env[k]) {
|
|
|
61
53
|
const full = {
|
|
62
54
|
at: new Date().toISOString(),
|
|
63
55
|
...rec,
|
|
64
|
-
unclear:
|
|
56
|
+
unclear: isAbstention(rec.answer)
|
|
65
57
|
};
|
|
66
58
|
try {
|
|
67
59
|
fs.appendFileSync(sink, `${JSON.stringify(full)}\n`, 'utf8');
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a worker child DIED — the one classification of a finished `runWorker`
|
|
3
|
+
* result, and the single place its precedence is written down.
|
|
4
|
+
*
|
|
5
|
+
* Why this module exists. `RunWorkerResult` reports each kill cause as its own
|
|
6
|
+
* optional field (`loopHit`, `timedOut`, `stalled`, `commandTimedOut`,
|
|
7
|
+
* `streamStalled`, `leakedToolCall`, `aborted`, `exitCode`), and every kill path
|
|
8
|
+
* ALSO sets `aborted` and a non-zero exit — killProc flips those on every route
|
|
9
|
+
* out. So a consumer cannot read the fields in any order it likes: the specific
|
|
10
|
+
* causes must be matched before the generic `aborted`/`exitCode` ones, or a dead
|
|
11
|
+
* backend is reported to the user as "you cancelled".
|
|
12
|
+
*
|
|
13
|
+
* That rule used to live as prose in three doc comments and as the source order
|
|
14
|
+
* of three hand-written ladders (`classifyEnforceChildFailure`,
|
|
15
|
+
* `classifyResearchWorker`, and the gate's own reading). Three copies of an
|
|
16
|
+
* ordering is three chances to drift, and it had already drifted: `streamStalled`
|
|
17
|
+
* was added to the result and to `finalAttemptFailed`, but the enforce ladder
|
|
18
|
+
* never grew an arm for it, so an enforcement child killed for a hung model
|
|
19
|
+
* stream fell all the way through to `if (aborted) return USER_CANCELLED` — the
|
|
20
|
+
* exact mislabel the comment above that line warns against.
|
|
21
|
+
*
|
|
22
|
+
* The fix is to make the order data. `FAILURE_RULES` is ordered, the first
|
|
23
|
+
* matching row wins, and consumers `switch` on the resulting `kind` instead of
|
|
24
|
+
* re-deriving the ladder. A new kill cause is one row here plus a compile error
|
|
25
|
+
* in every consumer that has not handled it — which is what makes the drift that
|
|
26
|
+
* caused the bug impossible rather than merely fixed.
|
|
27
|
+
*/
|
|
28
|
+
import type { LoopHit } from '../task/loop-detector.js';
|
|
29
|
+
/**
|
|
30
|
+
* The subset of a finished child result this classification reads.
|
|
31
|
+
*
|
|
32
|
+
* Structural on purpose: `runWorker` returns a superset, and
|
|
33
|
+
* `EnforceChildResult` is a hand-written narrowing of the same shape. Typing the
|
|
34
|
+
* input as what is actually READ lets both pass without either importing the
|
|
35
|
+
* other's interface.
|
|
36
|
+
*/
|
|
37
|
+
export interface WorkerFailureInput {
|
|
38
|
+
exitCode: number;
|
|
39
|
+
aborted: boolean;
|
|
40
|
+
timedOut?: boolean;
|
|
41
|
+
stalled?: boolean;
|
|
42
|
+
loopHit?: unknown;
|
|
43
|
+
leakedToolCall?: unknown;
|
|
44
|
+
commandTimedOut?: {
|
|
45
|
+
toolName: string;
|
|
46
|
+
timeoutMs: number;
|
|
47
|
+
};
|
|
48
|
+
streamStalled?: {
|
|
49
|
+
idleMs: number;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Why the child died, or `undefined` when it finished under its own power.
|
|
54
|
+
*
|
|
55
|
+
* Note what is NOT here: an empty answer, and a reported `modelError` on a run
|
|
56
|
+
* that still produced text. Neither is a kill — whether they count as a failure
|
|
57
|
+
* is the consumer's policy (research accepts a genuinely empty section; the gate
|
|
58
|
+
* does not), so folding them in would move a decision out of the module that
|
|
59
|
+
* owns it.
|
|
60
|
+
*/
|
|
61
|
+
export type WorkerFailure = {
|
|
62
|
+
kind: 'stalled';
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'command-timeout';
|
|
65
|
+
toolName: string;
|
|
66
|
+
timeoutMs: number;
|
|
67
|
+
} | {
|
|
68
|
+
kind: 'stream-stall';
|
|
69
|
+
idleMs: number;
|
|
70
|
+
} | {
|
|
71
|
+
kind: 'worker-timeout';
|
|
72
|
+
} | {
|
|
73
|
+
kind: 'loop';
|
|
74
|
+
hit: LoopHit;
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'leaked-tool-call';
|
|
77
|
+
text: string;
|
|
78
|
+
} | {
|
|
79
|
+
kind: 'aborted';
|
|
80
|
+
} | {
|
|
81
|
+
kind: 'exit';
|
|
82
|
+
code: number;
|
|
83
|
+
};
|
|
84
|
+
/** The `kind` of every row, for exhaustiveness checks in consumers. */
|
|
85
|
+
export type WorkerFailureKind = WorkerFailure['kind'];
|
|
86
|
+
/**
|
|
87
|
+
* Classify a finished child. Returns `undefined` when nothing killed it —
|
|
88
|
+
* which is not the same as "it answered": the text may still be empty, and that
|
|
89
|
+
* judgement belongs to the caller.
|
|
90
|
+
*/
|
|
91
|
+
export declare function classifyWorkerFailure(r: WorkerFailureInput): WorkerFailure | undefined;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a worker child DIED — the one classification of a finished `runWorker`
|
|
3
|
+
* result, and the single place its precedence is written down.
|
|
4
|
+
*
|
|
5
|
+
* Why this module exists. `RunWorkerResult` reports each kill cause as its own
|
|
6
|
+
* optional field (`loopHit`, `timedOut`, `stalled`, `commandTimedOut`,
|
|
7
|
+
* `streamStalled`, `leakedToolCall`, `aborted`, `exitCode`), and every kill path
|
|
8
|
+
* ALSO sets `aborted` and a non-zero exit — killProc flips those on every route
|
|
9
|
+
* out. So a consumer cannot read the fields in any order it likes: the specific
|
|
10
|
+
* causes must be matched before the generic `aborted`/`exitCode` ones, or a dead
|
|
11
|
+
* backend is reported to the user as "you cancelled".
|
|
12
|
+
*
|
|
13
|
+
* That rule used to live as prose in three doc comments and as the source order
|
|
14
|
+
* of three hand-written ladders (`classifyEnforceChildFailure`,
|
|
15
|
+
* `classifyResearchWorker`, and the gate's own reading). Three copies of an
|
|
16
|
+
* ordering is three chances to drift, and it had already drifted: `streamStalled`
|
|
17
|
+
* was added to the result and to `finalAttemptFailed`, but the enforce ladder
|
|
18
|
+
* never grew an arm for it, so an enforcement child killed for a hung model
|
|
19
|
+
* stream fell all the way through to `if (aborted) return USER_CANCELLED` — the
|
|
20
|
+
* exact mislabel the comment above that line warns against.
|
|
21
|
+
*
|
|
22
|
+
* The fix is to make the order data. `FAILURE_RULES` is ordered, the first
|
|
23
|
+
* matching row wins, and consumers `switch` on the resulting `kind` instead of
|
|
24
|
+
* re-deriving the ladder. A new kill cause is one row here plus a compile error
|
|
25
|
+
* in every consumer that has not handled it — which is what makes the drift that
|
|
26
|
+
* caused the bug impossible rather than merely fixed.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The ordered ladder. FIRST MATCH WINS — row order IS the precedence, and it is
|
|
30
|
+
* the only statement of it in the codebase.
|
|
31
|
+
*
|
|
32
|
+
* The order, and why:
|
|
33
|
+
*
|
|
34
|
+
* 1. `stalled` — no output AND the model endpoint did not answer a probe. The
|
|
35
|
+
* most specific diagnosis there is, and the one most easily lost: the kill
|
|
36
|
+
* aborts, so anything checked after `aborted` never sees it.
|
|
37
|
+
* 2. `command-timeout` — a watchdog kill naming the tool call that hung. Also
|
|
38
|
+
* aborts. Before the wall-clock timeout because it is the narrower cause
|
|
39
|
+
* (the two cannot be confused: a watchdog kill leaves the worker's own
|
|
40
|
+
* timeout flag false).
|
|
41
|
+
* 3. `stream-stall` — a watchdog kill for a model stream that went silent while
|
|
42
|
+
* no tool was running. Sits next to `command-timeout` because it is the same
|
|
43
|
+
* class of event: a watchdog, not the model, ended the attempt.
|
|
44
|
+
* 4. `worker-timeout` — the wall-clock backstop.
|
|
45
|
+
* 5. `loop` — killed for repeating one tool call past threshold. After the
|
|
46
|
+
* timeouts, matching the enforce ladder this replaces; in practice the two
|
|
47
|
+
* cannot both fire, since a loop kill stops the attempt before its own timer
|
|
48
|
+
* can expire.
|
|
49
|
+
* 6. `leaked-tool-call` — the model wrote a call as prose instead of invoking
|
|
50
|
+
* it. Only ever set on an otherwise clean run.
|
|
51
|
+
* 7. `aborted` — no specific cause survived, so this really is a cancel.
|
|
52
|
+
* 8. `exit` — a plain non-zero exit with no kill behind it: a crash.
|
|
53
|
+
*/
|
|
54
|
+
const FAILURE_RULES = [
|
|
55
|
+
r => (r.stalled === true ? { kind: 'stalled' } : null),
|
|
56
|
+
r => r.commandTimedOut ?
|
|
57
|
+
{
|
|
58
|
+
kind: 'command-timeout',
|
|
59
|
+
toolName: r.commandTimedOut.toolName,
|
|
60
|
+
timeoutMs: r.commandTimedOut.timeoutMs
|
|
61
|
+
}
|
|
62
|
+
: null,
|
|
63
|
+
r => (r.streamStalled ? { kind: 'stream-stall', idleMs: r.streamStalled.idleMs } : null),
|
|
64
|
+
r => (r.timedOut === true ? { kind: 'worker-timeout' } : null),
|
|
65
|
+
r => (r.loopHit ? { kind: 'loop', hit: r.loopHit } : null),
|
|
66
|
+
r => (r.leakedToolCall ? { kind: 'leaked-tool-call', text: String(r.leakedToolCall) } : null),
|
|
67
|
+
r => (r.aborted ? { kind: 'aborted' } : null),
|
|
68
|
+
r => (r.exitCode !== 0 ? { kind: 'exit', code: r.exitCode } : null)
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* Classify a finished child. Returns `undefined` when nothing killed it —
|
|
72
|
+
* which is not the same as "it answered": the text may still be empty, and that
|
|
73
|
+
* judgement belongs to the caller.
|
|
74
|
+
*/
|
|
75
|
+
export function classifyWorkerFailure(r) {
|
|
76
|
+
for (const rule of FAILURE_RULES) {
|
|
77
|
+
const hit = rule(r);
|
|
78
|
+
if (hit)
|
|
79
|
+
return hit;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.4",
|
|
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",
|