@mjasnikovs/pi-task 0.38.15 → 0.38.17
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/config.d.ts +26 -0
- package/dist/config/config.js +68 -17
- package/dist/shared/child-process.js +9 -16
- package/dist/task/accept-debt.d.ts +7 -5
- package/dist/task/accept-debt.js +18 -14
- package/dist/task/artifact-closure.js +18 -63
- package/dist/task/auto-orchestrator.js +211 -218
- package/dist/task/autofix-ledger.d.ts +113 -0
- package/dist/task/autofix-ledger.js +152 -0
- package/dist/task/boot-probe.d.ts +109 -1
- package/dist/task/boot-probe.js +139 -23
- package/dist/task/child-runner.d.ts +50 -6
- package/dist/task/child-runner.js +48 -69
- package/dist/task/command-run.d.ts +49 -6
- package/dist/task/command-run.js +154 -18
- package/dist/task/coverage-loop.d.ts +11 -0
- package/dist/task/coverage-loop.js +16 -0
- package/dist/task/external-context.d.ts +9 -12
- package/dist/task/external-context.js +5 -5
- package/dist/task/failure-classifier.d.ts +9 -1
- package/dist/task/failure-classifier.js +9 -0
- package/dist/task/final-gate-fix.d.ts +22 -26
- package/dist/task/final-gate-fix.js +16 -31
- package/dist/task/final-gate.d.ts +10 -2
- package/dist/task/final-gate.js +55 -89
- package/dist/task/fix-child.d.ts +64 -0
- package/dist/task/fix-child.js +66 -0
- package/dist/task/gate-deps.js +20 -13
- package/dist/task/lint-fix.d.ts +7 -0
- package/dist/task/lint-fix.js +45 -9
- package/dist/task/orchestrator.d.ts +33 -24
- package/dist/task/orchestrator.js +75 -46
- package/dist/task/phases.d.ts +120 -34
- package/dist/task/phases.js +221 -134
- package/dist/task/plan-orchestrator.js +2 -2
- package/dist/task/plan-rounds.d.ts +86 -0
- package/dist/task/plan-rounds.js +105 -0
- package/dist/task/plan-session.d.ts +31 -21
- package/dist/task/plan-session.js +97 -120
- package/dist/task/qa-transcript.d.ts +100 -0
- package/dist/task/qa-transcript.js +99 -0
- package/dist/task/question-source.d.ts +117 -0
- package/dist/task/question-source.js +174 -0
- package/dist/task/repo-health-check.d.ts +21 -21
- package/dist/task/repo-health-check.js +43 -112
- package/dist/task/run-end.d.ts +77 -0
- package/dist/task/run-end.js +37 -0
- package/dist/task/run-final-gate.js +71 -79
- package/dist/task/serve-entry.js +6 -57
- package/dist/task/shipped-source.d.ts +67 -0
- package/dist/task/shipped-source.js +144 -0
- package/dist/task/task-gates.d.ts +9 -1
- package/dist/task/task-gates.js +27 -6
- package/dist/task/terminal-outcome.d.ts +1 -1
- package/dist/task/terminal-outcome.js +12 -0
- package/dist/task/verify-work.d.ts +46 -0
- package/dist/task/verify-work.js +51 -3
- package/dist/workers/brave-search.d.ts +7 -0
- package/dist/workers/brave-search.js +36 -55
- package/dist/workers/ddg-search.d.ts +1 -1
- package/dist/workers/ddg-search.js +27 -47
- package/dist/workers/docs-core.d.ts +71 -1
- package/dist/workers/docs-core.js +131 -71
- package/dist/workers/exa-search.d.ts +2 -2
- package/dist/workers/exa-search.js +53 -68
- package/dist/workers/html-clean.js +67 -88
- package/dist/workers/http-request.d.ts +74 -0
- package/dist/workers/http-request.js +103 -0
- package/dist/workers/npm-version.js +37 -42
- package/dist/workers/pi-worker-core.d.ts +13 -2
- package/dist/workers/pi-worker-core.js +35 -25
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +49 -68
- package/dist/workers/pi-worker-fetch.d.ts +1 -1
- package/dist/workers/pi-worker-fetch.js +20 -21
- package/dist/workers/pi-worker-search.js +6 -4
- package/dist/workers/pi-worker.js +5 -4
- package/dist/workers/search-core.d.ts +1 -1
- package/dist/workers/search-core.js +36 -42
- package/dist/workers/search-types.d.ts +13 -0
- package/dist/workers/search-types.js +27 -0
- package/dist/workers/shared.d.ts +51 -11
- package/dist/workers/shared.js +0 -0
- package/dist/workers/worker-channels.d.ts +60 -0
- package/dist/workers/worker-channels.js +98 -0
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { Text } from '@earendil-works/pi-tui';
|
|
|
3
3
|
import { FetchAndCleanError } from './html-clean.js';
|
|
4
4
|
import { fetchFocused } from './fetch-core.js';
|
|
5
5
|
import { formatResultText } from '../shared/child-output.js';
|
|
6
|
-
import { makeWorkerTool } from './shared.js';
|
|
6
|
+
import { childFailureReason, makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
|
|
7
7
|
import { normalizeQuery } from './research-cache.js';
|
|
8
8
|
import { isAbstention } from './abstention.js';
|
|
9
9
|
const RENDER_QUERY_MAX = 100;
|
|
@@ -35,7 +35,7 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
35
35
|
new URL(params.url);
|
|
36
36
|
}
|
|
37
37
|
catch {
|
|
38
|
-
return
|
|
38
|
+
return workerUnavailable(`Invalid URL: ${params.url}`, {}, 'bad-url');
|
|
39
39
|
}
|
|
40
40
|
try {
|
|
41
41
|
const result = await fetchFocused({
|
|
@@ -50,7 +50,10 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
50
50
|
// (workers/focused-extractor.ts) — this used to re-map the result back into a
|
|
51
51
|
// ChildOutcome just to ask formatChildFailure the same question.
|
|
52
52
|
if (result.failure !== undefined) {
|
|
53
|
-
return
|
|
53
|
+
return workerUnavailable(result.failure, { childExitCode: result.childExitCode }, childFailureReason({
|
|
54
|
+
exitCode: result.childExitCode,
|
|
55
|
+
aborted: result.aborted
|
|
56
|
+
}));
|
|
54
57
|
}
|
|
55
58
|
const body = formatResultText('', // a fetched page answer carries no package header
|
|
56
59
|
{ answer: result.answer, excerpt: result.excerpt }, result.excerptVerified) || '(no output)';
|
|
@@ -58,26 +61,20 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
58
61
|
// in the TEXT, not only in details: details are for the harness, and the
|
|
59
62
|
// worker acts on what it reads.
|
|
60
63
|
const text = result.nextStep ? `${body}\n\n${result.nextStep}` : body;
|
|
61
|
-
return {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
anchoredSection: result.anchoredSection
|
|
70
|
-
}
|
|
71
|
-
};
|
|
64
|
+
return workerAnswer(text, {
|
|
65
|
+
childExitCode: 0,
|
|
66
|
+
answer: result.answer,
|
|
67
|
+
excerpt: result.excerpt,
|
|
68
|
+
excerptVerified: result.excerptVerified,
|
|
69
|
+
coverageMiss: result.coverageMiss,
|
|
70
|
+
anchoredSection: result.anchoredSection
|
|
71
|
+
});
|
|
72
72
|
}
|
|
73
73
|
catch (err) {
|
|
74
74
|
if (err instanceof FetchAndCleanError) {
|
|
75
|
-
return
|
|
75
|
+
return workerUnavailable(err.message, {}, 'fetch-failed');
|
|
76
76
|
}
|
|
77
|
-
return {
|
|
78
|
-
text: `Could not fetch ${params.url}: ${err instanceof Error ? err.message : String(err)}`,
|
|
79
|
-
details: {}
|
|
80
|
-
};
|
|
77
|
+
return workerUnavailable(`Could not fetch ${params.url}: ${err instanceof Error ? err.message : String(err)}`, {}, 'fetch-failed');
|
|
81
78
|
}
|
|
82
79
|
},
|
|
83
80
|
renderCall(args, theme) {
|
|
@@ -109,8 +106,10 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
109
106
|
* `docsCacheable`: pi-worker-fetch.test.ts carried a hand-retyped copy driving four
|
|
110
107
|
* tests, which a change to the shipped rule would leave green.
|
|
111
108
|
*/
|
|
112
|
-
export function fetchCacheable(
|
|
113
|
-
|
|
109
|
+
export function fetchCacheable(_d, text) {
|
|
110
|
+
// Answer QUALITY only — see docsCacheable. `childExitCode === 0` used to lead
|
|
111
|
+
// this rule and was true of an aborted child, so `"Fetch aborted."` cached.
|
|
112
|
+
return !isAbstention(text);
|
|
114
113
|
}
|
|
115
114
|
/** The fetch cache key. URL verbatim (path case can matter), question normalised —
|
|
116
115
|
* same page, different question is a different answer. */
|
|
@@ -2,7 +2,7 @@ import { Type } from '@sinclair/typebox';
|
|
|
2
2
|
import { Text } from '@earendil-works/pi-tui';
|
|
3
3
|
import { getConfig } from '../config/config.js';
|
|
4
4
|
import { search } from './search-core.js';
|
|
5
|
-
import { makeWorkerTool } from './shared.js';
|
|
5
|
+
import { makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
|
|
6
6
|
import { normalizeQuery } from './research-cache.js';
|
|
7
7
|
const Params = Type.Object({
|
|
8
8
|
query: Type.String({ description: 'Search query.' }),
|
|
@@ -38,14 +38,16 @@ export function registerPiWorkerSearch(pi, internals = {}) {
|
|
|
38
38
|
ddgSearch: internals.ddgSearch
|
|
39
39
|
});
|
|
40
40
|
if (result.kind === 'no_key' || result.kind === 'error') {
|
|
41
|
-
return
|
|
41
|
+
return workerUnavailable(result.message, { resultCount: 0 }, result.kind);
|
|
42
42
|
}
|
|
43
43
|
const { results } = result;
|
|
44
|
+
// Zero results IS an answer: the search ran and the web has nothing.
|
|
45
|
+
// Only a search that could not run is unavailable.
|
|
44
46
|
if (results.length === 0) {
|
|
45
|
-
return
|
|
47
|
+
return workerAnswer(`No results for: ${params.query}`, { resultCount: 0 });
|
|
46
48
|
}
|
|
47
49
|
const lines = results.map((r, i) => `${i + 1}. [${r.title}](${r.url}) — ${r.description}`);
|
|
48
|
-
return
|
|
50
|
+
return workerAnswer(lines.join('\n'), { resultCount: results.length });
|
|
49
51
|
},
|
|
50
52
|
renderCall(args, theme) {
|
|
51
53
|
let text = theme.fg('toolTitle', theme.bold('pi-worker-search '));
|
|
@@ -9,7 +9,7 @@
|
|
|
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 { formatChildFailure, makeWorkerTool } from './shared.js';
|
|
12
|
+
import { childFailureReason, formatChildFailure, makeWorkerTool, workerAnswer, workerUnavailable } 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.' })
|
|
@@ -41,9 +41,10 @@ export function registerPiWorker(pi) {
|
|
|
41
41
|
const result = await runWorker({ prompt: params.prompt, cwd: ctx.cwd, signal });
|
|
42
42
|
const details = { exitCode: result.exitCode };
|
|
43
43
|
const failure = formatChildFailure(result, 'Worker aborted.');
|
|
44
|
-
if (failure !== null)
|
|
45
|
-
return
|
|
46
|
-
|
|
44
|
+
if (failure !== null) {
|
|
45
|
+
return workerUnavailable(failure, details, childFailureReason(result));
|
|
46
|
+
}
|
|
47
|
+
return workerAnswer(result.text || '(no output)', details);
|
|
47
48
|
},
|
|
48
49
|
renderCall(args, theme) {
|
|
49
50
|
const prompt = args.prompt.replace(/\s+/g, ' ').trim();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { braveSearch as defaultBraveSearch } from './brave-search.js';
|
|
2
2
|
import { ddgSearch as defaultDdgSearch } from './ddg-search.js';
|
|
3
3
|
import { exaSearch as defaultExaSearch } from './exa-search.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type SearchProvider, type SearchResult } from './search-types.js';
|
|
5
5
|
export interface SearchCoreInput {
|
|
6
6
|
query: string;
|
|
7
7
|
count?: number;
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { getConfig } from '../config/config.js';
|
|
2
|
-
import { braveSearch as defaultBraveSearch
|
|
2
|
+
import { braveSearch as defaultBraveSearch } from './brave-search.js';
|
|
3
3
|
import { ddgSearch as defaultDdgSearch } from './ddg-search.js';
|
|
4
4
|
import { exaSearch as defaultExaSearch } from './exa-search.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
5
|
+
import { searchProviderKey, SEARCH_PROVIDER_LABELS } from './search-types.js';
|
|
6
|
+
const SEARCH_ADAPTERS = {
|
|
7
|
+
exa: {
|
|
8
|
+
run: (input, _key, opts) => (input.exaSearch ?? defaultExaSearch)(input.query, opts)
|
|
9
|
+
},
|
|
10
|
+
ddg: {
|
|
11
|
+
run: (input, _key, opts) => (input.ddgSearch ?? defaultDdgSearch)(input.query, opts)
|
|
12
|
+
},
|
|
13
|
+
brave: {
|
|
14
|
+
run: (input, key, opts) => (input.braveSearch ?? defaultBraveSearch)(input.query, { ...opts, apiKey: key }),
|
|
15
|
+
missingKeyMessage: 'Brave Search not configured. Set BRAVE_SEARCH_API_KEY env var '
|
|
16
|
+
+ '(get a key at https://api.search.brave.com/app/keys) or switch '
|
|
17
|
+
+ 'the search provider in /task-config.',
|
|
18
|
+
errorName: 'BraveSearchError',
|
|
19
|
+
wrapUnknownError: m => `Brave Search request failed: ${m}`
|
|
20
|
+
}
|
|
21
|
+
};
|
|
10
22
|
/**
|
|
11
23
|
* Provider selection is STRICT: the configured (or overridden) engine is the
|
|
12
24
|
* only one tried — a failure reports as an error rather than silently switching
|
|
@@ -14,54 +26,36 @@ function isLikeBraveSearchError(err) {
|
|
|
14
26
|
*/
|
|
15
27
|
export async function search(input) {
|
|
16
28
|
const provider = input.provider ?? getConfig().searchProvider;
|
|
17
|
-
|
|
18
|
-
return braveSearchCore(input);
|
|
19
|
-
const run = provider === 'exa' ?
|
|
20
|
-
() => (input.exaSearch ?? defaultExaSearch)(input.query, {
|
|
21
|
-
count: input.count,
|
|
22
|
-
signal: input.signal
|
|
23
|
-
})
|
|
24
|
-
: () => (input.ddgSearch ?? defaultDdgSearch)(input.query, {
|
|
25
|
-
count: input.count,
|
|
26
|
-
signal: input.signal
|
|
27
|
-
});
|
|
28
|
-
try {
|
|
29
|
-
return { kind: 'ok', results: await run() };
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
return {
|
|
33
|
-
kind: 'error',
|
|
34
|
-
message: err instanceof Error ? err.message : String(err)
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async function braveSearchCore(input) {
|
|
29
|
+
const adapter = SEARCH_ADAPTERS[provider];
|
|
39
30
|
const getEnv = input.getEnv ?? ((k) => process.env[k]);
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
if (!apiKey) {
|
|
31
|
+
const key = searchProviderKey(provider, getEnv);
|
|
32
|
+
if (key === null) {
|
|
43
33
|
return {
|
|
44
34
|
kind: 'no_key',
|
|
45
|
-
message:
|
|
46
|
-
|
|
47
|
-
+ 'the search provider in /task-config.'
|
|
35
|
+
message: adapter.missingKeyMessage
|
|
36
|
+
?? `${SEARCH_PROVIDER_LABELS[provider]} search is not configured.`
|
|
48
37
|
};
|
|
49
38
|
}
|
|
50
39
|
try {
|
|
51
|
-
const results = await
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
signal: input.signal
|
|
40
|
+
const results = await adapter.run(input, key, {
|
|
41
|
+
...(input.count === undefined ? {} : { count: input.count }),
|
|
42
|
+
...(input.signal === undefined ? {} : { signal: input.signal })
|
|
55
43
|
});
|
|
56
44
|
return { kind: 'ok', results };
|
|
57
45
|
}
|
|
58
46
|
catch (err) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
// Which throws already carry a finished user-facing message is the engine's
|
|
48
|
+
// own fact, so it is a row. This used to be an `err.name ===
|
|
49
|
+
// 'BraveSearchError'` STRING match in a brave-only branch — the check
|
|
50
|
+
// existed only because brave was reached down a different path from the
|
|
51
|
+
// other two, and it is the reason a subclass rename would have gone
|
|
52
|
+
// unnoticed.
|
|
53
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
54
|
+
const known = adapter.errorName === undefined
|
|
55
|
+
|| (err instanceof Error && err.name === adapter.errorName);
|
|
62
56
|
return {
|
|
63
57
|
kind: 'error',
|
|
64
|
-
message:
|
|
58
|
+
message: known ? message : (adapter.wrapUnknownError?.(message) ?? message)
|
|
65
59
|
};
|
|
66
60
|
}
|
|
67
61
|
}
|
|
@@ -12,6 +12,19 @@ export interface SearchResult {
|
|
|
12
12
|
*/
|
|
13
13
|
export type SearchProvider = 'exa' | 'ddg' | 'brave';
|
|
14
14
|
export declare const SEARCH_PROVIDERS: readonly SearchProvider[];
|
|
15
|
+
/**
|
|
16
|
+
* The API-key env vars an engine needs, in lookup order. Empty = keyless.
|
|
17
|
+
*
|
|
18
|
+
* Declared beside the engine ids rather than inside `search()`, because two
|
|
19
|
+
* places ask the question: the search itself, and `searchConfigured` in
|
|
20
|
+
* phases.ts, which decides whether the APIS research worker is even given the
|
|
21
|
+
* search tool. That second copy was a hand-written re-statement of brave's env
|
|
22
|
+
* pair with a comment saying it "mirrors search-core's lookup" — the shape a
|
|
23
|
+
* table exists to make impossible.
|
|
24
|
+
*/
|
|
25
|
+
export declare const SEARCH_PROVIDER_KEY_ENV: Record<SearchProvider, readonly string[]>;
|
|
26
|
+
/** The engine's key, or `null` when it needs one and none is set. `''` = keyless. */
|
|
27
|
+
export declare function searchProviderKey(provider: SearchProvider, getEnv: (k: string) => string | undefined): string | null;
|
|
15
28
|
/**
|
|
16
29
|
* Human-readable engine names for the config UI. The short ids stay the stored
|
|
17
30
|
* value (config-file compat); only the display layer uses these.
|
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
export const SEARCH_PROVIDERS = ['exa', 'ddg', 'brave'];
|
|
2
|
+
/**
|
|
3
|
+
* The API-key env vars an engine needs, in lookup order. Empty = keyless.
|
|
4
|
+
*
|
|
5
|
+
* Declared beside the engine ids rather than inside `search()`, because two
|
|
6
|
+
* places ask the question: the search itself, and `searchConfigured` in
|
|
7
|
+
* phases.ts, which decides whether the APIS research worker is even given the
|
|
8
|
+
* search tool. That second copy was a hand-written re-statement of brave's env
|
|
9
|
+
* pair with a comment saying it "mirrors search-core's lookup" — the shape a
|
|
10
|
+
* table exists to make impossible.
|
|
11
|
+
*/
|
|
12
|
+
export const SEARCH_PROVIDER_KEY_ENV = {
|
|
13
|
+
exa: [],
|
|
14
|
+
ddg: [],
|
|
15
|
+
brave: ['BRAVE_SEARCH_API_KEY', 'BRAVE_API_KEY']
|
|
16
|
+
};
|
|
17
|
+
/** The engine's key, or `null` when it needs one and none is set. `''` = keyless. */
|
|
18
|
+
export function searchProviderKey(provider, getEnv) {
|
|
19
|
+
const vars = SEARCH_PROVIDER_KEY_ENV[provider];
|
|
20
|
+
if (vars.length === 0)
|
|
21
|
+
return '';
|
|
22
|
+
for (const v of vars) {
|
|
23
|
+
const value = getEnv(v);
|
|
24
|
+
if (value)
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
2
29
|
/**
|
|
3
30
|
* Human-readable engine names for the config UI. The short ids stay the stored
|
|
4
31
|
* value (config-file compat); only the display layer uses these.
|
package/dist/workers/shared.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Static, TSchema } from '@sinclair/typebox';
|
|
|
2
2
|
import type { AgentToolResult } from '@earendil-works/pi-agent-core';
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';
|
|
4
4
|
import type { Text } from '@earendil-works/pi-tui';
|
|
5
|
+
import { type WorkerFailureInput } from './worker-failure.js';
|
|
5
6
|
/** Build a plain-text AgentToolResult. */
|
|
6
7
|
export declare function textResult<T>(text: string, details: T): AgentToolResult<T>;
|
|
7
8
|
/**
|
|
@@ -22,21 +23,60 @@ export interface ChildOutcome {
|
|
|
22
23
|
* (`pi-worker` skipped the `.trim()` the others applied).
|
|
23
24
|
*/
|
|
24
25
|
export declare function formatChildFailure(child: ChildOutcome, abortedMessage: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* What a worker tool PRODUCED — an answer, or a statement that it has none.
|
|
28
|
+
*
|
|
29
|
+
* This used to be a bare `{text, details}` bag, and "did it succeed" was
|
|
30
|
+
* re-derived downstream from `details.childExitCode === 0`. That derivation was
|
|
31
|
+
* wrong in the one case it most needed to be right: a signal-killed child reports
|
|
32
|
+
* `code ?? 0` = 0 (shared/child-process.ts), so an aborted lookup arrived with
|
|
33
|
+
* exit code 0, `docsCacheable`/`fetchCacheable` said yes, and `"Docs lookup
|
|
34
|
+
* aborted."` was memoised for the whole run and re-served to every later sibling —
|
|
35
|
+
* exactly the failure `abstention.ts` exists to stop, with escalation unable to
|
|
36
|
+
* re-fire. `docsFailureResult`'s own contract said it recorded the code "NOT as
|
|
37
|
+
* 0"; the value it copied was 0.
|
|
38
|
+
*
|
|
39
|
+
* Stating the outcome makes that unrepresentable: `makeWorkerTool` stores only an
|
|
40
|
+
* `answer`, so no cache rule has to know anything about process health, and the
|
|
41
|
+
* `cacheable` predicates shrink to what they are actually about — answer quality.
|
|
42
|
+
*/
|
|
43
|
+
export type WorkerOutcome<TDetails> = {
|
|
44
|
+
kind: 'answer';
|
|
45
|
+
text: string;
|
|
46
|
+
details: TDetails;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'unavailable';
|
|
49
|
+
text: string;
|
|
50
|
+
details: TDetails;
|
|
51
|
+
/**
|
|
52
|
+
* Why there is no answer, for the debug trail. A `WorkerFailureKind`
|
|
53
|
+
* where a child died (classifyWorkerFailure owns that precedence), or a
|
|
54
|
+
* short tag for the lookups that never got as far as a child.
|
|
55
|
+
*/
|
|
56
|
+
reason: string;
|
|
57
|
+
};
|
|
58
|
+
/** This call produced an answer. Cacheable, subject to the tool's own rule. */
|
|
59
|
+
export declare function workerAnswer<T>(text: string, details: T): WorkerOutcome<T>;
|
|
60
|
+
/**
|
|
61
|
+
* The `reason` tag for a child that died, read off the ONE ladder that owns the
|
|
62
|
+
* precedence (`classifyWorkerFailure`). `'no-answer'` when nothing killed it and
|
|
63
|
+
* the caller still has no answer to give.
|
|
64
|
+
*/
|
|
65
|
+
export declare function childFailureReason(child: WorkerFailureInput): string;
|
|
66
|
+
/** This call has no answer. NEVER cached, whatever the tool's rule says. */
|
|
67
|
+
export declare function workerUnavailable<T>(text: string, details: T, reason: string): WorkerOutcome<T>;
|
|
25
68
|
/**
|
|
26
69
|
* What a worker tool is, minus the registration ritual: a name/label/schema,
|
|
27
|
-
* a `run` that produces the
|
|
28
|
-
*
|
|
29
|
-
*
|
|
70
|
+
* a `run` that produces the outcome, and a `renderCall` for the TUI.
|
|
71
|
+
* `makeWorkerTool` owns `registerTool`, the parallel execution mode, and wrapping
|
|
72
|
+
* the result in `textResult`.
|
|
30
73
|
*/
|
|
31
74
|
export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
|
|
32
75
|
name: string;
|
|
33
76
|
label: string;
|
|
34
77
|
description: string;
|
|
35
78
|
parameters: TParams;
|
|
36
|
-
run(params: Static<TParams>, signal: AbortSignal | undefined, ctx: ExtensionContext): Promise<
|
|
37
|
-
text: string;
|
|
38
|
-
details: TDetails;
|
|
39
|
-
}>;
|
|
79
|
+
run(params: Static<TParams>, signal: AbortSignal | undefined, ctx: ExtensionContext): Promise<WorkerOutcome<TDetails>>;
|
|
40
80
|
renderCall(args: Static<TParams>, theme: Theme): Text;
|
|
41
81
|
/**
|
|
42
82
|
* Per-run research-cache policy (F10). Return a stable cache key for this call —
|
|
@@ -57,10 +97,10 @@ export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
|
|
|
57
97
|
*/
|
|
58
98
|
cachePkg?(params: Static<TParams>): string | undefined;
|
|
59
99
|
/**
|
|
60
|
-
* Whether
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
100
|
+
* Whether an ANSWER is safe to cache — a question about the answer's QUALITY
|
|
101
|
+
* (type-only, an abstention, an unverified excerpt), never about process
|
|
102
|
+
* health. An `unavailable` outcome never reaches this: `makeWorkerTool` has
|
|
103
|
+
* already refused it. Defaults to always-cacheable when omitted.
|
|
64
104
|
*/
|
|
65
105
|
cacheable?(details: TDetails, text: string): boolean;
|
|
66
106
|
}
|
package/dist/workers/shared.js
CHANGED
|
Binary file
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worker-channels — what a worker TOOL is, as data.
|
|
3
|
+
*
|
|
4
|
+
* `makeWorkerTool` already gives every worker tool one registration adapter, but
|
|
5
|
+
* `spec.name` never left the registration closure. So the same four name strings
|
|
6
|
+
* were re-typed as literals in three directories and had to agree by hand:
|
|
7
|
+
*
|
|
8
|
+
* • `phases.ts` paired `'…,pi-worker-docs'` with `DOCS_EXTENSION_PATH`, and
|
|
9
|
+
* `',pi-worker-search,pi-worker-fetch'` with `SEARCH_EXTENSION_PATH` — a tools
|
|
10
|
+
* string and an `-e` path list that mean the same thing, written twice and
|
|
11
|
+
* kept in step by eye.
|
|
12
|
+
* • `GROUNDING_RETRIEVAL_TOOLS` was a second copy of the names.
|
|
13
|
+
* • `summarizeToolArgs` was a third, and also re-stated each tool's parameter
|
|
14
|
+
* shape (`module`/`query`, `query`, `url`).
|
|
15
|
+
* • Worst for locality: `runWorker` — the GENERIC child runner — hardcoded one
|
|
16
|
+
* tool's identity AND its parameter, `call.name === 'pi-worker-docs' &&
|
|
17
|
+
* args.module === '.'`, to decide a fan-out deadline extension.
|
|
18
|
+
*
|
|
19
|
+
* A rename or a new tool was five edits in three directories with no compile
|
|
20
|
+
* error linking them. It is one row here now.
|
|
21
|
+
*
|
|
22
|
+
* What is NOT in a row: the tools string's non-worker members (`read`, `grep`,
|
|
23
|
+
* `find`, `ls`) are pi's own built-ins, not channels — they appear in
|
|
24
|
+
* {@link GROUNDING_RETRIEVAL_TOOLS} because grounding is about RETRIEVAL, not
|
|
25
|
+
* about which extension supplies it.
|
|
26
|
+
*/
|
|
27
|
+
/** One worker tool, and everything the rest of the codebase knows about it. */
|
|
28
|
+
export interface WorkerChannel {
|
|
29
|
+
/** The tool name the model calls. The same string `WorkerToolSpec.name` declares. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** The `-e` entry that registers it into a child pi. */
|
|
32
|
+
entryPath: string;
|
|
33
|
+
/** Does a call to this tool retrieve content a claim could be grounded in? */
|
|
34
|
+
grounding: boolean;
|
|
35
|
+
/** One line naming WHAT this call was about, for the debug log. */
|
|
36
|
+
summarize: (args: Record<string, unknown>) => string;
|
|
37
|
+
/**
|
|
38
|
+
* A call this tool's own consumers branch on. Only the docs channel has one:
|
|
39
|
+
* a `.` module is a PROJECT-SOURCE lookup, which reads the working tree
|
|
40
|
+
* rather than a package, and is what the fan-out deadline extends for.
|
|
41
|
+
*
|
|
42
|
+
* A property, not a method: it is read off the row and called on its own, so
|
|
43
|
+
* a method signature would invite an unbound `this`.
|
|
44
|
+
*/
|
|
45
|
+
isProjectSourceLookup?: (args: Record<string, unknown>) => boolean;
|
|
46
|
+
}
|
|
47
|
+
export declare const WORKER_CHANNELS: readonly WorkerChannel[];
|
|
48
|
+
/** The row for a tool call, or `undefined` when the tool is not a worker channel. */
|
|
49
|
+
export declare function workerChannel(toolName: string): WorkerChannel | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* The tools string and the `-e` paths for a set of channels, together — they are
|
|
52
|
+
* one fact and used to be two literals. Entry paths are de-duplicated: search and
|
|
53
|
+
* fetch ship in one extension file.
|
|
54
|
+
*/
|
|
55
|
+
export declare function channelSet(names: readonly string[]): {
|
|
56
|
+
tools: string;
|
|
57
|
+
extensions: string[];
|
|
58
|
+
};
|
|
59
|
+
/** True when a tool call retrieves content an APIS entry could be grounded in. */
|
|
60
|
+
export declare function isGroundingRetrieval(toolName: string): boolean;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worker-channels — what a worker TOOL is, as data.
|
|
3
|
+
*
|
|
4
|
+
* `makeWorkerTool` already gives every worker tool one registration adapter, but
|
|
5
|
+
* `spec.name` never left the registration closure. So the same four name strings
|
|
6
|
+
* were re-typed as literals in three directories and had to agree by hand:
|
|
7
|
+
*
|
|
8
|
+
* • `phases.ts` paired `'…,pi-worker-docs'` with `DOCS_EXTENSION_PATH`, and
|
|
9
|
+
* `',pi-worker-search,pi-worker-fetch'` with `SEARCH_EXTENSION_PATH` — a tools
|
|
10
|
+
* string and an `-e` path list that mean the same thing, written twice and
|
|
11
|
+
* kept in step by eye.
|
|
12
|
+
* • `GROUNDING_RETRIEVAL_TOOLS` was a second copy of the names.
|
|
13
|
+
* • `summarizeToolArgs` was a third, and also re-stated each tool's parameter
|
|
14
|
+
* shape (`module`/`query`, `query`, `url`).
|
|
15
|
+
* • Worst for locality: `runWorker` — the GENERIC child runner — hardcoded one
|
|
16
|
+
* tool's identity AND its parameter, `call.name === 'pi-worker-docs' &&
|
|
17
|
+
* args.module === '.'`, to decide a fan-out deadline extension.
|
|
18
|
+
*
|
|
19
|
+
* A rename or a new tool was five edits in three directories with no compile
|
|
20
|
+
* error linking them. It is one row here now.
|
|
21
|
+
*
|
|
22
|
+
* What is NOT in a row: the tools string's non-worker members (`read`, `grep`,
|
|
23
|
+
* `find`, `ls`) are pi's own built-ins, not channels — they appear in
|
|
24
|
+
* {@link GROUNDING_RETRIEVAL_TOOLS} because grounding is about RETRIEVAL, not
|
|
25
|
+
* about which extension supplies it.
|
|
26
|
+
*/
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
const DOCS_ENTRY = fileURLToPath(new URL('./docs-extension.js', import.meta.url));
|
|
29
|
+
const SEARCH_ENTRY = fileURLToPath(new URL('./search-extension.js', import.meta.url));
|
|
30
|
+
/** Clip an argument to one readable line. Shared by every row's `summarize`. */
|
|
31
|
+
function clip(s) {
|
|
32
|
+
const one = s.replace(/\s+/g, ' ').trim();
|
|
33
|
+
return one.length > 60 ? one.slice(0, 59) + '…' : one;
|
|
34
|
+
}
|
|
35
|
+
export const WORKER_CHANNELS = [
|
|
36
|
+
{
|
|
37
|
+
name: 'pi-worker-docs',
|
|
38
|
+
entryPath: DOCS_ENTRY,
|
|
39
|
+
grounding: true,
|
|
40
|
+
summarize: a => typeof a.module === 'string' && typeof a.query === 'string' ?
|
|
41
|
+
`${a.module} "${clip(a.query)}"`
|
|
42
|
+
: '',
|
|
43
|
+
isProjectSourceLookup: a => a.module === '.'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'pi-worker-search',
|
|
47
|
+
entryPath: SEARCH_ENTRY,
|
|
48
|
+
grounding: true,
|
|
49
|
+
// Without this the debug log shows a bare tool name and a run audit
|
|
50
|
+
// cannot tell WHAT was searched.
|
|
51
|
+
summarize: a => (typeof a.query === 'string' ? `"${clip(a.query)}"` : '')
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'pi-worker-fetch',
|
|
55
|
+
entryPath: SEARCH_ENTRY,
|
|
56
|
+
grounding: true,
|
|
57
|
+
summarize: a => (typeof a.url === 'string' ? clip(a.url) : '')
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
const BY_NAME = new Map(WORKER_CHANNELS.map(c => [c.name, c]));
|
|
61
|
+
/** The row for a tool call, or `undefined` when the tool is not a worker channel. */
|
|
62
|
+
export function workerChannel(toolName) {
|
|
63
|
+
return BY_NAME.get(toolName);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The tools string and the `-e` paths for a set of channels, together — they are
|
|
67
|
+
* one fact and used to be two literals. Entry paths are de-duplicated: search and
|
|
68
|
+
* fetch ship in one extension file.
|
|
69
|
+
*/
|
|
70
|
+
export function channelSet(names) {
|
|
71
|
+
// REFUSED, not dropped. Silently skipping an unrecognised name is the exact
|
|
72
|
+
// failure this table was built to eliminate: a typo or a half-finished rename
|
|
73
|
+
// yields a shorter tools string AND a shorter `-e` list, with no compile error
|
|
74
|
+
// and no runtime error, and the child just quietly loses a tool. At every call
|
|
75
|
+
// site that is indistinguishable from asking for fewer channels on purpose.
|
|
76
|
+
const unknown = names.filter(n => !BY_NAME.has(n));
|
|
77
|
+
if (unknown.length > 0) {
|
|
78
|
+
throw new Error(`channelSet: unknown worker channel(s): ${unknown.join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
const rows = names.map(n => BY_NAME.get(n)).filter((c) => c !== undefined);
|
|
81
|
+
return {
|
|
82
|
+
tools: rows.map(c => c.name).join(','),
|
|
83
|
+
extensions: [...new Set(rows.map(c => c.entryPath))]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Tool calls that retrieve content an APIS entry could be grounded in — the
|
|
88
|
+
* worker channels that say so, plus pi's own read/grep.
|
|
89
|
+
*/
|
|
90
|
+
const GROUNDING_RETRIEVAL_TOOLS = new Set([
|
|
91
|
+
'read',
|
|
92
|
+
'grep',
|
|
93
|
+
...WORKER_CHANNELS.filter(c => c.grounding).map(c => c.name)
|
|
94
|
+
]);
|
|
95
|
+
/** True when a tool call retrieves content an APIS entry could be grounded in. */
|
|
96
|
+
export function isGroundingRetrieval(toolName) {
|
|
97
|
+
return GROUNDING_RETRIEVAL_TOOLS.has(toolName);
|
|
98
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.17",
|
|
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",
|