@mjasnikovs/pi-task 0.38.31 → 0.39.0
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 +70 -0
- package/dist/config/option-picker.js +113 -0
- package/dist/config/reasoning.d.ts +26 -64
- package/dist/config/reasoning.js +31 -115
- package/dist/config/register.d.ts +144 -24
- package/dist/config/register.js +345 -56
- package/dist/index.js +2 -0
- package/dist/remote/push.js +1 -7
- 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/child-runner.d.ts +19 -16
- package/dist/task/child-runner.js +64 -36
- package/dist/task/context-usage.d.ts +46 -0
- package/dist/task/context-usage.js +41 -0
- 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-hold.d.ts +118 -0
- package/dist/task/implementation-hold.js +165 -0
- 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 +36 -4
- package/dist/task/phases.js +2 -2
- 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 +4 -3
- package/dist/workers/focused-extractor.js +5 -4
- 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 +7 -7
- package/dist/workers/pi-worker-core.js +4 -3
- 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/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
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
/** Base URLs of every custom provider pi is configured with (possibly none). */
|
|
2
2
|
export declare function discoverModelEndpoints(agentDir?: string): string[];
|
|
3
|
+
/**
|
|
4
|
+
* The provider/id a child pi process will actually resolve.
|
|
5
|
+
*
|
|
6
|
+
* Children carry no `-m` (CHILD_BASE_ARGS), so they fall back to pi's saved
|
|
7
|
+
* default — which is this, not the host session's model. `undefined` means we
|
|
8
|
+
* could not read it, and every caller here treats that as "do not guess".
|
|
9
|
+
*/
|
|
10
|
+
export interface ModelRef {
|
|
11
|
+
provider: string;
|
|
12
|
+
id: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function defaultModelRef(agentDir?: string): ModelRef | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The base URL one model is served from, or `undefined` for "not configured on
|
|
17
|
+
* this machine".
|
|
18
|
+
*
|
|
19
|
+
* Two files, two shapes, and the difference is not cosmetic. `models.json` is
|
|
20
|
+
* hand-written and hangs `baseUrl` off the PROVIDER; `models-store.json` is the
|
|
21
|
+
* cached remote catalogue and hangs it off each MODEL — its provider objects are
|
|
22
|
+
* `{models, checkedAt, lastModified, etag}` with no `baseUrl` key at all. A
|
|
23
|
+
* reader that knows only the first shape sees one endpoint on a machine that has
|
|
24
|
+
* fifteen.
|
|
25
|
+
*
|
|
26
|
+
* `undefined` is the honest answer for a model pi serves from `@earendil-works/
|
|
27
|
+
* pi-ai`'s built-in catalogue, whose URLs live in that package rather than on
|
|
28
|
+
* disk. We do not import it — see the header of this file — so we say we do not
|
|
29
|
+
* know, and the caller declines to probe rather than probing something else.
|
|
30
|
+
*/
|
|
31
|
+
export declare function modelBaseUrl(ref: ModelRef, agentDir?: string): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* What to probe on behalf of one child — the endpoint that child's own model
|
|
34
|
+
* uses, not every endpoint on the machine.
|
|
35
|
+
*
|
|
36
|
+
* The bug this closes is `probeModelEndpoints`'s `.some(Boolean)`: with a live
|
|
37
|
+
* cloud provider and a dead local one, the OR answers "reachable" and the stall
|
|
38
|
+
* guard is disarmed for a child that will never speak again. Handing it ONE url
|
|
39
|
+
* makes the OR a no-op and the verdict exact.
|
|
40
|
+
*
|
|
41
|
+
* `spec` is the child's OWN `provider/id`, as carried by its argv. `undefined`
|
|
42
|
+
* means the child carries no `--model` and will resolve pi's saved default,
|
|
43
|
+
* which is then the right thing to probe. Reading the saved default for a child
|
|
44
|
+
* that IS pinned asks about the wrong server in BOTH directions: it can kill a
|
|
45
|
+
* child whose own backend is healthy, and it can leave the guard disarmed for
|
|
46
|
+
* one whose backend is dead.
|
|
47
|
+
*
|
|
48
|
+
* Two escapes, both toward never killing:
|
|
49
|
+
* - model known, endpoint not on disk (a built-in provider) → `[]`, which
|
|
50
|
+
* `probeModelEndpoints` reads as reachable. We cannot see that server, so we
|
|
51
|
+
* do not get a vote. Probing some OTHER provider's url instead would import a
|
|
52
|
+
* false positive, which is the one thing today's blind OR never does.
|
|
53
|
+
* - no readable default → today's behaviour, unchanged.
|
|
54
|
+
*/
|
|
55
|
+
export declare function childModelEndpoints(spec?: string, agentDir?: string): string[];
|
|
3
56
|
/**
|
|
4
57
|
* true → at least one endpoint ANSWERED. Any HTTP status counts, because the
|
|
5
58
|
* question is liveness, not correctness: probing a path that 404s still returns
|
|
@@ -10,10 +10,15 @@
|
|
|
10
10
|
* while it runs, so only "no output AND the endpoint does not answer" counts as
|
|
11
11
|
* a dead backend.
|
|
12
12
|
*
|
|
13
|
-
* Discovery is generic —
|
|
14
|
-
* models.json `providers.*.baseUrl`, with no provider or server name hardcoded.
|
|
13
|
+
* Discovery is generic — no provider or server name is hardcoded anywhere here.
|
|
15
14
|
* No discoverable endpoint means nothing to probe, and the guard then NEVER
|
|
16
15
|
* kills: a child on a backend we cannot see gets the benefit of the doubt.
|
|
16
|
+
*
|
|
17
|
+
* `childModelEndpoints` is what the guards call; `discoverModelEndpoints` is its
|
|
18
|
+
* last resort. The difference is the point. Discovery answers "every endpoint on
|
|
19
|
+
* this machine", and `probeModelEndpoints` ORs over it, so one healthy provider
|
|
20
|
+
* says "reachable" for a child stranded on a dead one. Resolving the child's OWN
|
|
21
|
+
* model first turns that OR into a single exact question.
|
|
17
22
|
*/
|
|
18
23
|
import * as fs from 'node:fs';
|
|
19
24
|
import * as os from 'node:os';
|
|
@@ -33,6 +38,97 @@ export function discoverModelEndpoints(agentDir = path.join(os.homedir(), '.pi',
|
|
|
33
38
|
return [];
|
|
34
39
|
}
|
|
35
40
|
}
|
|
41
|
+
/** A `provider/id` spec as a ref, splitting on the FIRST slash. */
|
|
42
|
+
function specRef(spec) {
|
|
43
|
+
if (spec === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
const i = spec.indexOf('/');
|
|
46
|
+
if (i <= 0 || i === spec.length - 1)
|
|
47
|
+
return undefined;
|
|
48
|
+
return { provider: spec.slice(0, i), id: spec.slice(i + 1) };
|
|
49
|
+
}
|
|
50
|
+
export function defaultModelRef(agentDir = path.join(os.homedir(), '.pi', 'agent')) {
|
|
51
|
+
try {
|
|
52
|
+
const j = JSON.parse(fs.readFileSync(path.join(agentDir, 'settings.json'), 'utf8'));
|
|
53
|
+
const provider = j.defaultProvider;
|
|
54
|
+
const id = j.defaultModel;
|
|
55
|
+
if (typeof provider !== 'string' || provider === '')
|
|
56
|
+
return undefined;
|
|
57
|
+
if (typeof id !== 'string' || id === '')
|
|
58
|
+
return undefined;
|
|
59
|
+
return { provider, id };
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The base URL one model is served from, or `undefined` for "not configured on
|
|
67
|
+
* this machine".
|
|
68
|
+
*
|
|
69
|
+
* Two files, two shapes, and the difference is not cosmetic. `models.json` is
|
|
70
|
+
* hand-written and hangs `baseUrl` off the PROVIDER; `models-store.json` is the
|
|
71
|
+
* cached remote catalogue and hangs it off each MODEL — its provider objects are
|
|
72
|
+
* `{models, checkedAt, lastModified, etag}` with no `baseUrl` key at all. A
|
|
73
|
+
* reader that knows only the first shape sees one endpoint on a machine that has
|
|
74
|
+
* fifteen.
|
|
75
|
+
*
|
|
76
|
+
* `undefined` is the honest answer for a model pi serves from `@earendil-works/
|
|
77
|
+
* pi-ai`'s built-in catalogue, whose URLs live in that package rather than on
|
|
78
|
+
* disk. We do not import it — see the header of this file — so we say we do not
|
|
79
|
+
* know, and the caller declines to probe rather than probing something else.
|
|
80
|
+
*/
|
|
81
|
+
export function modelBaseUrl(ref, agentDir = path.join(os.homedir(), '.pi', 'agent')) {
|
|
82
|
+
const url = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
83
|
+
try {
|
|
84
|
+
const j = JSON.parse(fs.readFileSync(path.join(agentDir, 'models.json'), 'utf8'));
|
|
85
|
+
const p = j.providers?.[ref.provider];
|
|
86
|
+
if (p) {
|
|
87
|
+
const own = p.models?.find(m => m.id === ref.id);
|
|
88
|
+
return url(own?.baseUrl) ?? url(p.baseUrl);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* fall through to the store */
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const j = JSON.parse(fs.readFileSync(path.join(agentDir, 'models-store.json'), 'utf8'));
|
|
96
|
+
return url(j[ref.provider]?.models?.find(m => m.id === ref.id)?.baseUrl);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* What to probe on behalf of one child — the endpoint that child's own model
|
|
104
|
+
* uses, not every endpoint on the machine.
|
|
105
|
+
*
|
|
106
|
+
* The bug this closes is `probeModelEndpoints`'s `.some(Boolean)`: with a live
|
|
107
|
+
* cloud provider and a dead local one, the OR answers "reachable" and the stall
|
|
108
|
+
* guard is disarmed for a child that will never speak again. Handing it ONE url
|
|
109
|
+
* makes the OR a no-op and the verdict exact.
|
|
110
|
+
*
|
|
111
|
+
* `spec` is the child's OWN `provider/id`, as carried by its argv. `undefined`
|
|
112
|
+
* means the child carries no `--model` and will resolve pi's saved default,
|
|
113
|
+
* which is then the right thing to probe. Reading the saved default for a child
|
|
114
|
+
* that IS pinned asks about the wrong server in BOTH directions: it can kill a
|
|
115
|
+
* child whose own backend is healthy, and it can leave the guard disarmed for
|
|
116
|
+
* one whose backend is dead.
|
|
117
|
+
*
|
|
118
|
+
* Two escapes, both toward never killing:
|
|
119
|
+
* - model known, endpoint not on disk (a built-in provider) → `[]`, which
|
|
120
|
+
* `probeModelEndpoints` reads as reachable. We cannot see that server, so we
|
|
121
|
+
* do not get a vote. Probing some OTHER provider's url instead would import a
|
|
122
|
+
* false positive, which is the one thing today's blind OR never does.
|
|
123
|
+
* - no readable default → today's behaviour, unchanged.
|
|
124
|
+
*/
|
|
125
|
+
export function childModelEndpoints(spec, agentDir = path.join(os.homedir(), '.pi', 'agent')) {
|
|
126
|
+
const ref = specRef(spec) ?? defaultModelRef(agentDir);
|
|
127
|
+
if (!ref)
|
|
128
|
+
return discoverModelEndpoints(agentDir);
|
|
129
|
+
const url = modelBaseUrl(ref, agentDir);
|
|
130
|
+
return url === undefined ? [] : [url];
|
|
131
|
+
}
|
|
36
132
|
/**
|
|
37
133
|
* true → at least one endpoint ANSWERED. Any HTTP status counts, because the
|
|
38
134
|
* question is liveness, not correctness: probing a path that 404s still returns
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* line-for-line identical to it, ladder included. Nothing here imports pi-ai, so
|
|
31
31
|
* an upstream change will not fail a test — the two have to be re-compared.
|
|
32
32
|
*/
|
|
33
|
-
import { type
|
|
33
|
+
import { type ChildGroup, type GroupSetting } from '../config/reasoning.js';
|
|
34
34
|
/**
|
|
35
35
|
* pi's own level ladder, in order — the same seven names, in the same sequence,
|
|
36
36
|
* as `EXTENDED_THINKING_LEVELS` in pi-ai's models.js. The order IS the algorithm:
|
|
@@ -67,16 +67,36 @@ export declare function supportedThinkingLevels(model: ReasoningModelFacts): Lad
|
|
|
67
67
|
* the model supports it — which is what makes the inequality a mismatch test.
|
|
68
68
|
*/
|
|
69
69
|
export declare function clampToModel(model: ReasoningModelFacts, level: LadderLevel): LadderLevel;
|
|
70
|
-
/** One group whose configured setting the
|
|
70
|
+
/** One group whose configured setting the model it runs on will not honour. */
|
|
71
71
|
export interface ReasoningMismatch {
|
|
72
|
-
group:
|
|
72
|
+
group: ChildGroup;
|
|
73
|
+
/**
|
|
74
|
+
* The model THIS GROUP runs on. Per-item, not per-warning, because groups no
|
|
75
|
+
* longer share one model: a line that opens `model "X" will not run …` while
|
|
76
|
+
* listing groups that run on Y is itself a lie about what it checked.
|
|
77
|
+
*/
|
|
78
|
+
modelName: string;
|
|
73
79
|
/** What /task-config says. Never `inherit` — an inherited group asks for nothing. */
|
|
74
80
|
wanted: LadderLevel;
|
|
75
81
|
/** What pi will send instead. */
|
|
76
82
|
actual: LadderLevel;
|
|
77
83
|
}
|
|
84
|
+
/** What a group runs on, as much of it as this check needs. */
|
|
85
|
+
export interface GroupModelFacts extends ReasoningModelFacts {
|
|
86
|
+
/** What to call it in the warning. */
|
|
87
|
+
name: string;
|
|
88
|
+
/** Where it is served from, for the `/props` probe. Absent ⇒ not probeable. */
|
|
89
|
+
baseUrl?: string;
|
|
90
|
+
}
|
|
78
91
|
/**
|
|
79
|
-
* Every group whose setting the model will silently change.
|
|
92
|
+
* Every group whose setting the model IT RUNS ON will silently change.
|
|
93
|
+
*
|
|
94
|
+
* `modelFor` is a FUNCTION rather than a `Record`, for two reasons: a record
|
|
95
|
+
* would build eleven identical entries for the overwhelmingly common
|
|
96
|
+
* all-`inherit` case, and a function is drivable from a test with two literals.
|
|
97
|
+
* It answers `undefined` for a group whose model cannot be resolved — nothing is
|
|
98
|
+
* reported for those, because the run degrades to the session default and the
|
|
99
|
+
* separate model hint is what names them.
|
|
80
100
|
*
|
|
81
101
|
* `inherit` groups are skipped entirely, because an inherited group asks for
|
|
82
102
|
* nothing. That is not the same as a quiet default: the shipped table is mostly
|
|
@@ -91,4 +111,4 @@ export interface ReasoningMismatch {
|
|
|
91
111
|
* comparison. Warning about one direction while staying silent about the other
|
|
92
112
|
* would ship this feature unable to see its own failure mode.
|
|
93
113
|
*/
|
|
94
|
-
export declare function reasoningMismatches(
|
|
114
|
+
export declare function reasoningMismatches(modelFor: (group: ChildGroup) => GroupModelFacts | undefined, levels: Readonly<Record<ChildGroup, GroupSetting>>): ReasoningMismatch[];
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* line-for-line identical to it, ladder included. Nothing here imports pi-ai, so
|
|
31
31
|
* an upstream change will not fail a test — the two have to be re-compared.
|
|
32
32
|
*/
|
|
33
|
-
import {
|
|
33
|
+
import { CHILD_GROUPS } from '../config/reasoning.js';
|
|
34
34
|
/**
|
|
35
35
|
* pi's own level ladder, in order — the same seven names, in the same sequence,
|
|
36
36
|
* as `EXTENDED_THINKING_LEVELS` in pi-ai's models.js. The order IS the algorithm:
|
|
@@ -87,7 +87,14 @@ export function clampToModel(model, level) {
|
|
|
87
87
|
return available[0] ?? 'off';
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
|
-
* Every group whose setting the model will silently change.
|
|
90
|
+
* Every group whose setting the model IT RUNS ON will silently change.
|
|
91
|
+
*
|
|
92
|
+
* `modelFor` is a FUNCTION rather than a `Record`, for two reasons: a record
|
|
93
|
+
* would build eleven identical entries for the overwhelmingly common
|
|
94
|
+
* all-`inherit` case, and a function is drivable from a test with two literals.
|
|
95
|
+
* It answers `undefined` for a group whose model cannot be resolved — nothing is
|
|
96
|
+
* reported for those, because the run degrades to the session default and the
|
|
97
|
+
* separate model hint is what names them.
|
|
91
98
|
*
|
|
92
99
|
* `inherit` groups are skipped entirely, because an inherited group asks for
|
|
93
100
|
* nothing. That is not the same as a quiet default: the shipped table is mostly
|
|
@@ -102,19 +109,21 @@ export function clampToModel(model, level) {
|
|
|
102
109
|
* comparison. Warning about one direction while staying silent about the other
|
|
103
110
|
* would ship this feature unable to see its own failure mode.
|
|
104
111
|
*/
|
|
105
|
-
export function reasoningMismatches(
|
|
106
|
-
// No model resolved yet (session still starting, or none selected): say
|
|
107
|
-
// nothing. A warning naming no model is noise, not information.
|
|
108
|
-
if (!model)
|
|
109
|
-
return [];
|
|
112
|
+
export function reasoningMismatches(modelFor, levels) {
|
|
110
113
|
const out = [];
|
|
111
|
-
for (const group of
|
|
114
|
+
for (const group of CHILD_GROUPS) {
|
|
112
115
|
const wanted = levels[group];
|
|
113
116
|
if (wanted === 'inherit')
|
|
114
117
|
continue;
|
|
118
|
+
// No model resolved for this group (session still starting, none
|
|
119
|
+
// selected, or a spec this machine cannot resolve): say nothing. A
|
|
120
|
+
// warning naming no model is noise, not information.
|
|
121
|
+
const model = modelFor(group);
|
|
122
|
+
if (!model)
|
|
123
|
+
continue;
|
|
115
124
|
const actual = clampToModel(model, wanted);
|
|
116
125
|
if (actual !== wanted)
|
|
117
|
-
out.push({ group, wanted, actual });
|
|
126
|
+
out.push({ group, modelName: model.name, wanted, actual });
|
|
118
127
|
}
|
|
119
128
|
return out;
|
|
120
129
|
}
|
|
@@ -63,10 +63,9 @@ export declare function guardKillError(name: string, r: PhaseRunResult, opts?: {
|
|
|
63
63
|
* The dead-backend probe killed a phase child on its LAST attempt.
|
|
64
64
|
*
|
|
65
65
|
* Reaching this means every attempt found no endpoint answering, not one. The
|
|
66
|
-
* single-probe verdict is not trusted on its own
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* failed probes cost ~15s; one wrong verdict costs the run.
|
|
66
|
+
* single-probe verdict is not trusted on its own, because one sample cannot tell
|
|
67
|
+
* a dead server from a blip. Three failed probes cost ~15s; one wrong verdict
|
|
68
|
+
* costs the run.
|
|
70
69
|
*/
|
|
71
70
|
export declare class BackendDownError extends Error {
|
|
72
71
|
readonly childName: string;
|
|
@@ -125,12 +124,16 @@ export interface PhaseRunResult {
|
|
|
125
124
|
}
|
|
126
125
|
export declare function childArgs(tools: string, extensions?: readonly string[],
|
|
127
126
|
/**
|
|
128
|
-
*
|
|
129
|
-
* Resolved by the CALLER, never here
|
|
130
|
-
* ROLE, and this function is handed tools and extensions, not
|
|
131
|
-
* Omitted ⇒ byte-identical argv to the version before
|
|
127
|
+
* This child's group fragment: `--model` then `--thinking`, either half
|
|
128
|
+
* possibly absent. Resolved by the CALLER, never here — both are properties
|
|
129
|
+
* of the child's ROLE, and this function is handed tools and extensions, not
|
|
130
|
+
* a name. Omitted ⇒ byte-identical argv to the version before group profiles.
|
|
131
|
+
*
|
|
132
|
+
* ONE field rather than a `model` beside a `thinking`, because nothing may
|
|
133
|
+
* compose the two halves by hand: `groupChildArgs` is the only producer, so a
|
|
134
|
+
* doubled `--thinking` is unreachable rather than merely unlikely.
|
|
132
135
|
*/
|
|
133
|
-
|
|
136
|
+
groupArgs?: readonly string[]): string[];
|
|
134
137
|
export declare const USER_CANCELLED = "__user_cancelled__";
|
|
135
138
|
/**
|
|
136
139
|
* Run a child pi process with JSON event-stream output, loop detection, and
|
|
@@ -175,10 +178,10 @@ export interface ChildRun {
|
|
|
175
178
|
*/
|
|
176
179
|
contextWindow?: number;
|
|
177
180
|
/**
|
|
178
|
-
* The resolved
|
|
179
|
-
*
|
|
181
|
+
* The resolved argv fragment for this child's group — `--model` then
|
|
182
|
+
* `--thinking` — or `[]`/omitted to inherit both defaults as before.
|
|
180
183
|
*/
|
|
181
|
-
|
|
184
|
+
groupArgs?: readonly string[];
|
|
182
185
|
/**
|
|
183
186
|
* This attempt's per-command ceiling, already halved for prior hangs by the
|
|
184
187
|
* caller's strike loop. Omitted -> the `phase` row's full configured ceiling,
|
|
@@ -195,7 +198,7 @@ export interface ChildRun {
|
|
|
195
198
|
* one — the mislabel class runDegradedFinalAttempt's own comment warns about.
|
|
196
199
|
*/
|
|
197
200
|
export declare function phasePolicy(): WorkerGuardPolicy;
|
|
198
|
-
export declare function runChild({ cwd, tools, prompt, signal, onLine, onContextUsage, onToolCall, spawn: spawnFn, extensions, onToolResult, contextWindow,
|
|
201
|
+
export declare function runChild({ cwd, tools, prompt, signal, onLine, onContextUsage, onToolCall, spawn: spawnFn, extensions, onToolResult, contextWindow, groupArgs, commandCeilingMs }: ChildRun): Promise<PhaseRunResult>;
|
|
199
202
|
export interface PhaseDeps {
|
|
200
203
|
cwd: string;
|
|
201
204
|
taskId: string;
|
|
@@ -337,15 +340,15 @@ export type PhaseSeams = Omit<PhaseDeps, 'cwd' | 'taskId' | 'signal' | 'onChildO
|
|
|
337
340
|
* exit status describes our SIGTERM and says nothing about its verdict.
|
|
338
341
|
*/
|
|
339
342
|
/**
|
|
340
|
-
* The
|
|
343
|
+
* The group fragment for a named child, or `[]` when the name is unmapped.
|
|
341
344
|
*
|
|
342
345
|
* An unmapped name INHERITS rather than throwing: a child that reaches the model
|
|
343
346
|
* with today's argv is always safe, and aborting a user's task over a missing
|
|
344
347
|
* table row would be a worse failure than the one it reports. The guard that
|
|
345
|
-
* makes the table complete is `
|
|
348
|
+
* makes the table complete is `config/groups.test.ts`, which fails the BUILD —
|
|
346
349
|
* where someone can actually fix it.
|
|
347
350
|
*/
|
|
348
|
-
export declare function
|
|
351
|
+
export declare function groupArgsForChild(name: string): string[];
|
|
349
352
|
export declare function runPhaseChild(deps: PhaseDeps, name: string, tools: string, prompt: string, opts?: PhaseChildOptions): Promise<string>;
|
|
350
353
|
export declare function formatLoopHint(hit: LoopHit): string;
|
|
351
354
|
/**
|
|
@@ -15,14 +15,15 @@ import { detectLeakedToolCall, leakedToolCallHint, MAX_LEAK_RETRIES } from '../s
|
|
|
15
15
|
import { readSection, setTaskSection } from './task-io.js';
|
|
16
16
|
import { streamStallCause } from '../shared/stream-watchdog.js';
|
|
17
17
|
import { commandCeilingForAttempt, commandTimeoutHint, commandWatch } from '../shared/command-watchdog.js';
|
|
18
|
-
import {
|
|
18
|
+
import { childModelEndpoints, probeModelEndpoints } from '../shared/model-endpoint.js';
|
|
19
|
+
import { modelSpecFromArgs } from '../config/group-models.js';
|
|
19
20
|
// VALUE import, and it is only safe because worker-profiles.ts reads its loop
|
|
20
21
|
// constants from loop-detector.ts. Point those back at this file and the graph
|
|
21
22
|
// closes into a TDZ ReferenceError that no compile step catches.
|
|
22
23
|
import { workerPolicy } from '../workers/worker-profiles.js';
|
|
23
24
|
import { getConfig } from '../config/config.js';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
25
|
+
import { groupChildArgs, groupWindow } from '../config/group-args.js';
|
|
26
|
+
import { groupForChild } from '../config/groups.js';
|
|
26
27
|
// ─── Phase-child wall-clock cap ──────────────────────────────────────────────
|
|
27
28
|
/**
|
|
28
29
|
* Optional wall-clock bound on ONE spawn of a phase child. DEFAULT: OFF.
|
|
@@ -115,10 +116,11 @@ export function guardKillError(name, r, opts = {}) {
|
|
|
115
116
|
if (r.commandKill)
|
|
116
117
|
return new CommandTimeoutError(name, r.commandKill);
|
|
117
118
|
// A dead-backend verdict is only trusted once every attempt has produced it.
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
// a
|
|
121
|
-
//
|
|
119
|
+
// The probe now asks about this child's own endpoint rather than ORing over
|
|
120
|
+
// every provider, so it is exact — but it is still one network call at one
|
|
121
|
+
// instant, and a blip is indistinguishable from a death in a single sample.
|
|
122
|
+
// The asymmetry settles it: a backend that really is down costs three 5s
|
|
123
|
+
// probes, a wrong verdict costs the whole run.
|
|
122
124
|
if (r.stalled)
|
|
123
125
|
return opts.finalAttempt === false ? null : new BackendDownError(name);
|
|
124
126
|
return null;
|
|
@@ -127,10 +129,9 @@ export function guardKillError(name, r, opts = {}) {
|
|
|
127
129
|
* The dead-backend probe killed a phase child on its LAST attempt.
|
|
128
130
|
*
|
|
129
131
|
* Reaching this means every attempt found no endpoint answering, not one. The
|
|
130
|
-
* single-probe verdict is not trusted on its own
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* failed probes cost ~15s; one wrong verdict costs the run.
|
|
132
|
+
* single-probe verdict is not trusted on its own, because one sample cannot tell
|
|
133
|
+
* a dead server from a blip. Three failed probes cost ~15s; one wrong verdict
|
|
134
|
+
* costs the run.
|
|
134
135
|
*/
|
|
135
136
|
export class BackendDownError extends Error {
|
|
136
137
|
childName;
|
|
@@ -246,12 +247,16 @@ const defaultSleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
246
247
|
// ─── Spawn helpers ───────────────────────────────────────────────────────────
|
|
247
248
|
export function childArgs(tools, extensions = [],
|
|
248
249
|
/**
|
|
249
|
-
*
|
|
250
|
-
* Resolved by the CALLER, never here
|
|
251
|
-
* ROLE, and this function is handed tools and extensions, not
|
|
252
|
-
* Omitted ⇒ byte-identical argv to the version before
|
|
250
|
+
* This child's group fragment: `--model` then `--thinking`, either half
|
|
251
|
+
* possibly absent. Resolved by the CALLER, never here — both are properties
|
|
252
|
+
* of the child's ROLE, and this function is handed tools and extensions, not
|
|
253
|
+
* a name. Omitted ⇒ byte-identical argv to the version before group profiles.
|
|
254
|
+
*
|
|
255
|
+
* ONE field rather than a `model` beside a `thinking`, because nothing may
|
|
256
|
+
* compose the two halves by hand: `groupChildArgs` is the only producer, so a
|
|
257
|
+
* doubled `--thinking` is unreachable rather than merely unlikely.
|
|
253
258
|
*/
|
|
254
|
-
|
|
259
|
+
groupArgs = []) {
|
|
255
260
|
// `--mode json` puts the child into the structured event stream the
|
|
256
261
|
// unified runner parses in `mode: 'json-events'`. Without it the child
|
|
257
262
|
// emits plain text, every line fails JSON.parse, finalText stays empty,
|
|
@@ -273,7 +278,7 @@ thinking = []) {
|
|
|
273
278
|
// one — the guards all hang off pi's `tool_call` hook.
|
|
274
279
|
const toolFlags = tools === '' ? ['--no-tools'] : ['--tools', tools];
|
|
275
280
|
const internal = tools === '' ? [] : extensions;
|
|
276
|
-
return [...childBaseArgs(internal), ...
|
|
281
|
+
return [...childBaseArgs(internal), ...groupArgs, '--mode', 'json', ...toolFlags];
|
|
277
282
|
}
|
|
278
283
|
// Sentinel error thrown when the user dismisses a grill-me dialog.
|
|
279
284
|
// Defined here (not in failure-classifier.ts) to avoid circular dependency.
|
|
@@ -292,8 +297,8 @@ export function phasePolicy() {
|
|
|
292
297
|
streamInactivityMs: getConfig().streamInactivityMs
|
|
293
298
|
});
|
|
294
299
|
}
|
|
295
|
-
export async function runChild({ cwd, tools, prompt, signal, onLine, onContextUsage, onToolCall, spawn: spawnFn, extensions, onToolResult, contextWindow,
|
|
296
|
-
const invocation = getPiInvocation(childArgs(tools, extensions,
|
|
300
|
+
export async function runChild({ cwd, tools, prompt, signal, onLine, onContextUsage, onToolCall, spawn: spawnFn, extensions, onToolResult, contextWindow, groupArgs, commandCeilingMs }) {
|
|
301
|
+
const invocation = getPiInvocation(childArgs(tools, extensions, groupArgs), prompt);
|
|
297
302
|
let loopHit;
|
|
298
303
|
const guards = phasePolicy().guards;
|
|
299
304
|
// Null when the user set the ceiling to `off`. Why a phase child needs this at
|
|
@@ -315,7 +320,7 @@ export async function runChild({ cwd, tools, prompt, signal, onLine, onContextUs
|
|
|
315
320
|
stall: {
|
|
316
321
|
afterMs: guards.stalled.afterMs,
|
|
317
322
|
probe: guards.stalled.probe
|
|
318
|
-
?? (() => probeModelEndpoints(
|
|
323
|
+
?? (() => probeModelEndpoints(childModelEndpoints(modelSpecFromArgs(groupArgs ?? []))))
|
|
319
324
|
}
|
|
320
325
|
}),
|
|
321
326
|
onLine,
|
|
@@ -461,17 +466,17 @@ async function triageChildResult(deps, name, r, attempt, budget, verb) {
|
|
|
461
466
|
* exit status describes our SIGTERM and says nothing about its verdict.
|
|
462
467
|
*/
|
|
463
468
|
/**
|
|
464
|
-
* The
|
|
469
|
+
* The group fragment for a named child, or `[]` when the name is unmapped.
|
|
465
470
|
*
|
|
466
471
|
* An unmapped name INHERITS rather than throwing: a child that reaches the model
|
|
467
472
|
* with today's argv is always safe, and aborting a user's task over a missing
|
|
468
473
|
* table row would be a worse failure than the one it reports. The guard that
|
|
469
|
-
* makes the table complete is `
|
|
474
|
+
* makes the table complete is `config/groups.test.ts`, which fails the BUILD —
|
|
470
475
|
* where someone can actually fix it.
|
|
471
476
|
*/
|
|
472
|
-
export function
|
|
473
|
-
const group =
|
|
474
|
-
return group ?
|
|
477
|
+
export function groupArgsForChild(name) {
|
|
478
|
+
const group = groupForChild(name);
|
|
479
|
+
return group ? groupChildArgs(group) : [];
|
|
475
480
|
}
|
|
476
481
|
/**
|
|
477
482
|
* What a PHASE child's invocation carries, said once.
|
|
@@ -482,14 +487,29 @@ export function thinkingForChild(name) {
|
|
|
482
487
|
* which is what the degrade's own comment ("the degrade changes the TOOLS, not
|
|
483
488
|
* the role") claimed while three bare `undefined`s quietly made it false.
|
|
484
489
|
*/
|
|
485
|
-
|
|
490
|
+
/**
|
|
491
|
+
* The context window a child of this group runs against.
|
|
492
|
+
*
|
|
493
|
+
* The GROUP's window when this session resolved one, else the run's. Too small a
|
|
494
|
+
* window makes the churn rule fire early and kill a healthy child, so an
|
|
495
|
+
* `inherit` or unresolved group keeps the parent's number rather than a guess.
|
|
496
|
+
*/
|
|
497
|
+
function childContextWindow(deps, group) {
|
|
498
|
+
return (group === undefined ? undefined : groupWindow(group)) ?? deps.contextWindow;
|
|
499
|
+
}
|
|
500
|
+
function phaseChildRun(deps,
|
|
501
|
+
/** This child's group, for the window. Undefined ⇒ the run's own window. */
|
|
502
|
+
group, over) {
|
|
486
503
|
return {
|
|
487
504
|
cwd: deps.cwd,
|
|
488
505
|
onLine: deps.onChildOutput,
|
|
489
506
|
onContextUsage: deps.onContextUsage,
|
|
490
507
|
spawn: deps.spawn,
|
|
491
508
|
extensions: deps.childExtensions,
|
|
492
|
-
|
|
509
|
+
// The GROUP's window when this session resolved one, else the run's.
|
|
510
|
+
// Too small a window makes the churn rule fire early and kill a healthy
|
|
511
|
+
// child, so an unresolved group keeps the parent's number.
|
|
512
|
+
contextWindow: (group === undefined ? undefined : groupWindow(group)) ?? deps.contextWindow,
|
|
493
513
|
...over
|
|
494
514
|
};
|
|
495
515
|
}
|
|
@@ -500,8 +520,9 @@ export async function runPhaseChild(deps, name, tools, prompt, opts = {}) {
|
|
|
500
520
|
// between a loop-kill and its retry would otherwise make the two attempts
|
|
501
521
|
// different experiments, and the retry exists to repeat the first one with a
|
|
502
522
|
// hint. An unmapped name inherits, which is today's argv — the build-time
|
|
503
|
-
// guard for that is
|
|
504
|
-
const
|
|
523
|
+
// guard for that is config/groups.test.ts, not a throw in a user's run.
|
|
524
|
+
const groupArgs = groupArgsForChild(name);
|
|
525
|
+
const group = groupForChild(name);
|
|
505
526
|
const verb = opts.verb ?? 'retry';
|
|
506
527
|
let hint = null;
|
|
507
528
|
const loopHistory = [];
|
|
@@ -528,15 +549,21 @@ export async function runPhaseChild(deps, name, tools, prompt, opts = {}) {
|
|
|
528
549
|
// context WINDOW, so a detector that waited to be told one would sit at 0,
|
|
529
550
|
// and the churn rule returns false on a non-positive window. The parent
|
|
530
551
|
// knows the value at spawn time — say it then, not later.
|
|
531
|
-
|
|
552
|
+
//
|
|
553
|
+
// The SAME number the child is handed below. Arming it from the run's
|
|
554
|
+
// window while the child runs on a bigger model's would judge the child
|
|
555
|
+
// against a window it does not have, for exactly the stretch before the
|
|
556
|
+
// first `context_usage` event corrects it — which is the stretch this
|
|
557
|
+
// line exists to cover.
|
|
558
|
+
stall?.noteContext(childContextWindow(deps, group) ?? 0);
|
|
532
559
|
const clock = phaseTimeout(deps.signal, budgetMs);
|
|
533
560
|
let r;
|
|
534
561
|
try {
|
|
535
|
-
r = await runChild(phaseChildRun(deps, {
|
|
562
|
+
r = await runChild(phaseChildRun(deps, group, {
|
|
536
563
|
tools,
|
|
537
564
|
prompt: prependHint(hint, prompt),
|
|
538
565
|
signal: clock.signal,
|
|
539
|
-
|
|
566
|
+
groupArgs,
|
|
540
567
|
onContextUsage: snapshot => {
|
|
541
568
|
// Real window or nothing: `noteContext` ignores 0, which is
|
|
542
569
|
// why the parent's value must be supplied at spawn — a
|
|
@@ -691,14 +718,15 @@ async function runDegradedFinalAttempt(deps, name, prompt, hit, loopHistory) {
|
|
|
691
718
|
const clock = phaseTimeout(deps.signal, deps.timeoutMs ?? PHASE_CHILD_TIMEOUT_MS);
|
|
692
719
|
let r;
|
|
693
720
|
try {
|
|
694
|
-
r = await runChild(phaseChildRun(deps, {
|
|
721
|
+
r = await runChild(phaseChildRun(deps, groupForChild(name), {
|
|
695
722
|
tools: '', // --no-tools: the model cannot read/grep/list, only answer
|
|
696
723
|
prompt: prependHint(formatDegradeHint(hit), prompt),
|
|
697
724
|
signal: clock.signal,
|
|
698
725
|
// Same group as the attempts that led here. The degrade changes the
|
|
699
|
-
// TOOLS, not the role — running it
|
|
700
|
-
// make the fallback a different
|
|
701
|
-
|
|
726
|
+
// TOOLS, not the role — running it on a different model, or at a
|
|
727
|
+
// different thinking level, would make the fallback a different
|
|
728
|
+
// experiment from the thing it rescues.
|
|
729
|
+
groupArgs: groupArgsForChild(name)
|
|
702
730
|
}));
|
|
703
731
|
}
|
|
704
732
|
finally {
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* directly, because its state is the whole-run `WidgetState`, not one child's.
|
|
6
6
|
*/
|
|
7
7
|
import type { ContextSnapshot } from '../shared/child-process.js';
|
|
8
|
+
import { type PiTaskConfig } from '../config/config.js';
|
|
9
|
+
import type { ChildGroup } from '../config/groups.js';
|
|
8
10
|
/**
|
|
9
11
|
* The parent session's context window, or 0 when the model doesn't expose it.
|
|
10
12
|
*
|
|
@@ -22,6 +24,50 @@ export declare function getParentContextWindow(ctx: {
|
|
|
22
24
|
contextWindow?: number;
|
|
23
25
|
};
|
|
24
26
|
}): number;
|
|
27
|
+
/**
|
|
28
|
+
* The window for the model ONE GROUP's children will actually run on.
|
|
29
|
+
*
|
|
30
|
+
* This number drives the widget and, more importantly, `StallDetector`'s
|
|
31
|
+
* context-churn rule, and the two error directions are not symmetric. A parent
|
|
32
|
+
* window LARGER than the child's makes churn fire late — degraded, and the
|
|
33
|
+
* no-new-ground rule still covers it. A parent window SMALLER makes churn fire
|
|
34
|
+
* early and KILL A HEALTHY CHILD. A big-context research model under a small
|
|
35
|
+
* host model is a real false positive, which is why this exists at all.
|
|
36
|
+
*
|
|
37
|
+
* For the same reason there is no `min(parent, group)`: that would import the
|
|
38
|
+
* dangerous direction on purpose.
|
|
39
|
+
*
|
|
40
|
+
* `inherit`, an unresolvable spec, or a model with no declared window all return
|
|
41
|
+
* exactly `getParentContextWindow(ctx)` — byte-identical to the behaviour before
|
|
42
|
+
* per-group models existed.
|
|
43
|
+
*
|
|
44
|
+
* Callers WITHOUT a ctx read `groupWindow` from config/group-args.ts instead,
|
|
45
|
+
* which the session pass fills from this. One producer, so the two views cannot
|
|
46
|
+
* describe different models.
|
|
47
|
+
*/
|
|
48
|
+
export declare function contextWindowForGroup(ctx: {
|
|
49
|
+
model?: {
|
|
50
|
+
contextWindow?: number;
|
|
51
|
+
};
|
|
52
|
+
modelRegistry?: {
|
|
53
|
+
find: (p: string, i: string) => unknown;
|
|
54
|
+
};
|
|
55
|
+
}, group: ChildGroup, cfg?: PiTaskConfig): number;
|
|
56
|
+
/**
|
|
57
|
+
* The same answer for a spec the caller already has.
|
|
58
|
+
*
|
|
59
|
+
* The session pass needs this: it walks an INJECTED spec table, and reaching for
|
|
60
|
+
* `getConfig()` here would let the window it stores describe a different model
|
|
61
|
+
* from the one it just checked.
|
|
62
|
+
*/
|
|
63
|
+
export declare function contextWindowForSpec(ctx: {
|
|
64
|
+
model?: {
|
|
65
|
+
contextWindow?: number;
|
|
66
|
+
};
|
|
67
|
+
modelRegistry?: {
|
|
68
|
+
find: (p: string, i: string) => unknown;
|
|
69
|
+
};
|
|
70
|
+
}, spec: string): number;
|
|
25
71
|
/**
|
|
26
72
|
* Fold a raw context snapshot into a display snapshot: prefer the child's own
|
|
27
73
|
* contextWindow, else the last known one, else the parent session's; then derive
|