@mjasnikovs/pi-task 0.38.18 → 0.38.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/shared/child-process.d.ts +19 -0
- package/dist/shared/child-process.js +9 -10
- package/dist/task/child-runner.d.ts +18 -1
- package/dist/task/child-runner.js +18 -3
- package/dist/task/child-status.d.ts +11 -3
- package/dist/task/child-status.js +14 -3
- package/dist/task/context-usage.d.ts +1 -1
- package/dist/task/context-usage.js +1 -1
- package/dist/task/gate-child.js +4 -1
- package/dist/task/orchestrator.js +4 -0
- package/dist/task/stall-detector.d.ts +5 -0
- package/dist/task/stall-detector.js +5 -0
- package/dist/workers/pi-worker-core.d.ts +11 -4
- package/dist/workers/pi-worker-core.js +4 -1
- package/package.json +4 -4
|
@@ -115,6 +115,25 @@ export interface RunChildJsonEventsOptions {
|
|
|
115
115
|
mode: 'json-events';
|
|
116
116
|
onLine?: (line: string) => void;
|
|
117
117
|
onContextUsage?: (snapshot: ContextSnapshot) => void;
|
|
118
|
+
/**
|
|
119
|
+
* The child's context window in tokens, supplied BY THE CALLER — pi's event
|
|
120
|
+
* stream does not carry one (GitHub issue #16).
|
|
121
|
+
*
|
|
122
|
+
* Verified against the published tarballs of @earendil-works/pi-coding-agent
|
|
123
|
+
* and @earendil-works/pi-agent-core at 0.80.2 and 0.84.2, and against pi's
|
|
124
|
+
* own docs/json.md: the wire union is session / agent_* / turn_* / message_*
|
|
125
|
+
* / tool_execution_* / queue_update / compaction_* / auto_retry_*, the
|
|
126
|
+
* session header is {type,version,id,timestamp,cwd,parentSession}, and no
|
|
127
|
+
* member of either carries a window or even a model id. `contextUsage`
|
|
128
|
+
* exists ONLY as the in-process `ctx.getContextUsage()` extension API, which
|
|
129
|
+
* a `--mode json` child never speaks back to its parent.
|
|
130
|
+
*
|
|
131
|
+
* The parent therefore has to say. Children are spawned without `-m`
|
|
132
|
+
* (CHILD_BASE_ARGS), so they resolve the same default model the parent runs
|
|
133
|
+
* and the parent session's window is the honest answer. 0 / omitted keeps
|
|
134
|
+
* the old behaviour: report the token count with no window.
|
|
135
|
+
*/
|
|
136
|
+
contextWindow?: number;
|
|
118
137
|
onToolCall?: (call: ToolCall) => LoopHit | null;
|
|
119
138
|
/**
|
|
120
139
|
* Fires when a tool call finishes, carrying its RESULT (mx5 run 10 item 6: the
|
|
@@ -88,15 +88,12 @@ export class JsonEventSink {
|
|
|
88
88
|
handleEvent(evt) {
|
|
89
89
|
const opts = this.opts;
|
|
90
90
|
const t = typeof evt.type === 'string' ? evt.type : '';
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
91
|
+
// `message_end` is the ONLY context readout pi gives a `--mode json`
|
|
92
|
+
// child's parent. There used to be a `context_usage` branch above this
|
|
93
|
+
// one that was preferred over it; no released pi has ever emitted such
|
|
94
|
+
// an event (see `contextWindow` on RunChildJsonEventsOptions), so it was
|
|
95
|
+
// unreachable, and its presence is what made the zero window below look
|
|
96
|
+
// like a harmless fallback rather than the only path. Issue #16.
|
|
100
97
|
if (t === 'message_end' && opts.onContextUsage) {
|
|
101
98
|
const msg = evt.message;
|
|
102
99
|
if (msg?.role === 'assistant') {
|
|
@@ -107,7 +104,9 @@ export class JsonEventSink {
|
|
|
107
104
|
+ Number(usage.cacheWrite ?? 0)
|
|
108
105
|
+ Number(usage.output ?? 0);
|
|
109
106
|
if (tokens > 0) {
|
|
110
|
-
|
|
107
|
+
const cw = Math.max(0, Number(opts.contextWindow ?? 0));
|
|
108
|
+
const percent = cw > 0 ? Math.min(100, (tokens / cw) * 100) : 0;
|
|
109
|
+
opts.onContextUsage({ tokens, contextWindow: cw, percent });
|
|
111
110
|
}
|
|
112
111
|
}
|
|
113
112
|
}
|
|
@@ -82,13 +82,30 @@ extensions?: readonly string[],
|
|
|
82
82
|
* needs the size of what actually entered the child's context, which the
|
|
83
83
|
* CALL alone does not carry (task/stall-detector.ts).
|
|
84
84
|
*/
|
|
85
|
-
onToolResult?: (text: string, isError: boolean) => void
|
|
85
|
+
onToolResult?: (text: string, isError: boolean) => void,
|
|
86
|
+
/**
|
|
87
|
+
* The child's context window in tokens. Nothing in pi's event stream reports
|
|
88
|
+
* one (issue #16), so the parent hands its own down — children carry no `-m`
|
|
89
|
+
* and resolve the same default model. 0 / omitted = unknown, as before.
|
|
90
|
+
*/
|
|
91
|
+
contextWindow?: number): Promise<PhaseRunResult>;
|
|
86
92
|
interface PhaseDeps {
|
|
87
93
|
cwd: string;
|
|
88
94
|
taskId: string;
|
|
89
95
|
signal: AbortSignal;
|
|
90
96
|
onChildOutput?: (line: string) => void;
|
|
91
97
|
onContextUsage?: (snapshot: ContextSnapshot) => void;
|
|
98
|
+
/**
|
|
99
|
+
* The parent session's context window in tokens, handed down to every child.
|
|
100
|
+
*
|
|
101
|
+
* pi's `--mode json` stream reports token counts but no window (issue #16),
|
|
102
|
+
* so without this the gauge shows a bare number and — worse — the
|
|
103
|
+
* StallDetector's CONTEXT CHURN rule, which is gated on a positive window,
|
|
104
|
+
* can never fire. Children are spawned without `-m` (CHILD_BASE_ARGS) and so
|
|
105
|
+
* run the parent's own default model; its window is the honest value.
|
|
106
|
+
* Absent = unknown, and both consumers degrade exactly as they did before.
|
|
107
|
+
*/
|
|
108
|
+
contextWindow?: number;
|
|
92
109
|
/**
|
|
93
110
|
* Record a sub-step duration under the currently running top-level phase.
|
|
94
111
|
* The orchestrator rebinds this between phases so each call lands in the
|
|
@@ -172,7 +172,13 @@ extensions,
|
|
|
172
172
|
* needs the size of what actually entered the child's context, which the
|
|
173
173
|
* CALL alone does not carry (task/stall-detector.ts).
|
|
174
174
|
*/
|
|
175
|
-
onToolResult
|
|
175
|
+
onToolResult,
|
|
176
|
+
/**
|
|
177
|
+
* The child's context window in tokens. Nothing in pi's event stream reports
|
|
178
|
+
* one (issue #16), so the parent hands its own down — children carry no `-m`
|
|
179
|
+
* and resolve the same default model. 0 / omitted = unknown, as before.
|
|
180
|
+
*/
|
|
181
|
+
contextWindow) {
|
|
176
182
|
const invocation = getPiInvocation(childArgs(tools, extensions), prompt);
|
|
177
183
|
let loopHit;
|
|
178
184
|
const result = await runChildUnified(spawnFn ?? spawn, invocation, cwd, signal, {
|
|
@@ -184,6 +190,7 @@ onToolResult) {
|
|
|
184
190
|
streamInactivityMs: getConfig().streamInactivityMs,
|
|
185
191
|
onLine,
|
|
186
192
|
onContextUsage,
|
|
193
|
+
...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
|
|
187
194
|
onToolResult: onToolResult ? r => onToolResult(r.text, r.isError) : undefined,
|
|
188
195
|
onToolCall: call => {
|
|
189
196
|
if (!onToolCall)
|
|
@@ -316,13 +323,21 @@ export async function runPhaseChild(deps, name, tools, prompt, opts = {}) {
|
|
|
316
323
|
throw new Error(USER_CANCELLED);
|
|
317
324
|
const detector = new LoopDetector(LOOP_WINDOW, LOOP_THRESHOLD);
|
|
318
325
|
const stall = new StallDetector();
|
|
326
|
+
// Arm the churn rule BEFORE the first tool call. The window used to reach
|
|
327
|
+
// the detector only through a context snapshot, and pi's stream never
|
|
328
|
+
// carries one, so it was always 0 and rule 2 never fired (issue #16). The
|
|
329
|
+
// parent knows the value at spawn time — say it then, not later.
|
|
330
|
+
stall.noteContext(deps.contextWindow ?? 0);
|
|
319
331
|
const clock = phaseTimeout(deps.signal, budgetMs);
|
|
320
332
|
let r;
|
|
321
333
|
try {
|
|
322
334
|
r = await runChild(deps.cwd, tools, prependHint(hint, prompt), clock.signal, deps.onChildOutput, snapshot => {
|
|
335
|
+
// Real window or nothing: noteContext ignores 0, and until
|
|
336
|
+
// deps.contextWindow existed 0 was all it ever saw, which
|
|
337
|
+
// left the churn rule permanently disarmed (issue #16).
|
|
323
338
|
stall.noteContext(snapshot.contextWindow);
|
|
324
339
|
deps.onContextUsage?.(snapshot);
|
|
325
|
-
}, call => detector.record(call) ?? stall.record(call), deps.spawn, deps.childExtensions, (text, isError) => stall.noteResult(text, isError));
|
|
340
|
+
}, call => detector.record(call) ?? stall.record(call), deps.spawn, deps.childExtensions, (text, isError) => stall.noteResult(text, isError), deps.contextWindow);
|
|
326
341
|
}
|
|
327
342
|
finally {
|
|
328
343
|
clock.cleanup();
|
|
@@ -430,7 +445,7 @@ async function appendLoopEvent(cwd, taskId, phase, hit, strike, outcome) {
|
|
|
430
445
|
async function runDegradedFinalAttempt(deps, name, prompt, hit, loopHistory) {
|
|
431
446
|
deps.logDebug?.(`${name}: loop budget exhausted — degrading to a no-tools final attempt`);
|
|
432
447
|
const r = await runChild(deps.cwd, '', // --no-tools: the model cannot read/grep/list, only answer
|
|
433
|
-
prependHint(formatDegradeHint(hit), prompt), deps.signal, deps.onChildOutput, deps.onContextUsage, undefined, deps.spawn);
|
|
448
|
+
prependHint(formatDegradeHint(hit), prompt), deps.signal, deps.onChildOutput, deps.onContextUsage, undefined, deps.spawn, undefined, undefined, deps.contextWindow);
|
|
434
449
|
if (r.exitCode !== 0 || r.modelError || r.text.trim().length === 0) {
|
|
435
450
|
throw new LoopExhaustedError(name, loopHistory);
|
|
436
451
|
}
|
|
@@ -34,10 +34,15 @@ export declare class ChildStatus {
|
|
|
34
34
|
private readonly _parentContextWindow;
|
|
35
35
|
private readonly _startLoader;
|
|
36
36
|
constructor(deps: ChildStatusDeps);
|
|
37
|
+
/**
|
|
38
|
+
* The parent session's window — the value handed DOWN to each child so its
|
|
39
|
+
* own readout carries one, and the last fallback when a child reports none.
|
|
40
|
+
*/
|
|
41
|
+
get parentContextWindow(): number;
|
|
37
42
|
/** The child's latest stream line. Bind as `onChildOutput` / `onLine`. */
|
|
38
43
|
onLine(line: string): void;
|
|
39
44
|
/**
|
|
40
|
-
* Fold a raw
|
|
45
|
+
* Fold a raw context snapshot into the gauge: the child's own window,
|
|
41
46
|
* else the last known one, else the parent's (`resolveContextUsage`).
|
|
42
47
|
*/
|
|
43
48
|
onContextUsage(snapshot: ContextSnapshot): void;
|
|
@@ -91,5 +96,8 @@ export declare function runPlanningChild(opts: {
|
|
|
91
96
|
prompt: string;
|
|
92
97
|
loader: PlanningChildLoader;
|
|
93
98
|
}): Promise<string>;
|
|
94
|
-
/**
|
|
95
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Wire a `ChildStatus` as a phase child's stream callbacks — plus the window the
|
|
101
|
+
* child must be TOLD, since pi's event stream never reports one (issue #16).
|
|
102
|
+
*/
|
|
103
|
+
export declare function statusCallbacks(status: ChildStatus): Pick<PhaseDeps, 'onChildOutput' | 'onContextUsage' | 'contextWindow'>;
|
|
@@ -30,12 +30,19 @@ export class ChildStatus {
|
|
|
30
30
|
this._parentContextWindow = deps.parentContextWindow;
|
|
31
31
|
this._startLoader = deps.startLoader ?? startAutoLoader;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The parent session's window — the value handed DOWN to each child so its
|
|
35
|
+
* own readout carries one, and the last fallback when a child reports none.
|
|
36
|
+
*/
|
|
37
|
+
get parentContextWindow() {
|
|
38
|
+
return this._parentContextWindow;
|
|
39
|
+
}
|
|
33
40
|
/** The child's latest stream line. Bind as `onChildOutput` / `onLine`. */
|
|
34
41
|
onLine(line) {
|
|
35
42
|
this._lastLine = line;
|
|
36
43
|
}
|
|
37
44
|
/**
|
|
38
|
-
* Fold a raw
|
|
45
|
+
* Fold a raw context snapshot into the gauge: the child's own window,
|
|
39
46
|
* else the last known one, else the parent's (`resolveContextUsage`).
|
|
40
47
|
*/
|
|
41
48
|
onContextUsage(snapshot) {
|
|
@@ -90,10 +97,14 @@ export async function runPlanningChild(opts) {
|
|
|
90
97
|
startedAt
|
|
91
98
|
}), () => runPhaseChild(phaseDeps, name, tools, prompt));
|
|
92
99
|
}
|
|
93
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Wire a `ChildStatus` as a phase child's stream callbacks — plus the window the
|
|
102
|
+
* child must be TOLD, since pi's event stream never reports one (issue #16).
|
|
103
|
+
*/
|
|
94
104
|
export function statusCallbacks(status) {
|
|
95
105
|
return {
|
|
96
106
|
onChildOutput: line => status.onLine(line),
|
|
97
|
-
onContextUsage: snapshot => status.onContextUsage(snapshot)
|
|
107
|
+
onContextUsage: snapshot => status.onContextUsage(snapshot),
|
|
108
|
+
contextWindow: status.parentContextWindow
|
|
98
109
|
};
|
|
99
110
|
}
|
|
@@ -9,7 +9,7 @@ import type { ContextSnapshot } from '../shared/child-process.js';
|
|
|
9
9
|
/** The parent session's context window, or 0 when the model doesn't expose it. */
|
|
10
10
|
export declare function getParentContextWindow(ctx: ExtensionCommandContext): number;
|
|
11
11
|
/**
|
|
12
|
-
* Fold a raw
|
|
12
|
+
* Fold a raw context snapshot into a display snapshot: prefer the child's
|
|
13
13
|
* own contextWindow, else the last known one, else the parent session's; then
|
|
14
14
|
* derive percent against it — falling back to the child's reported percent when
|
|
15
15
|
* no window is known at all.
|
|
@@ -9,7 +9,7 @@ export function getParentContextWindow(ctx) {
|
|
|
9
9
|
return (ctx.model?.contextWindow ?? 0);
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Fold a raw
|
|
12
|
+
* Fold a raw context snapshot into a display snapshot: prefer the child's
|
|
13
13
|
* own contextWindow, else the last known one, else the parent session's; then
|
|
14
14
|
* derive percent against it — falling back to the child's reported percent when
|
|
15
15
|
* no window is known at all.
|
package/dist/task/gate-child.js
CHANGED
|
@@ -119,7 +119,10 @@ export function makeGateChild(deps) {
|
|
|
119
119
|
+ deps.truncateToolResult(text), 'stream')
|
|
120
120
|
}
|
|
121
121
|
: {}),
|
|
122
|
-
onContextUsage: snapshot => deps.status.onContextUsage(snapshot)
|
|
122
|
+
onContextUsage: snapshot => deps.status.onContextUsage(snapshot),
|
|
123
|
+
// The gate child has to be TOLD its window: nothing in pi's
|
|
124
|
+
// event stream reports one (issue #16).
|
|
125
|
+
contextWindow: deps.status.parentContextWindow
|
|
123
126
|
});
|
|
124
127
|
}
|
|
125
128
|
finally {
|
|
@@ -122,6 +122,10 @@ export class TaskRunner {
|
|
|
122
122
|
onChildOutput: (line) => {
|
|
123
123
|
this._widgetState.lastLine = line;
|
|
124
124
|
},
|
|
125
|
+
// Handed DOWN so the child's own snapshot carries a window and the
|
|
126
|
+
// StallDetector's churn rule can arm; resolveContextUsage below stays
|
|
127
|
+
// as the fallback for a child that still reports none (issue #16).
|
|
128
|
+
contextWindow: parentContextWindow,
|
|
125
129
|
onContextUsage: snapshot => {
|
|
126
130
|
this._widgetState.contextUsage = resolveContextUsage(snapshot, this._widgetState.contextUsage, parentContextWindow);
|
|
127
131
|
},
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
* rule 1 alone would not have caught it. The bound scales with the model's
|
|
39
39
|
* OWN window, so a 1M-context model gets a 1M-context allowance.
|
|
40
40
|
*
|
|
41
|
+
* The window is supplied by the PARENT at spawn (`PhaseDeps.contextWindow`),
|
|
42
|
+
* not read off the child's event stream: pi emits no context event at all,
|
|
43
|
+
* which is why this rule sat disarmed from the day it was written until
|
|
44
|
+
* GitHub issue #16.
|
|
45
|
+
*
|
|
41
46
|
* Neither rule can fire on a child that is thinking rather than calling tools:
|
|
42
47
|
* that case is bounded by the model's max tokens (server-enforced) and by the
|
|
43
48
|
* stream watchdog if the stream goes silent. Between the three there is no
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
* rule 1 alone would not have caught it. The bound scales with the model's
|
|
39
39
|
* OWN window, so a 1M-context model gets a 1M-context allowance.
|
|
40
40
|
*
|
|
41
|
+
* The window is supplied by the PARENT at spawn (`PhaseDeps.contextWindow`),
|
|
42
|
+
* not read off the child's event stream: pi emits no context event at all,
|
|
43
|
+
* which is why this rule sat disarmed from the day it was written until
|
|
44
|
+
* GitHub issue #16.
|
|
45
|
+
*
|
|
41
46
|
* Neither rule can fire on a child that is thinking rather than calling tools:
|
|
42
47
|
* that case is bounded by the model's max tokens (server-enforced) and by the
|
|
43
48
|
* stream watchdog if the stream goes silent. Between the three there is no
|
|
@@ -62,12 +62,19 @@ export interface RunWorkerInput {
|
|
|
62
62
|
toolCallId?: string;
|
|
63
63
|
}) => void;
|
|
64
64
|
/**
|
|
65
|
-
* Called for each
|
|
66
|
-
* stream the phase children parse). Lets a
|
|
67
|
-
* tokens/window progress bar for the worker
|
|
68
|
-
* instead of omitting it.
|
|
65
|
+
* Called for each context snapshot derived from the child's `message_end`
|
|
66
|
+
* events (same `--mode json` stream the phase children parse). Lets a
|
|
67
|
+
* caller's status widget show the tokens/window progress bar for the worker
|
|
68
|
+
* exactly like the phase widget, instead of omitting it.
|
|
69
69
|
*/
|
|
70
70
|
onContextUsage?: (snapshot: ContextSnapshot) => void;
|
|
71
|
+
/**
|
|
72
|
+
* The worker child's context window in tokens. pi's event stream carries no
|
|
73
|
+
* window (issue #16), so a caller that wants a progress bar rather than a
|
|
74
|
+
* bare token count has to supply the parent session's — which is the child's
|
|
75
|
+
* too, since workers are spawned without `-m`.
|
|
76
|
+
*/
|
|
77
|
+
contextWindow?: number;
|
|
71
78
|
/**
|
|
72
79
|
* Per-worker wall-clock timeout in ms. Defaults to RESEARCH_WORKER_TIMEOUT_MS.
|
|
73
80
|
* Pass 0 to disable the timeout entirely (run until the child exits on its
|
|
@@ -549,7 +549,10 @@ export async function runWorker(input) {
|
|
|
549
549
|
cmdWatch?.onEnd(r.toolCallId);
|
|
550
550
|
input.onToolResult?.(r);
|
|
551
551
|
},
|
|
552
|
-
onContextUsage: input.onContextUsage
|
|
552
|
+
onContextUsage: input.onContextUsage,
|
|
553
|
+
...(input.contextWindow && input.contextWindow > 0 ?
|
|
554
|
+
{ contextWindow: input.contextWindow }
|
|
555
|
+
: {})
|
|
553
556
|
}, input.spawn);
|
|
554
557
|
}
|
|
555
558
|
finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.19",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"ws": "8.21.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@earendil-works/pi-agent-core": "0.
|
|
36
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
37
|
-
"@earendil-works/pi-tui": "0.
|
|
35
|
+
"@earendil-works/pi-agent-core": "0.84.2",
|
|
36
|
+
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
37
|
+
"@earendil-works/pi-tui": "0.84.2",
|
|
38
38
|
"@eslint/js": "10.0.1",
|
|
39
39
|
"@types/bun": "1.3.12",
|
|
40
40
|
"@types/qrcode": "1.5.6",
|