@mjasnikovs/pi-task 0.40.40 → 0.40.41

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.
@@ -96,6 +96,7 @@ export declare function parseEnforceVerdict(text: string): {
96
96
  /** The subset of a runWorker result the enforcement-child mapping reads. */
97
97
  export interface EnforceChildResult extends WorkerFailureInput {
98
98
  text: string;
99
+ modelError?: string;
99
100
  }
100
101
  /**
101
102
  * Map the enforcement child's runWorker result to a fatal error message, or null
@@ -236,9 +236,19 @@ export function parseEnforceVerdict(text) {
236
236
  * effects don't get re-classified as a user cancel or a crash.
237
237
  */
238
238
  export function classifyEnforceChildFailure(r) {
239
- const failure = classifyWorkerFailure(r);
240
- if (!failure)
241
- return null;
239
+ const kill = classifyWorkerFailure(r);
240
+ const named = kill ? describeKill(kill) : null;
241
+ if (named !== null)
242
+ return named;
243
+ // Not a kill, so the ladder leaves it to the consumer: pi reports a failed
244
+ // turn as exit 0, empty text, and the cause in `modelError`. Unread, the
245
+ // empty text parses as "no verdict" and a dead provider is blamed on the work.
246
+ // Checked after a surviving loop too — that arm is a warning, not an answer.
247
+ return r.modelError && r.text.trim().length === 0 ?
248
+ `model error — ${r.modelError.slice(0, 200)}`
249
+ : null;
250
+ }
251
+ function describeKill(failure) {
242
252
  switch (failure.kind) {
243
253
  case 'stalled':
244
254
  // Otherwise a long silence is reported as a user cancel.
@@ -12,4 +12,10 @@
12
12
  * — the tool whitelist, not the extension flag, is the bound that holds.
13
13
  */
14
14
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
15
- export declare function registerPiWorker(pi: ExtensionAPI): void;
15
+ import type { SpawnFn } from '../shared/child-process.js';
16
+ /** Test seams: a fake child, and no real backoff sleep between its attempts. */
17
+ export interface PiWorkerInternals {
18
+ spawn?: SpawnFn;
19
+ sleepFor?: (ms: number) => Promise<void>;
20
+ }
21
+ export declare function registerPiWorker(pi: ExtensionAPI, internals?: PiWorkerInternals): void;
@@ -19,10 +19,28 @@ import { runWorker } from './pi-worker-core.js';
19
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
+ const STDERR_TAIL = 500;
23
+ function workerDetails(r) {
24
+ return {
25
+ exitCode: r.exitCode,
26
+ attempts: r.attempts,
27
+ restarts: r.restarts.map(x => x.reason),
28
+ ...(r.modelError !== undefined ? { modelError: r.modelError } : {}),
29
+ ...(r.stderr ? { stderr: r.stderr.slice(-STDERR_TAIL) } : {})
30
+ };
31
+ }
32
+ /** An empty final answer, with the spawns it cost, so the model can tell a
33
+ * retried failure from a child that simply had nothing to say. */
34
+ function describeEmptyAnswer(r) {
35
+ if (r.restarts.length === 0)
36
+ return '(no output)';
37
+ const reasons = r.restarts.map(x => x.reason).join(', ');
38
+ return `(no output after ${r.attempts} attempts; discarded: ${reasons})`;
39
+ }
22
40
  const WorkerParams = Type.Object({
23
41
  prompt: Type.String({ description: 'Task for the worker to perform.' })
24
42
  });
25
- export function registerPiWorker(pi) {
43
+ export function registerPiWorker(pi, internals = {}) {
26
44
  makeWorkerTool(pi, {
27
45
  name: 'pi-worker',
28
46
  label: 'Pi Worker',
@@ -67,14 +85,25 @@ export function registerPiWorker(pi) {
67
85
  // model and its window is the honest one. Without this the churn
68
86
  // rule cannot fire — see RunWorkerInput.contextWindow.
69
87
  contextWindow: contextWindowForGroup(ctx, 'research') || 'unknown',
70
- groupArgs: groupChildArgs('research')
88
+ groupArgs: groupChildArgs('research'),
89
+ ...(internals.spawn ? { spawn: internals.spawn } : {}),
90
+ ...(internals.sleepFor ? { sleepFor: internals.sleepFor } : {})
71
91
  });
72
- const details = { exitCode: result.exitCode };
92
+ const details = workerDetails(result);
73
93
  const failure = formatChildFailure(result, 'Worker aborted.');
74
94
  if (failure !== null) {
75
95
  return workerUnavailable(failure, details, childFailureReason(result));
76
96
  }
77
- return workerAnswer(result.text || '(no output)', details);
97
+ // Not a kill, so the ladder leaves it to us: pi reports a failed turn
98
+ // as exit 0, empty text, and the cause in `modelError`.
99
+ if (result.modelError && result.text.trim().length === 0) {
100
+ return workerUnavailable(`Worker failed: model error — ${result.modelError.slice(0, 200)}`, details, 'model-error');
101
+ }
102
+ const text = result.text.trim();
103
+ if (text.length === 0) {
104
+ return workerUnavailable(describeEmptyAnswer(result), details, 'no-answer');
105
+ }
106
+ return workerAnswer(result.text, details);
78
107
  },
79
108
  renderCall(args, theme) {
80
109
  const prompt = args.prompt.replace(/\s+/g, ' ').trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.40.40",
3
+ "version": "0.40.41",
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",