@mjasnikovs/pi-task 0.18.4 → 0.18.6

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 (40) hide show
  1. package/dist/config/config.d.ts +18 -0
  2. package/dist/config/config.js +10 -1
  3. package/dist/config/register.js +32 -5
  4. package/dist/task/accept-debt.d.ts +52 -0
  5. package/dist/task/accept-debt.js +0 -0
  6. package/dist/task/auto-orchestrator.d.ts +2 -0
  7. package/dist/task/auto-orchestrator.js +20 -0
  8. package/dist/task/final-gate.d.ts +8 -0
  9. package/dist/task/final-gate.js +27 -7
  10. package/dist/task/frozen-path-guard.d.ts +39 -0
  11. package/dist/task/frozen-path-guard.js +116 -0
  12. package/dist/task/gate-deps.js +25 -0
  13. package/dist/task/phases.d.ts +6 -2
  14. package/dist/task/phases.js +7 -2
  15. package/dist/task/repo-health-check.d.ts +11 -0
  16. package/dist/task/repo-health-check.js +26 -3
  17. package/dist/task/service-blocks.d.ts +2 -2
  18. package/dist/task/service-blocks.js +3 -1
  19. package/dist/task/task-gates.d.ts +24 -0
  20. package/dist/task/task-gates.js +78 -8
  21. package/dist/workers/brave-search.d.ts +2 -5
  22. package/dist/workers/brave-warning.d.ts +4 -6
  23. package/dist/workers/brave-warning.js +11 -10
  24. package/dist/workers/ddg-search.d.ts +24 -0
  25. package/dist/workers/ddg-search.js +130 -0
  26. package/dist/workers/exa-search.d.ts +24 -0
  27. package/dist/workers/exa-search.js +164 -0
  28. package/dist/workers/pi-worker-docs.js +13 -1
  29. package/dist/workers/pi-worker-fetch.js +10 -1
  30. package/dist/workers/pi-worker-search.d.ts +7 -1
  31. package/dist/workers/pi-worker-search.js +18 -5
  32. package/dist/workers/research-cache.d.ts +39 -0
  33. package/dist/workers/research-cache.js +140 -0
  34. package/dist/workers/search-core.d.ts +14 -2
  35. package/dist/workers/search-core.js +34 -2
  36. package/dist/workers/search-types.d.ts +15 -0
  37. package/dist/workers/search-types.js +4 -0
  38. package/dist/workers/shared.d.ts +17 -0
  39. package/dist/workers/shared.js +0 -0
  40. package/package.json +1 -1
@@ -8,6 +8,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
10
  import { formatChildFailure, makeWorkerTool } from './shared.js';
11
+ import { normalizeQuery } from './research-cache.js';
11
12
  import { projectDocsRaw, buildProjectPrompt } from './docs-project.js';
12
13
  const CHILD_ARGS = [...CHILD_BASE_ARGS, '--no-tools'];
13
14
  const RENDER_QUERY_MAX = 100;
@@ -239,6 +240,17 @@ export function registerPiWorkerDocs(pi, internals = {}) {
239
240
  text += theme.fg('accent', label);
240
241
  text += `\n${theme.fg('dim', ` query: ${truncated}`)}`;
241
242
  return new Text(text, 0, 0);
242
- }
243
+ },
244
+ // Cache npm-package answers per run (a package's installed types/README + latest
245
+ // version do not change within a run). A project-source `.` lookup is NOT cached:
246
+ // the working tree mutates as tasks implement, so its answer can go stale mid-run
247
+ // (the docs SQLite index already keys those on file mtime).
248
+ cacheKey: params => params.module === '.' ?
249
+ null
250
+ : `${normalizeQuery(params.module)}::${normalizeQuery(params.query)}`,
251
+ // Only a completed lookup (child exited 0) is a real answer; not-installed,
252
+ // no-chunks, resolve/cache errors, and aborts omit childExitCode:0 and fall
253
+ // through to a live retry next time.
254
+ cacheable: d => d.childExitCode === 0
243
255
  });
244
256
  }
@@ -3,6 +3,7 @@ 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
5
  import { formatChildFailure, makeWorkerTool } from './shared.js';
6
+ import { normalizeQuery } from './research-cache.js';
6
7
  const RENDER_QUERY_MAX = 100;
7
8
  const Params = Type.Object({
8
9
  url: Type.String({ description: 'URL to fetch. Must be http or https.' }),
@@ -79,6 +80,14 @@ export function registerPiWorkerFetch(pi, internals = {}) {
79
80
  text += theme.fg('accent', args.url);
80
81
  text += `\n${theme.fg('dim', ` query: ${truncatedQuery}`)}`;
81
82
  return new Text(text, 0, 0);
82
- }
83
+ },
84
+ // Cache fetch answers per run (the same page re-fetched across sibling tasks
85
+ // otherwise). The URL is kept verbatim (path case can matter); the query is
86
+ // normalised. Both parts key the entry — same page, different question is a
87
+ // different answer.
88
+ cacheKey: params => `${params.url.trim()}::${normalizeQuery(params.query)}`,
89
+ // Only a completed fetch (child exited 0) is a real answer; invalid-URL,
90
+ // fetch failures, and aborts omit childExitCode:0 and fall through.
91
+ cacheable: d => d.childExitCode === 0
83
92
  });
84
93
  }
@@ -1,7 +1,13 @@
1
1
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
- import { braveSearch as defaultBraveSearch } from './brave-search.js';
2
+ import type { braveSearch as defaultBraveSearch } from './brave-search.js';
3
+ import type { ddgSearch as defaultDdgSearch } from './ddg-search.js';
4
+ import type { exaSearch as defaultExaSearch } from './exa-search.js';
5
+ import type { SearchProvider } from './search-types.js';
3
6
  export interface PiWorkerSearchInternals {
4
7
  braveSearch?: typeof defaultBraveSearch;
8
+ exaSearch?: typeof defaultExaSearch;
9
+ ddgSearch?: typeof defaultDdgSearch;
10
+ provider?: SearchProvider;
5
11
  getEnv?: (key: string) => string | undefined;
6
12
  }
7
13
  export declare function registerPiWorkerSearch(pi: ExtensionAPI, internals?: PiWorkerSearchInternals): void;
@@ -1,7 +1,9 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { Text } from '@earendil-works/pi-tui';
3
+ import { getConfig } from '../config/config.js';
3
4
  import { search } from './search-core.js';
4
5
  import { makeWorkerTool } from './shared.js';
6
+ import { normalizeQuery } from './research-cache.js';
5
7
  const Params = Type.Object({
6
8
  query: Type.String({ description: 'Search query.' }),
7
9
  count: Type.Optional(Type.Integer({
@@ -11,26 +13,29 @@ const Params = Type.Object({
11
13
  }))
12
14
  });
13
15
  export function registerPiWorkerSearch(pi, internals = {}) {
16
+ const provider = () => internals.provider ?? getConfig().searchProvider;
14
17
  makeWorkerTool(pi, {
15
18
  name: 'pi-worker-search',
16
19
  label: 'Pi Worker Search',
17
- description: 'Search the live web via Brave Search. CALL THIS BEFORE ANSWERING any '
20
+ description: 'Search the live web. CALL THIS BEFORE ANSWERING any '
18
21
  + 'question about current or version-specific external facts: '
19
22
  + 'library/framework versions and their APIs, latest releases, recently '
20
23
  + 'shipped features, current events, prices, or who currently holds a '
21
24
  + 'role. Your built-in knowledge is out of date — do NOT answer such '
22
25
  + 'questions from memory and do NOT shell out with bash to guess. Returns '
23
26
  + 'a compact markdown list of up to 10 results (title, URL, snippet); then '
24
- + 'call `pi-worker-fetch` on the URL you want to read. '
25
- + 'Requires BRAVE_SEARCH_API_KEY env var.',
27
+ + 'call `pi-worker-fetch` on the URL you want to read.',
26
28
  parameters: Params,
27
29
  async run(params, signal) {
28
30
  const result = await search({
29
31
  query: params.query,
30
32
  count: params.count,
31
33
  signal,
34
+ provider: internals.provider,
32
35
  getEnv: internals.getEnv,
33
- braveSearch: internals.braveSearch
36
+ braveSearch: internals.braveSearch,
37
+ exaSearch: internals.exaSearch,
38
+ ddgSearch: internals.ddgSearch
34
39
  });
35
40
  if (result.kind === 'no_key' || result.kind === 'error') {
36
41
  return { text: result.message, details: { resultCount: 0 } };
@@ -49,6 +54,14 @@ export function registerPiWorkerSearch(pi, internals = {}) {
49
54
  text += theme.fg('dim', ` (count=${args.count})`);
50
55
  }
51
56
  return new Text(text, 0, 0);
52
- }
57
+ },
58
+ // Cache search results per run (the same query re-run across sibling tasks hits
59
+ // the live web anew otherwise). Count is part of the key — a larger request is a
60
+ // different result set — and so is the provider: two engines' result sets for
61
+ // one query are different answers and must not serve for each other.
62
+ cacheKey: params => `${provider()}::${normalizeQuery(params.query)}::${params.count ?? ''}`,
63
+ // Only a non-empty result set is worth caching; no-key, error, and empty results
64
+ // (resultCount 0) fall through so a later attempt can succeed.
65
+ cacheable: d => d.resultCount > 0
53
66
  });
54
67
  }
@@ -0,0 +1,39 @@
1
+ /** The env var the orchestrator stamps with the per-run id children inherit. */
2
+ export declare const RESEARCH_RUN_ID_ENV = "PI_TASK_RUN_ID";
3
+ export declare function researchCacheFile(cwd: string): string;
4
+ /**
5
+ * The current run's id, or undefined when caching is off (the orchestrator did not
6
+ * stamp one for this run). A worker treats undefined as "do not cache".
7
+ */
8
+ export declare function researchRunId(): string | undefined;
9
+ /** A fresh, per-invocation run token — stable within one run, unique across runs. */
10
+ export declare function newRunToken(): string;
11
+ /**
12
+ * Orchestrator hook: called once at the start of every /task-auto invocation. When
13
+ * caching is enabled it stamps a FRESH token (so a long-lived host never reuses a
14
+ * prior run's token, and planAuto + the task loop of THIS run share one id); when
15
+ * disabled it clears any token a prior run left, so the workers cache nothing.
16
+ */
17
+ export declare function configureResearchRun(enabled: boolean): string | undefined;
18
+ /**
19
+ * Normalise a query/module string for the cache KEY: collapse whitespace and
20
+ * lowercase, so trivially-varied phrasings of the same question share a digest. The
21
+ * stored value is the real answer, so a case/spacing collision only means two ways
22
+ * of asking the same thing resolve to the same (correct) result.
23
+ */
24
+ export declare function normalizeQuery(s: string): string;
25
+ /**
26
+ * Look up a cached result for `key` in the current run. Returns undefined on a miss,
27
+ * a stale-run file (different id ⇒ another run's digest, ignored), or any failure.
28
+ */
29
+ export declare function lookupResearch(cwd: string, runId: string, key: string): Promise<{
30
+ text: string;
31
+ details: unknown;
32
+ } | undefined>;
33
+ /**
34
+ * Store a successful result under `key` for the current run. A file written for a
35
+ * different run id is discarded and started fresh (first write of a new run drops the
36
+ * prior run's contents — self-healing per-run isolation without an explicit clear).
37
+ * Best-effort: any failure is swallowed, leaving the caller's live result untouched.
38
+ */
39
+ export declare function storeResearch(cwd: string, runId: string, key: string, text: string, details: unknown): Promise<void>;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * research-cache — a per-run cache of docs/search/fetch worker RESULTS, shared
3
+ * across the sibling task pipelines of one /task-auto run.
4
+ *
5
+ * The failure this serves (mx5 run 8, F10): the research phase alone burned 75 of
6
+ * 363 minutes because ~20 sibling task pipelines each re-fetched the SAME external
7
+ * docs and re-ran the SAME searches (the tailwind CLI docs fetched anew for task
8
+ * after task). Each of those worker results is a deterministic function of (tool,
9
+ * package/url, query) that does not change within a run — so the first pipeline to
10
+ * ask a question can answer every later one from a shared digest instead of a fresh
11
+ * network round-trip plus child-summariser spawn.
12
+ *
13
+ * SCOPE — stable external lookups only: npm-package docs, web search, web fetch. A
14
+ * PROJECT-SOURCE (`.`) docs lookup is deliberately NOT cached: the working tree
15
+ * mutates as tasks implement, so a `.` answer from an early task can be stale by a
16
+ * later one (the docs SQLite index already keys those on file mtime). Only a result
17
+ * the tool marks successful is cached — an error, an empty result, or an abort is
18
+ * never memoised, so a transient failure cannot poison the run.
19
+ *
20
+ * PER-RUN ISOLATION: the orchestrator stamps a FRESH run id into the environment
21
+ * (PI_TASK_RUN_ID) at the start of every /task-auto invocation; the research-worker
22
+ * children inherit it. The cache file records the run id it was written for, and any
23
+ * read or write for a different id discards the stale contents. So a long-lived host
24
+ * process running many /task-auto runs never serves one run's digest to another, and
25
+ * a run started with the feature flag OFF (no id in the environment) does not cache
26
+ * at all — the cache is inert unless the orchestrator turned it on for this run.
27
+ *
28
+ * Stored under `.pi-tasks/` (sibling of env-notes.md / contracts.md), which the
29
+ * git-state guard and discardEdits both exclude. Best-effort throughout: any I/O or
30
+ * parse failure falls back to a live fetch — the cache only ever saves time, it can
31
+ * never change an answer or block a worker.
32
+ */
33
+ import * as fsp from 'node:fs/promises';
34
+ import * as path from 'node:path';
35
+ import { tasksDir } from '../task/task-io.js';
36
+ const RESEARCH_CACHE_FILE = 'research-cache.json';
37
+ /** The env var the orchestrator stamps with the per-run id children inherit. */
38
+ export const RESEARCH_RUN_ID_ENV = 'PI_TASK_RUN_ID';
39
+ /**
40
+ * Cap stored entries so a chatty run cannot grow the file unboundedly; the newest
41
+ * (by write time) are kept. Sized well above a 20-task run's distinct external
42
+ * lookups (dozens), so a real run never evicts a still-useful digest.
43
+ */
44
+ const MAX_ENTRIES = 250;
45
+ export function researchCacheFile(cwd) {
46
+ return path.join(tasksDir(cwd), RESEARCH_CACHE_FILE);
47
+ }
48
+ /**
49
+ * The current run's id, or undefined when caching is off (the orchestrator did not
50
+ * stamp one for this run). A worker treats undefined as "do not cache".
51
+ */
52
+ export function researchRunId() {
53
+ const v = process.env[RESEARCH_RUN_ID_ENV]?.trim();
54
+ return v && v.length > 0 ? v : undefined;
55
+ }
56
+ /** A fresh, per-invocation run token — stable within one run, unique across runs. */
57
+ export function newRunToken() {
58
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
59
+ }
60
+ /**
61
+ * Orchestrator hook: called once at the start of every /task-auto invocation. When
62
+ * caching is enabled it stamps a FRESH token (so a long-lived host never reuses a
63
+ * prior run's token, and planAuto + the task loop of THIS run share one id); when
64
+ * disabled it clears any token a prior run left, so the workers cache nothing.
65
+ */
66
+ export function configureResearchRun(enabled) {
67
+ if (!enabled) {
68
+ delete process.env[RESEARCH_RUN_ID_ENV];
69
+ return undefined;
70
+ }
71
+ const token = newRunToken();
72
+ process.env[RESEARCH_RUN_ID_ENV] = token;
73
+ return token;
74
+ }
75
+ /**
76
+ * Normalise a query/module string for the cache KEY: collapse whitespace and
77
+ * lowercase, so trivially-varied phrasings of the same question share a digest. The
78
+ * stored value is the real answer, so a case/spacing collision only means two ways
79
+ * of asking the same thing resolve to the same (correct) result.
80
+ */
81
+ export function normalizeQuery(s) {
82
+ return s.replace(/\s+/g, ' ').trim().toLowerCase();
83
+ }
84
+ async function readCacheFile(cwd) {
85
+ try {
86
+ const raw = await fsp.readFile(researchCacheFile(cwd), 'utf8');
87
+ const parsed = JSON.parse(raw);
88
+ if (parsed
89
+ && typeof parsed === 'object'
90
+ && typeof parsed.runId === 'string'
91
+ && typeof parsed.entries === 'object'
92
+ && parsed.entries !== null) {
93
+ return parsed;
94
+ }
95
+ }
96
+ catch {
97
+ // missing or corrupt ⇒ treated as empty
98
+ }
99
+ return null;
100
+ }
101
+ /**
102
+ * Look up a cached result for `key` in the current run. Returns undefined on a miss,
103
+ * a stale-run file (different id ⇒ another run's digest, ignored), or any failure.
104
+ */
105
+ export async function lookupResearch(cwd, runId, key) {
106
+ const file = await readCacheFile(cwd);
107
+ if (!file || file.runId !== runId)
108
+ return undefined;
109
+ const entry = file.entries[key];
110
+ return entry ? { text: entry.text, details: entry.details } : undefined;
111
+ }
112
+ /**
113
+ * Store a successful result under `key` for the current run. A file written for a
114
+ * different run id is discarded and started fresh (first write of a new run drops the
115
+ * prior run's contents — self-healing per-run isolation without an explicit clear).
116
+ * Best-effort: any failure is swallowed, leaving the caller's live result untouched.
117
+ */
118
+ export async function storeResearch(cwd, runId, key, text, details) {
119
+ try {
120
+ const existing = await readCacheFile(cwd);
121
+ const entries = existing && existing.runId === runId ? existing.entries : {};
122
+ entries[key] = { text, details, at: Date.now() };
123
+ // Evict oldest by write time if over the cap.
124
+ const keys = Object.keys(entries);
125
+ if (keys.length > MAX_ENTRIES) {
126
+ const ordered = keys.sort((a, b) => entries[a].at - entries[b].at);
127
+ for (const k of ordered.slice(0, keys.length - MAX_ENTRIES))
128
+ delete entries[k];
129
+ }
130
+ const out = { runId, entries };
131
+ await fsp.mkdir(tasksDir(cwd), { recursive: true });
132
+ // Atomic-ish write so a concurrent reader never sees a half-written file.
133
+ const tmp = `${researchCacheFile(cwd)}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
134
+ await fsp.writeFile(tmp, JSON.stringify(out), 'utf8');
135
+ await fsp.rename(tmp, researchCacheFile(cwd));
136
+ }
137
+ catch {
138
+ // best-effort cache
139
+ }
140
+ }
@@ -1,14 +1,21 @@
1
- import { braveSearch as defaultBraveSearch, type BraveResult } from './brave-search.js';
1
+ import { braveSearch as defaultBraveSearch } from './brave-search.js';
2
+ import { ddgSearch as defaultDdgSearch } from './ddg-search.js';
3
+ import { exaSearch as defaultExaSearch } from './exa-search.js';
4
+ import type { SearchProvider, SearchResult } from './search-types.js';
2
5
  export interface SearchCoreInput {
3
6
  query: string;
4
7
  count?: number;
5
8
  signal?: AbortSignal;
6
9
  getEnv?: (key: string) => string | undefined;
10
+ /** Engine override; defaults to the configured `searchProvider` (exa). */
11
+ provider?: SearchProvider;
7
12
  braveSearch?: typeof defaultBraveSearch;
13
+ exaSearch?: typeof defaultExaSearch;
14
+ ddgSearch?: typeof defaultDdgSearch;
8
15
  }
9
16
  export type SearchCoreResult = {
10
17
  kind: 'ok';
11
- results: BraveResult[];
18
+ results: SearchResult[];
12
19
  } | {
13
20
  kind: 'no_key';
14
21
  message: string;
@@ -16,4 +23,9 @@ export type SearchCoreResult = {
16
23
  kind: 'error';
17
24
  message: string;
18
25
  };
26
+ /**
27
+ * Provider selection is STRICT: the configured (or overridden) engine is the
28
+ * only one tried — a failure reports as an error rather than silently switching
29
+ * engines, so results always come from where the user thinks they do.
30
+ */
19
31
  export declare function search(input: SearchCoreInput): Promise<SearchCoreResult>;
@@ -1,18 +1,50 @@
1
+ import { getConfig } from '../config/config.js';
1
2
  import { braveSearch as defaultBraveSearch, BraveSearchError } from './brave-search.js';
3
+ import { ddgSearch as defaultDdgSearch } from './ddg-search.js';
4
+ import { exaSearch as defaultExaSearch } from './exa-search.js';
2
5
  function isLikeBraveSearchError(err) {
3
6
  return (typeof err === 'object'
4
7
  && err !== null
5
8
  && err.name === 'BraveSearchError');
6
9
  }
10
+ /**
11
+ * Provider selection is STRICT: the configured (or overridden) engine is the
12
+ * only one tried — a failure reports as an error rather than silently switching
13
+ * engines, so results always come from where the user thinks they do.
14
+ */
7
15
  export async function search(input) {
16
+ const provider = input.provider ?? getConfig().searchProvider;
17
+ if (provider === 'brave')
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) {
8
39
  const getEnv = input.getEnv ?? ((k) => process.env[k]);
9
40
  const braveSearch = input.braveSearch ?? defaultBraveSearch;
10
41
  const apiKey = getEnv('BRAVE_SEARCH_API_KEY') ?? getEnv('BRAVE_API_KEY');
11
42
  if (!apiKey) {
12
43
  return {
13
44
  kind: 'no_key',
14
- message: 'Brave Search not configured. Set BRAVE_SEARCH_API_KEY env var. '
15
- + 'Get a key at https://api.search.brave.com/app/keys'
45
+ message: 'Brave Search not configured. Set BRAVE_SEARCH_API_KEY env var '
46
+ + '(get a key at https://api.search.brave.com/app/keys) or switch '
47
+ + 'the search provider in /task-config.'
16
48
  };
17
49
  }
18
50
  try {
@@ -0,0 +1,15 @@
1
+ /** One web-search hit, in the shape every provider normalises to. */
2
+ export interface SearchResult {
3
+ title: string;
4
+ url: string;
5
+ description: string;
6
+ }
7
+ /**
8
+ * Which engine backs pi-worker-search and the freshness/enrichment lookups.
9
+ * - `exa` — Exa's public MCP endpoint; no key needed (default).
10
+ * - `ddg` — DuckDuckGo's HTML endpoint; no key needed.
11
+ * - `brave` — Brave Search API; needs BRAVE_SEARCH_API_KEY.
12
+ */
13
+ export type SearchProvider = 'exa' | 'ddg' | 'brave';
14
+ export declare const SEARCH_PROVIDERS: readonly SearchProvider[];
15
+ export declare function isSearchProvider(value: unknown): value is SearchProvider;
@@ -0,0 +1,4 @@
1
+ export const SEARCH_PROVIDERS = ['exa', 'ddg', 'brave'];
2
+ export function isSearchProvider(value) {
3
+ return typeof value === 'string' && SEARCH_PROVIDERS.includes(value);
4
+ }
@@ -38,6 +38,23 @@ export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
38
38
  details: TDetails;
39
39
  }>;
40
40
  renderCall(args: Static<TParams>, theme: Theme): Text;
41
+ /**
42
+ * Per-run research-cache policy (F10). Return a stable cache key for this call —
43
+ * a result keyed on it is a deterministic function of the inputs that does not
44
+ * change within a run, so a later sibling task can reuse it instead of re-running
45
+ * the network fetch + child summariser. Return `null` to opt a particular call
46
+ * OUT of caching (e.g. a project-source `.` lookup, whose answer the working tree
47
+ * mutates within a run). Omit entirely and the tool is never cached. The stored
48
+ * key is namespaced by tool name, so keys need only be unique within a tool.
49
+ */
50
+ cacheKey?(params: Static<TParams>): string | null;
51
+ /**
52
+ * Whether a produced result is safe to cache. Only a SUCCESS is memoised — an
53
+ * error, empty, or aborted result must fall through so a transient failure never
54
+ * poisons the run. Defaults to always-cacheable when omitted (but a tool with a
55
+ * cacheKey should always supply this).
56
+ */
57
+ cacheable?(details: TDetails, text: string): boolean;
41
58
  }
42
59
  /** Register a worker tool from its spec, supplying the shared registration ritual. */
43
60
  export declare function makeWorkerTool<TParams extends TSchema, TDetails>(pi: ExtensionAPI, spec: WorkerToolSpec<TParams, TDetails>): void;
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.18.4",
3
+ "version": "0.18.6",
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",