@mjasnikovs/pi-task 0.40.39 → 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.
|
@@ -318,12 +318,18 @@ export function judgeDeepSession(f) {
|
|
|
318
318
|
&& lastHopJudgesTheClient(r)
|
|
319
319
|
&& (r.mimeType ?? '').startsWith('text/html'));
|
|
320
320
|
if (swallowed) {
|
|
321
|
+
const landed = swallowed.finalPath ?? swallowed.path;
|
|
322
|
+
const hop = landed === swallowed.path ? '' : ` → \`${landed}\``;
|
|
323
|
+
const cause = swallowed.redirected ?
|
|
324
|
+
'The call was bounced to a page, so the session the client carried is not one this '
|
|
325
|
+
+ 'server accepts and the answer is the SPA shell.'
|
|
326
|
+
: 'The route is not mounted and the catch-all answered instead, so the client sees a '
|
|
327
|
+
+ '200 it cannot parse.';
|
|
321
328
|
return {
|
|
322
329
|
outcome: 'fail',
|
|
323
|
-
detail: `\`${swallowed.method} ${swallowed.path}
|
|
330
|
+
detail: `\`${swallowed.method} ${swallowed.path}\`${hop} → ${String(swallowed.status)} `
|
|
324
331
|
+ `${swallowed.mimeType ?? ''}: an XHR asked this server for data and got the SPA `
|
|
325
|
-
+
|
|
326
|
-
+ 'sees a 200 it cannot parse. No status check can see this.'
|
|
332
|
+
+ `shell. ${cause} No status check can see this.`
|
|
327
333
|
};
|
|
328
334
|
}
|
|
329
335
|
const { method, path: p, status, failed, redirected: authRedirected } = f.authRequest;
|
|
@@ -695,6 +701,7 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
695
701
|
const existing = requests.get(id);
|
|
696
702
|
if (p.redirectResponse !== undefined && existing) {
|
|
697
703
|
existing.finalUrl = String(req?.url ?? existing.finalUrl);
|
|
704
|
+
existing.redirected = true;
|
|
698
705
|
}
|
|
699
706
|
else {
|
|
700
707
|
requests.set(id, {
|
|
@@ -705,6 +712,7 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
705
712
|
status: null,
|
|
706
713
|
mimeType: null,
|
|
707
714
|
failed: false,
|
|
715
|
+
redirected: false,
|
|
708
716
|
seq: nextSeq++
|
|
709
717
|
});
|
|
710
718
|
}
|
|
@@ -779,7 +787,7 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
779
787
|
mimeType: r.mimeType,
|
|
780
788
|
failed: r.failed,
|
|
781
789
|
initiator: initiatorOf(r),
|
|
782
|
-
redirected: r.
|
|
790
|
+
redirected: r.redirected,
|
|
783
791
|
phase: id === authId ? 'auth'
|
|
784
792
|
: r.seq >= postSeq ? 'post'
|
|
785
793
|
: 'pre'
|
|
@@ -840,12 +848,11 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
840
848
|
?? firstRequest(r => r.seq >= fillSeq && sameOrigin(r) && r.method !== 'GET' && r.type === 'Document');
|
|
841
849
|
// An SSO sign-in addresses this app and ends on the provider, so it is absent
|
|
842
850
|
// from the same-origin log above and "no request to our own origin" would
|
|
843
|
-
// misname it.
|
|
844
|
-
//
|
|
845
|
-
// name a bogus identity provider.
|
|
846
|
-
const offsiteSignIn = authId !== null ? null : (firstRequest(r => r.seq >= fillSeq
|
|
851
|
+
// misname it. The two windows are authId's, for the same reasons: anything the
|
|
852
|
+
// submit issued counts, and only a NAVIGATION counts back to the fill, where the
|
|
853
|
+
// background traffic it races would otherwise name a bogus identity provider.
|
|
854
|
+
const offsiteSignIn = authId !== null ? null : (firstRequest(r => (r.seq >= submitSeq || (r.seq >= fillSeq && r.type === 'Document'))
|
|
847
855
|
&& r.method !== 'GET'
|
|
848
|
-
&& r.type === 'Document'
|
|
849
856
|
&& addressedOrigin(r)
|
|
850
857
|
&& !sameOrigin(r)
|
|
851
858
|
&& r.finalUrl.startsWith('http')));
|
|
@@ -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
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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",
|