@mjasnikovs/pi-task 0.40.25 → 0.40.27
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/workers/docs-chunk.js +18 -2
- package/dist/workers/pi-worker-docs.d.ts +16 -10
- package/dist/workers/pi-worker-docs.js +20 -16
- package/dist/workers/pi-worker-fetch.d.ts +3 -1
- package/dist/workers/pi-worker-fetch.js +12 -6
- package/dist/workers/shared.d.ts +7 -3
- package/dist/workers/shared.js +0 -0
- package/package.json +1 -1
|
@@ -99,6 +99,11 @@ export function splitAtMatches(text, re) {
|
|
|
99
99
|
* possible hallucination. Only reachable on non-ASCII text past the chunk ceiling.
|
|
100
100
|
*/
|
|
101
101
|
export function sliceBytes(s, maxBytes) {
|
|
102
|
+
// A non-positive cap never shrinks the buffer, so the loop below runs forever.
|
|
103
|
+
// Guarded here rather than at the callers: the cap is usually computed, and the
|
|
104
|
+
// next caller to compute one must not have to rediscover this.
|
|
105
|
+
if (maxBytes <= 0)
|
|
106
|
+
return s ? [s] : [];
|
|
102
107
|
const out = [];
|
|
103
108
|
let buf = Buffer.from(s, 'utf8');
|
|
104
109
|
while (buf.length > maxBytes) {
|
|
@@ -134,8 +139,19 @@ export function headedSlices(header, body, maxBytes) {
|
|
|
134
139
|
const prefixed = `${header}\n${body}`;
|
|
135
140
|
if (Buffer.byteLength(prefixed, 'utf8') <= maxBytes)
|
|
136
141
|
return [prefixed];
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
// The header is a LABEL, and `chunkReadme` builds it from an unbounded heading line.
|
|
143
|
+
// Left whole it starves the body: a header two bytes short of the cap turned a 200 KB
|
|
144
|
+
// section into 100,000 two-byte chunks, each re-carrying the 8 KB header. Splitting the
|
|
145
|
+
// cap evenly is the one division that needs no tuning, and a cut label still names the
|
|
146
|
+
// source.
|
|
147
|
+
const head = sliceBytes(header, Math.floor(maxBytes / 2))[0] ?? '';
|
|
148
|
+
const room = maxBytes - Buffer.byteLength(`${head}\n`, 'utf8');
|
|
149
|
+
// Only a cap of a byte or two reaches this. Slice the prefixed string and accept the
|
|
150
|
+
// degenerate result: the body pieces carry no provenance — the very defect this
|
|
151
|
+
// function exists to fix, but the alternative here is emitting nothing.
|
|
152
|
+
if (room <= 0)
|
|
153
|
+
return sliceBytes(prefixed, maxBytes);
|
|
154
|
+
return sliceBytes(body, room).map(slice => `${head}\n${slice}`);
|
|
139
155
|
}
|
|
140
156
|
/**
|
|
141
157
|
* Chunk a declaration file, one chunk per declaration, each labelled with the
|
|
@@ -41,6 +41,12 @@ interface DocsDetails {
|
|
|
41
41
|
/** The project-lookup budget for this attempt is spent, so the call was refused
|
|
42
42
|
* before any work. Only set when PI_TASK_PROJECT_DOCS_BUDGET is configured. */
|
|
43
43
|
budgetSpent?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The child declined to answer. Recorded HERE, at the one place the bare
|
|
46
|
+
* `<answer>` is in hand: the tool text a cache predicate is handed leads with a
|
|
47
|
+
* provenance header, and the anchored matcher scores that as a real answer.
|
|
48
|
+
*/
|
|
49
|
+
abstained?: boolean;
|
|
44
50
|
}
|
|
45
51
|
/**
|
|
46
52
|
* Pull `@see {@link https://…}` pointers out of retrieved .d.ts/README text.
|
|
@@ -70,15 +76,6 @@ export interface PiWorkerDocsInternals {
|
|
|
70
76
|
npmVersionLookup?: typeof defaultNpmVersionLookup;
|
|
71
77
|
}
|
|
72
78
|
export declare function registerPiWorkerDocs(pi: ExtensionAPI, internals?: PiWorkerDocsInternals): void;
|
|
73
|
-
/**
|
|
74
|
-
* The cache rule for the docs channel, as a NAMED export rather than an anonymous
|
|
75
|
-
* property of an adapter literal.
|
|
76
|
-
*
|
|
77
|
-
* As a property of the adapter literal it would be reachable only through
|
|
78
|
-
* `registerTool → execute()`, so a test would have to retype the rule and would then
|
|
79
|
-
* assert against its own copy — green even after the shipped rule changed. Exported,
|
|
80
|
-
* the test imports the rule it is checking.
|
|
81
|
-
*/
|
|
82
79
|
/**
|
|
83
80
|
* Did the excerpt cite a word the source never wrote?
|
|
84
81
|
*
|
|
@@ -89,7 +86,16 @@ export declare function registerPiWorkerDocs(pi: ExtensionAPI, internals?: PiWor
|
|
|
89
86
|
export declare function excerptFabricated(check: {
|
|
90
87
|
absent: readonly string[];
|
|
91
88
|
} | undefined): boolean;
|
|
92
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The cache rule for the docs channel, as a NAMED export rather than an anonymous
|
|
91
|
+
* property of an adapter literal.
|
|
92
|
+
*
|
|
93
|
+
* As a property of the adapter literal it would be reachable only through
|
|
94
|
+
* `registerTool → execute()`, so a test would have to retype the rule and would then
|
|
95
|
+
* assert against its own copy — green even after the shipped rule changed. Exported,
|
|
96
|
+
* the test imports the rule it is checking.
|
|
97
|
+
*/
|
|
98
|
+
export declare function docsCacheable(d: Pick<DocsDetails, 'typeOnly' | 'excerptVerified' | 'excerptFabricated' | 'abstained'>): boolean;
|
|
93
99
|
/** The docs cache key: a package's answer is per (module, question), with the question
|
|
94
100
|
* lowercased and its whitespace collapsed so phrasing variants share one entry. Returns
|
|
95
101
|
* null for the project-source `.` lookup, which is never cached — the working tree
|
|
@@ -219,7 +219,8 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
219
219
|
return workerAnswer(text, {
|
|
220
220
|
...baseDetails,
|
|
221
221
|
excerptVerified: verified,
|
|
222
|
-
excerptFabricated: excerptFabricated(extraction.excerptCheck)
|
|
222
|
+
excerptFabricated: excerptFabricated(extraction.excerptCheck),
|
|
223
|
+
...(isAbstention(extraction.answer) ? { abstained: true } : {})
|
|
223
224
|
});
|
|
224
225
|
}
|
|
225
226
|
// ── npm package lookup (existing path) ──────────────────────────
|
|
@@ -355,7 +356,8 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
355
356
|
...baseDetails,
|
|
356
357
|
excerptVerified: verified,
|
|
357
358
|
excerptFabricated: excerptFabricated(extraction.excerptCheck),
|
|
358
|
-
...(typeOnly.typeOnly ? { typeOnly: true } : {})
|
|
359
|
+
...(typeOnly.typeOnly ? { typeOnly: true } : {}),
|
|
360
|
+
...(isAbstention(extraction.answer) ? { abstained: true } : {})
|
|
359
361
|
});
|
|
360
362
|
},
|
|
361
363
|
renderCall(args, theme) {
|
|
@@ -384,21 +386,9 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
384
386
|
// child that ran fine and answered "unclear from this package" exits 0 — so a rule
|
|
385
387
|
// keyed on exit code would memoise that non-answer and re-serve it as a hit to
|
|
386
388
|
// every later sibling, with nothing left to re-trigger an escalation.
|
|
387
|
-
//
|
|
388
|
-
// `text` is supplied by makeWorkerTool (shared.ts) alongside details, so the
|
|
389
|
-
// content check needs no new plumbing.
|
|
390
389
|
cacheable: docsCacheable
|
|
391
390
|
});
|
|
392
391
|
}
|
|
393
|
-
/**
|
|
394
|
-
* The cache rule for the docs channel, as a NAMED export rather than an anonymous
|
|
395
|
-
* property of an adapter literal.
|
|
396
|
-
*
|
|
397
|
-
* As a property of the adapter literal it would be reachable only through
|
|
398
|
-
* `registerTool → execute()`, so a test would have to retype the rule and would then
|
|
399
|
-
* assert against its own copy — green even after the shipped rule changed. Exported,
|
|
400
|
-
* the test imports the rule it is checking.
|
|
401
|
-
*/
|
|
402
392
|
/**
|
|
403
393
|
* Did the excerpt cite a word the source never wrote?
|
|
404
394
|
*
|
|
@@ -409,7 +399,16 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
409
399
|
export function excerptFabricated(check) {
|
|
410
400
|
return check !== undefined && check.absent.length > 0;
|
|
411
401
|
}
|
|
412
|
-
|
|
402
|
+
/**
|
|
403
|
+
* The cache rule for the docs channel, as a NAMED export rather than an anonymous
|
|
404
|
+
* property of an adapter literal.
|
|
405
|
+
*
|
|
406
|
+
* As a property of the adapter literal it would be reachable only through
|
|
407
|
+
* `registerTool → execute()`, so a test would have to retype the rule and would then
|
|
408
|
+
* assert against its own copy — green even after the shipped rule changed. Exported,
|
|
409
|
+
* the test imports the rule it is checking.
|
|
410
|
+
*/
|
|
411
|
+
export function docsCacheable(d) {
|
|
413
412
|
// Answer QUALITY only. Whether there IS an answer is `WorkerOutcome.kind`, and
|
|
414
413
|
// `makeWorkerTool` has already refused an `unavailable` before reaching here —
|
|
415
414
|
// opening this with `childExitCode === 0` memoises an aborted lookup for the
|
|
@@ -420,7 +419,12 @@ export function docsCacheable(d, text) {
|
|
|
420
419
|
// classifier defect 18 added, 41 of 41 unverified excerpts across seven runs are
|
|
421
420
|
// STITCHED — every span verbatim, not one absent word. The gate was refusing
|
|
422
421
|
// non-contiguous quoting, and every sibling paid a fresh child for it.
|
|
423
|
-
|
|
422
|
+
//
|
|
423
|
+
// The abstention is read off DETAILS, not off the tool text. `makeWorkerTool`
|
|
424
|
+
// hands this the FINAL text, which leads with `Per <pkg>@<version>:` — and
|
|
425
|
+
// `isAbstention` is anchored, so testing that text scored every abstention as a
|
|
426
|
+
// real answer and memoised the dead end for the whole run.
|
|
427
|
+
return d.typeOnly !== true && d.excerptFabricated !== true && d.abstained !== true;
|
|
424
428
|
}
|
|
425
429
|
/** The docs cache key: a package's answer is per (module, question), with the question
|
|
426
430
|
* lowercased and its whitespace collapsed so phrasing variants share one entry. Returns
|
|
@@ -6,6 +6,8 @@ interface FetchDetails {
|
|
|
6
6
|
answer?: string;
|
|
7
7
|
excerpt?: string;
|
|
8
8
|
excerptVerified?: boolean;
|
|
9
|
+
coverageMiss?: boolean;
|
|
10
|
+
anchoredSection?: string;
|
|
9
11
|
}
|
|
10
12
|
interface ProcLike extends EventEmitter {
|
|
11
13
|
stdout: EventEmitter | null;
|
|
@@ -32,7 +34,7 @@ export declare function registerPiWorkerFetch(pi: ExtensionAPI, internals?: PiWo
|
|
|
32
34
|
* ABOUT that page, and re-fetching cannot change it — only the abstention sentinel is
|
|
33
35
|
* refused.
|
|
34
36
|
*/
|
|
35
|
-
export declare function fetchCacheable(
|
|
37
|
+
export declare function fetchCacheable(d: Pick<FetchDetails, 'answer'>): boolean;
|
|
36
38
|
/** The fetch cache key. URL verbatim (path case can matter), question normalised —
|
|
37
39
|
* same page, different question is a different answer. */
|
|
38
40
|
export declare function fetchCacheKey(params: {
|
|
@@ -61,6 +61,8 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
61
61
|
// in the TEXT, not only in details: details are for the harness, and the
|
|
62
62
|
// worker acts on what it reads.
|
|
63
63
|
const text = result.nextStep ? `${body}\n\n${result.nextStep}` : body;
|
|
64
|
+
// Named, not inferred: `workerAnswer<T>` reads T off the literal, so a
|
|
65
|
+
// field added here would never reach the declaration the cache rules Pick from.
|
|
64
66
|
return workerAnswer(text, {
|
|
65
67
|
childExitCode: 0,
|
|
66
68
|
answer: result.answer,
|
|
@@ -112,12 +114,16 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
112
114
|
* ABOUT that page, and re-fetching cannot change it — only the abstention sentinel is
|
|
113
115
|
* refused.
|
|
114
116
|
*/
|
|
115
|
-
export function fetchCacheable(
|
|
116
|
-
// Answer QUALITY only — see docsCacheable.
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
117
|
+
export function fetchCacheable(d) {
|
|
118
|
+
// Answer QUALITY only — see docsCacheable. An aborted fetch is kept out of the
|
|
119
|
+
// cache by its `unavailable` outcome upstream, never by this rule; leading with
|
|
120
|
+
// `childExitCode === 0` would not work, because an aborted child settles at 0.
|
|
121
|
+
//
|
|
122
|
+
// It reads the child's bare answer, not the rendered tool text: `isAbstention` is
|
|
123
|
+
// anchored, and the text leads with an excerpt NOTE/WARNING whenever the excerpt
|
|
124
|
+
// did not verify — which, since rule 4 asks for the closest related text, is the
|
|
125
|
+
// ordinary shape of an abstention.
|
|
126
|
+
return d.answer !== undefined && !isAbstention(d.answer);
|
|
121
127
|
}
|
|
122
128
|
/** The fetch cache key. URL verbatim (path case can matter), question normalised —
|
|
123
129
|
* same page, different question is a different answer. */
|
package/dist/workers/shared.d.ts
CHANGED
|
@@ -99,8 +99,8 @@ export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
|
|
|
99
99
|
* the network fetch + child summariser. Return `null` to opt a particular call
|
|
100
100
|
* OUT of caching (e.g. a project-source `.` lookup, whose answer the working tree
|
|
101
101
|
* mutates within a run). Omit entirely and the tool is never cached. The stored
|
|
102
|
-
* key is namespaced by tool name — joined with a
|
|
103
|
-
*
|
|
102
|
+
* key is namespaced by tool name — joined with a NUL, a byte no tool name or key
|
|
103
|
+
* can contain, so no pair of them can collide by concatenation.
|
|
104
104
|
*/
|
|
105
105
|
cacheKey?(params: Static<TParams>): string | null;
|
|
106
106
|
/**
|
|
@@ -121,8 +121,12 @@ export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
|
|
|
121
121
|
* (type-only, an abstention, an unverified excerpt), never about process
|
|
122
122
|
* health. An `unavailable` outcome never reaches this: `makeWorkerTool` has
|
|
123
123
|
* already refused it. Defaults to always-cacheable when omitted.
|
|
124
|
+
*
|
|
125
|
+
* It sees `details` only. Handed the rendered tool text as well, a rule reaches
|
|
126
|
+
* for it and reads a string that leads with provenance and excerpt notes — and an
|
|
127
|
+
* anchored abstention matcher then scores every abstention as an answer.
|
|
124
128
|
*/
|
|
125
|
-
cacheable?(details: TDetails
|
|
129
|
+
cacheable?(details: TDetails): boolean;
|
|
126
130
|
}
|
|
127
131
|
/** Register a worker tool from its spec, supplying the shared registration ritual. */
|
|
128
132
|
export declare function makeWorkerTool<TParams extends TSchema, TDetails>(pi: ExtensionAPI, spec: WorkerToolSpec<TParams, TDetails>): void;
|
package/dist/workers/shared.js
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.27",
|
|
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",
|