@mjasnikovs/pi-task 0.38.32 → 0.39.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/config/group-args.d.ts +24 -9
- package/dist/config/group-args.js +38 -28
- package/dist/config/option-picker.d.ts +39 -11
- package/dist/config/option-picker.js +52 -12
- package/dist/config/reasoning.d.ts +10 -7
- package/dist/config/reasoning.js +19 -32
- package/dist/config/register.d.ts +66 -41
- package/dist/config/register.js +201 -162
- package/dist/shared/child-process.d.ts +34 -32
- package/dist/shared/child-process.js +44 -58
- package/dist/shared/command-watchdog.d.ts +12 -4
- package/dist/shared/command-watchdog.js +6 -7
- package/dist/shared/connection-error.d.ts +7 -0
- package/dist/shared/connection-error.js +65 -0
- package/dist/shared/model-endpoint.d.ts +12 -24
- package/dist/shared/model-endpoint.js +32 -82
- package/dist/shared/model-resolve.d.ts +105 -0
- package/dist/shared/model-resolve.js +97 -0
- package/dist/shared/reasoning-capability.d.ts +20 -0
- package/dist/shared/reasoning-capability.js +32 -1
- package/dist/shared/stall-probe.d.ts +51 -0
- package/dist/shared/stall-probe.js +79 -0
- package/dist/task/child-runner.d.ts +76 -278
- package/dist/task/child-runner.js +186 -722
- package/dist/task/context-usage.js +2 -7
- package/dist/task/failure-classifier.js +53 -81
- package/dist/task/gate-child.js +1 -1
- package/dist/task/impl-widget.d.ts +2 -0
- package/dist/task/impl-widget.js +4 -0
- package/dist/task/implementation-hold.d.ts +11 -0
- package/dist/task/implementation-hold.js +20 -0
- package/dist/task/implementation-scope.d.ts +24 -0
- package/dist/task/implementation-scope.js +34 -0
- package/dist/task/loop-detector.d.ts +13 -5
- package/dist/task/loop-detector.js +11 -5
- package/dist/task/model-hold-stash.js +4 -14
- package/dist/task/orchestrator.d.ts +1 -8
- package/dist/task/orchestrator.js +11 -34
- package/dist/task/phases.js +2 -2
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +1 -1
- package/dist/workers/model-warning.d.ts +4 -16
- package/dist/workers/model-warning.js +14 -70
- package/dist/workers/pi-worker-core.d.ts +65 -20
- package/dist/workers/pi-worker-core.js +109 -50
- package/dist/workers/reasoning-warning.js +2 -24
- package/dist/workers/worker-failure.d.ts +2 -0
- package/dist/workers/worker-failure.js +2 -1
- package/dist/workers/worker-kill.d.ts +30 -11
- package/dist/workers/worker-kill.js +68 -20
- package/dist/workers/worker-profiles.d.ts +20 -0
- package/dist/workers/worker-profiles.js +22 -9
- package/package.json +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { MODEL_INHERIT, splitSpec } from '../config/group-models.js';
|
|
2
|
+
import { CHILD_GROUPS } from '../config/groups.js';
|
|
3
|
+
/** The `provider/id` inverse of {@link splitSpec}. */
|
|
4
|
+
export function specOf(model) {
|
|
5
|
+
return `${model.provider}/${model.id}`;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `inherit` is the session's model. That is decision 3 of the model table —
|
|
9
|
+
* children are NOT switched to follow the host — and the honest value is
|
|
10
|
+
* settings.json's default, which need not be the session's. Naming the
|
|
11
|
+
* session's model is still the better of the two: it is the one the user can
|
|
12
|
+
* see, and on every machine with one provider the two agree.
|
|
13
|
+
*
|
|
14
|
+
* `find` is EXACT, deliberately stricter than pi's own CLI, which also
|
|
15
|
+
* substring-matches. We store a canonical `provider/id`, so exact is the only
|
|
16
|
+
* match that should ever count.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveModel(ctx, spec) {
|
|
19
|
+
try {
|
|
20
|
+
const parts = spec === MODEL_INHERIT ? undefined : splitSpec(spec);
|
|
21
|
+
const handle = spec === MODEL_INHERIT ?
|
|
22
|
+
ctx.model
|
|
23
|
+
: parts && ctx.modelRegistry?.find(parts.provider, parts.id);
|
|
24
|
+
if (!handle)
|
|
25
|
+
return undefined;
|
|
26
|
+
const provider = parts?.provider ?? handle.provider;
|
|
27
|
+
const extensionProviders = ctx.modelRegistry?.getRegisteredProviderIds?.() ?? [];
|
|
28
|
+
return {
|
|
29
|
+
spec: parts ? spec : specOf(handle),
|
|
30
|
+
name: handle.name || handle.id,
|
|
31
|
+
reasoning: handle.reasoning,
|
|
32
|
+
...(handle.thinkingLevelMap === undefined ?
|
|
33
|
+
{}
|
|
34
|
+
: { thinkingLevelMap: handle.thinkingLevelMap }),
|
|
35
|
+
...(handle.baseUrl ? { baseUrl: handle.baseUrl } : {}),
|
|
36
|
+
contextWindow: handle.contextWindow ?? 0,
|
|
37
|
+
fromExtension: extensionProviders.includes(provider),
|
|
38
|
+
handle
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Every group's cell in ONE registry walk, so the argv drop, the churn window
|
|
47
|
+
* and the hint can never disagree about which model a group runs on.
|
|
48
|
+
*
|
|
49
|
+
* An `inherit` cell gets NO window. Storing the parent's would freeze a
|
|
50
|
+
* session_start snapshot in front of the live per-run value: a user who
|
|
51
|
+
* switches the session model with Ctrl+P to a bigger one would have every
|
|
52
|
+
* child judged against the old window, and the churn rule then fires early and
|
|
53
|
+
* kills a healthy child. An unresolved cell gets none for the same reason —
|
|
54
|
+
* such a child runs on the live default, whichever that is by then.
|
|
55
|
+
*
|
|
56
|
+
* A registry that cannot answer condemns NOTHING: every cell stays usable and
|
|
57
|
+
* carries no window. Claiming every spec unresolved would drop every `--model`
|
|
58
|
+
* on a session whose runtime merely was not ready.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveGroupModels(ctx, specs) {
|
|
61
|
+
const registry = readRegistry(ctx);
|
|
62
|
+
const cell = (spec) => {
|
|
63
|
+
if (spec === MODEL_INHERIT || registry === undefined)
|
|
64
|
+
return { spec, usable: true };
|
|
65
|
+
const found = resolveModel({ modelRegistry: registry }, spec);
|
|
66
|
+
if (!found)
|
|
67
|
+
return { spec, usable: false, problem: 'unresolved' };
|
|
68
|
+
return {
|
|
69
|
+
spec,
|
|
70
|
+
usable: true,
|
|
71
|
+
...(found.contextWindow > 0 ? { contextWindow: found.contextWindow } : {}),
|
|
72
|
+
...(found.fromExtension ? { problem: 'extension' } : {})
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
return Object.fromEntries(CHILD_GROUPS.map(g => [g, cell(specs[g])]));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `spec → baseUrl` for every model this session can use, for the dead-backend
|
|
79
|
+
* probe (shared/model-endpoint.ts). Empty when the registry cannot answer,
|
|
80
|
+
* which the probe reads as "cannot see, so never kill".
|
|
81
|
+
*/
|
|
82
|
+
export function resolveModelEndpoints(ctx) {
|
|
83
|
+
const out = new Map();
|
|
84
|
+
for (const m of readRegistry(ctx)?.getAvailable?.() ?? []) {
|
|
85
|
+
if (m.baseUrl)
|
|
86
|
+
out.set(specOf(m), m.baseUrl);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function readRegistry(ctx) {
|
|
91
|
+
try {
|
|
92
|
+
return ctx.modelRegistry;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -67,6 +67,26 @@ 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
|
+
/**
|
|
71
|
+
* The settings a /task-config row may offer, given the model its group runs on.
|
|
72
|
+
*
|
|
73
|
+
* The INTERSECTION with `REASONING_SETTINGS`, not `supportedThinkingLevels`
|
|
74
|
+
* directly: that returns the whole ladder including `xhigh` and `max`, which
|
|
75
|
+
* the menu excludes on purpose (see config/reasoning.ts) because pi's own UI
|
|
76
|
+
* may not offer them. A model declaring `xhigh` must not smuggle it in.
|
|
77
|
+
*/
|
|
78
|
+
export declare function offeredLevels(facts: ReasoningModelFacts | undefined): GroupSetting[];
|
|
79
|
+
/**
|
|
80
|
+
* The setting a row will really run at, inside the menu's own vocabulary.
|
|
81
|
+
*
|
|
82
|
+
* ONE function for the picker's preselect and the writer, because they used to
|
|
83
|
+
* be two copies of the same clamp and only one re-projected into
|
|
84
|
+
* {@link offeredLevels}. `clampToModel` walks UP first and knows the whole
|
|
85
|
+
* ladder, so a model declaring `xhigh` can land on a level the menu excludes;
|
|
86
|
+
* a writer that stored it would put a value in the table that no row can show.
|
|
87
|
+
* The highest OFFERED level is the honest neighbour of an excluded one.
|
|
88
|
+
*/
|
|
89
|
+
export declare function effectiveSetting(facts: ReasoningModelFacts | undefined, wanted: GroupSetting): GroupSetting;
|
|
70
90
|
/** One group whose configured setting the model it runs on will not honour. */
|
|
71
91
|
export interface ReasoningMismatch {
|
|
72
92
|
group: ChildGroup;
|
|
@@ -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 { CHILD_GROUPS } from '../config/reasoning.js';
|
|
33
|
+
import { CHILD_GROUPS, REASONING_SETTINGS } 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:
|
|
@@ -86,6 +86,37 @@ export function clampToModel(model, level) {
|
|
|
86
86
|
}
|
|
87
87
|
return available[0] ?? 'off';
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* The settings a /task-config row may offer, given the model its group runs on.
|
|
91
|
+
*
|
|
92
|
+
* The INTERSECTION with `REASONING_SETTINGS`, not `supportedThinkingLevels`
|
|
93
|
+
* directly: that returns the whole ladder including `xhigh` and `max`, which
|
|
94
|
+
* the menu excludes on purpose (see config/reasoning.ts) because pi's own UI
|
|
95
|
+
* may not offer them. A model declaring `xhigh` must not smuggle it in.
|
|
96
|
+
*/
|
|
97
|
+
export function offeredLevels(facts) {
|
|
98
|
+
if (facts === undefined)
|
|
99
|
+
return [...REASONING_SETTINGS];
|
|
100
|
+
const supported = supportedThinkingLevels(facts);
|
|
101
|
+
return REASONING_SETTINGS.filter(s => s === 'inherit' || supported.includes(s));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The setting a row will really run at, inside the menu's own vocabulary.
|
|
105
|
+
*
|
|
106
|
+
* ONE function for the picker's preselect and the writer, because they used to
|
|
107
|
+
* be two copies of the same clamp and only one re-projected into
|
|
108
|
+
* {@link offeredLevels}. `clampToModel` walks UP first and knows the whole
|
|
109
|
+
* ladder, so a model declaring `xhigh` can land on a level the menu excludes;
|
|
110
|
+
* a writer that stored it would put a value in the table that no row can show.
|
|
111
|
+
* The highest OFFERED level is the honest neighbour of an excluded one.
|
|
112
|
+
*/
|
|
113
|
+
export function effectiveSetting(facts, wanted) {
|
|
114
|
+
if (facts === undefined || wanted === 'inherit')
|
|
115
|
+
return wanted;
|
|
116
|
+
const offered = offeredLevels(facts);
|
|
117
|
+
const clamped = clampToModel(facts, wanted);
|
|
118
|
+
return offered.includes(clamped) ? clamped : (offered.at(-1) ?? 'inherit');
|
|
119
|
+
}
|
|
89
120
|
/**
|
|
90
121
|
* Every group whose setting the model IT RUNS ON will silently change.
|
|
91
122
|
*
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead-backend stall probe — the third child guard machine, beside the stream and
|
|
3
|
+
* command watchdogs.
|
|
4
|
+
*
|
|
5
|
+
* The failure it serves: the model server dies mid-child and the child hangs
|
|
6
|
+
* MUTE. pi's own connection handling runs from a catch, so a request that never
|
|
7
|
+
* answers never reaches it. Silence alone is not evidence — prompt processing
|
|
8
|
+
* legitimately emits nothing for minutes — so only "no output for `afterMs` AND
|
|
9
|
+
* the endpoint does not answer a probe" counts as a dead backend.
|
|
10
|
+
*
|
|
11
|
+
* Liveness is OUTPUT PROGRESS: any chunk resets the window. A reachable probe
|
|
12
|
+
* also resets it, so the next probe is a full window away rather than every
|
|
13
|
+
* tick, and a probe that itself throws proves nothing and is treated as
|
|
14
|
+
* reachable. The machine takes its clock and scheduler as deps for the same
|
|
15
|
+
* reason the two watchdogs do: so it can be driven by a fake in a unit test
|
|
16
|
+
* instead of only through a real spawn.
|
|
17
|
+
*/
|
|
18
|
+
import type { TimerHandle } from './command-watchdog.js';
|
|
19
|
+
export interface StallProbeDeps {
|
|
20
|
+
/** Silence that triggers a probe, in ms. */
|
|
21
|
+
afterMs: number;
|
|
22
|
+
/** true → the model endpoint answered; false → it did not. */
|
|
23
|
+
probe: () => Promise<boolean>;
|
|
24
|
+
now: () => number;
|
|
25
|
+
schedule: (fn: () => void, ms: number) => TimerHandle;
|
|
26
|
+
cancel: (handle: TimerHandle) => void;
|
|
27
|
+
/** Fires ONCE, when the window elapsed and the probe found nobody home. */
|
|
28
|
+
onDead: () => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Half the window, clamped. Never tighter than 50ms, never looser than 15s: a
|
|
32
|
+
* probe is one network call, so it must not be re-issued every tick, and a
|
|
33
|
+
* window of minutes must still be noticed within seconds of elapsing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function stallPollIntervalMs(afterMs: number): number;
|
|
36
|
+
export declare class StallProbe {
|
|
37
|
+
private readonly deps;
|
|
38
|
+
private timer;
|
|
39
|
+
private lastActivity;
|
|
40
|
+
private probing;
|
|
41
|
+
private dead;
|
|
42
|
+
constructor(deps: StallProbeDeps);
|
|
43
|
+
start(): void;
|
|
44
|
+
/** Any output from the child: the window starts over. */
|
|
45
|
+
note(): void;
|
|
46
|
+
stop(): void;
|
|
47
|
+
/** @internal Exposed for the poll callback and tests. */
|
|
48
|
+
check(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
/** Real-clock poll deps. REF'd for the reason stream-watchdog.ts records. */
|
|
51
|
+
export declare const realStallTimerDeps: Pick<StallProbeDeps, 'now' | 'schedule' | 'cancel'>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead-backend stall probe — the third child guard machine, beside the stream and
|
|
3
|
+
* command watchdogs.
|
|
4
|
+
*
|
|
5
|
+
* The failure it serves: the model server dies mid-child and the child hangs
|
|
6
|
+
* MUTE. pi's own connection handling runs from a catch, so a request that never
|
|
7
|
+
* answers never reaches it. Silence alone is not evidence — prompt processing
|
|
8
|
+
* legitimately emits nothing for minutes — so only "no output for `afterMs` AND
|
|
9
|
+
* the endpoint does not answer a probe" counts as a dead backend.
|
|
10
|
+
*
|
|
11
|
+
* Liveness is OUTPUT PROGRESS: any chunk resets the window. A reachable probe
|
|
12
|
+
* also resets it, so the next probe is a full window away rather than every
|
|
13
|
+
* tick, and a probe that itself throws proves nothing and is treated as
|
|
14
|
+
* reachable. The machine takes its clock and scheduler as deps for the same
|
|
15
|
+
* reason the two watchdogs do: so it can be driven by a fake in a unit test
|
|
16
|
+
* instead of only through a real spawn.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Half the window, clamped. Never tighter than 50ms, never looser than 15s: a
|
|
20
|
+
* probe is one network call, so it must not be re-issued every tick, and a
|
|
21
|
+
* window of minutes must still be noticed within seconds of elapsing.
|
|
22
|
+
*/
|
|
23
|
+
export function stallPollIntervalMs(afterMs) {
|
|
24
|
+
return Math.max(50, Math.min(afterMs / 2, 15_000));
|
|
25
|
+
}
|
|
26
|
+
export class StallProbe {
|
|
27
|
+
deps;
|
|
28
|
+
timer;
|
|
29
|
+
lastActivity = 0;
|
|
30
|
+
probing = false;
|
|
31
|
+
dead = false;
|
|
32
|
+
constructor(deps) {
|
|
33
|
+
this.deps = deps;
|
|
34
|
+
}
|
|
35
|
+
start() {
|
|
36
|
+
if (this.timer !== undefined)
|
|
37
|
+
return;
|
|
38
|
+
this.lastActivity = this.deps.now();
|
|
39
|
+
this.timer = this.deps.schedule(() => void this.check(), stallPollIntervalMs(this.deps.afterMs));
|
|
40
|
+
}
|
|
41
|
+
/** Any output from the child: the window starts over. */
|
|
42
|
+
note() {
|
|
43
|
+
this.lastActivity = this.deps.now();
|
|
44
|
+
}
|
|
45
|
+
stop() {
|
|
46
|
+
if (this.timer !== undefined)
|
|
47
|
+
this.deps.cancel(this.timer);
|
|
48
|
+
this.timer = undefined;
|
|
49
|
+
}
|
|
50
|
+
/** @internal Exposed for the poll callback and tests. */
|
|
51
|
+
async check() {
|
|
52
|
+
if (this.probing || this.dead)
|
|
53
|
+
return;
|
|
54
|
+
if (this.deps.now() - this.lastActivity < this.deps.afterMs)
|
|
55
|
+
return;
|
|
56
|
+
this.probing = true;
|
|
57
|
+
let reachable;
|
|
58
|
+
try {
|
|
59
|
+
reachable = await this.deps.probe();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
reachable = true;
|
|
63
|
+
}
|
|
64
|
+
this.probing = false;
|
|
65
|
+
if (reachable) {
|
|
66
|
+
this.lastActivity = this.deps.now();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.dead = true;
|
|
70
|
+
this.stop();
|
|
71
|
+
this.deps.onDead();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Real-clock poll deps. REF'd for the reason stream-watchdog.ts records. */
|
|
75
|
+
export const realStallTimerDeps = {
|
|
76
|
+
now: () => Date.now(),
|
|
77
|
+
schedule: (fn, ms) => setInterval(fn, ms),
|
|
78
|
+
cancel: handle => clearInterval(handle)
|
|
79
|
+
};
|