@mjasnikovs/pi-task 0.13.5 → 0.13.7

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.
Files changed (39) hide show
  1. package/dist/remote/bridge.js +5 -4
  2. package/dist/remote/broadcast.d.ts +0 -1
  3. package/dist/remote/broadcast.js +0 -7
  4. package/dist/remote/events.js +5 -5
  5. package/dist/remote/push.d.ts +12 -3
  6. package/dist/remote/push.js +63 -9
  7. package/dist/remote/server.js +0 -16
  8. package/dist/remote/ui-script.d.ts +3 -0
  9. package/dist/remote/ui-script.js +804 -0
  10. package/dist/remote/ui-styles.d.ts +1 -0
  11. package/dist/remote/ui-styles.js +202 -0
  12. package/dist/remote/ui.js +4 -1000
  13. package/dist/shared/child-process.d.ts +27 -0
  14. package/dist/shared/child-process.js +151 -139
  15. package/dist/task/auto-orchestrator.js +3 -6
  16. package/dist/task/child-runner.js +1 -1
  17. package/dist/task/context-usage.d.ts +16 -0
  18. package/dist/task/context-usage.js +22 -0
  19. package/dist/task/external-context.d.ts +27 -0
  20. package/dist/task/external-context.js +93 -0
  21. package/dist/task/failure-classifier.js +1 -1
  22. package/dist/task/orchestrator.js +7 -13
  23. package/dist/task/parsers.d.ts +1 -15
  24. package/dist/task/parsers.js +17 -84
  25. package/dist/task/phases.d.ts +5 -7
  26. package/dist/task/phases.js +40 -84
  27. package/dist/task/prompts.d.ts +1 -0
  28. package/dist/task/prompts.js +9 -0
  29. package/dist/task/spec-validation.d.ts +23 -0
  30. package/dist/task/spec-validation.js +90 -0
  31. package/dist/task/widget.d.ts +1 -1
  32. package/dist/task/widget.js +1 -1
  33. package/dist/workers/pi-worker-docs.js +69 -58
  34. package/dist/workers/pi-worker-fetch.js +25 -21
  35. package/dist/workers/pi-worker-search.js +7 -13
  36. package/dist/workers/pi-worker.js +8 -14
  37. package/dist/workers/shared.d.ts +40 -0
  38. package/dist/workers/shared.js +31 -0
  39. package/package.json +1 -1
@@ -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 { textResult } from './shared.js';
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.registerTool({
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
- executionMode: 'parallel',
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 textResult(`Project docs unavailable: cache open failed (${cacheError}).`, {});
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 textResult(`Project docs error: ${projectResult.message}`, {});
84
+ return { text: `Project docs error: ${projectResult.message}`, details: {} };
83
85
  }
84
86
  if (projectResult.kind === 'no_chunks') {
85
- return textResult(`Project "${projectResult.projectName}" has no .ts/.tsx files indexed.`, {
86
- hitCache: projectResult.hitCache,
87
- indexedFiles: projectResult.filesIngested
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
- if (child.aborted) {
102
- return textResult('Project docs lookup aborted.', {
103
- ...baseDetails,
104
- aborted: true,
105
- childExitCode: child.exitCode
106
- });
107
- }
108
- if (child.exitCode !== 0) {
109
- const tail = child.stderr.trim().slice(-500) || '(no stderr)';
110
- return textResult(`Worker exited ${child.exitCode}.\n${tail}`, {
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 textResult(text, {
125
- ...baseDetails,
126
- childExitCode: 0,
127
- excerptVerified: verified
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 textResult(npmHeader + rawResult.message, details);
165
+ return { text: npmHeader + rawResult.message, details };
161
166
  }
162
167
  if (rawResult.kind === 'not_installed') {
163
- return textResult(npmHeader
164
- + `Package "${rawResult.pkg}" is not installed and auto-install failed.`, { resolveError: 'not_installed', ...npmDetails });
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 textResult(npmHeader
168
- + `Package ${rawResult.pkg.name}@${rawResult.pkg.version} has no .d.ts files or README. Use pi-worker to read source directly.`, {
169
- version: rawResult.pkg.version,
170
- hitCache: rawResult.hitCache,
171
- indexedFiles: rawResult.indexedFiles ?? 0,
172
- cacheError: rawResult.cacheError,
173
- autoInstalled: rawResult.autoInstalled,
174
- ...npmDetails
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
- if (child.aborted) {
193
- return textResult(npmHeader + 'Docs lookup aborted.', {
194
- ...baseDetails,
195
- aborted: true,
196
- childExitCode: child.exitCode
197
- });
198
- }
199
- if (child.exitCode !== 0) {
200
- const tail = child.stderr.trim().slice(-500) || '(no stderr)';
201
- return textResult(npmHeader + `Worker exited ${child.exitCode}.\n${tail}`, {
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 textResult(text, {
210
- ...baseDetails,
211
- childExitCode: 0,
212
- excerptVerified: verified
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 { textResult } from './shared.js';
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.registerTool({
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
- executionMode: 'parallel',
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 textResult(`Invalid URL: ${params.url}`, {});
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
- if (result.aborted) {
41
- return textResult('Fetch aborted.', { childExitCode: result.childExitCode });
42
- }
43
- if (result.childExitCode !== 0) {
44
- const tail = result.stderr.trim().slice(-500) || '(no stderr)';
45
- return textResult(`Worker exited ${result.childExitCode}.\n${tail}`, {
46
- childExitCode: result.childExitCode
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 textResult(text, {
51
- childExitCode: 0,
52
- answer: result.answer,
53
- excerpt: result.excerpt,
54
- excerptVerified: result.excerptVerified
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 textResult(err.message, {});
60
+ return { text: err.message, details: {} };
60
61
  }
61
- return textResult(`Could not fetch ${params.url}: ${err instanceof Error ? err.message : String(err)}`, {});
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 { textResult } from './shared.js';
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.registerTool({
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
- executionMode: 'parallel',
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 textResult(result.message, { resultCount: 0 });
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 textResult(`No results for: ${params.query}`, {
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 textResult(lines.join('\n'), { resultCount: results.length });
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 { textResult } from './shared.js';
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.registerTool({
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
- executionMode: 'parallel',
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
- if (result.aborted) {
44
- return textResult('Worker aborted.', { exitCode: result.exitCode });
45
- }
46
- if (result.exitCode !== 0) {
47
- const tail = result.stderr.slice(-500) || '(no stderr)';
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();
@@ -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;
@@ -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.5",
3
+ "version": "0.13.7",
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",