@mjasnikovs/pi-task 0.38.28 → 0.38.29
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/task/context-usage.d.ts +14 -3
- package/dist/task/context-usage.js +10 -2
- package/dist/task/gate-child.js +1 -0
- package/dist/task/phases.js +1 -0
- package/dist/task/research-worker.d.ts +7 -0
- package/dist/task/research-worker.js +2 -0
- package/dist/workers/pi-worker-core.d.ts +39 -5
- package/dist/workers/pi-worker-core.js +12 -3
- package/dist/workers/pi-worker.js +6 -0
- package/package.json +1 -1
|
@@ -4,10 +4,21 @@
|
|
|
4
4
|
* the planning and gate children; the single-task widget (TaskRunner) calls it
|
|
5
5
|
* directly, because its state is the whole-run `WidgetState`, not one child's.
|
|
6
6
|
*/
|
|
7
|
-
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
8
7
|
import type { ContextSnapshot } from '../shared/child-process.js';
|
|
9
|
-
/**
|
|
10
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The parent session's context window, or 0 when the model doesn't expose it.
|
|
10
|
+
*
|
|
11
|
+
* The parameter is the STRUCTURAL MINIMUM this reads, not one of pi's context
|
|
12
|
+
* interfaces. It took `ExtensionCommandContext` and immediately cast it away,
|
|
13
|
+
* which is a lie that also excluded the plain `ExtensionContext` a TOOL is
|
|
14
|
+
* handed — the very caller (`workers/pi-worker.ts`) that needs the window to arm
|
|
15
|
+
* the churn rule. Both pi contexts satisfy this shape.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getParentContextWindow(ctx: {
|
|
18
|
+
model?: {
|
|
19
|
+
contextWindow?: number;
|
|
20
|
+
};
|
|
21
|
+
}): number;
|
|
11
22
|
/**
|
|
12
23
|
* Fold a raw context snapshot into a display snapshot: prefer the child's
|
|
13
24
|
* own contextWindow, else the last known one, else the parent session's; then
|
|
@@ -4,9 +4,17 @@
|
|
|
4
4
|
* the planning and gate children; the single-task widget (TaskRunner) calls it
|
|
5
5
|
* directly, because its state is the whole-run `WidgetState`, not one child's.
|
|
6
6
|
*/
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* The parent session's context window, or 0 when the model doesn't expose it.
|
|
9
|
+
*
|
|
10
|
+
* The parameter is the STRUCTURAL MINIMUM this reads, not one of pi's context
|
|
11
|
+
* interfaces. It took `ExtensionCommandContext` and immediately cast it away,
|
|
12
|
+
* which is a lie that also excluded the plain `ExtensionContext` a TOOL is
|
|
13
|
+
* handed — the very caller (`workers/pi-worker.ts`) that needs the window to arm
|
|
14
|
+
* the churn rule. Both pi contexts satisfy this shape.
|
|
15
|
+
*/
|
|
8
16
|
export function getParentContextWindow(ctx) {
|
|
9
|
-
return
|
|
17
|
+
return ctx.model?.contextWindow ?? 0;
|
|
10
18
|
}
|
|
11
19
|
/**
|
|
12
20
|
* Fold a raw context snapshot into a display snapshot: prefer the child's
|
package/dist/task/gate-child.js
CHANGED
|
@@ -105,6 +105,7 @@ export function makeGateChild(deps) {
|
|
|
105
105
|
// burned two attempts reads exactly like one that ran clean.
|
|
106
106
|
onRestart: rs => log(`=== ${deps.kind} RESTART (attempt ${rs.attempt} discarded)`
|
|
107
107
|
+ ` reason=${rs.reason} wall=${rs.wallMs}ms`
|
|
108
|
+
+ ` discarded=${rs.partialChars}ch`
|
|
108
109
|
+ (rs.detail ? ` — ${rs.detail}` : '')
|
|
109
110
|
+ ' ==='),
|
|
110
111
|
onLine: line => {
|
package/dist/task/phases.js
CHANGED
|
@@ -679,6 +679,7 @@ export async function phaseResearch(deps, refined) {
|
|
|
679
679
|
*/
|
|
680
680
|
const drive = (spec, prior) => runResearchWorker(spec, {
|
|
681
681
|
runWorker: runWorkerFn,
|
|
682
|
+
contextWindow: deps.contextWindow ?? 'unknown',
|
|
682
683
|
cwd: deps.cwd,
|
|
683
684
|
taskId: deps.taskId,
|
|
684
685
|
signal: deps.signal,
|
|
@@ -65,6 +65,13 @@ export interface ResearchWorkerSpec {
|
|
|
65
65
|
*/
|
|
66
66
|
export interface ResearchWorkerRun {
|
|
67
67
|
runWorker: (label: string, input: RunWorkerInput) => Promise<RunWorkerResult>;
|
|
68
|
+
/**
|
|
69
|
+
* The parent session's context window, forwarded to every worker child.
|
|
70
|
+
* `PhaseDeps.contextWindow` already carried it and this driver dropped it, so
|
|
71
|
+
* the churn rule was dead for all four workers — see
|
|
72
|
+
* `RunWorkerInput.contextWindow`.
|
|
73
|
+
*/
|
|
74
|
+
contextWindow: number | 'unknown';
|
|
68
75
|
cwd: string;
|
|
69
76
|
taskId: string;
|
|
70
77
|
signal: AbortSignal;
|
|
@@ -247,6 +247,7 @@ export async function runResearchWorker(spec, run, prior = []) {
|
|
|
247
247
|
const runOnce = (extraPreamble) => run.record(spec.label, run.runWorker(spec.label, {
|
|
248
248
|
prompt: extraPreamble ? `${extraPreamble}\n\n${basePrompt}` : basePrompt,
|
|
249
249
|
cwd: run.cwd,
|
|
250
|
+
contextWindow: run.contextWindow,
|
|
250
251
|
signal: run.signal,
|
|
251
252
|
spawn: run.spawn,
|
|
252
253
|
// ONE CELL PER WORKER since 2026-08-28. They used to share
|
|
@@ -286,6 +287,7 @@ export async function runResearchWorker(spec, run, prior = []) {
|
|
|
286
287
|
onRestart: rs => {
|
|
287
288
|
run.logDebug?.(`${spec.label}: RESTART (attempt ${rs.attempt} discarded)`
|
|
288
289
|
+ ` reason=${rs.reason} wall=${rs.wallMs}ms`
|
|
290
|
+
+ ` discarded=${rs.partialChars}ch`
|
|
289
291
|
+ ` wait=${rs.waitMs}ms work=${rs.workMs}ms`
|
|
290
292
|
+ (rs.detail ? ` — ${rs.detail}` : ''));
|
|
291
293
|
run.onChildOutput?.(`${spec.label}: restart (${rs.reason})`);
|
|
@@ -82,12 +82,29 @@ export interface RunWorkerInput {
|
|
|
82
82
|
*/
|
|
83
83
|
onContextUsage?: (snapshot: ContextSnapshot) => void;
|
|
84
84
|
/**
|
|
85
|
-
* The worker child's context window in tokens
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
85
|
+
* The worker child's context window in tokens, or `'unknown'` when the
|
|
86
|
+
* caller genuinely has none.
|
|
87
|
+
*
|
|
88
|
+
* REQUIRED, and required for the same reason `profile` below is. This was
|
|
89
|
+
* `contextWindow?: number`, and two of the three production call sites simply
|
|
90
|
+
* did not write it: `pi-worker.ts` and `research-worker.ts`. pi's event
|
|
91
|
+
* stream carries no window (issue #16), so `noteContext` only ever saw 0, and
|
|
92
|
+
* `StallDetector`'s CONTEXT CHURN rule — gated on a positive window
|
|
93
|
+
* (`stall-detector.ts`) — could never fire for the ad-hoc worker or for any
|
|
94
|
+
* of the four research workers. Nothing was red. The optional was the whole
|
|
95
|
+
* defect: a rule that silently does not exist reads exactly like a rule that
|
|
96
|
+
* exists and did not trip.
|
|
97
|
+
*
|
|
98
|
+
* WHY A WORD AND NOT `0` OR `null`. Both of those are what a caller types
|
|
99
|
+
* when it has not thought about the question, and both disarm the rule
|
|
100
|
+
* silently — which is the state this replaces. `'unknown'` cannot be typed by
|
|
101
|
+
* accident, is greppable, and shows up in a diff as a decision.
|
|
102
|
+
*
|
|
103
|
+
* Two consumers read it: the churn rule, and the caller's progress bar, which
|
|
104
|
+
* shows a bare token count without a window. Both degrade exactly as before
|
|
105
|
+
* on `'unknown'`.
|
|
89
106
|
*/
|
|
90
|
-
contextWindow
|
|
107
|
+
contextWindow: number | 'unknown';
|
|
91
108
|
/**
|
|
92
109
|
* WHICH KIND of worker child this is — the whole guard policy, in one word.
|
|
93
110
|
*
|
|
@@ -178,6 +195,23 @@ export interface WorkerRestart {
|
|
|
178
195
|
workMs: number;
|
|
179
196
|
/** Reason-specific diagnosis: the looping call, the hung tool, the error text. */
|
|
180
197
|
detail?: string;
|
|
198
|
+
/**
|
|
199
|
+
* Characters of ANSWER TEXT this attempt had produced at the moment it was
|
|
200
|
+
* thrown away.
|
|
201
|
+
*
|
|
202
|
+
* Recorded whatever `carryForward` says, because the DISCARD is the thing a
|
|
203
|
+
* reader cannot otherwise see. A restart line reported how long an attempt
|
|
204
|
+
* ran and why it died, and never what died with it — so "the guards worked
|
|
205
|
+
* and the run still returned 52 characters" and "the guards worked and the
|
|
206
|
+
* run threw away a finished answer" print identically. Measured on the
|
|
207
|
+
* ad-hoc `pi-worker` corpus: T024 lost an attempt to a dropped model socket
|
|
208
|
+
* at 275s and returned 52 chars over 620s; nothing in the run said whether
|
|
209
|
+
* those 275s held anything.
|
|
210
|
+
*
|
|
211
|
+
* It is an OBSERVATION, not a decision: harvesting into `salvage` is still
|
|
212
|
+
* gated on the profile, and this number changes no behaviour.
|
|
213
|
+
*/
|
|
214
|
+
partialChars: number;
|
|
181
215
|
}
|
|
182
216
|
export interface RunWorkerResult {
|
|
183
217
|
text: string;
|
|
@@ -20,6 +20,14 @@ import { applyOverride, WORKER_PROFILES } from './worker-profiles.js';
|
|
|
20
20
|
// model starts producing — making waitMs the real queue/cold-start cost and
|
|
21
21
|
// workMs the real generation+tool-call cost.
|
|
22
22
|
const DEFAULT_TOOLS = 'read,grep,find,ls';
|
|
23
|
+
/**
|
|
24
|
+
* The one place `'unknown'` becomes the 0 both consumers already treat as
|
|
25
|
+
* "no window". Written once so a future reader cannot re-introduce the optional
|
|
26
|
+
* by handling the union at only one of the two sites that read it.
|
|
27
|
+
*/
|
|
28
|
+
function contextWindowTokens(cw) {
|
|
29
|
+
return cw === 'unknown' || cw <= 0 ? 0 : cw;
|
|
30
|
+
}
|
|
23
31
|
/**
|
|
24
32
|
* Tool calls that can GROUND an APIS claim — i.e. return content a signature or
|
|
25
33
|
* command could be cited from. `pi-worker-docs` (the primary), `read` and `grep`
|
|
@@ -500,7 +508,7 @@ export async function runWorker(input) {
|
|
|
500
508
|
// Arm the churn rule BEFORE the first tool call. pi's stream carries no
|
|
501
509
|
// context event (issue #16), so waiting for one leaves the rule
|
|
502
510
|
// permanently disarmed. The parent knows the window at spawn time.
|
|
503
|
-
stallDetector?.noteContext(input.contextWindow
|
|
511
|
+
stallDetector?.noteContext(contextWindowTokens(input.contextWindow));
|
|
504
512
|
// Capture the hit the detector reports (it also returns it to the unified
|
|
505
513
|
// runner, which kills the child on a hit). Without capturing it here the
|
|
506
514
|
// SIGTERM that kill produces would surface as a bare non-zero exit the
|
|
@@ -579,8 +587,8 @@ export async function runWorker(input) {
|
|
|
579
587
|
stallDetector?.noteContext(snapshot.contextWindow);
|
|
580
588
|
input.onContextUsage?.(snapshot);
|
|
581
589
|
},
|
|
582
|
-
...(input.contextWindow
|
|
583
|
-
{ contextWindow: input.contextWindow }
|
|
590
|
+
...(contextWindowTokens(input.contextWindow) > 0 ?
|
|
591
|
+
{ contextWindow: contextWindowTokens(input.contextWindow) }
|
|
584
592
|
: {})
|
|
585
593
|
}, input.spawn);
|
|
586
594
|
}
|
|
@@ -601,6 +609,7 @@ export async function runWorker(input) {
|
|
|
601
609
|
wallMs: tEnd - tAttemptStart,
|
|
602
610
|
waitMs,
|
|
603
611
|
workMs,
|
|
612
|
+
partialChars: text.trim().length,
|
|
604
613
|
...(detail ? { detail } : {})
|
|
605
614
|
};
|
|
606
615
|
restarts.push(record);
|
|
@@ -11,6 +11,7 @@ import { Type } from '@sinclair/typebox';
|
|
|
11
11
|
import { getConfig } from '../config/config.js';
|
|
12
12
|
import { groupThinkingArgs } from '../config/reasoning-args.js';
|
|
13
13
|
import { runWorker } from './pi-worker-core.js';
|
|
14
|
+
import { getParentContextWindow } from '../task/context-usage.js';
|
|
14
15
|
import { childFailureReason, formatChildFailure, makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
|
|
15
16
|
const RENDER_PROMPT_MAX = 120;
|
|
16
17
|
const WorkerParams = Type.Object({
|
|
@@ -54,6 +55,11 @@ export function registerPiWorker(pi) {
|
|
|
54
55
|
// not policy for the same reason the gate's two ceilings are: the
|
|
55
56
|
// number is the user's, the decision to arm it is the profile's.
|
|
56
57
|
policyInputs: { streamInactivityMs: getConfig().streamInactivityMs },
|
|
58
|
+
// The session's own window, handed down. The child is spawned
|
|
59
|
+
// without `-m`, so the parent's model IS the child's model and
|
|
60
|
+
// its window is the honest one. Without this the churn rule
|
|
61
|
+
// cannot fire — see RunWorkerInput.contextWindow.
|
|
62
|
+
contextWindow: getParentContextWindow(ctx) || 'unknown',
|
|
57
63
|
thinking: groupThinkingArgs('research')
|
|
58
64
|
});
|
|
59
65
|
const details = { exitCode: result.exitCode };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.29",
|
|
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",
|