@mjasnikovs/pi-task 0.38.30 → 0.38.32
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 -1
- package/dist/config/config.d.ts +16 -2
- package/dist/config/config.js +7 -2
- package/dist/config/group-args.d.ts +52 -0
- package/dist/config/group-args.js +110 -0
- package/dist/config/group-models.d.ts +88 -0
- package/dist/config/group-models.js +117 -0
- package/dist/config/groups.d.ts +76 -0
- package/dist/config/groups.js +110 -0
- package/dist/config/option-picker.d.ts +42 -0
- package/dist/config/option-picker.js +73 -0
- package/dist/config/reasoning.d.ts +22 -63
- package/dist/config/reasoning.js +37 -108
- package/dist/config/register.d.ts +98 -12
- package/dist/config/register.js +228 -23
- package/dist/index.js +4 -0
- package/dist/remote/push.js +1 -7
- package/dist/shared/command-watchdog.d.ts +63 -0
- package/dist/shared/command-watchdog.js +87 -0
- package/dist/shared/data-home.d.ts +8 -0
- package/dist/shared/data-home.js +14 -0
- package/dist/shared/model-endpoint.d.ts +53 -0
- package/dist/shared/model-endpoint.js +98 -2
- package/dist/shared/reasoning-capability.d.ts +25 -5
- package/dist/shared/reasoning-capability.js +18 -9
- package/dist/task/auto-orchestrator.js +14 -3
- package/dist/task/child-runner.d.ts +92 -15
- package/dist/task/child-runner.js +303 -66
- package/dist/task/context-usage.d.ts +46 -0
- package/dist/task/context-usage.js +41 -0
- package/dist/task/failure-classifier.js +24 -1
- package/dist/task/gate-child.d.ts +15 -4
- package/dist/task/gate-child.js +2 -2
- package/dist/task/gate-deps.js +7 -2
- package/dist/task/implementation-guards.d.ts +26 -0
- package/dist/task/implementation-guards.js +177 -0
- package/dist/task/implementation-hold.d.ts +118 -0
- package/dist/task/implementation-hold.js +165 -0
- package/dist/task/implementation-turn.d.ts +5 -0
- package/dist/task/implementation-turn.js +12 -1
- package/dist/task/loop-detector.d.ts +18 -0
- package/dist/task/loop-detector.js +22 -2
- package/dist/task/model-hold-stash.d.ts +43 -0
- package/dist/task/model-hold-stash.js +70 -0
- package/dist/task/orchestrator.d.ts +18 -5
- package/dist/task/orchestrator.js +63 -6
- package/dist/task/phases.js +18 -5
- package/dist/task/research-worker.d.ts +2 -2
- package/dist/task/research-worker.js +1 -1
- package/dist/workers/docs-core.js +2 -2
- package/dist/workers/docs-lookup.d.ts +4 -3
- package/dist/workers/docs-lookup.js +1 -1
- package/dist/workers/fetch-core.js +2 -2
- package/dist/workers/focused-extractor.d.ts +6 -4
- package/dist/workers/focused-extractor.js +17 -5
- package/dist/workers/index.js +2 -0
- package/dist/workers/model-warning.d.ts +69 -0
- package/dist/workers/model-warning.js +113 -0
- package/dist/workers/pi-worker-core.d.ts +9 -38
- package/dist/workers/pi-worker-core.js +8 -86
- package/dist/workers/pi-worker-docs.js +2 -2
- package/dist/workers/pi-worker.js +4 -4
- package/dist/workers/reasoning-warning.d.ts +17 -9
- package/dist/workers/reasoning-warning.js +69 -22
- package/dist/workers/single-read-guard.d.ts +6 -6
- package/dist/workers/single-read-guard.js +8 -8
- package/dist/workers/worker-profiles.d.ts +11 -3
- package/dist/workers/worker-profiles.js +33 -1
- package/package.json +1 -1
- package/dist/config/reasoning-args.d.ts +0 -23
- package/dist/config/reasoning-args.js +0 -28
- package/dist/task/implementation-thinking.d.ts +0 -56
- package/dist/task/implementation-thinking.js +0 -32
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve every model cell ONCE per session, and say what does not work.
|
|
3
|
+
*
|
|
4
|
+
* WHY HERE AND NOT AT ARGV TIME
|
|
5
|
+
* -----------------------------
|
|
6
|
+
* The honest question is "can a `--no-extensions` child resolve this spec?", and
|
|
7
|
+
* only `ctx.modelRegistry` can answer it. Five of the six argv producers have no
|
|
8
|
+
* `ctx` at all, and the answer cannot be read from disk either: models.json plus
|
|
9
|
+
* models-store.json are only part of the catalogue, since pi-ai ships built-in
|
|
10
|
+
* lists for 39 providers and this project does not depend on pi-ai. So it is
|
|
11
|
+
* asked at `session_start`, where ctx exists and every task is still in the
|
|
12
|
+
* future, and the verdict is left in group-args.ts for the producers to consult.
|
|
13
|
+
*
|
|
14
|
+
* WHY DROPPING THE FLAG IS THE RIGHT DEGRADE
|
|
15
|
+
* ------------------------------------------
|
|
16
|
+
* A spec whose model is gone but whose PROVIDER still has other models does not
|
|
17
|
+
* make pi exit. `buildFallbackModel` invents a synthetic model id, forces
|
|
18
|
+
* `reasoning: true` onto it, inherits the provider's default baseUrl, and
|
|
19
|
+
* answers at exit 0 — so the child silently runs a model nobody chose. Dropping
|
|
20
|
+
* the flag runs the child exactly as it ran last week and names the cell.
|
|
21
|
+
*
|
|
22
|
+
* A SEPARATE hint from the reasoning one, with its own widget key: the two have
|
|
23
|
+
* different fixes (`models.json` vs `/task-config`) and that line is already at
|
|
24
|
+
* its length budget.
|
|
25
|
+
*/
|
|
26
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
27
|
+
import { type ChildGroup } from '../config/groups.js';
|
|
28
|
+
/** One cell that will not do what it says. */
|
|
29
|
+
export interface ModelProblem {
|
|
30
|
+
group: ChildGroup;
|
|
31
|
+
spec: string;
|
|
32
|
+
/**
|
|
33
|
+
* `unresolved` — no such model here, so the flag is dropped.
|
|
34
|
+
* `extension` — resolvable only because a host extension registered its
|
|
35
|
+
* provider. Children run `--no-extensions`, which disables DISCOVERY only,
|
|
36
|
+
* so it works exactly when that extension is in the child whitelist. We
|
|
37
|
+
* cannot tell which extension registered it: `getRegisteredProviderIds()`
|
|
38
|
+
* gives ids, and the `{name, config, extensionPath}` triples live in the
|
|
39
|
+
* runner's internal state. So this is a warning, not a drop — and getting it
|
|
40
|
+
* wrong fails loudly anyway, since the child's resolver reports "not found"
|
|
41
|
+
* and exits 1.
|
|
42
|
+
*/
|
|
43
|
+
why: 'unresolved' | 'extension';
|
|
44
|
+
}
|
|
45
|
+
/** The registry questions this needs, so a test can answer them with a literal. */
|
|
46
|
+
export interface ModelLookup {
|
|
47
|
+
find: (provider: string, id: string) => unknown;
|
|
48
|
+
extensionProviders: ReadonlySet<string>;
|
|
49
|
+
}
|
|
50
|
+
export declare function findModelProblems(lookup: ModelLookup, specs: Readonly<Record<ChildGroup, string>>): ModelProblem[];
|
|
51
|
+
/**
|
|
52
|
+
* The hint line, or null when every cell is fine.
|
|
53
|
+
*
|
|
54
|
+
* Names at most two cells per cause and appends `(+N more)`, the same budget the
|
|
55
|
+
* reasoning line keeps and for the same reason.
|
|
56
|
+
*/
|
|
57
|
+
export declare function formatModelWarning(problems: readonly ModelProblem[]): string | null;
|
|
58
|
+
export declare function registerModelWarning(pi: ExtensionAPI,
|
|
59
|
+
/** Injected by tests, which must not depend on the developer's saved config. */
|
|
60
|
+
readSpecs?: () => Readonly<Record<ChildGroup, string>>): void;
|
|
61
|
+
/**
|
|
62
|
+
* Answer, once, what every model cell resolves to — and leave both answers where
|
|
63
|
+
* the code that has no `ctx` can read them.
|
|
64
|
+
*
|
|
65
|
+
* `unresolved` only feeds the argv drop: an extension-provided model may well
|
|
66
|
+
* work, since children load explicitly whitelisted extensions, and dropping its
|
|
67
|
+
* flag would break a config that is merely fragile.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveModelCells(ctx: ExtensionContext, specs: Readonly<Record<ChildGroup, string>>): ModelProblem[];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { getConfig } from '../config/config.js';
|
|
2
|
+
import { setGroupWindows, setUnusableSpecs } from '../config/group-args.js';
|
|
3
|
+
import { MODEL_INHERIT, splitSpec } from '../config/group-models.js';
|
|
4
|
+
import { CHILD_GROUPS } from '../config/groups.js';
|
|
5
|
+
import { contextWindowForSpec } from '../task/context-usage.js';
|
|
6
|
+
import { registerSessionHint } from './session-hint.js';
|
|
7
|
+
const WIDGET_KEY = 'pi-task-model-warning';
|
|
8
|
+
export function findModelProblems(lookup, specs) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const group of CHILD_GROUPS) {
|
|
11
|
+
const spec = specs[group];
|
|
12
|
+
if (spec === MODEL_INHERIT)
|
|
13
|
+
continue;
|
|
14
|
+
const parts = splitSpec(spec);
|
|
15
|
+
if (!parts || lookup.find(parts.provider, parts.id) === undefined) {
|
|
16
|
+
out.push({ group, spec, why: 'unresolved' });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (lookup.extensionProviders.has(parts.provider)) {
|
|
20
|
+
out.push({ group, spec, why: 'extension' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The hint line, or null when every cell is fine.
|
|
27
|
+
*
|
|
28
|
+
* Names at most two cells per cause and appends `(+N more)`, the same budget the
|
|
29
|
+
* reasoning line keeps and for the same reason.
|
|
30
|
+
*/
|
|
31
|
+
export function formatModelWarning(problems) {
|
|
32
|
+
if (problems.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
const list = (why) => {
|
|
35
|
+
const hits = problems.filter(p => p.why === why);
|
|
36
|
+
const shown = hits.slice(0, 2).map(p => `${p.group}→${p.spec}`);
|
|
37
|
+
return shown.join(', ') + (hits.length > 2 ? ` (+${hits.length - 2} more)` : '');
|
|
38
|
+
};
|
|
39
|
+
const parts = [];
|
|
40
|
+
if (problems.some(p => p.why === 'unresolved')) {
|
|
41
|
+
parts.push(`no such model here — ${list('unresolved')}. Those steps run on pi's default `
|
|
42
|
+
+ 'instead; fix the entry in ~/.pi/agent/models.json or pick another model in '
|
|
43
|
+
+ '/task-config');
|
|
44
|
+
}
|
|
45
|
+
if (problems.some(p => p.why === 'extension')) {
|
|
46
|
+
parts.push(`provider comes from an extension — ${list('extension')}. Children run `
|
|
47
|
+
+ '--no-extensions, so add that extension under "child extensions" in '
|
|
48
|
+
+ '/task-config or those steps exit 1');
|
|
49
|
+
}
|
|
50
|
+
return `⚠ pi-task models: ${parts.join('. Also: ')}`;
|
|
51
|
+
}
|
|
52
|
+
export function registerModelWarning(pi,
|
|
53
|
+
/** Injected by tests, which must not depend on the developer's saved config. */
|
|
54
|
+
readSpecs = () => getConfig().groupModels) {
|
|
55
|
+
// TWO handlers, deliberately. The resolution pass must run in EVERY mode:
|
|
56
|
+
// `registerSessionHint` returns early when `ctx.mode !== 'tui'`, so folding
|
|
57
|
+
// this into it would leave the argv drop and the churn windows disarmed for
|
|
58
|
+
// every headless and `--print` run — a guard that only works when someone is
|
|
59
|
+
// watching is not a guard.
|
|
60
|
+
pi.on('session_start', (_event, ctx) => {
|
|
61
|
+
resolveModelCells(ctx, readSpecs());
|
|
62
|
+
});
|
|
63
|
+
registerSessionHint(pi, WIDGET_KEY, ctx => {
|
|
64
|
+
const text = formatModelWarning(findModelProblems(lookupFor(ctx), readSpecs()));
|
|
65
|
+
return text === null ? null : { text };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Answer, once, what every model cell resolves to — and leave both answers where
|
|
70
|
+
* the code that has no `ctx` can read them.
|
|
71
|
+
*
|
|
72
|
+
* `unresolved` only feeds the argv drop: an extension-provided model may well
|
|
73
|
+
* work, since children load explicitly whitelisted extensions, and dropping its
|
|
74
|
+
* flag would break a config that is merely fragile.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveModelCells(ctx, specs) {
|
|
77
|
+
const problems = findModelProblems(lookupFor(ctx), specs);
|
|
78
|
+
setUnusableSpecs(problems.filter(p => p.why === 'unresolved').map(p => p.spec));
|
|
79
|
+
// The SAME walk fills the window table, so the argv and the churn rule can
|
|
80
|
+
// never disagree about which model a group runs on.
|
|
81
|
+
//
|
|
82
|
+
// An `inherit` cell gets NO entry, not the parent's window. Storing one
|
|
83
|
+
// would freeze a session_start snapshot in front of the live per-run value,
|
|
84
|
+
// so a user who switches the session model with Ctrl+P to a bigger one would
|
|
85
|
+
// have every child judged against the old window — the churn rule then fires
|
|
86
|
+
// early and kills a healthy child.
|
|
87
|
+
//
|
|
88
|
+
// The whole walk is guarded because `ctx.model` and `ctx.modelRegistry` are
|
|
89
|
+
// GETTERS that call `assertActive()` and throw on a stale context. Losing the
|
|
90
|
+
// windows must not also lose the `setUnusableSpecs` above it, nor the hint.
|
|
91
|
+
try {
|
|
92
|
+
setGroupWindows(Object.fromEntries(CHILD_GROUPS.filter(g => specs[g] !== MODEL_INHERIT).map(g => [g, contextWindowForSpec(ctx, specs[g])])));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
setGroupWindows({});
|
|
96
|
+
}
|
|
97
|
+
return problems;
|
|
98
|
+
}
|
|
99
|
+
function lookupFor(ctx) {
|
|
100
|
+
try {
|
|
101
|
+
const registry = ctx.modelRegistry;
|
|
102
|
+
return {
|
|
103
|
+
find: (provider, id) => registry.find(provider, id),
|
|
104
|
+
extensionProviders: new Set(registry.getRegisteredProviderIds())
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// A registry that cannot answer must not condemn every cell. Claiming
|
|
109
|
+
// every spec is unresolved would drop every --model flag on a session
|
|
110
|
+
// whose runtime simply was not ready.
|
|
111
|
+
return { find: () => ({}), extensionProviders: new Set() };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ContextSnapshot, type LoopHit, type SpawnFn } from '../shared/child-process.js';
|
|
2
|
+
import { type CommandKill } from '../shared/command-watchdog.js';
|
|
3
|
+
export { commandCeilingForAttempt } from '../shared/command-watchdog.js';
|
|
2
4
|
import { RESTART_ORDER } from './worker-kill.js';
|
|
3
5
|
import { type WorkerGuardOverride, type WorkerGuardPolicy, type WorkerPolicyInputs, type WorkerProfileId } from './worker-profiles.js';
|
|
4
6
|
/**
|
|
@@ -155,15 +157,15 @@ export interface RunWorkerInput {
|
|
|
155
157
|
promptCharsBefore: number;
|
|
156
158
|
}) => void;
|
|
157
159
|
/**
|
|
158
|
-
* An already-resolved `
|
|
159
|
-
* inherit
|
|
160
|
+
* An already-resolved group fragment — `--model` then `--thinking` — or
|
|
161
|
+
* `[]`/omitted to inherit both defaults exactly as before.
|
|
160
162
|
*
|
|
161
|
-
* Resolved by the CALLER because runWorker serves three different
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
163
|
+
* Resolved by the CALLER because runWorker serves three different groups —
|
|
164
|
+
* the research workers, the post-implementation gates, and the ad-hoc
|
|
165
|
+
* `pi-worker` tool — and has nothing in its input that tells them apart.
|
|
166
|
+
* Guessing here would give a verify gate the research workers' model.
|
|
165
167
|
*/
|
|
166
|
-
|
|
168
|
+
groupArgs?: readonly string[];
|
|
167
169
|
/**
|
|
168
170
|
* Called once per DISCARDED attempt, at the moment the worker decides to
|
|
169
171
|
* re-spawn — the only window in which a restart is observable at all.
|
|
@@ -337,29 +339,6 @@ export interface RunWorkerResult {
|
|
|
337
339
|
idleMs: number;
|
|
338
340
|
};
|
|
339
341
|
}
|
|
340
|
-
/**
|
|
341
|
-
* The per-command ceiling for attempt N, halving each time a hang recurs.
|
|
342
|
-
*
|
|
343
|
-
* The first attempt gets the full configured ceiling — a genuinely slow build or
|
|
344
|
-
* test suite deserves it. But every hang-caused restart carries
|
|
345
|
-
* commandTimeoutHint, which tells the model in as many words to bound its
|
|
346
|
-
* command; a SECOND hang means it ignored an explicit instruction, and a third
|
|
347
|
-
* means it ignored it twice. Giving a non-complying child the full ceiling again
|
|
348
|
-
* makes the worst case three times the ceiling, resting entirely on the model
|
|
349
|
-
* obeying prose. Halving bounds it at under twice the ceiling while costing a
|
|
350
|
-
* complying child nothing.
|
|
351
|
-
*
|
|
352
|
-
* `priorHangs` counts watchdog kills specifically, NOT total restarts — the
|
|
353
|
-
* restart budget is shared with loop kills, and a child restarted for LOOPING
|
|
354
|
-
* never received the bound-your-command hint, so its first hang still deserves
|
|
355
|
-
* the full ceiling. Only a hang after a hang is defiance.
|
|
356
|
-
*
|
|
357
|
-
* Floored at 30s so repeated halving cannot shrink the ceiling to something no
|
|
358
|
-
* real command could finish inside — but the floor is `min(base, 30s)`, never
|
|
359
|
-
* above the configured ceiling, so a caller asking for 10s keeps 10s at every
|
|
360
|
-
* hang count. A base of 0 or less disables the watchdog and stays 0.
|
|
361
|
-
*/
|
|
362
|
-
export declare function commandCeilingForAttempt(baseMs: number, priorHangs: number): number;
|
|
363
342
|
/**
|
|
364
343
|
* Everything the restart ladder reads about one finished attempt, plus the
|
|
365
344
|
* budgets it draws on. Assembled once per attempt so the rules below can be
|
|
@@ -438,12 +417,4 @@ interface RestartRule {
|
|
|
438
417
|
* becoming visible in `restarts`.
|
|
439
418
|
*/
|
|
440
419
|
export declare const RESTART_RULES: readonly RestartRule[];
|
|
441
|
-
/** What the command watchdog recorded when it killed an attempt. */
|
|
442
|
-
interface CommandKill {
|
|
443
|
-
toolName: string;
|
|
444
|
-
timeoutMs: number;
|
|
445
|
-
/** The command line itself, when the tool carried one — quoted into the hint
|
|
446
|
-
* so the fresh child knows which call it must not repeat unbounded. */
|
|
447
|
-
detail?: string;
|
|
448
|
-
}
|
|
449
420
|
export declare function runWorker(input: RunWorkerInput): Promise<RunWorkerResult>;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { getPiInvocation } from '../shared/pi-invocation.js';
|
|
2
2
|
import { runChildDefault } from '../shared/child-process.js';
|
|
3
|
-
import {
|
|
3
|
+
import { commandCeilingForAttempt, commandTimeoutHint, commandWatch } from '../shared/command-watchdog.js';
|
|
4
|
+
export { commandCeilingForAttempt } from '../shared/command-watchdog.js';
|
|
4
5
|
import { isGroundingRetrieval as isGrounding, workerChannel } from './worker-channels.js';
|
|
5
6
|
import { childBaseArgs } from '../shared/child-extensions.js';
|
|
6
|
-
import { LoopDetector } from '../task/loop-detector.js';
|
|
7
|
+
import { LoopDetector, MAX_LOOP_RESTARTS } from '../task/loop-detector.js';
|
|
7
8
|
import { StallDetector, formatStallHint } from '../task/stall-detector.js';
|
|
8
|
-
import {
|
|
9
|
+
import { formatLoopHint, isConnectionError, connectionRetryBackoffMs } from '../task/child-runner.js';
|
|
9
10
|
import { detectLeakedToolCall, leakedToolCallHint, MAX_LEAK_RETRIES } from '../shared/leaked-tool-call.js';
|
|
10
|
-
import {
|
|
11
|
+
import { childModelEndpoints, probeModelEndpoints } from '../shared/model-endpoint.js';
|
|
12
|
+
import { modelSpecFromArgs } from '../config/group-models.js';
|
|
11
13
|
import { streamStallHint } from '../shared/stream-watchdog.js';
|
|
12
14
|
import { classifyWorkerFailure } from './worker-failure.js';
|
|
13
15
|
import { CARRY_FORWARD_IDS, RESTART_ORDER } from './worker-kill.js';
|
|
@@ -216,34 +218,6 @@ absoluteCeilingMs) {
|
|
|
216
218
|
}
|
|
217
219
|
};
|
|
218
220
|
}
|
|
219
|
-
/**
|
|
220
|
-
* The per-command ceiling for attempt N, halving each time a hang recurs.
|
|
221
|
-
*
|
|
222
|
-
* The first attempt gets the full configured ceiling — a genuinely slow build or
|
|
223
|
-
* test suite deserves it. But every hang-caused restart carries
|
|
224
|
-
* commandTimeoutHint, which tells the model in as many words to bound its
|
|
225
|
-
* command; a SECOND hang means it ignored an explicit instruction, and a third
|
|
226
|
-
* means it ignored it twice. Giving a non-complying child the full ceiling again
|
|
227
|
-
* makes the worst case three times the ceiling, resting entirely on the model
|
|
228
|
-
* obeying prose. Halving bounds it at under twice the ceiling while costing a
|
|
229
|
-
* complying child nothing.
|
|
230
|
-
*
|
|
231
|
-
* `priorHangs` counts watchdog kills specifically, NOT total restarts — the
|
|
232
|
-
* restart budget is shared with loop kills, and a child restarted for LOOPING
|
|
233
|
-
* never received the bound-your-command hint, so its first hang still deserves
|
|
234
|
-
* the full ceiling. Only a hang after a hang is defiance.
|
|
235
|
-
*
|
|
236
|
-
* Floored at 30s so repeated halving cannot shrink the ceiling to something no
|
|
237
|
-
* real command could finish inside — but the floor is `min(base, 30s)`, never
|
|
238
|
-
* above the configured ceiling, so a caller asking for 10s keeps 10s at every
|
|
239
|
-
* hang count. A base of 0 or less disables the watchdog and stays 0.
|
|
240
|
-
*/
|
|
241
|
-
export function commandCeilingForAttempt(baseMs, priorHangs) {
|
|
242
|
-
if (!(baseMs > 0))
|
|
243
|
-
return 0;
|
|
244
|
-
const floor = Math.min(baseMs, 30_000);
|
|
245
|
-
return Math.max(floor, Math.round(baseMs / 2 ** priorHangs));
|
|
246
|
-
}
|
|
247
221
|
/**
|
|
248
222
|
* The restart ladder, in precedence order. FIRST MATCH WINS.
|
|
249
223
|
*
|
|
@@ -360,58 +334,6 @@ export const RESTART_RULES = [
|
|
|
360
334
|
counters: { leak: true }
|
|
361
335
|
}
|
|
362
336
|
];
|
|
363
|
-
/**
|
|
364
|
-
* Build the child-side command watchdog for ONE attempt: a per-tool-call timer
|
|
365
|
-
* machine (shared with the main session) whose `onFire` aborts `signal`, which
|
|
366
|
-
* runChild turns into a process-GROUP kill — reaping the hung command itself,
|
|
367
|
-
* not just the pi child holding it.
|
|
368
|
-
*
|
|
369
|
-
* LIMIT: the group kill only reaches processes still IN the group. A hung command
|
|
370
|
-
* that detached a daemon (setsid, nohup, a background dev server) leaves it
|
|
371
|
-
* running, so the fresh attempt can hit a port the dead attempt's escapee still
|
|
372
|
-
* holds. There is no cheap fix from here; the restart hint's "check current state"
|
|
373
|
-
* line is the mitigation.
|
|
374
|
-
*
|
|
375
|
-
* Returns null when the watchdog is off, so the caller keeps the plain timeout
|
|
376
|
-
* signal and no per-call bookkeeping happens at all.
|
|
377
|
-
*/
|
|
378
|
-
function commandWatch(timeoutMs) {
|
|
379
|
-
if (!(timeoutMs > 0))
|
|
380
|
-
return null;
|
|
381
|
-
const ctrl = new AbortController();
|
|
382
|
-
// pi's toolCallId pairs start↔end. When it is absent (a fake stream in a
|
|
383
|
-
// test, an older pi), fall back to one shared slot: tool executions in a
|
|
384
|
-
// child are sequential, so a single slot is still correctly paired.
|
|
385
|
-
const key = (id) => id ?? 'anon';
|
|
386
|
-
const details = new Map();
|
|
387
|
-
let killed;
|
|
388
|
-
const watchdog = new CommandWatchdog({
|
|
389
|
-
getTimeoutMs: () => timeoutMs,
|
|
390
|
-
...realTimerDeps,
|
|
391
|
-
onFire: (toolCallId, toolName, ms) => {
|
|
392
|
-
killed = {
|
|
393
|
-
toolName,
|
|
394
|
-
timeoutMs: ms,
|
|
395
|
-
...(details.has(toolCallId) ? { detail: details.get(toolCallId) } : {})
|
|
396
|
-
};
|
|
397
|
-
ctrl.abort();
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
return {
|
|
401
|
-
onStart: call => {
|
|
402
|
-
const id = key(call.toolCallId);
|
|
403
|
-
const args = call.args;
|
|
404
|
-
if (typeof args?.command === 'string') {
|
|
405
|
-
details.set(id, args.command.slice(0, 120));
|
|
406
|
-
}
|
|
407
|
-
watchdog.onStart(id, call.name);
|
|
408
|
-
},
|
|
409
|
-
onEnd: id => watchdog.onEnd(key(id)),
|
|
410
|
-
killed: () => killed,
|
|
411
|
-
signal: ctrl.signal,
|
|
412
|
-
clear: () => watchdog.clearAll()
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
337
|
export async function runWorker(input) {
|
|
416
338
|
const tools = input.tools ?? DEFAULT_TOOLS;
|
|
417
339
|
// `--mode json` makes pi emit structured events as they happen instead of
|
|
@@ -423,7 +345,7 @@ export async function runWorker(input) {
|
|
|
423
345
|
// moments before close and leave workMs at nearly zero.
|
|
424
346
|
const baseArgs = [
|
|
425
347
|
...childBaseArgs(input.extensions ?? []),
|
|
426
|
-
...(input.
|
|
348
|
+
...(input.groupArgs ?? []),
|
|
427
349
|
'--mode',
|
|
428
350
|
'json',
|
|
429
351
|
'--tools',
|
|
@@ -528,7 +450,7 @@ export async function runWorker(input) {
|
|
|
528
450
|
// Kept as data so a resolved policy stays plain
|
|
529
451
|
// comparable data — see StalledGuard.probe.
|
|
530
452
|
probe: guards.stalled.probe
|
|
531
|
-
?? (() => probeModelEndpoints(
|
|
453
|
+
?? (() => probeModelEndpoints(childModelEndpoints(modelSpecFromArgs(input.groupArgs ?? []))))
|
|
532
454
|
}
|
|
533
455
|
}),
|
|
534
456
|
...(guards['stream-stall'] ? { streamInactivityMs: guards['stream-stall'] } : {}),
|
|
@@ -16,7 +16,7 @@ import { normalizeQuery } from './research-cache.js';
|
|
|
16
16
|
import { projectDocsRaw } from './docs-project.js';
|
|
17
17
|
import { projectDocsBudget, projectDocsBudgetExhausted } from '../task/research-fanout-budget.js';
|
|
18
18
|
import { isAbstention } from './abstention.js';
|
|
19
|
-
import {
|
|
19
|
+
import { groupChildArgs } from '../config/group-args.js';
|
|
20
20
|
const RENDER_QUERY_MAX = 100;
|
|
21
21
|
const Params = Type.Object({
|
|
22
22
|
module: Type.String({
|
|
@@ -139,7 +139,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
139
139
|
cwd: ctx.cwd,
|
|
140
140
|
signal,
|
|
141
141
|
spawn,
|
|
142
|
-
|
|
142
|
+
groupArgs: groupChildArgs('extraction')
|
|
143
143
|
});
|
|
144
144
|
// ── Project source lookup ───────────────────────────────────────
|
|
145
145
|
if (params.module === '.') {
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
import { Text } from '@earendil-works/pi-tui';
|
|
15
15
|
import { Type } from '@sinclair/typebox';
|
|
16
16
|
import { getConfig } from '../config/config.js';
|
|
17
|
-
import {
|
|
17
|
+
import { groupChildArgs } from '../config/group-args.js';
|
|
18
18
|
import { runWorker } from './pi-worker-core.js';
|
|
19
|
-
import {
|
|
19
|
+
import { contextWindowForGroup } from '../task/context-usage.js';
|
|
20
20
|
import { childFailureReason, formatChildFailure, makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
|
|
21
21
|
const RENDER_PROMPT_MAX = 120;
|
|
22
22
|
const WorkerParams = Type.Object({
|
|
@@ -66,8 +66,8 @@ export function registerPiWorker(pi) {
|
|
|
66
66
|
// this codebase passes `-m`, so the parent's model IS the child's
|
|
67
67
|
// model and its window is the honest one. Without this the churn
|
|
68
68
|
// rule cannot fire — see RunWorkerInput.contextWindow.
|
|
69
|
-
contextWindow:
|
|
70
|
-
|
|
69
|
+
contextWindow: contextWindowForGroup(ctx, 'research') || 'unknown',
|
|
70
|
+
groupArgs: groupChildArgs('research')
|
|
71
71
|
});
|
|
72
72
|
const details = { exitCode: result.exitCode };
|
|
73
73
|
const failure = formatChildFailure(result, 'Worker aborted.');
|
|
@@ -20,19 +20,22 @@
|
|
|
20
20
|
* silences it: an all-`inherit` table yields no mismatches for any model.
|
|
21
21
|
*/
|
|
22
22
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
23
|
-
import { type GroupSetting, type
|
|
23
|
+
import { type GroupSetting, type ChildGroup } from '../config/reasoning.js';
|
|
24
24
|
import { type ReasoningMismatch } from '../shared/reasoning-capability.js';
|
|
25
25
|
import { type ChatTemplateCaps } from '../shared/model-endpoint.js';
|
|
26
26
|
/**
|
|
27
27
|
* The warning line for a set of mismatches.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
29
|
+
* Each item names its OWN model — `phase@acme/small medium→off` — because groups
|
|
30
|
+
* can now run on different ones. A single leading `model "X" will not run …`
|
|
31
|
+
* would be a lie about what was checked the moment two groups differ, and a
|
|
32
|
+
* warning that misdescribes its own subject cannot be acted on.
|
|
33
|
+
*
|
|
34
|
+
* Names at most two groups and appends `(+N more)` only when there are more than
|
|
35
|
+
* two, since a line long enough to list every group is a line nobody reads. Null
|
|
36
|
+
* when nothing mismatched.
|
|
34
37
|
*/
|
|
35
|
-
export declare function formatReasoningWarning(
|
|
38
|
+
export declare function formatReasoningWarning(mismatches: readonly ReasoningMismatch[]): string | null;
|
|
36
39
|
/**
|
|
37
40
|
* The extra cause line, when the SERVER disagrees with models.json.
|
|
38
41
|
*
|
|
@@ -49,11 +52,16 @@ export declare function registerReasoningWarning(pi: ExtensionAPI,
|
|
|
49
52
|
* `session_start` so a /task-config change since the last session counts.
|
|
50
53
|
* Injected by tests, which must not depend on the developer's saved config.
|
|
51
54
|
*/
|
|
52
|
-
readSettings?: () => Readonly<Record<
|
|
55
|
+
readSettings?: () => Readonly<Record<ChildGroup, GroupSetting>>,
|
|
53
56
|
/**
|
|
54
57
|
* The server-side chat-template probe. Injected so the REFINE path — the
|
|
55
58
|
* only half of this hint that talks to a network — is drivable at all; with
|
|
56
59
|
* the real probe it is reachable only from a model entry carrying a
|
|
57
60
|
* `baseUrl`, which no test model has.
|
|
58
61
|
*/
|
|
59
|
-
probe?: (baseUrl: string) => Promise<ChatTemplateCaps | null
|
|
62
|
+
probe?: (baseUrl: string) => Promise<ChatTemplateCaps | null>,
|
|
63
|
+
/**
|
|
64
|
+
* Which model each group runs on. Injected for the same reason `readSettings`
|
|
65
|
+
* is: a test must not depend on the developer's saved config.
|
|
66
|
+
*/
|
|
67
|
+
readSpecs?: () => Readonly<Record<ChildGroup, string>>): void;
|
|
@@ -22,27 +22,31 @@
|
|
|
22
22
|
import { getConfig } from '../config/config.js';
|
|
23
23
|
import { effectiveReasoning } from '../config/reasoning.js';
|
|
24
24
|
import { reasoningMismatches } from '../shared/reasoning-capability.js';
|
|
25
|
+
import { MODEL_INHERIT, splitSpec } from '../config/group-models.js';
|
|
25
26
|
import { probeChatTemplateCaps } from '../shared/model-endpoint.js';
|
|
26
27
|
import { registerSessionHint } from './session-hint.js';
|
|
27
28
|
const WIDGET_KEY = 'pi-task-reasoning-warning';
|
|
28
29
|
/**
|
|
29
30
|
* The warning line for a set of mismatches.
|
|
30
31
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* Each item names its OWN model — `phase@acme/small medium→off` — because groups
|
|
33
|
+
* can now run on different ones. A single leading `model "X" will not run …`
|
|
34
|
+
* would be a lie about what was checked the moment two groups differ, and a
|
|
35
|
+
* warning that misdescribes its own subject cannot be acted on.
|
|
36
|
+
*
|
|
37
|
+
* Names at most two groups and appends `(+N more)` only when there are more than
|
|
38
|
+
* two, since a line long enough to list every group is a line nobody reads. Null
|
|
39
|
+
* when nothing mismatched.
|
|
36
40
|
*/
|
|
37
|
-
export function formatReasoningWarning(
|
|
41
|
+
export function formatReasoningWarning(mismatches) {
|
|
38
42
|
if (mismatches.length === 0)
|
|
39
43
|
return null;
|
|
40
44
|
const shown = mismatches
|
|
41
45
|
.slice(0, 2)
|
|
42
|
-
.map(m => `${m.group} ${m.wanted}→${m.actual}`)
|
|
46
|
+
.map(m => `${m.group}@${m.modelName} ${m.wanted}→${m.actual}`)
|
|
43
47
|
.join(', ');
|
|
44
48
|
const rest = mismatches.length > 2 ? ` (+${mismatches.length - 2} more)` : '';
|
|
45
|
-
return (
|
|
49
|
+
return ('⚠ pi-task: some steps will not run the reasoning levels /task-config asks '
|
|
46
50
|
+ `for — ${shown}${rest}. pi clamps to what the model declares. Fix "reasoning" / `
|
|
47
51
|
+ '"thinkingLevelMap" for it in ~/.pi/agent/models.json, or set those steps back to '
|
|
48
52
|
+ '"inherit" in /task-config');
|
|
@@ -82,31 +86,74 @@ readSettings = () => effectiveReasoning(getConfig()),
|
|
|
82
86
|
* the real probe it is reachable only from a model entry carrying a
|
|
83
87
|
* `baseUrl`, which no test model has.
|
|
84
88
|
*/
|
|
85
|
-
probe = probeChatTemplateCaps
|
|
89
|
+
probe = probeChatTemplateCaps,
|
|
90
|
+
/**
|
|
91
|
+
* Which model each group runs on. Injected for the same reason `readSettings`
|
|
92
|
+
* is: a test must not depend on the developer's saved config.
|
|
93
|
+
*/
|
|
94
|
+
readSpecs = () => getConfig().groupModels) {
|
|
86
95
|
registerSessionHint(pi, WIDGET_KEY, ctx => {
|
|
87
|
-
const
|
|
88
|
-
const mismatches = reasoningMismatches(
|
|
89
|
-
|
|
90
|
-
return null;
|
|
91
|
-
const base = formatReasoningWarning(model?.name ?? model?.id ?? 'unknown', mismatches);
|
|
96
|
+
const facts = (g) => groupModelFacts(ctx, readSpecs()[g]);
|
|
97
|
+
const mismatches = reasoningMismatches(facts, readSettings());
|
|
98
|
+
const base = formatReasoningWarning(mismatches);
|
|
92
99
|
if (base === null)
|
|
93
100
|
return null;
|
|
94
101
|
// Fire-and-forget: the server probe only ever REFINES the cause line, so it
|
|
95
102
|
// must not delay the warning or be able to prevent it. `probeChatTemplateCaps`
|
|
96
103
|
// carries its own short timeout and returns null on any failure, so a
|
|
97
104
|
// backend that does not answer `/props` costs nothing.
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
//
|
|
106
|
+
// One probe per DISTINCT baseUrl among the mismatching groups, not one
|
|
107
|
+
// per group: eleven groups usually collapse to one or two servers, and
|
|
108
|
+
// four research workers on one server would otherwise print the same
|
|
109
|
+
// sentence four times. `allSettled`, so one dead endpoint cannot blank
|
|
110
|
+
// the line for the others.
|
|
111
|
+
const probes = distinctBackends(mismatches, facts);
|
|
112
|
+
if (probes.length === 0)
|
|
100
113
|
return { text: base };
|
|
101
|
-
const declares = model.reasoning;
|
|
102
114
|
return {
|
|
103
115
|
text: base,
|
|
104
|
-
refine:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
refine: Promise.allSettled(probes.map(async (b) => {
|
|
117
|
+
const caps = await probe(b.baseUrl);
|
|
118
|
+
return caps === null ? null : (formatCapabilityConflict(caps.supportsReasoningEffort, b.declares));
|
|
119
|
+
})).then(results => {
|
|
120
|
+
const causes = new Set(results.flatMap(r => r.status === 'fulfilled' && r.value !== null ? [r.value] : []));
|
|
121
|
+
return causes.size === 0 ? null : base + [...causes].join('');
|
|
109
122
|
})
|
|
110
123
|
};
|
|
111
124
|
});
|
|
112
125
|
}
|
|
126
|
+
/** What one group runs on, as {@link reasoningMismatches} wants it. */
|
|
127
|
+
function groupModelFacts(ctx, spec) {
|
|
128
|
+
// `inherit` is the session's model. That is decision 3 of the model table —
|
|
129
|
+
// children are NOT switched to follow the host — and the honest value is
|
|
130
|
+
// settings.json's default, which need not be the session's. Naming the
|
|
131
|
+
// session's model is still the better of the two: it is the one the user can
|
|
132
|
+
// see, and on every machine with one provider the two agree.
|
|
133
|
+
const model = spec === MODEL_INHERIT ?
|
|
134
|
+
ctx.model
|
|
135
|
+
: (() => {
|
|
136
|
+
const parts = splitSpec(spec);
|
|
137
|
+
return parts ? ctx.modelRegistry.find(parts.provider, parts.id) : undefined;
|
|
138
|
+
})();
|
|
139
|
+
if (!model)
|
|
140
|
+
return undefined;
|
|
141
|
+
return {
|
|
142
|
+
name: model.name || model.id,
|
|
143
|
+
reasoning: model.reasoning,
|
|
144
|
+
...(model.thinkingLevelMap === undefined ? {} : { thinkingLevelMap: model.thinkingLevelMap }),
|
|
145
|
+
...(model.baseUrl ? { baseUrl: model.baseUrl } : {})
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** The distinct servers behind a set of mismatches, deduped by URL. */
|
|
149
|
+
function distinctBackends(mismatches, facts) {
|
|
150
|
+
const byUrl = new Map();
|
|
151
|
+
for (const m of mismatches) {
|
|
152
|
+
const f = facts(m.group);
|
|
153
|
+
if (!f?.baseUrl)
|
|
154
|
+
continue;
|
|
155
|
+
if (!byUrl.has(f.baseUrl))
|
|
156
|
+
byUrl.set(f.baseUrl, { baseUrl: f.baseUrl, declares: f.reasoning });
|
|
157
|
+
}
|
|
158
|
+
return [...byUrl.values()];
|
|
159
|
+
}
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
*
|
|
22
22
|
* - RepeatedCallGuard: "no identical search twice", for grep/find/ls — the
|
|
23
23
|
* shapes the read guard cannot see, such as the same grep pattern re-run
|
|
24
|
-
* against the same path. Keyed
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* against the same path. Keyed with `loopKey`, the same identity
|
|
25
|
+
* `LoopDetector.record` uses, so argument key order never causes a miss and
|
|
26
|
+
* only an identical repeat trips. A different pattern on the same file still
|
|
27
|
+
* passes.
|
|
28
28
|
*
|
|
29
29
|
* Pure logic, no I/O — the extension does path resolution and tool routing.
|
|
30
30
|
*/
|
|
@@ -62,8 +62,8 @@ export declare class RepeatedCallGuard {
|
|
|
62
62
|
/**
|
|
63
63
|
* Record a `toolName` call with `args`. Returns a ReadBlock the second time
|
|
64
64
|
* the same (toolName, stable-stringified args) pair is seen (and every time
|
|
65
|
-
* after), else null on the first.
|
|
66
|
-
*
|
|
65
|
+
* after), else null on the first. Shares the LoopDetector's key, so argument
|
|
66
|
+
* key-order never causes a miss; only byte-identical calls collapse.
|
|
67
67
|
*/
|
|
68
68
|
check(toolName: string, args: unknown): ReadBlock | null;
|
|
69
69
|
}
|