@mjasnikovs/pi-task 0.38.22 → 0.38.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/README.md +2 -2
- package/dist/config/reasoning-args.d.ts +12 -1
- package/dist/config/reasoning-args.js +5 -2
- package/dist/config/reasoning.d.ts +60 -2
- package/dist/config/reasoning.js +344 -37
- package/dist/config/register.d.ts +76 -32
- package/dist/config/register.js +124 -82
- package/dist/shared/reasoning-capability.d.ts +2 -5
- package/dist/shared/reasoning-capability.js +31 -4
- package/dist/task/auto-orchestrator.d.ts +2 -0
- package/dist/task/auto-orchestrator.js +28 -41
- package/dist/task/child-runner.d.ts +89 -24
- package/dist/task/child-runner.js +67 -46
- package/dist/task/orchestrator.d.ts +14 -20
- package/dist/task/orchestrator.js +12 -9
- package/dist/task/phases.d.ts +16 -23
- package/dist/task/phases.js +47 -452
- package/dist/task/question-dialog.d.ts +56 -0
- package/dist/task/question-dialog.js +53 -0
- package/dist/task/research-worker.d.ts +180 -0
- package/dist/task/research-worker.js +432 -0
- package/dist/workers/brave-warning.js +4 -30
- package/dist/workers/docs-core.d.ts +8 -4
- package/dist/workers/docs-core.js +30 -21
- package/dist/workers/docs-lookup.d.ts +72 -0
- package/dist/workers/docs-lookup.js +53 -0
- package/dist/workers/docs-project.d.ts +9 -0
- package/dist/workers/docs-project.js +15 -0
- package/dist/workers/pi-worker-core.d.ts +87 -1
- package/dist/workers/pi-worker-core.js +3 -7
- package/dist/workers/pi-worker-docs.js +27 -31
- package/dist/workers/reasoning-warning.d.ts +10 -16
- package/dist/workers/reasoning-warning.js +25 -57
- package/dist/workers/session-hint.d.ts +37 -0
- package/dist/workers/session-hint.js +82 -0
- package/dist/workers/worker-failure.d.ts +34 -0
- package/dist/workers/worker-failure.js +27 -16
- package/dist/workers/worker-kill.d.ts +84 -0
- package/dist/workers/worker-kill.js +124 -0
- package/package.json +1 -1
- package/dist/task/reasoning-groups.d.ts +0 -36
- package/dist/task/reasoning-groups.js +0 -36
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* widget line, clarify's plan-shape and triage pre-emption, plan's control
|
|
29
29
|
* actions. Those genuinely differ.
|
|
30
30
|
*/
|
|
31
|
+
import { stripInlineMarkdown } from './inline-markdown.js';
|
|
31
32
|
/** True when the question is a binary fork rather than a single recommendation. */
|
|
32
33
|
export function isTwoOption(p) {
|
|
33
34
|
return p.suggested !== undefined && p.alt !== undefined;
|
|
@@ -87,3 +88,55 @@ export function resolveAnswer(p, raw) {
|
|
|
87
88
|
}
|
|
88
89
|
return { answer: typed, source: 'typed' };
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Settle one question: YOLO short-circuit, cards, ask, record.
|
|
93
|
+
*
|
|
94
|
+
* Returns `'cancelled'` rather than throwing, because that is the one thing the
|
|
95
|
+
* two callers genuinely disagree about — grill throws `USER_CANCELLED` into the
|
|
96
|
+
* phase ladder, clarify announces and returns null from the plan. Everything
|
|
97
|
+
* before it was written out twice at ~50 lines each and had already drifted on
|
|
98
|
+
* `recommended2`, harmless today only because the bridge re-guards it.
|
|
99
|
+
*/
|
|
100
|
+
export async function settleQuestion(input) {
|
|
101
|
+
const { ui, transcript, plain, shown, render, yolo } = input;
|
|
102
|
+
const plainSuggested = input.suggested === undefined ? undefined : stripInlineMarkdown(input.suggested);
|
|
103
|
+
const plainAlt = input.alt === undefined ? undefined : stripInlineMarkdown(input.alt);
|
|
104
|
+
if (yolo !== null) {
|
|
105
|
+
// The stamp is a RECORD fact, and which kinds are stamped is the
|
|
106
|
+
// transcript policy's call — never this call site's.
|
|
107
|
+
if (yolo.kind === 'answer') {
|
|
108
|
+
transcript.add('yolo', plain, stripInlineMarkdown(yolo.answer));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
transcript.add('yolo-skip', plain, `(skipped — ${yolo.note})`);
|
|
112
|
+
}
|
|
113
|
+
return 'settled';
|
|
114
|
+
}
|
|
115
|
+
const pending = {
|
|
116
|
+
plain,
|
|
117
|
+
shown,
|
|
118
|
+
...(plainSuggested !== undefined && {
|
|
119
|
+
suggested: plainSuggested,
|
|
120
|
+
shownSuggested: render(input.suggested)
|
|
121
|
+
}),
|
|
122
|
+
...(plainAlt !== undefined && { alt: plainAlt, shownAlt: render(input.alt) })
|
|
123
|
+
};
|
|
124
|
+
const options = buildOptionCards(pending);
|
|
125
|
+
input.onAsk?.();
|
|
126
|
+
const a = await ui.ask({
|
|
127
|
+
localTitle: shown,
|
|
128
|
+
displayQuestion: shown,
|
|
129
|
+
question: plain,
|
|
130
|
+
recommended: plainSuggested,
|
|
131
|
+
...(plainAlt !== undefined && { recommended2: plainAlt }),
|
|
132
|
+
allowSkip: plainSuggested === undefined && plainAlt === undefined,
|
|
133
|
+
...(options && { options })
|
|
134
|
+
});
|
|
135
|
+
if (a === undefined)
|
|
136
|
+
return 'cancelled';
|
|
137
|
+
// An accept covers both routes to it: submitting empty, and pressing the
|
|
138
|
+
// single green card.
|
|
139
|
+
const resolved = resolveAnswer(pending, a);
|
|
140
|
+
transcript.add(resolved.source === 'accepted' ? 'accepted' : 'typed', plain, resolved.answer);
|
|
141
|
+
return 'settled';
|
|
142
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE research worker, cache-skip to persist.
|
|
3
|
+
*
|
|
4
|
+
* WHY IT IS A MODULE. This was a 228-line closure inside `phaseResearch` over
|
|
5
|
+
* eleven locals, and inside it live the three RETRY GATES and their precedence:
|
|
6
|
+
* the EMPTY-SECTION gate (the only one that can fail the phase), the
|
|
7
|
+
* ZERO-RETRIEVAL gate and the SILENT gate (both of which discard a failed retry
|
|
8
|
+
* and ship the original). Getting that order wrong is how a run either dies on a
|
|
9
|
+
* legitimately empty section or ships one written from memory.
|
|
10
|
+
*
|
|
11
|
+
* The cost was in the TESTS. Reaching the gates meant a temp dir, a real task
|
|
12
|
+
* file, and a fake spawn routed on prose lifted out of `prompts.ts` — plus, for
|
|
13
|
+
* attempt-1-vs-attempt-2, a second sentence lifted out of a module-private
|
|
14
|
+
* preamble constant. In a codebase whose whole workflow is re-wording prompts and
|
|
15
|
+
* measuring what changed, that means a reworded preamble silently stops the gate
|
|
16
|
+
* tests from testing the gate. Behind this interface a test scripts
|
|
17
|
+
* `runWorker(label, attempt)` and states the `RunWorkerResult` fields a gate
|
|
18
|
+
* reads.
|
|
19
|
+
*
|
|
20
|
+
* THE INVARIANT, stated once: the empty gate runs FIRST and is the only one that
|
|
21
|
+
* can throw; the other two keep the original when their retry does not improve a
|
|
22
|
+
* named measure; and `confirmedEmpty` suppresses the silent gate, because that
|
|
23
|
+
* retry already asked "you wrote nothing" and got the same answer.
|
|
24
|
+
*/
|
|
25
|
+
import type { RunWorkerInput, RunWorkerResult } from '../workers/pi-worker-core.js';
|
|
26
|
+
import type { SpawnFn } from '../shared/child-process.js';
|
|
27
|
+
import type { DebugLine } from './debug-log.js';
|
|
28
|
+
/**
|
|
29
|
+
* One research worker's row. `section` is the heading its output is assembled
|
|
30
|
+
* and cached under; `label` is its child NAME — what the loader, the debug trail
|
|
31
|
+
* and the A/B ledgers print, and the key into `REASONING_GROUP_BY_CHILD`.
|
|
32
|
+
*/
|
|
33
|
+
export interface ResearchWorkerSpec {
|
|
34
|
+
section: string;
|
|
35
|
+
label: string;
|
|
36
|
+
/** Static, or built from the sections completed so far (serial mode hands
|
|
37
|
+
* APIS the finished FILES map; parallel mode hands it nothing). */
|
|
38
|
+
prompt: string | ((prior: ReadonlyArray<{
|
|
39
|
+
name: string;
|
|
40
|
+
text: string;
|
|
41
|
+
}>) => string);
|
|
42
|
+
tools?: string;
|
|
43
|
+
extensions?: string[];
|
|
44
|
+
/** Deterministic gate over the worker's own output, run BEFORE the section is
|
|
45
|
+
* persisted, so nothing it rejects survives into the cache or into compose. */
|
|
46
|
+
postProcess?: (text: string) => string;
|
|
47
|
+
/** When set, a non-empty section produced with ZERO grounding-retrieval calls is
|
|
48
|
+
* re-run ONCE with this preamble prepended, forcing a retrieval-first pass. The
|
|
49
|
+
* retry replaces the original only if it actually retrieved. */
|
|
50
|
+
zeroRetrievalRetry?: string;
|
|
51
|
+
/** When set, a section that comes out SILENT — zero parseable bullets from a
|
|
52
|
+
* loop-degrade banner or a hallucinated non-bullet fragment — is re-run ONCE
|
|
53
|
+
* with this preamble. A legitimately-empty section is NOT retried. */
|
|
54
|
+
retryIfSilent?: string;
|
|
55
|
+
/** This worker can issue project-source docs lookups, so the 5B fan-out bounds
|
|
56
|
+
* apply to it (see task/research-fanout-budget.ts). */
|
|
57
|
+
fanoutBounded?: true;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The small record the driver needs: one way to run a worker, and where to put
|
|
61
|
+
* what comes back.
|
|
62
|
+
*
|
|
63
|
+
* Everything here is a fact about THIS RUN, not about the worker — which is what
|
|
64
|
+
* lets the four rows be plain data and the driver be a function of two arguments.
|
|
65
|
+
*/
|
|
66
|
+
export interface ResearchWorkerRun {
|
|
67
|
+
runWorker: (label: string, input: RunWorkerInput) => Promise<RunWorkerResult>;
|
|
68
|
+
cwd: string;
|
|
69
|
+
taskId: string;
|
|
70
|
+
signal: AbortSignal;
|
|
71
|
+
spawn?: SpawnFn;
|
|
72
|
+
/** The `--thinking` fragment for a named child. */
|
|
73
|
+
thinkingFor: (label: string) => string[];
|
|
74
|
+
logDebug?: (msg: string, kind?: DebugLine) => void;
|
|
75
|
+
onChildOutput?: (line: string) => void;
|
|
76
|
+
/** Record one finished worker's timing splits. */
|
|
77
|
+
record: (label: string, p: Promise<RunWorkerResult>) => Promise<RunWorkerResult>;
|
|
78
|
+
/** Called once per worker that reaches an outcome, cached ones included. */
|
|
79
|
+
onDone: () => void;
|
|
80
|
+
/**
|
|
81
|
+
* Read one worker's cached output back, or '' when there is none.
|
|
82
|
+
*
|
|
83
|
+
* A SEAM, and the symmetric half of `persistSection`. The cache skip is one
|
|
84
|
+
* of the four outcomes this driver has, and reaching it used to require a
|
|
85
|
+
* real task file on disk — which is most of why the gate tests needed a temp
|
|
86
|
+
* dir at all.
|
|
87
|
+
*/
|
|
88
|
+
readCached: (heading: string) => Promise<string>;
|
|
89
|
+
/** Write one validated section to the task file. Serialised by the caller. */
|
|
90
|
+
persistSection: (heading: string, text: string) => Promise<void>;
|
|
91
|
+
/** 5B RESCUE / SCALE knobs, resolved once for the phase. */
|
|
92
|
+
carryForward: boolean;
|
|
93
|
+
fanoutTimeout: {
|
|
94
|
+
perLookupMs: number;
|
|
95
|
+
ceilingMs: number;
|
|
96
|
+
} | null;
|
|
97
|
+
progressCeilingMs: number | null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Task-file heading under which a research worker's validated output is cached.
|
|
101
|
+
* A resumed research phase reads these to skip workers that already succeeded,
|
|
102
|
+
* instead of re-running all four from scratch when one of them fails — the
|
|
103
|
+
* expensive case (e.g. 3 healthy workers thrown away because the 4th looped).
|
|
104
|
+
*/
|
|
105
|
+
export declare function researchWorkerCacheHeading(section: string): string;
|
|
106
|
+
/**
|
|
107
|
+
* Classify a research worker's result so the phase can react per-worker instead
|
|
108
|
+
* of treating every failure the same. Two distinct failure shapes:
|
|
109
|
+
*
|
|
110
|
+
* - 'runaway' (loop-kill OR per-worker wall-clock timeout): the worker explored
|
|
111
|
+
* too long and was killed *after* burning its MAX_LOOP_RESTARTS restarts. It
|
|
112
|
+
* did real work and left partial text; the other three workers are unaffected.
|
|
113
|
+
* Failing the whole task here would throw away every already-good worker AND
|
|
114
|
+
* abort the entire auto-run over the weakest section — and because the loop is
|
|
115
|
+
* deterministic, a resume just re-loops and re-fails. So this DEGRADES: keep
|
|
116
|
+
* the partial answer (marked), cache it, move on. A loop-kill is a SIGTERM
|
|
117
|
+
* (exit 143) OR a clean exit 0 with truncated text, so loopHit/timedOut — not
|
|
118
|
+
* exitCode — are the reliable signal and are checked first.
|
|
119
|
+
*
|
|
120
|
+
* - 'fatal' (non-zero exit that isn't a loop-kill, a provider error behind an
|
|
121
|
+
* empty answer, or a leaked never-executed tool call): the output is
|
|
122
|
+
* untrustworthy in a way partial text can't paper over (broken env, model
|
|
123
|
+
* disconnect, wrong tool-call dialect). These still throw — degrading them
|
|
124
|
+
* would launder a real breakage into a plausible-looking section.
|
|
125
|
+
*
|
|
126
|
+
* - 'empty' (clean exit 0, no provider error, no loop/timeout — the model simply
|
|
127
|
+
* wrote nothing): NOT a failure. On an extremely simple task ("create a folder
|
|
128
|
+
* with an index.html in it") three of the four workers have genuinely nothing
|
|
129
|
+
* to report, and each worker prompt tells the model to emit ONLY what this task
|
|
130
|
+
* touches and to drop everything else — so silence is the CORRECT answer and
|
|
131
|
+
* was killing the whole task at research (issue #10). Measured live on the
|
|
132
|
+
* issue's own prompt (30 reps/worker, local Qwen3.6-27B): every APIS answer was
|
|
133
|
+
* semantically "there is nothing here", and 2/30 were literally zero bytes on a
|
|
134
|
+
* clean exit — the other 28 survived only because the model happened to wrap the
|
|
135
|
+
* same non-answer in a parenthetical, which is model style, not signal. The
|
|
136
|
+
* caller retries once and then accepts an explicit empty section; what stays
|
|
137
|
+
* fatal is silence WITH a reported cause, which is the masked-disconnect case
|
|
138
|
+
* this branch was written for and which `modelError` now names outright.
|
|
139
|
+
*
|
|
140
|
+
* Returns null when the result is trustworthy.
|
|
141
|
+
*/
|
|
142
|
+
export declare function classifyResearchWorker(name: string, result: RunWorkerResult): {
|
|
143
|
+
kind: 'runaway';
|
|
144
|
+
reason: string;
|
|
145
|
+
} | {
|
|
146
|
+
kind: 'fatal';
|
|
147
|
+
error: Error;
|
|
148
|
+
} | {
|
|
149
|
+
kind: 'empty';
|
|
150
|
+
} | null;
|
|
151
|
+
/**
|
|
152
|
+
* Build a degraded section body for a runaway worker: a one-line marker naming
|
|
153
|
+
* the failure (so downstream phases and a human reading the task file know this
|
|
154
|
+
* section is incomplete) followed by whatever partial answer the worker streamed
|
|
155
|
+
* before it was killed. The marker is always present even when there is no
|
|
156
|
+
* partial text, so an empty degrade is never mistaken for a real finding.
|
|
157
|
+
*/
|
|
158
|
+
export declare function degradedSectionBody(name: string, reason: string, partial: string): string;
|
|
159
|
+
/**
|
|
160
|
+
* The body written for a research section the worker confirmed has no entries.
|
|
161
|
+
*
|
|
162
|
+
* Three states have to stay distinguishable to anyone — human or later phase —
|
|
163
|
+
* reading a research section, so each carries its own marker:
|
|
164
|
+
* `(none — …)` the worker RAN and answered "nothing applies" (this)
|
|
165
|
+
* `(degraded: …)` the worker was killed mid-answer, text may be partial
|
|
166
|
+
* (degradedSectionBody)
|
|
167
|
+
* section absent the worker never got that far — the phase threw
|
|
168
|
+
*
|
|
169
|
+
* Naming the worker inside the marker keeps it true after assembly, where the
|
|
170
|
+
* section headings are all that separate the four workers' output.
|
|
171
|
+
*/
|
|
172
|
+
export declare function emptySectionBody(name: string): string;
|
|
173
|
+
export declare function isBareNoneAnswer(text: string): boolean;
|
|
174
|
+
export declare function runResearchWorker(spec: ResearchWorkerSpec, run: ResearchWorkerRun, prior?: ReadonlyArray<{
|
|
175
|
+
name: string;
|
|
176
|
+
text: string;
|
|
177
|
+
}>): Promise<{
|
|
178
|
+
name: string;
|
|
179
|
+
text: string;
|
|
180
|
+
}>;
|