@mjasnikovs/pi-task 0.37.1 → 0.37.3
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.
|
@@ -4,6 +4,7 @@ import { type ExcerptVerification } from '../shared/child-output.js';
|
|
|
4
4
|
/** The exact non-answers the child is instructed to emit, matched at the tool layer. */
|
|
5
5
|
export declare const UNCLEAR_ANSWER = "unclear from this page";
|
|
6
6
|
export declare const NOT_COVERED_ANSWER = "not covered by this page";
|
|
7
|
+
export declare function normaliseSourceUrl(url: string): string;
|
|
7
8
|
export interface FetchRawInput {
|
|
8
9
|
url: string;
|
|
9
10
|
signal?: AbortSignal;
|
|
@@ -51,6 +52,15 @@ export interface FetchFocusedResult {
|
|
|
51
52
|
* "page has no answer" and "answer is ambiguous" as the same thing (PROMPT-3 item 3).
|
|
52
53
|
*/
|
|
53
54
|
coverageMiss: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* What to do INSTEAD, present only on a coverage miss. `coverageMiss` was computed and
|
|
57
|
+
* stored and read nowhere, so the distinct channel it exists to provide never reached the
|
|
58
|
+
* worker: all it ever saw was the bare sentence "not covered by this page", which reads
|
|
59
|
+
* as "ask again, differently". It did — 9 of the 84 corpus fetches re-read a URL that had
|
|
60
|
+
* already returned a non-answer, one release-notes page three times with near-identical
|
|
61
|
+
* questions.
|
|
62
|
+
*/
|
|
63
|
+
nextStep?: string;
|
|
54
64
|
/** The #fragment slug that was anchored, when the URL carried one and it was located. */
|
|
55
65
|
anchoredSection?: string;
|
|
56
66
|
/** Retained evidence for a false `excerptVerified`, so it is diagnosable without re-fetch. */
|
|
@@ -63,6 +73,16 @@ export interface FetchFocusedResult {
|
|
|
63
73
|
stdout: string;
|
|
64
74
|
}
|
|
65
75
|
export declare function fetchFocused(input: FetchFocusedInput): Promise<FetchFocusedResult>;
|
|
76
|
+
/**
|
|
77
|
+
* The instruction that goes with a coverage miss. It says two things the bare sentinel does
|
|
78
|
+
* not: WHICH page was actually read (after the blob rewrite these differ, and a worker that
|
|
79
|
+
* cannot see that would "retry" the raw URL it already got), and that re-reading this page
|
|
80
|
+
* with a reworded question returns the same answer — so the next move is a different URL.
|
|
81
|
+
*
|
|
82
|
+
* Deliberately not a suggestion of WHICH other URL. Nothing at this layer knows one, and
|
|
83
|
+
* naming a guess is how a dead end becomes two dead ends.
|
|
84
|
+
*/
|
|
85
|
+
export declare function coverageMissNextStep(requestedUrl: string, fetchedUrl: string): string;
|
|
66
86
|
export interface SelectedContent {
|
|
67
87
|
content: string;
|
|
68
88
|
/** The #fragment slug that was anchored, if the URL carried one AND it was located. */
|
|
@@ -11,18 +11,58 @@ const TRUNCATION_MARKER = '\n\n[...page continues, truncated...]\n\n';
|
|
|
11
11
|
/** The exact non-answers the child is instructed to emit, matched at the tool layer. */
|
|
12
12
|
export const UNCLEAR_ANSWER = 'unclear from this page';
|
|
13
13
|
export const NOT_COVERED_ANSWER = 'not covered by this page';
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Rule 6 asks the child to write the sentinel and NOTHING else, so the sentinel is the whole
|
|
16
|
+
* answer — anchored, not a substring search. A substring search misreads the opposite case:
|
|
17
|
+
* rule 5 tells the child to answer partially and say what is missing, and it says it in the
|
|
18
|
+
* prompt's own words ("… `obs_add_raw_audio_callback` and `obs_remove_raw_audio_callback`
|
|
19
|
+
* are not covered by this page"). That is a sourced answer, and the loose match filed it as
|
|
20
|
+
* a coverage miss. Observed twice in 5 reps of scripts/fetch-url-normalise-ab.ts once the
|
|
21
|
+
* rewrite started delivering pages that could half-answer; never in the 84 recorded corpus
|
|
22
|
+
* fetches, where 11 of 11 sentinel answers are the bare sentinel — so tightening it changes
|
|
23
|
+
* no recorded verdict.
|
|
24
|
+
*/
|
|
25
|
+
const NOT_COVERED_RE = /^not covered by this page[.\s]*$/i;
|
|
15
26
|
const childArgs = () => [...childBaseArgs(), '--no-tools'];
|
|
27
|
+
/**
|
|
28
|
+
* `github.com/{owner}/{repo}/blob/{ref}/{path}` renders the file through a client-side
|
|
29
|
+
* viewer, so the HTML we clean carries GitHub chrome ("Sign in", "Appearance settings")
|
|
30
|
+
* and none of the file. Every one of the 8 blob URLs in the three-project research-cache
|
|
31
|
+
* corpus came back a non-answer for that reason. `raw.githubusercontent.com` serves the
|
|
32
|
+
* same bytes as text/plain.
|
|
33
|
+
*
|
|
34
|
+
* Only the URL handed to the fetcher is rewritten — the caller's URL still keys the cache
|
|
35
|
+
* and still supplies the #fragment — so a worker that retries the raw URL by hand hits the
|
|
36
|
+
* cache, and a run's own recorded URLs stay what the run asked for.
|
|
37
|
+
*
|
|
38
|
+
* The query string is dropped: every blob query param (`?plain=1`, `?w=1`, …) is a viewer
|
|
39
|
+
* setting with no meaning on raw. The #fragment is dropped from the request too (it never
|
|
40
|
+
* reaches a server) but is preserved for {@link selectContent} via the original URL.
|
|
41
|
+
*/
|
|
42
|
+
const GH_BLOB_RE = /^https?:\/\/(?:www\.)?github\.com\/([^/?#]+)\/([^/?#]+)\/blob\/([^?#]+?)\/*(?:[?#].*)?$/i;
|
|
43
|
+
export function normaliseSourceUrl(url) {
|
|
44
|
+
const m = GH_BLOB_RE.exec(url.trim());
|
|
45
|
+
if (!m)
|
|
46
|
+
return url;
|
|
47
|
+
const [, owner, repo, refAndPath] = m;
|
|
48
|
+
// `/blob/{ref}/{path}` — a bare `/blob/{ref}` with no path names no file.
|
|
49
|
+
if (!refAndPath.includes('/'))
|
|
50
|
+
return url;
|
|
51
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${refAndPath}`;
|
|
52
|
+
}
|
|
16
53
|
export async function fetchRaw(input) {
|
|
17
54
|
const fetchAndCleanFn = input.fetchAndClean ?? defaultFetchAndClean;
|
|
18
|
-
const cleaned = await fetchAndCleanFn(input.url, {
|
|
55
|
+
const cleaned = await fetchAndCleanFn(normaliseSourceUrl(input.url), {
|
|
56
|
+
signal: input.signal
|
|
57
|
+
});
|
|
19
58
|
return { markdown: cleaned.markdown, finalUrl: cleaned.finalUrl, title: cleaned.title };
|
|
20
59
|
}
|
|
21
60
|
export async function fetchFocused(input) {
|
|
22
61
|
const fetchAndCleanFn = input.fetchAndClean ?? defaultFetchAndClean;
|
|
23
62
|
const spawnFn = input.spawn ?? defaultSpawn;
|
|
24
63
|
const strategy = input.strategy ?? shippedStrategy;
|
|
25
|
-
const
|
|
64
|
+
const fetchedUrl = normaliseSourceUrl(input.url);
|
|
65
|
+
const cleaned = await fetchAndCleanFn(fetchedUrl, { signal: input.signal });
|
|
26
66
|
// The #fragment is a client-side concern the server never sees, so anchor from the
|
|
27
67
|
// ORIGINALLY REQUESTED url, not the post-redirect finalUrl (which will have dropped it).
|
|
28
68
|
const selected = strategy.selectContent(cleaned.markdown, input.url);
|
|
@@ -60,6 +100,7 @@ export async function fetchFocused(input) {
|
|
|
60
100
|
};
|
|
61
101
|
}
|
|
62
102
|
const parsed = parseChildOutput(childResult.stdout);
|
|
103
|
+
const coverageMiss = NOT_COVERED_RE.test(parsed.answer.trim());
|
|
63
104
|
// Verify against the FULL page, not the anchored slice: the slice is a substring of it,
|
|
64
105
|
// so a genuine excerpt still verifies, and an excerpt the child pulled from memory still
|
|
65
106
|
// fails — the detector's discrimination is unchanged by fragment anchoring.
|
|
@@ -69,12 +110,32 @@ export async function fetchFocused(input) {
|
|
|
69
110
|
excerpt: parsed.excerpt,
|
|
70
111
|
excerptVerified: check?.verified,
|
|
71
112
|
excerptCheck: check,
|
|
72
|
-
coverageMiss
|
|
113
|
+
coverageMiss,
|
|
114
|
+
nextStep: coverageMiss ? coverageMissNextStep(input.url, fetchedUrl) : undefined,
|
|
73
115
|
childExitCode: 0,
|
|
74
116
|
aborted: false,
|
|
75
117
|
...base
|
|
76
118
|
};
|
|
77
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* The instruction that goes with a coverage miss. It says two things the bare sentinel does
|
|
122
|
+
* not: WHICH page was actually read (after the blob rewrite these differ, and a worker that
|
|
123
|
+
* cannot see that would "retry" the raw URL it already got), and that re-reading this page
|
|
124
|
+
* with a reworded question returns the same answer — so the next move is a different URL.
|
|
125
|
+
*
|
|
126
|
+
* Deliberately not a suggestion of WHICH other URL. Nothing at this layer knows one, and
|
|
127
|
+
* naming a guess is how a dead end becomes two dead ends.
|
|
128
|
+
*/
|
|
129
|
+
export function coverageMissNextStep(requestedUrl, fetchedUrl) {
|
|
130
|
+
const rewritten = fetchedUrl !== requestedUrl ?
|
|
131
|
+
` ${requestedUrl} is a GitHub file viewer whose HTML does not carry the file, so the`
|
|
132
|
+
+ ` file itself was already read from ${fetchedUrl} — fetching that raw URL by hand`
|
|
133
|
+
+ ` returns exactly this.`
|
|
134
|
+
: '';
|
|
135
|
+
return (`NEXT STEP: this page does not contain the answer.${rewritten}`
|
|
136
|
+
+ ` Asking ${fetchedUrl} the same question a different way returns this same result —`
|
|
137
|
+
+ ` do not re-read it. Try a different URL, or search for one.`);
|
|
138
|
+
}
|
|
78
139
|
/** Read the #fragment from a URL. Empty string when there is none. */
|
|
79
140
|
function fragmentOf(url) {
|
|
80
141
|
const h = url.indexOf('#');
|
|
@@ -52,7 +52,11 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
52
52
|
if (failure !== null) {
|
|
53
53
|
return { text: failure, details: { childExitCode: result.childExitCode } };
|
|
54
54
|
}
|
|
55
|
-
const
|
|
55
|
+
const body = formatResultText({ answer: result.answer, excerpt: result.excerpt }, result.excerptVerified) || '(no output)';
|
|
56
|
+
// The coverage miss is the one outcome that carries an instruction. It goes
|
|
57
|
+
// in the TEXT, not only in details: details are for the harness, and the
|
|
58
|
+
// worker acts on what it reads.
|
|
59
|
+
const text = result.nextStep ? `${body}\n\n${result.nextStep}` : body;
|
|
56
60
|
return {
|
|
57
61
|
text,
|
|
58
62
|
details: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.3",
|
|
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",
|