@mjasnikovs/pi-task 0.40.50 → 0.41.0
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/README.md +7 -4
- package/dist/config/config.d.ts +61 -18
- package/dist/config/config.js +50 -6
- package/dist/config/register.js +18 -5
- package/dist/shared/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- package/dist/task/accept-debt.d.ts +15 -1
- package/dist/task/accept-debt.js +4 -0
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +59 -7
- package/dist/task/auto-io.js +140 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +126 -112
- package/dist/task/child-runner.d.ts +18 -0
- package/dist/task/child-runner.js +7 -8
- package/dist/task/command-run.d.ts +4 -0
- package/dist/task/command-run.js +4 -2
- package/dist/task/constraint-policy.d.ts +52 -0
- package/dist/task/constraint-policy.js +66 -0
- package/dist/task/context-silence.d.ts +5 -4
- package/dist/task/context-silence.js +13 -5
- package/dist/task/coverage-loop.d.ts +7 -4
- package/dist/task/coverage-loop.js +16 -138
- package/dist/task/debug-log.d.ts +25 -2
- package/dist/task/debug-log.js +64 -10
- package/dist/task/decompose-fidelity.d.ts +17 -4
- package/dist/task/decompose-fidelity.js +95 -55
- package/dist/task/decompose-granularity.d.ts +7 -7
- package/dist/task/decompose-granularity.js +8 -8
- package/dist/task/env-notes.d.ts +65 -19
- package/dist/task/env-notes.js +152 -50
- package/dist/task/external-context.js +25 -5
- package/dist/task/fix-context.d.ts +39 -0
- package/dist/task/fix-context.js +58 -0
- package/dist/task/gate-deps.d.ts +49 -0
- package/dist/task/gate-deps.js +199 -38
- package/dist/task/gate-evidence.d.ts +51 -0
- package/dist/task/gate-evidence.js +90 -0
- package/dist/task/gate-resolution.d.ts +119 -0
- package/dist/task/gate-resolution.js +120 -0
- package/dist/task/git-state-guard.js +3 -26
- package/dist/task/handoff.d.ts +35 -0
- package/dist/task/handoff.js +100 -0
- package/dist/task/health-baseline.d.ts +87 -0
- package/dist/task/health-baseline.js +158 -0
- package/dist/task/ledger.d.ts +10 -0
- package/dist/task/ledger.js +22 -11
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +14 -8
- package/dist/task/loop-detector.d.ts +52 -3
- package/dist/task/loop-detector.js +81 -5
- package/dist/task/mentions.d.ts +17 -0
- package/dist/task/mentions.js +86 -0
- package/dist/task/orchestrator.d.ts +53 -9
- package/dist/task/orchestrator.js +104 -28
- package/dist/task/orientation.d.ts +135 -29
- package/dist/task/orientation.js +259 -67
- package/dist/task/owned-freeze-reassign.d.ts +7 -3
- package/dist/task/owned-freeze-reassign.js +13 -5
- package/dist/task/parsers.d.ts +6 -1
- package/dist/task/parsers.js +21 -4
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +125 -153
- package/dist/task/plan-orchestrator.js +5 -6
- package/dist/task/plan-rounds.d.ts +1 -0
- package/dist/task/plan-rounds.js +8 -1
- package/dist/task/prohibition-probe.d.ts +6 -1
- package/dist/task/prohibition-probe.js +26 -6
- package/dist/task/prompts.js +13 -2
- package/dist/task/qa-transcript.d.ts +17 -0
- package/dist/task/qa-transcript.js +28 -0
- package/dist/task/question-source.d.ts +16 -2
- package/dist/task/question-source.js +21 -8
- package/dist/task/repo-health-check.d.ts +27 -0
- package/dist/task/repo-health-check.js +39 -5
- package/dist/task/requirements.d.ts +67 -13
- package/dist/task/requirements.js +129 -67
- package/dist/task/research-worker.d.ts +43 -2
- package/dist/task/research-worker.js +103 -8
- package/dist/task/run-bracket.d.ts +9 -4
- package/dist/task/run-bracket.js +9 -0
- package/dist/task/run-context.d.ts +152 -0
- package/dist/task/run-context.js +277 -0
- package/dist/task/spec-doc.d.ts +78 -0
- package/dist/task/spec-doc.js +205 -0
- package/dist/task/spec-model.d.ts +70 -0
- package/dist/task/spec-model.js +139 -0
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +9 -5
- package/dist/task/state-dir.d.ts +29 -0
- package/dist/task/state-dir.js +80 -0
- package/dist/task/suppression-probe.d.ts +67 -0
- package/dist/task/suppression-probe.js +78 -0
- package/dist/task/task-gates.d.ts +39 -57
- package/dist/task/task-gates.js +115 -195
- package/dist/task/task-io.d.ts +10 -0
- package/dist/task/task-io.js +69 -18
- package/dist/task/task-parsers.js +6 -6
- package/dist/task/task-types.d.ts +7 -0
- package/dist/task/timings.d.ts +11 -0
- package/dist/task/timings.js +21 -0
- package/dist/task/tree-hash.d.ts +8 -0
- package/dist/task/tree-hash.js +55 -0
- package/dist/task/verify-reconcile.d.ts +4 -3
- package/dist/task/verify-reconcile.js +7 -8
- package/dist/task/verify-resolution.d.ts +28 -5
- package/dist/task/verify-resolution.js +70 -13
- package/dist/task/verify-work.d.ts +98 -35
- package/dist/task/verify-work.js +227 -60
- package/dist/task/yolo.d.ts +0 -13
- package/dist/task/yolo.js +9 -21
- package/dist/workers/pi-worker-core.d.ts +20 -0
- package/dist/workers/pi-worker-core.js +16 -11
- package/dist/workers/pi-worker-docs.d.ts +6 -5
- package/dist/workers/pi-worker-docs.js +8 -7
- package/dist/workers/pi-worker-fetch.d.ts +3 -2
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +5 -4
- package/dist/workers/research-cache.d.ts +23 -1
- package/dist/workers/research-cache.js +63 -3
- package/dist/workers/shared.d.ts +18 -0
- package/dist/workers/shared.js +38 -22
- package/package.json +1 -1
|
@@ -35,8 +35,9 @@ export declare function registerPiWorkerFetch(pi: ExtensionAPI, internals?: PiWo
|
|
|
35
35
|
* refused.
|
|
36
36
|
*/
|
|
37
37
|
export declare function fetchCacheable(d: Pick<FetchDetails, 'answer'>): boolean;
|
|
38
|
-
/** The fetch cache key. URL verbatim (path case can matter), question
|
|
39
|
-
* same page, different question is a different answer
|
|
38
|
+
/** The fetch cache key. URL verbatim (path case can matter), question as its sorted
|
|
39
|
+
* distinctive tokens — same page, different question is a different answer, but two
|
|
40
|
+
* phrasings of one question are not. */
|
|
40
41
|
export declare function fetchCacheKey(params: {
|
|
41
42
|
url: string;
|
|
42
43
|
query: string;
|
|
@@ -4,7 +4,7 @@ import { fetchAndClean as defaultFetchAndClean, FetchAndCleanError } from './htm
|
|
|
4
4
|
import { fetchFocused } from './fetch-core.js';
|
|
5
5
|
import { formatResultText } from '../shared/child-output.js';
|
|
6
6
|
import { childFailureReason, makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
|
|
7
|
-
import {
|
|
7
|
+
import { queryTokenKey } from './research-cache.js';
|
|
8
8
|
import { isAbstention } from './abstention.js';
|
|
9
9
|
const RENDER_QUERY_MAX = 100;
|
|
10
10
|
const Params = Type.Object({
|
|
@@ -125,8 +125,9 @@ export function fetchCacheable(d) {
|
|
|
125
125
|
// ordinary shape of an abstention.
|
|
126
126
|
return d.answer !== undefined && !isAbstention(d.answer);
|
|
127
127
|
}
|
|
128
|
-
/** The fetch cache key. URL verbatim (path case can matter), question
|
|
129
|
-
* same page, different question is a different answer
|
|
128
|
+
/** The fetch cache key. URL verbatim (path case can matter), question as its sorted
|
|
129
|
+
* distinctive tokens — same page, different question is a different answer, but two
|
|
130
|
+
* phrasings of one question are not. */
|
|
130
131
|
export function fetchCacheKey(params) {
|
|
131
|
-
return `${params.url.trim()}
|
|
132
|
+
return `${params.url.trim()}\u0000${queryTokenKey(params.query)}`;
|
|
132
133
|
}
|
|
@@ -3,7 +3,7 @@ import { Text } from '@earendil-works/pi-tui';
|
|
|
3
3
|
import { getConfig } from '../config/config.js';
|
|
4
4
|
import { search } from './search-core.js';
|
|
5
5
|
import { makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
|
|
6
|
-
import {
|
|
6
|
+
import { queryTokenKey } from './research-cache.js';
|
|
7
7
|
const Params = Type.Object({
|
|
8
8
|
query: Type.String({ description: 'Search query.' }),
|
|
9
9
|
count: Type.Optional(Type.Integer({
|
|
@@ -62,9 +62,10 @@ export function registerPiWorkerSearch(pi, internals = {}) {
|
|
|
62
62
|
// the live web again. Three parts key the entry: the PROVIDER, because two
|
|
63
63
|
// engines' result sets for one query are different answers and must not serve
|
|
64
64
|
// for each other; the query, lowercased and whitespace-collapsed by
|
|
65
|
-
// `
|
|
66
|
-
// larger request is a different result set.
|
|
67
|
-
|
|
65
|
+
// `queryTokenKey` so phrasing variants share an entry; and the COUNT, because a
|
|
66
|
+
// larger request is a different result set. The tokens go LAST because that is
|
|
67
|
+
// where research-cache.ts looks for the question it may widen a hit from.
|
|
68
|
+
cacheKey: params => `${provider()}::${params.count ?? ''}\u0000${queryTokenKey(params.query)}`,
|
|
68
69
|
// Only a non-empty result set is worth caching. An empty one falls through so a
|
|
69
70
|
// later attempt can succeed; no-key and engine errors never reach this at all,
|
|
70
71
|
// since makeWorkerTool refuses to store an `unavailable` outcome.
|
|
@@ -16,8 +16,11 @@ export declare function newRunToken(): string;
|
|
|
16
16
|
* caching is enabled it stamps a FRESH token (so a long-lived host never reuses a
|
|
17
17
|
* prior run's token, and planAuto + the task loop of THIS run share one id); when
|
|
18
18
|
* disabled it clears any token a prior run left, so the workers cache nothing.
|
|
19
|
+
*
|
|
20
|
+
* `token` lets the caller supply the run's OWN id (task/run-context.ts) rather
|
|
21
|
+
* than a second one minted here — one run, one id.
|
|
19
22
|
*/
|
|
20
|
-
export declare function configureResearchRun(enabled: boolean): string | undefined;
|
|
23
|
+
export declare function configureResearchRun(enabled: boolean, token?: string): string | undefined;
|
|
21
24
|
/**
|
|
22
25
|
* Normalise a query/module string for the cache KEY: collapse whitespace and
|
|
23
26
|
* lowercase, so trivially-varied phrasings of the same question share a digest. The
|
|
@@ -25,6 +28,25 @@ export declare function configureResearchRun(enabled: boolean): string | undefin
|
|
|
25
28
|
* of asking the same thing resolve to the same (correct) result.
|
|
26
29
|
*/
|
|
27
30
|
export declare function normalizeQuery(s: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* The QUESTION half of a cache key: the query's distinctive tokens, sorted.
|
|
33
|
+
*
|
|
34
|
+
* Normalised text made the key an exact-phrasing match, and two children asking
|
|
35
|
+
* one question in two sentences missed each other every time — the run's hit rate
|
|
36
|
+
* was ~0%. Sorted tokens collapse word order and phrasing; what they do NOT
|
|
37
|
+
* collapse is scope, which is why a hit also has to pass {@link coversQuery}.
|
|
38
|
+
*/
|
|
39
|
+
export declare function queryTokenKey(query: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Is `cached` an answer to `asked`? Only when the answer was produced for a
|
|
42
|
+
* question that asked for AT LEAST as much: every token of the new query must
|
|
43
|
+
* appear in the cached one.
|
|
44
|
+
*
|
|
45
|
+
* The direction matters and is not symmetric. An answer to "hono routing and
|
|
46
|
+
* middleware" contains the answer to "hono routing"; the reverse does not hold,
|
|
47
|
+
* and serving it would quietly drop half the question.
|
|
48
|
+
*/
|
|
49
|
+
export declare function coversQuery(cached: string, asked: string): boolean;
|
|
28
50
|
/**
|
|
29
51
|
* Resume hook: keep the interrupted run's cache id and PRUNE the entries the manifest
|
|
30
52
|
* has invalidated, rather than discarding the whole cache because anything moved. Returns
|
|
@@ -28,6 +28,12 @@
|
|
|
28
28
|
* A resume therefore REUSES the interrupted run's id — but only on POSITIVE evidence
|
|
29
29
|
* that the digests still describe the same dependency surface.
|
|
30
30
|
*
|
|
31
|
+
* KEY SHAPE: `<tool>\0<subject>\0<sorted query tokens>` — the tool name, the package
|
|
32
|
+
* or url the answer is about, and the query reduced to its distinctive tokens. The
|
|
33
|
+
* token field is both the key and the stored token set, so there is one statement of
|
|
34
|
+
* it; a lookup that misses exactly falls back to any entry for the same subject whose
|
|
35
|
+
* tokens CONTAIN all of the new query's (see `coversQuery`).
|
|
36
|
+
*
|
|
31
37
|
* PER-PACKAGE INVALIDATION. One fingerprint over the whole dependency block cannot be
|
|
32
38
|
* that evidence: a greenfield run ADDS dependencies every few tasks, so every resume
|
|
33
39
|
* sees a moved fingerprint and keeps nothing. Adding a package invalidates nothing that
|
|
@@ -78,6 +84,7 @@
|
|
|
78
84
|
*/
|
|
79
85
|
import * as fsp from 'node:fs/promises';
|
|
80
86
|
import * as path from 'node:path';
|
|
87
|
+
import { contentTokens } from '../shared/content-tokens.js';
|
|
81
88
|
import { tasksDir } from '../task/task-io.js';
|
|
82
89
|
import { ECOSYSTEMS } from './docs-ecosystems.js';
|
|
83
90
|
const RESEARCH_CACHE_FILE = 'research-cache.json';
|
|
@@ -156,13 +163,15 @@ export function newRunToken() {
|
|
|
156
163
|
* caching is enabled it stamps a FRESH token (so a long-lived host never reuses a
|
|
157
164
|
* prior run's token, and planAuto + the task loop of THIS run share one id); when
|
|
158
165
|
* disabled it clears any token a prior run left, so the workers cache nothing.
|
|
166
|
+
*
|
|
167
|
+
* `token` lets the caller supply the run's OWN id (task/run-context.ts) rather
|
|
168
|
+
* than a second one minted here — one run, one id.
|
|
159
169
|
*/
|
|
160
|
-
export function configureResearchRun(enabled) {
|
|
170
|
+
export function configureResearchRun(enabled, token = newRunToken()) {
|
|
161
171
|
if (!enabled) {
|
|
162
172
|
delete process.env[RESEARCH_RUN_ID_ENV];
|
|
163
173
|
return undefined;
|
|
164
174
|
}
|
|
165
|
-
const token = newRunToken();
|
|
166
175
|
process.env[RESEARCH_RUN_ID_ENV] = token;
|
|
167
176
|
return token;
|
|
168
177
|
}
|
|
@@ -175,6 +184,40 @@ export function configureResearchRun(enabled) {
|
|
|
175
184
|
export function normalizeQuery(s) {
|
|
176
185
|
return s.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
177
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* The QUESTION half of a cache key: the query's distinctive tokens, sorted.
|
|
189
|
+
*
|
|
190
|
+
* Normalised text made the key an exact-phrasing match, and two children asking
|
|
191
|
+
* one question in two sentences missed each other every time — the run's hit rate
|
|
192
|
+
* was ~0%. Sorted tokens collapse word order and phrasing; what they do NOT
|
|
193
|
+
* collapse is scope, which is why a hit also has to pass {@link coversQuery}.
|
|
194
|
+
*/
|
|
195
|
+
export function queryTokenKey(query) {
|
|
196
|
+
return [...contentTokens(query)].sort().join(' ');
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Is `cached` an answer to `asked`? Only when the answer was produced for a
|
|
200
|
+
* question that asked for AT LEAST as much: every token of the new query must
|
|
201
|
+
* appear in the cached one.
|
|
202
|
+
*
|
|
203
|
+
* The direction matters and is not symmetric. An answer to "hono routing and
|
|
204
|
+
* middleware" contains the answer to "hono routing"; the reverse does not hold,
|
|
205
|
+
* and serving it would quietly drop half the question.
|
|
206
|
+
*/
|
|
207
|
+
export function coversQuery(cached, asked) {
|
|
208
|
+
const have = new Set(cached.split(' ').filter(t => t.length > 0));
|
|
209
|
+
return asked.split(' ').every(t => t.length === 0 || have.has(t));
|
|
210
|
+
}
|
|
211
|
+
/** The `tool\0subject\0tokens` split of a key, or null when it carries no token
|
|
212
|
+
* field (a URL fetch, whose question is the URL itself). */
|
|
213
|
+
function splitTokenKey(key) {
|
|
214
|
+
const i = key.lastIndexOf('\u0000');
|
|
215
|
+
if (i <= 0)
|
|
216
|
+
return null;
|
|
217
|
+
const scope = key.slice(0, i + 1);
|
|
218
|
+
const tokens = key.slice(i + 1);
|
|
219
|
+
return scope.split('\u0000').length < 3 ? null : { scope, tokens };
|
|
220
|
+
}
|
|
178
221
|
/** The same question for any ecosystem: what does its manifest pin, name to version. */
|
|
179
222
|
function depsMapFor(cwd, ecosystem) {
|
|
180
223
|
return ECOSYSTEMS[ecosystem].declaredDeps(cwd);
|
|
@@ -280,9 +323,26 @@ export async function lookupResearch(cwd, runId, key) {
|
|
|
280
323
|
const file = await readCacheFile(cwd);
|
|
281
324
|
if (!file || file.runId !== runId)
|
|
282
325
|
return undefined;
|
|
283
|
-
const entry = file.entries[key];
|
|
326
|
+
const entry = file.entries[key] ?? coveringEntry(file, key);
|
|
284
327
|
return entry ? { text: entry.text, details: entry.details } : undefined;
|
|
285
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* A cached answer to a WIDER question about the same subject, for a key whose last
|
|
331
|
+
* field is a token set. Exact match is tried first; this is what turns two
|
|
332
|
+
* phrasings of one question into one lookup.
|
|
333
|
+
*/
|
|
334
|
+
function coveringEntry(file, key) {
|
|
335
|
+
const asked = splitTokenKey(key);
|
|
336
|
+
if (!asked)
|
|
337
|
+
return undefined;
|
|
338
|
+
for (const [k, entry] of Object.entries(file.entries)) {
|
|
339
|
+
if (!k.startsWith(asked.scope))
|
|
340
|
+
continue;
|
|
341
|
+
if (coversQuery(k.slice(asked.scope.length), asked.tokens))
|
|
342
|
+
return entry;
|
|
343
|
+
}
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
286
346
|
/** Write the cache file atomic-ish, so a concurrent reader never sees it half-written. */
|
|
287
347
|
async function writeCacheFile(cwd, out) {
|
|
288
348
|
const tmp = `${researchCacheFile(cwd)}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
package/dist/workers/shared.d.ts
CHANGED
|
@@ -128,5 +128,23 @@ export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
|
|
|
128
128
|
*/
|
|
129
129
|
cacheable?(details: TDetails): boolean;
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* The per-run research cache, as a call wrapper.
|
|
133
|
+
*
|
|
134
|
+
* Extracted from `makeWorkerTool` because the host fans out to the SAME workers
|
|
135
|
+
* outside any tool call: `gatherExternalContext` (task/external-context.ts) looks
|
|
136
|
+
* up docs, pages and live versions for every task's research phase, in-process,
|
|
137
|
+
* and paid the network for all of it again on every task of the run. The cache was
|
|
138
|
+
* never about tool registration; it is about (tool, subject, question) -> answer.
|
|
139
|
+
*
|
|
140
|
+
* A null `rawKey`, or no run id stamped, means the call is not cached at all.
|
|
141
|
+
*/
|
|
142
|
+
export declare function cachedWorkerCall<TDetails>(cwd: string, tool: string, rawKey: string | null, run: () => Promise<WorkerOutcome<TDetails>>, rules?: {
|
|
143
|
+
cacheable?: (details: TDetails) => boolean;
|
|
144
|
+
pkg?: (details: TDetails) => CachePackage | undefined;
|
|
145
|
+
}): Promise<{
|
|
146
|
+
text: string;
|
|
147
|
+
details: TDetails;
|
|
148
|
+
}>;
|
|
131
149
|
/** Register a worker tool from its spec, supplying the shared registration ritual. */
|
|
132
150
|
export declare function makeWorkerTool<TParams extends TSchema, TDetails>(pi: ExtensionAPI, spec: WorkerToolSpec<TParams, TDetails>): void;
|
package/dist/workers/shared.js
CHANGED
|
@@ -39,6 +39,40 @@ export function childFailureReason(child) {
|
|
|
39
39
|
export function workerUnavailable(text, details, reason) {
|
|
40
40
|
return { kind: 'unavailable', text, details, reason };
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The per-run research cache, as a call wrapper.
|
|
44
|
+
*
|
|
45
|
+
* Extracted from `makeWorkerTool` because the host fans out to the SAME workers
|
|
46
|
+
* outside any tool call: `gatherExternalContext` (task/external-context.ts) looks
|
|
47
|
+
* up docs, pages and live versions for every task's research phase, in-process,
|
|
48
|
+
* and paid the network for all of it again on every task of the run. The cache was
|
|
49
|
+
* never about tool registration; it is about (tool, subject, question) -> answer.
|
|
50
|
+
*
|
|
51
|
+
* A null `rawKey`, or no run id stamped, means the call is not cached at all.
|
|
52
|
+
*/
|
|
53
|
+
export async function cachedWorkerCall(cwd, tool, rawKey, run, rules = {}) {
|
|
54
|
+
// The stored key is namespaced by tool name, joined with a NUL — a byte no tool
|
|
55
|
+
// name or key can contain, so no pair of them can collide by concatenation.
|
|
56
|
+
const runId = researchRunId();
|
|
57
|
+
const cacheKey = runId !== undefined && rawKey !== null ? `${tool}\u0000${rawKey}` : null;
|
|
58
|
+
if (runId !== undefined && cacheKey !== null) {
|
|
59
|
+
const hit = await lookupResearch(cwd, runId, cacheKey);
|
|
60
|
+
if (hit !== undefined)
|
|
61
|
+
return { text: hit.text, details: hit.details };
|
|
62
|
+
}
|
|
63
|
+
const outcome = await run();
|
|
64
|
+
const { text, details } = outcome;
|
|
65
|
+
// A non-answer is never stored, whatever the caller's own rule says. The rule
|
|
66
|
+
// decides answer QUALITY; this decides whether there is an answer.
|
|
67
|
+
if (outcome.kind === 'answer'
|
|
68
|
+
&& runId !== undefined
|
|
69
|
+
&& cacheKey !== null
|
|
70
|
+
&& (rules.cacheable ? rules.cacheable(details) : true)) {
|
|
71
|
+
const provenance = rules.pkg?.(details);
|
|
72
|
+
await storeResearch(cwd, runId, cacheKey, text, details, provenance?.pkg, provenance?.ecosystem);
|
|
73
|
+
}
|
|
74
|
+
return { text, details };
|
|
75
|
+
}
|
|
42
76
|
/** Register a worker tool from its spec, supplying the shared registration ritual. */
|
|
43
77
|
export function makeWorkerTool(pi, spec) {
|
|
44
78
|
pi.registerTool({
|
|
@@ -48,28 +82,10 @@ export function makeWorkerTool(pi, spec) {
|
|
|
48
82
|
parameters: spec.parameters,
|
|
49
83
|
executionMode: 'parallel',
|
|
50
84
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const rawKey = spec.cacheKey ? spec.cacheKey(params) : null;
|
|
56
|
-
const cacheKey = runId !== undefined && rawKey !== null ? `${spec.name}\u0000${rawKey}` : null;
|
|
57
|
-
if (runId !== undefined && cacheKey !== null) {
|
|
58
|
-
const hit = await lookupResearch(ctx.cwd, runId, cacheKey);
|
|
59
|
-
if (hit !== undefined)
|
|
60
|
-
return textResult(hit.text, hit.details);
|
|
61
|
-
}
|
|
62
|
-
const outcome = await spec.run(params, signal, ctx);
|
|
63
|
-
const { text, details } = outcome;
|
|
64
|
-
// A non-answer is never stored, whatever the tool's own rule says. The
|
|
65
|
-
// rule decides answer QUALITY; this decides whether there is an answer.
|
|
66
|
-
if (outcome.kind === 'answer'
|
|
67
|
-
&& runId !== undefined
|
|
68
|
-
&& cacheKey !== null
|
|
69
|
-
&& (spec.cacheable ? spec.cacheable(details) : true)) {
|
|
70
|
-
const provenance = spec.cachePkg?.(params, details);
|
|
71
|
-
await storeResearch(ctx.cwd, runId, cacheKey, text, details, provenance?.pkg, provenance?.ecosystem);
|
|
72
|
-
}
|
|
85
|
+
const { text, details } = await cachedWorkerCall(ctx.cwd, spec.name, spec.cacheKey ? spec.cacheKey(params) : null, () => spec.run(params, signal, ctx), {
|
|
86
|
+
...(spec.cacheable ? { cacheable: d => spec.cacheable(d) } : {}),
|
|
87
|
+
...(spec.cachePkg ? { pkg: d => spec.cachePkg(params, d) } : {})
|
|
88
|
+
});
|
|
73
89
|
return textResult(text, details);
|
|
74
90
|
},
|
|
75
91
|
renderCall: (args, theme) => spec.renderCall(args, theme)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
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",
|