@mjasnikovs/pi-task 0.13.6 → 0.13.8
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/config/register.js +1 -1
- package/dist/remote/push.d.ts +12 -3
- package/dist/remote/push.js +63 -9
- package/dist/remote/register.js +7 -3
- package/dist/remote/server.d.ts +4 -2
- package/dist/remote/server.js +7 -3
- package/dist/remote/tailscale.d.ts +8 -2
- package/dist/remote/tailscale.js +13 -6
- package/dist/remote/ui-script.d.ts +3 -0
- package/dist/remote/ui-script.js +804 -0
- package/dist/remote/ui-styles.d.ts +1 -0
- package/dist/remote/ui-styles.js +202 -0
- package/dist/remote/ui.js +4 -1000
- package/dist/shared/child-process.d.ts +27 -0
- package/dist/shared/child-process.js +151 -139
- package/dist/task/auto-orchestrator.js +43 -13
- package/dist/task/auto-prompts.d.ts +4 -3
- package/dist/task/auto-prompts.js +9 -6
- package/dist/task/child-runner.js +1 -1
- package/dist/task/context-usage.d.ts +16 -0
- package/dist/task/context-usage.js +22 -0
- package/dist/task/external-context.d.ts +27 -0
- package/dist/task/external-context.js +93 -0
- package/dist/task/failure-classifier.js +1 -1
- package/dist/task/orchestrator.js +7 -13
- package/dist/task/parsers.d.ts +4 -15
- package/dist/task/parsers.js +48 -87
- package/dist/task/phases.d.ts +5 -7
- package/dist/task/phases.js +29 -84
- package/dist/task/prompts.d.ts +1 -0
- package/dist/task/prompts.js +9 -0
- package/dist/task/spec-validation.d.ts +23 -0
- package/dist/task/spec-validation.js +90 -0
- package/dist/task/widget.d.ts +1 -1
- package/dist/task/widget.js +1 -1
- package/dist/workers/html-clean.js +7 -4
- package/dist/workers/pi-worker-docs.js +69 -58
- package/dist/workers/pi-worker-fetch.js +25 -21
- package/dist/workers/pi-worker-search.js +7 -13
- package/dist/workers/pi-worker.js +8 -14
- package/dist/workers/shared.d.ts +40 -0
- package/dist/workers/shared.js +31 -0
- package/package.json +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec gate — the guards that decide whether a composed spec is acceptable at
|
|
3
|
+
* handoff. Unlike the informational parsers in parsers.ts, these answer a
|
|
4
|
+
* yes/no (or "what's wrong") question the orchestrator and critique phase act
|
|
5
|
+
* on: is the VERIFY block runnable, is the shape well-formed, did critique come
|
|
6
|
+
* back CLEAN. Self-contained (no imports) so the gate doesn't drag in the phase
|
|
7
|
+
* pipeline.
|
|
8
|
+
*/
|
|
9
|
+
// ─── Verify block parser ─────────────────────────────────────────────────────
|
|
10
|
+
export function parseVerifyBlock(spec) {
|
|
11
|
+
const lines = spec.split('\n');
|
|
12
|
+
let i = 0;
|
|
13
|
+
while (i < lines.length && !/^VERIFY:\s*$/.test(lines[i]))
|
|
14
|
+
i++;
|
|
15
|
+
if (i >= lines.length)
|
|
16
|
+
return null;
|
|
17
|
+
i++;
|
|
18
|
+
while (i < lines.length && lines[i].trim() === '')
|
|
19
|
+
i++;
|
|
20
|
+
if (i >= lines.length)
|
|
21
|
+
return null;
|
|
22
|
+
if (!/^```(sh|bash)?\s*$/.test(lines[i]))
|
|
23
|
+
return null;
|
|
24
|
+
i++;
|
|
25
|
+
const cmds = [];
|
|
26
|
+
while (i < lines.length && !/^```\s*$/.test(lines[i])) {
|
|
27
|
+
const line = lines[i].trim();
|
|
28
|
+
if (line.length > 0 && !line.startsWith('#'))
|
|
29
|
+
cmds.push({ raw: line });
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
return cmds;
|
|
33
|
+
}
|
|
34
|
+
// ─── Critique triage gate ────────────────────────────────────────────────────
|
|
35
|
+
// The critique-triage prompt instructs the worker to emit the literal token
|
|
36
|
+
// `CLEAN` on its own line when the compose draft has no substantive defects, so
|
|
37
|
+
// we can skip the expensive full-rewrite pass. Anything else is treated as a
|
|
38
|
+
// defect list that gets fed into the rewrite. Empty output is NOT clean — that
|
|
39
|
+
// would be a silent crash, and treating it as clean would skip review entirely.
|
|
40
|
+
export function isCritiqueClean(text) {
|
|
41
|
+
const firstLine = text
|
|
42
|
+
.split('\n')
|
|
43
|
+
.map(l => l.trim())
|
|
44
|
+
.find(l => l.length > 0);
|
|
45
|
+
if (!firstLine)
|
|
46
|
+
return false;
|
|
47
|
+
return /^CLEAN[.!]?$/i.test(firstLine);
|
|
48
|
+
}
|
|
49
|
+
// ─── Spec shape gate ─────────────────────────────────────────────────────────
|
|
50
|
+
/**
|
|
51
|
+
* Drop any preamble the model emitted before the spec's GOAL header. The
|
|
52
|
+
* thinking model sometimes narrates ("Now I have all the context. Here's the
|
|
53
|
+
* rewritten spec:") before GOAL — the prompts forbid it, but the critique
|
|
54
|
+
* validator only checks for a VERIFY block, so it leaked into the delivered
|
|
55
|
+
* spec. We slice from the first line that begins a GOAL section so the spec
|
|
56
|
+
* starts at GOAL. No GOAL line → returned unchanged (validation then flags it).
|
|
57
|
+
*/
|
|
58
|
+
export function stripSpecPreamble(spec) {
|
|
59
|
+
const lines = spec.split('\n');
|
|
60
|
+
const idx = lines.findIndex(l => /^GOAL\b/i.test(l));
|
|
61
|
+
if (idx <= 0)
|
|
62
|
+
return spec;
|
|
63
|
+
// Only strip plain narration. If the lead-in is a markdown fence or a
|
|
64
|
+
// cat-heredoc wrapper, leave it untouched — that's a malformation
|
|
65
|
+
// validateSpecShape must reject (and compose must retry on), not something
|
|
66
|
+
// to silently unwrap into a passing spec.
|
|
67
|
+
const preamble = lines.slice(0, idx);
|
|
68
|
+
if (preamble.some(l => /^\s*```/.test(l) || /^\s*cat\s*<</.test(l)))
|
|
69
|
+
return spec;
|
|
70
|
+
return lines.slice(idx).join('\n');
|
|
71
|
+
}
|
|
72
|
+
export function validateSpecShape(spec) {
|
|
73
|
+
const trimmed = spec.trim();
|
|
74
|
+
if (trimmed.length === 0)
|
|
75
|
+
return 'spec is empty';
|
|
76
|
+
const firstLine = trimmed.split('\n', 1)[0];
|
|
77
|
+
if (/^\s*```/.test(firstLine))
|
|
78
|
+
return 'spec starts with a markdown fence';
|
|
79
|
+
if (/^\s*cat\s*<<\s*['"]?[A-Za-z_][A-Za-z0-9_]*['"]?/.test(firstLine)) {
|
|
80
|
+
return 'spec is wrapped in a cat heredoc';
|
|
81
|
+
}
|
|
82
|
+
if (!/^GOAL\b/i.test(trimmed))
|
|
83
|
+
return 'spec does not start with GOAL';
|
|
84
|
+
for (const section of ['CONSTRAINTS', 'ACCEPTANCE', 'VERIFY']) {
|
|
85
|
+
if (!new RegExp(`^\\s*${section}\\b`, 'm').test(trimmed)) {
|
|
86
|
+
return `spec missing required section: ${section}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
package/dist/task/widget.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* context usage, and the latest child-process line.
|
|
6
6
|
*/
|
|
7
7
|
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
8
|
-
import { type PhaseName, type TaskState } from './task-
|
|
8
|
+
import { type PhaseName, type TaskState } from './task-types.js';
|
|
9
9
|
export interface WidgetState {
|
|
10
10
|
taskId: string;
|
|
11
11
|
title: string;
|
package/dist/task/widget.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Renders a live-updating status block showing task id, phase, elapsed time,
|
|
5
5
|
* context usage, and the latest child-process line.
|
|
6
6
|
*/
|
|
7
|
-
import { PHASE_INDEX, PHASE_ORDER } from './task-
|
|
7
|
+
import { PHASE_INDEX, PHASE_ORDER } from './task-types.js';
|
|
8
8
|
import { setTaskWidget } from '../remote/session-state.js';
|
|
9
9
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
10
10
|
export const WIDGET_KEY = 'pi-tasks';
|
|
@@ -11,21 +11,22 @@ const turndown = new TurndownService({
|
|
|
11
11
|
});
|
|
12
12
|
export function cleanHtml(html, baseUrl) {
|
|
13
13
|
const { document } = parseHTML(html);
|
|
14
|
+
const doc = document;
|
|
14
15
|
const reader = new Readability(document);
|
|
15
16
|
const parsed = reader.parse();
|
|
16
17
|
if (parsed && parsed.content) {
|
|
17
18
|
return {
|
|
18
|
-
title: parsed.title ||
|
|
19
|
+
title: parsed.title || doc.title || new URL(baseUrl).hostname,
|
|
19
20
|
markdown: turndown.turndown(parsed.content).trim(),
|
|
20
21
|
finalUrl: baseUrl
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
// Fallback: turndown the body
|
|
24
|
-
const body =
|
|
25
|
+
const body = doc.body;
|
|
25
26
|
const bodyHtml = body ? body.innerHTML : '';
|
|
26
27
|
const markdown = turndown.turndown(bodyHtml).trim();
|
|
27
28
|
return {
|
|
28
|
-
title:
|
|
29
|
+
title: doc.title || new URL(baseUrl).hostname,
|
|
29
30
|
markdown,
|
|
30
31
|
finalUrl: baseUrl
|
|
31
32
|
};
|
|
@@ -147,7 +148,9 @@ export async function fetchAndClean(url, opts = {}) {
|
|
|
147
148
|
let bytesRead = 0;
|
|
148
149
|
try {
|
|
149
150
|
while (true) {
|
|
150
|
-
|
|
151
|
+
// response.body's stream type doesn't resolve here, so the chunk
|
|
152
|
+
// surfaces as `any`; pin it to the Uint8Array the reader yields.
|
|
153
|
+
const { value, done } = (await reader.read());
|
|
151
154
|
if (done)
|
|
152
155
|
break;
|
|
153
156
|
if (value) {
|
|
@@ -7,7 +7,7 @@ import { formatNpmVersionSection } from './npm-version.js';
|
|
|
7
7
|
import { runChild, CHILD_BASE_ARGS } from '../shared/child-process.js';
|
|
8
8
|
import { parseChildOutput, isExcerptInContent } from '../shared/child-output.js';
|
|
9
9
|
import { getPiInvocation } from '../shared/pi-invocation.js';
|
|
10
|
-
import {
|
|
10
|
+
import { formatChildFailure, makeWorkerTool } from './shared.js';
|
|
11
11
|
import { projectDocsRaw, buildProjectPrompt } from './docs-project.js';
|
|
12
12
|
const CHILD_ARGS = [...CHILD_BASE_ARGS, '--no-tools'];
|
|
13
13
|
const RENDER_QUERY_MAX = 100;
|
|
@@ -20,7 +20,7 @@ const Params = Type.Object({
|
|
|
20
20
|
})
|
|
21
21
|
});
|
|
22
22
|
export function registerPiWorkerDocs(pi, internals = {}) {
|
|
23
|
-
pi
|
|
23
|
+
makeWorkerTool(pi, {
|
|
24
24
|
name: 'pi-worker-docs',
|
|
25
25
|
label: 'Pi Worker Docs',
|
|
26
26
|
description: 'Look up an INSTALLED npm package and return a focused, version-pinned '
|
|
@@ -56,8 +56,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
56
56
|
+ 'Skip when:\n'
|
|
57
57
|
+ '- You need docs for a specific newer version than what is installed — use pi-worker-fetch on the upstream docs site',
|
|
58
58
|
parameters: Params,
|
|
59
|
-
|
|
60
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
59
|
+
async run(params, signal, ctx) {
|
|
61
60
|
const spawn = internals.spawn
|
|
62
61
|
?? (globalThis.Bun !== undefined ?
|
|
63
62
|
globalThis.Bun.spawn
|
|
@@ -74,18 +73,24 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
74
73
|
cacheError = err instanceof Error ? err.message : String(err);
|
|
75
74
|
}
|
|
76
75
|
if (!cache) {
|
|
77
|
-
return
|
|
76
|
+
return {
|
|
77
|
+
text: `Project docs unavailable: cache open failed (${cacheError}).`,
|
|
78
|
+
details: {}
|
|
79
|
+
};
|
|
78
80
|
}
|
|
79
81
|
const retrieveChunks = internals.retrieveChunks ?? defaultRetrieveChunks;
|
|
80
82
|
const projectResult = projectDocsRaw(cache, ctx.cwd, params.query, retrieveChunks);
|
|
81
83
|
if (projectResult.kind === 'error') {
|
|
82
|
-
return
|
|
84
|
+
return { text: `Project docs error: ${projectResult.message}`, details: {} };
|
|
83
85
|
}
|
|
84
86
|
if (projectResult.kind === 'no_chunks') {
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
return {
|
|
88
|
+
text: `Project "${projectResult.projectName}" has no .ts/.tsx files indexed.`,
|
|
89
|
+
details: {
|
|
90
|
+
hitCache: projectResult.hitCache,
|
|
91
|
+
indexedFiles: projectResult.filesIngested
|
|
92
|
+
}
|
|
93
|
+
};
|
|
89
94
|
}
|
|
90
95
|
const { projectName, chunks, hitCache, filesIngested, indexingMs } = projectResult;
|
|
91
96
|
const baseDetails = {
|
|
@@ -98,19 +103,16 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
98
103
|
const prompt = buildProjectPrompt(projectName, params.query, concatenated);
|
|
99
104
|
const invocation = getPiInvocation([...CHILD_ARGS, prompt]);
|
|
100
105
|
const child = await runChild(spawn, invocation, ctx.cwd, signal);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
...baseDetails,
|
|
112
|
-
childExitCode: child.exitCode
|
|
113
|
-
});
|
|
106
|
+
const failure = formatChildFailure(child, 'Project docs lookup aborted.');
|
|
107
|
+
if (failure !== null) {
|
|
108
|
+
return {
|
|
109
|
+
text: failure,
|
|
110
|
+
details: {
|
|
111
|
+
...baseDetails,
|
|
112
|
+
...(child.aborted ? { aborted: true } : {}),
|
|
113
|
+
childExitCode: child.exitCode
|
|
114
|
+
}
|
|
115
|
+
};
|
|
114
116
|
}
|
|
115
117
|
const parsed = parseChildOutput(child.stdout);
|
|
116
118
|
const verified = parsed.excerpt ? isExcerptInContent(parsed.excerpt, concatenated) : undefined;
|
|
@@ -121,11 +123,14 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
121
123
|
entryDts: null,
|
|
122
124
|
readme: null
|
|
123
125
|
}, parsed, verified);
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
return {
|
|
127
|
+
text,
|
|
128
|
+
details: {
|
|
129
|
+
...baseDetails,
|
|
130
|
+
childExitCode: 0,
|
|
131
|
+
excerptVerified: verified
|
|
132
|
+
}
|
|
133
|
+
};
|
|
129
134
|
}
|
|
130
135
|
// ── npm package lookup (existing path) ──────────────────────────
|
|
131
136
|
const rawResult = await docsRaw({
|
|
@@ -157,22 +162,28 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
157
162
|
autoInstalled: rawResult.autoInstalled,
|
|
158
163
|
...npmDetails
|
|
159
164
|
};
|
|
160
|
-
return
|
|
165
|
+
return { text: npmHeader + rawResult.message, details };
|
|
161
166
|
}
|
|
162
167
|
if (rawResult.kind === 'not_installed') {
|
|
163
|
-
return
|
|
164
|
-
|
|
168
|
+
return {
|
|
169
|
+
text: npmHeader
|
|
170
|
+
+ `Package "${rawResult.pkg}" is not installed and auto-install failed.`,
|
|
171
|
+
details: { resolveError: 'not_installed', ...npmDetails }
|
|
172
|
+
};
|
|
165
173
|
}
|
|
166
174
|
if (rawResult.kind === 'no_chunks') {
|
|
167
|
-
return
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
return {
|
|
176
|
+
text: npmHeader
|
|
177
|
+
+ `Package ${rawResult.pkg.name}@${rawResult.pkg.version} has no .d.ts files or README. Use pi-worker to read source directly.`,
|
|
178
|
+
details: {
|
|
179
|
+
version: rawResult.pkg.version,
|
|
180
|
+
hitCache: rawResult.hitCache,
|
|
181
|
+
indexedFiles: rawResult.indexedFiles ?? 0,
|
|
182
|
+
cacheError: rawResult.cacheError,
|
|
183
|
+
autoInstalled: rawResult.autoInstalled,
|
|
184
|
+
...npmDetails
|
|
185
|
+
}
|
|
186
|
+
};
|
|
176
187
|
}
|
|
177
188
|
// kind === 'ok'
|
|
178
189
|
const { pkg, chunks, hitCache, indexingMs, cacheError, autoInstalled } = rawResult;
|
|
@@ -189,28 +200,28 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
189
200
|
const prompt = buildPrompt(pkg, params.query, concatenated);
|
|
190
201
|
const invocation = getPiInvocation([...CHILD_ARGS, prompt]);
|
|
191
202
|
const child = await runChild(spawn, invocation, ctx.cwd, signal);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
...baseDetails,
|
|
203
|
-
childExitCode: child.exitCode
|
|
204
|
-
});
|
|
203
|
+
const failure = formatChildFailure(child, 'Docs lookup aborted.');
|
|
204
|
+
if (failure !== null) {
|
|
205
|
+
return {
|
|
206
|
+
text: npmHeader + failure,
|
|
207
|
+
details: {
|
|
208
|
+
...baseDetails,
|
|
209
|
+
...(child.aborted ? { aborted: true } : {}),
|
|
210
|
+
childExitCode: child.exitCode
|
|
211
|
+
}
|
|
212
|
+
};
|
|
205
213
|
}
|
|
206
214
|
const parsed = parseChildOutput(child.stdout);
|
|
207
215
|
const verified = parsed.excerpt ? isExcerptInContent(parsed.excerpt, concatenated) : undefined;
|
|
208
216
|
const text = npmHeader + formatResultText(pkg, parsed, verified);
|
|
209
|
-
return
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
return {
|
|
218
|
+
text,
|
|
219
|
+
details: {
|
|
220
|
+
...baseDetails,
|
|
221
|
+
childExitCode: 0,
|
|
222
|
+
excerptVerified: verified
|
|
223
|
+
}
|
|
224
|
+
};
|
|
214
225
|
},
|
|
215
226
|
renderCall(args, theme) {
|
|
216
227
|
const query = args.query.replace(/\s+/g, ' ').trim();
|
|
@@ -2,7 +2,7 @@ import { Type } from '@sinclair/typebox';
|
|
|
2
2
|
import { Text } from '@earendil-works/pi-tui';
|
|
3
3
|
import { FetchAndCleanError } from './html-clean.js';
|
|
4
4
|
import { fetchFocused, formatResultText } from './fetch-core.js';
|
|
5
|
-
import {
|
|
5
|
+
import { formatChildFailure, makeWorkerTool } from './shared.js';
|
|
6
6
|
const RENDER_QUERY_MAX = 100;
|
|
7
7
|
const Params = Type.Object({
|
|
8
8
|
url: Type.String({ description: 'URL to fetch. Must be http or https.' }),
|
|
@@ -11,7 +11,7 @@ const Params = Type.Object({
|
|
|
11
11
|
})
|
|
12
12
|
});
|
|
13
13
|
export function registerPiWorkerFetch(pi, internals = {}) {
|
|
14
|
-
pi
|
|
14
|
+
makeWorkerTool(pi, {
|
|
15
15
|
name: 'pi-worker-fetch',
|
|
16
16
|
label: 'Pi Worker Fetch',
|
|
17
17
|
description: 'Fetch a web page or text resource (HTML, markdown, plain text, JSON, '
|
|
@@ -20,13 +20,12 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
20
20
|
+ 'focused answer. Use after `pi-worker-search` (or with a known URL) to '
|
|
21
21
|
+ 'avoid stuffing raw content into the main context.',
|
|
22
22
|
parameters: Params,
|
|
23
|
-
|
|
24
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
23
|
+
async run(params, signal, ctx) {
|
|
25
24
|
try {
|
|
26
25
|
new URL(params.url);
|
|
27
26
|
}
|
|
28
27
|
catch {
|
|
29
|
-
return
|
|
28
|
+
return { text: `Invalid URL: ${params.url}`, details: {} };
|
|
30
29
|
}
|
|
31
30
|
try {
|
|
32
31
|
const result = await fetchFocused({
|
|
@@ -37,28 +36,33 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
37
36
|
fetchAndClean: internals.fetchAndClean,
|
|
38
37
|
spawn: internals.spawn
|
|
39
38
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
39
|
+
const failure = formatChildFailure({
|
|
40
|
+
aborted: result.aborted,
|
|
41
|
+
exitCode: result.childExitCode,
|
|
42
|
+
stderr: result.stderr
|
|
43
|
+
}, 'Fetch aborted.');
|
|
44
|
+
if (failure !== null) {
|
|
45
|
+
return { text: failure, details: { childExitCode: result.childExitCode } };
|
|
48
46
|
}
|
|
49
47
|
const text = formatResultText({ answer: result.answer, excerpt: result.excerpt }, result.excerptVerified) || '(no output)';
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
return {
|
|
49
|
+
text,
|
|
50
|
+
details: {
|
|
51
|
+
childExitCode: 0,
|
|
52
|
+
answer: result.answer,
|
|
53
|
+
excerpt: result.excerpt,
|
|
54
|
+
excerptVerified: result.excerptVerified
|
|
55
|
+
}
|
|
56
|
+
};
|
|
56
57
|
}
|
|
57
58
|
catch (err) {
|
|
58
59
|
if (err instanceof FetchAndCleanError) {
|
|
59
|
-
return
|
|
60
|
+
return { text: err.message, details: {} };
|
|
60
61
|
}
|
|
61
|
-
return
|
|
62
|
+
return {
|
|
63
|
+
text: `Could not fetch ${params.url}: ${err instanceof Error ? err.message : String(err)}`,
|
|
64
|
+
details: {}
|
|
65
|
+
};
|
|
62
66
|
}
|
|
63
67
|
},
|
|
64
68
|
renderCall(args, theme) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
2
|
import { Text } from '@earendil-works/pi-tui';
|
|
3
3
|
import { search } from './search-core.js';
|
|
4
|
-
import {
|
|
4
|
+
import { makeWorkerTool } from './shared.js';
|
|
5
5
|
const Params = Type.Object({
|
|
6
6
|
query: Type.String({ description: 'Search query.' }),
|
|
7
7
|
count: Type.Optional(Type.Integer({
|
|
@@ -11,7 +11,7 @@ const Params = Type.Object({
|
|
|
11
11
|
}))
|
|
12
12
|
});
|
|
13
13
|
export function registerPiWorkerSearch(pi, internals = {}) {
|
|
14
|
-
pi
|
|
14
|
+
makeWorkerTool(pi, {
|
|
15
15
|
name: 'pi-worker-search',
|
|
16
16
|
label: 'Pi Worker Search',
|
|
17
17
|
description: 'Search the live web via Brave Search. CALL THIS BEFORE ANSWERING any '
|
|
@@ -24,8 +24,7 @@ export function registerPiWorkerSearch(pi, internals = {}) {
|
|
|
24
24
|
+ 'call `pi-worker-fetch` on the URL you want to read. '
|
|
25
25
|
+ 'Requires BRAVE_SEARCH_API_KEY env var.',
|
|
26
26
|
parameters: Params,
|
|
27
|
-
|
|
28
|
-
async execute(_toolCallId, params, signal) {
|
|
27
|
+
async run(params, signal) {
|
|
29
28
|
const result = await search({
|
|
30
29
|
query: params.query,
|
|
31
30
|
count: params.count,
|
|
@@ -33,20 +32,15 @@ export function registerPiWorkerSearch(pi, internals = {}) {
|
|
|
33
32
|
getEnv: internals.getEnv,
|
|
34
33
|
braveSearch: internals.braveSearch
|
|
35
34
|
});
|
|
36
|
-
if (result.kind === 'no_key') {
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
if (result.kind === 'error') {
|
|
40
|
-
return textResult(result.message, { resultCount: 0 });
|
|
35
|
+
if (result.kind === 'no_key' || result.kind === 'error') {
|
|
36
|
+
return { text: result.message, details: { resultCount: 0 } };
|
|
41
37
|
}
|
|
42
38
|
const { results } = result;
|
|
43
39
|
if (results.length === 0) {
|
|
44
|
-
return
|
|
45
|
-
resultCount: 0
|
|
46
|
-
});
|
|
40
|
+
return { text: `No results for: ${params.query}`, details: { resultCount: 0 } };
|
|
47
41
|
}
|
|
48
42
|
const lines = results.map((r, i) => `${i + 1}. [${r.title}](${r.url}) — ${r.description}`);
|
|
49
|
-
return
|
|
43
|
+
return { text: lines.join('\n'), details: { resultCount: results.length } };
|
|
50
44
|
},
|
|
51
45
|
renderCall(args, theme) {
|
|
52
46
|
let text = theme.fg('toolTitle', theme.bold('pi-worker-search '));
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
import { Text } from '@earendil-works/pi-tui';
|
|
10
10
|
import { Type } from '@sinclair/typebox';
|
|
11
11
|
import { runWorker } from './pi-worker-core.js';
|
|
12
|
-
import {
|
|
12
|
+
import { formatChildFailure, makeWorkerTool } from './shared.js';
|
|
13
13
|
const RENDER_PROMPT_MAX = 120;
|
|
14
14
|
const WorkerParams = Type.Object({
|
|
15
15
|
prompt: Type.String({ description: 'Task for the worker to perform.' })
|
|
16
16
|
});
|
|
17
17
|
export function registerPiWorker(pi) {
|
|
18
|
-
pi
|
|
18
|
+
makeWorkerTool(pi, {
|
|
19
19
|
name: 'pi-worker',
|
|
20
20
|
label: 'Pi Worker',
|
|
21
21
|
description: 'Dispatch an isolated child Pi to investigate and return its CONCLUSION — '
|
|
@@ -37,19 +37,13 @@ export function registerPiWorker(pi) {
|
|
|
37
37
|
+ '- The task needs writes/edits (worker is read-only)\n'
|
|
38
38
|
+ '- The task needs the web — use `pi-worker-search` / `pi-worker-fetch`',
|
|
39
39
|
parameters: WorkerParams,
|
|
40
|
-
|
|
41
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
40
|
+
async run(params, signal, ctx) {
|
|
42
41
|
const result = await runWorker({ prompt: params.prompt, cwd: ctx.cwd, signal });
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return textResult(`Worker exited ${result.exitCode}.\n${tail}`, {
|
|
49
|
-
exitCode: result.exitCode
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
return textResult(result.text || '(no output)', { exitCode: result.exitCode });
|
|
42
|
+
const details = { exitCode: result.exitCode };
|
|
43
|
+
const failure = formatChildFailure(result, 'Worker aborted.');
|
|
44
|
+
if (failure !== null)
|
|
45
|
+
return { text: failure, details };
|
|
46
|
+
return { text: result.text || '(no output)', details };
|
|
53
47
|
},
|
|
54
48
|
renderCall(args, theme) {
|
|
55
49
|
const prompt = args.prompt.replace(/\s+/g, ' ').trim();
|
package/dist/workers/shared.d.ts
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
import type { Static, TSchema } from '@sinclair/typebox';
|
|
1
2
|
import type { AgentToolResult } from '@earendil-works/pi-agent-core';
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';
|
|
4
|
+
import type { Text } from '@earendil-works/pi-tui';
|
|
2
5
|
/** Build a plain-text AgentToolResult. */
|
|
3
6
|
export declare function textResult<T>(text: string, details: T): AgentToolResult<T>;
|
|
7
|
+
/**
|
|
8
|
+
* The slice of a child-process result a worker needs to decide failure.
|
|
9
|
+
* `exitCode` is normalised here — `fetch-core` exposes it as `childExitCode`,
|
|
10
|
+
* the others as `exitCode`; callers map to this single name.
|
|
11
|
+
*/
|
|
12
|
+
export interface ChildOutcome {
|
|
13
|
+
aborted: boolean;
|
|
14
|
+
exitCode: number;
|
|
15
|
+
stderr: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The one place worker child-failure is turned into a user-facing message.
|
|
19
|
+
* Returns `null` when the child succeeded (caller proceeds to format output),
|
|
20
|
+
* otherwise the standard abort/exit message. Concentrating this here keeps the
|
|
21
|
+
* stderr-tail rule identical across every worker — it had already drifted
|
|
22
|
+
* (`pi-worker` skipped the `.trim()` the others applied).
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatChildFailure(child: ChildOutcome, abortedMessage: string): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* What a worker tool is, minus the registration ritual: a name/label/schema,
|
|
27
|
+
* a `run` that produces the focused text + structured details, and a `renderCall`
|
|
28
|
+
* for the TUI. `makeWorkerTool` owns `registerTool`, the parallel execution mode,
|
|
29
|
+
* and wrapping the result in `textResult`.
|
|
30
|
+
*/
|
|
31
|
+
export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
|
|
32
|
+
name: string;
|
|
33
|
+
label: string;
|
|
34
|
+
description: string;
|
|
35
|
+
parameters: TParams;
|
|
36
|
+
run(params: Static<TParams>, signal: AbortSignal | undefined, ctx: ExtensionContext): Promise<{
|
|
37
|
+
text: string;
|
|
38
|
+
details: TDetails;
|
|
39
|
+
}>;
|
|
40
|
+
renderCall(args: Static<TParams>, theme: Theme): Text;
|
|
41
|
+
}
|
|
42
|
+
/** Register a worker tool from its spec, supplying the shared registration ritual. */
|
|
43
|
+
export declare function makeWorkerTool<TParams extends TSchema, TDetails>(pi: ExtensionAPI, spec: WorkerToolSpec<TParams, TDetails>): void;
|
package/dist/workers/shared.js
CHANGED
|
@@ -2,3 +2,34 @@
|
|
|
2
2
|
export function textResult(text, details) {
|
|
3
3
|
return { content: [{ type: 'text', text }], details };
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* The one place worker child-failure is turned into a user-facing message.
|
|
7
|
+
* Returns `null` when the child succeeded (caller proceeds to format output),
|
|
8
|
+
* otherwise the standard abort/exit message. Concentrating this here keeps the
|
|
9
|
+
* stderr-tail rule identical across every worker — it had already drifted
|
|
10
|
+
* (`pi-worker` skipped the `.trim()` the others applied).
|
|
11
|
+
*/
|
|
12
|
+
export function formatChildFailure(child, abortedMessage) {
|
|
13
|
+
if (child.aborted)
|
|
14
|
+
return abortedMessage;
|
|
15
|
+
if (child.exitCode !== 0) {
|
|
16
|
+
const tail = child.stderr.trim().slice(-500) || '(no stderr)';
|
|
17
|
+
return `Worker exited ${child.exitCode}.\n${tail}`;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/** Register a worker tool from its spec, supplying the shared registration ritual. */
|
|
22
|
+
export function makeWorkerTool(pi, spec) {
|
|
23
|
+
pi.registerTool({
|
|
24
|
+
name: spec.name,
|
|
25
|
+
label: spec.label,
|
|
26
|
+
description: spec.description,
|
|
27
|
+
parameters: spec.parameters,
|
|
28
|
+
executionMode: 'parallel',
|
|
29
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
30
|
+
const { text, details } = await spec.run(params, signal, ctx);
|
|
31
|
+
return textResult(text, details);
|
|
32
|
+
},
|
|
33
|
+
renderCall: (args, theme) => spec.renderCall(args, theme)
|
|
34
|
+
});
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.8",
|
|
4
4
|
"description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|