@mjasnikovs/pi-task 0.38.23 → 0.38.24
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 +2 -2
- package/dist/config/reasoning-args.d.ts +12 -1
- package/dist/config/reasoning-args.js +5 -2
- package/dist/config/reasoning.d.ts +47 -6
- package/dist/config/reasoning.js +84 -9
- package/dist/config/register.d.ts +50 -26
- package/dist/config/register.js +96 -80
- package/dist/shared/reasoning-capability.d.ts +2 -5
- package/dist/shared/reasoning-capability.js +31 -4
- package/dist/task/auto-orchestrator.d.ts +2 -0
- package/dist/task/auto-orchestrator.js +28 -41
- package/dist/task/child-runner.d.ts +89 -24
- package/dist/task/child-runner.js +67 -46
- package/dist/task/orchestrator.d.ts +14 -20
- package/dist/task/orchestrator.js +12 -9
- package/dist/task/phases.d.ts +0 -23
- package/dist/task/phases.js +46 -459
- package/dist/task/question-dialog.d.ts +56 -0
- package/dist/task/question-dialog.js +53 -0
- package/dist/task/research-worker.d.ts +180 -0
- package/dist/task/research-worker.js +432 -0
- package/dist/workers/brave-warning.js +4 -30
- package/dist/workers/docs-core.d.ts +8 -4
- package/dist/workers/docs-core.js +30 -21
- package/dist/workers/docs-lookup.d.ts +72 -0
- package/dist/workers/docs-lookup.js +53 -0
- package/dist/workers/docs-project.d.ts +9 -0
- package/dist/workers/docs-project.js +15 -0
- package/dist/workers/pi-worker-core.d.ts +87 -1
- package/dist/workers/pi-worker-core.js +3 -7
- package/dist/workers/pi-worker-docs.js +27 -31
- package/dist/workers/reasoning-warning.d.ts +10 -16
- package/dist/workers/reasoning-warning.js +25 -57
- package/dist/workers/session-hint.d.ts +37 -0
- package/dist/workers/session-hint.js +82 -0
- package/dist/workers/worker-failure.d.ts +34 -0
- package/dist/workers/worker-failure.js +27 -16
- package/dist/workers/worker-kill.d.ts +84 -0
- package/dist/workers/worker-kill.js +124 -0
- package/package.json +1 -1
- package/dist/task/reasoning-groups.d.ts +0 -36
- package/dist/task/reasoning-groups.js +0 -36
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The docs TAIL — concatenate the chunks, extract against them, verify the
|
|
3
|
+
* citation, format the answer — written once, with the corpus as a row.
|
|
4
|
+
*
|
|
5
|
+
* WHY. This sequence existed three times: the project-source arm of
|
|
6
|
+
* `pi-worker-docs`, its package arm, and `docsFocused` in `docs-core`. The fetch
|
|
7
|
+
* channel proves the shape is avoidable — `fetchFocused` is one core and
|
|
8
|
+
* `pi-worker-fetch`'s `run` is 60 lines, while the docs registration was 293 for
|
|
9
|
+
* the same job over two corpora.
|
|
10
|
+
*
|
|
11
|
+
* The copies had drifted, in the place hand-flattening always drifts: the
|
|
12
|
+
* package arm's ERROR path dropped `autoInstallPin`, which both of its sibling
|
|
13
|
+
* paths keep — so a package that was auto-installed and then failed to re-resolve
|
|
14
|
+
* lost the `versionSource`/`declaredRange` provenance the last defect in this
|
|
15
|
+
* area was about.
|
|
16
|
+
*
|
|
17
|
+
* A CORPUS is what genuinely varies: the prompt, the header the answer is
|
|
18
|
+
* introduced by, and what an abort of it is called. Everything a corpus does NOT
|
|
19
|
+
* vary — where the content comes from, the version banner, the type-only
|
|
20
|
+
* detector, the details bag — stays with its caller, because those differ in kind
|
|
21
|
+
* and not in value.
|
|
22
|
+
*/
|
|
23
|
+
import type { SpawnFn } from '../shared/child-process.js';
|
|
24
|
+
import { type FocusedAnswer, type FocusedFailure } from './focused-extractor.js';
|
|
25
|
+
/** The two corpora a docs lookup can read. A third (`page`) already has a prompt. */
|
|
26
|
+
export type DocsCorpusId = 'package' | 'project';
|
|
27
|
+
export interface DocsCorpus {
|
|
28
|
+
id: DocsCorpusId;
|
|
29
|
+
/** The extraction prompt for this corpus, over the concatenated content. */
|
|
30
|
+
buildPrompt: (query: string, content: string) => string;
|
|
31
|
+
/** The line the formatted answer is introduced by. */
|
|
32
|
+
header: string;
|
|
33
|
+
/** What an abort of this corpus's lookup is called, in the failure text. */
|
|
34
|
+
abortedMessage: string;
|
|
35
|
+
}
|
|
36
|
+
export interface DocsLookupInput {
|
|
37
|
+
corpus: DocsCorpus;
|
|
38
|
+
/** The retrieved chunks, in retrieval order. */
|
|
39
|
+
chunks: ReadonlyArray<{
|
|
40
|
+
content: string;
|
|
41
|
+
}>;
|
|
42
|
+
query: string;
|
|
43
|
+
cwd: string;
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
spawn?: SpawnFn;
|
|
46
|
+
/**
|
|
47
|
+
* The `extraction` group's `--thinking` fragment. Resolved by the CALLER so
|
|
48
|
+
* this module — like the extractor it wraps — never reads ambient config.
|
|
49
|
+
*/
|
|
50
|
+
thinking: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
export type DocsLookup = {
|
|
53
|
+
kind: 'answer';
|
|
54
|
+
/** The formatted answer: header, answer, and the verified excerpt. */
|
|
55
|
+
body: string;
|
|
56
|
+
/** Exactly what was prompted with, and what the citation was verified against. */
|
|
57
|
+
content: string;
|
|
58
|
+
extraction: FocusedAnswer;
|
|
59
|
+
/** Undefined when there was no excerpt to check. */
|
|
60
|
+
excerptVerified?: boolean;
|
|
61
|
+
} | {
|
|
62
|
+
kind: 'failed';
|
|
63
|
+
extraction: FocusedFailure;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Run one docs lookup over already-retrieved chunks.
|
|
67
|
+
*
|
|
68
|
+
* The citation is verified against exactly the text that was prompted with — the
|
|
69
|
+
* concatenation, not a superset. (`fetch` is the one site that verifies against a
|
|
70
|
+
* superset; see `FocusedRequest.verifyAgainst`.)
|
|
71
|
+
*/
|
|
72
|
+
export declare function docsLookup(input: DocsLookupInput): Promise<DocsLookup>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The docs TAIL — concatenate the chunks, extract against them, verify the
|
|
3
|
+
* citation, format the answer — written once, with the corpus as a row.
|
|
4
|
+
*
|
|
5
|
+
* WHY. This sequence existed three times: the project-source arm of
|
|
6
|
+
* `pi-worker-docs`, its package arm, and `docsFocused` in `docs-core`. The fetch
|
|
7
|
+
* channel proves the shape is avoidable — `fetchFocused` is one core and
|
|
8
|
+
* `pi-worker-fetch`'s `run` is 60 lines, while the docs registration was 293 for
|
|
9
|
+
* the same job over two corpora.
|
|
10
|
+
*
|
|
11
|
+
* The copies had drifted, in the place hand-flattening always drifts: the
|
|
12
|
+
* package arm's ERROR path dropped `autoInstallPin`, which both of its sibling
|
|
13
|
+
* paths keep — so a package that was auto-installed and then failed to re-resolve
|
|
14
|
+
* lost the `versionSource`/`declaredRange` provenance the last defect in this
|
|
15
|
+
* area was about.
|
|
16
|
+
*
|
|
17
|
+
* A CORPUS is what genuinely varies: the prompt, the header the answer is
|
|
18
|
+
* introduced by, and what an abort of it is called. Everything a corpus does NOT
|
|
19
|
+
* vary — where the content comes from, the version banner, the type-only
|
|
20
|
+
* detector, the details bag — stays with its caller, because those differ in kind
|
|
21
|
+
* and not in value.
|
|
22
|
+
*/
|
|
23
|
+
import { formatResultText } from '../shared/child-output.js';
|
|
24
|
+
import { runFocusedExtraction } from './focused-extractor.js';
|
|
25
|
+
/**
|
|
26
|
+
* Run one docs lookup over already-retrieved chunks.
|
|
27
|
+
*
|
|
28
|
+
* The citation is verified against exactly the text that was prompted with — the
|
|
29
|
+
* concatenation, not a superset. (`fetch` is the one site that verifies against a
|
|
30
|
+
* superset; see `FocusedRequest.verifyAgainst`.)
|
|
31
|
+
*/
|
|
32
|
+
export async function docsLookup(input) {
|
|
33
|
+
const content = input.chunks.map(c => c.content).join('\n\n');
|
|
34
|
+
const extraction = await runFocusedExtraction({
|
|
35
|
+
prompt: input.corpus.buildPrompt(input.query, content),
|
|
36
|
+
verifyAgainst: content,
|
|
37
|
+
cwd: input.cwd,
|
|
38
|
+
signal: input.signal,
|
|
39
|
+
spawn: input.spawn,
|
|
40
|
+
thinking: input.thinking,
|
|
41
|
+
abortedMessage: input.corpus.abortedMessage
|
|
42
|
+
});
|
|
43
|
+
if (!extraction.ok)
|
|
44
|
+
return { kind: 'failed', extraction };
|
|
45
|
+
const excerptVerified = extraction.excerptVerified;
|
|
46
|
+
return {
|
|
47
|
+
kind: 'answer',
|
|
48
|
+
body: formatResultText(input.corpus.header, extraction, excerptVerified),
|
|
49
|
+
content,
|
|
50
|
+
extraction,
|
|
51
|
+
excerptVerified
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CacheHandle } from './docs-cache.js';
|
|
2
2
|
import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
|
|
3
3
|
import type { RetrievedChunk } from './docs-retrieve.js';
|
|
4
|
+
import type { DocsCorpus } from './docs-lookup.js';
|
|
4
5
|
export declare function getProjectName(cwd: string): string;
|
|
5
6
|
export declare function cwdKey(cwd: string): string;
|
|
6
7
|
/**
|
|
@@ -50,3 +51,11 @@ export declare function projectDocsRaw(cache: CacheHandle, cwd: string, query: s
|
|
|
50
51
|
/** How to enumerate the project's sources. See getProjectFiles. */
|
|
51
52
|
listFiles?: (cwd: string) => string[]): ProjectDocsRawResult;
|
|
52
53
|
export declare function buildProjectPrompt(projectName: string, query: string, content: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* The PROJECT corpus row: the current repo's own indexed `.ts`/`.tsx` source.
|
|
56
|
+
*
|
|
57
|
+
* Named after the project so a reader of the answer can tell a project-source
|
|
58
|
+
* citation from a package one at a glance — they are read and cited the same way
|
|
59
|
+
* and mean very different things.
|
|
60
|
+
*/
|
|
61
|
+
export declare function projectCorpus(projectName: string): DocsCorpus;
|
|
@@ -208,3 +208,18 @@ export function buildProjectPrompt(projectName, query, content) {
|
|
|
208
208
|
content
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* The PROJECT corpus row: the current repo's own indexed `.ts`/`.tsx` source.
|
|
213
|
+
*
|
|
214
|
+
* Named after the project so a reader of the answer can tell a project-source
|
|
215
|
+
* citation from a package one at a glance — they are read and cited the same way
|
|
216
|
+
* and mean very different things.
|
|
217
|
+
*/
|
|
218
|
+
export function projectCorpus(projectName) {
|
|
219
|
+
return {
|
|
220
|
+
id: 'project',
|
|
221
|
+
buildPrompt: (query, content) => buildProjectPrompt(projectName, query, content),
|
|
222
|
+
header: `Per ${projectName} (project source):`,
|
|
223
|
+
abortedMessage: 'Project docs lookup aborted.'
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ContextSnapshot, type LoopHit, type SpawnFn } from '../shared/child-process.js';
|
|
2
|
+
import { RESTART_ORDER } from './worker-kill.js';
|
|
2
3
|
/**
|
|
3
4
|
* Tool calls that can GROUND an APIS claim — i.e. return content a signature or
|
|
4
5
|
* command could be cited from. `pi-worker-docs` (the primary), `read` and `grep`
|
|
@@ -246,7 +247,7 @@ export interface RunWorkerInput {
|
|
|
246
247
|
* Why an attempt was thrown away. One value per restart branch in runWorker, so
|
|
247
248
|
* a log line naming the reason points at exactly one piece of code.
|
|
248
249
|
*/
|
|
249
|
-
export type WorkerRestartReason =
|
|
250
|
+
export type WorkerRestartReason = (typeof RESTART_ORDER)[number];
|
|
250
251
|
/** One DISCARDED attempt: its cause and the wall clock it consumed and lost. */
|
|
251
252
|
export interface WorkerRestart {
|
|
252
253
|
/** 1-based number of the attempt being discarded (the 1st restart ends attempt 1). */
|
|
@@ -411,4 +412,89 @@ export interface RunWorkerResult {
|
|
|
411
412
|
* itself, or a caller asking for 10s would silently get 30.
|
|
412
413
|
*/
|
|
413
414
|
export declare function commandCeilingForAttempt(baseMs: number, priorHangs: number): number;
|
|
415
|
+
/**
|
|
416
|
+
* Everything the restart ladder reads about one finished attempt, plus the
|
|
417
|
+
* budgets it draws on. Assembled once per attempt so the rules below can be
|
|
418
|
+
* module-level data instead of six `if` blocks welded into `runWorker`'s closure.
|
|
419
|
+
*/
|
|
420
|
+
interface RestartState {
|
|
421
|
+
loopHit?: LoopHit;
|
|
422
|
+
commandKill?: CommandKill;
|
|
423
|
+
streamStalled?: {
|
|
424
|
+
idleMs: number;
|
|
425
|
+
};
|
|
426
|
+
timedOut: boolean;
|
|
427
|
+
modelError?: string;
|
|
428
|
+
leaked: string | null;
|
|
429
|
+
/** The cap this attempt actually died against — the SCALE arm moves it. */
|
|
430
|
+
effectiveCapMs: number;
|
|
431
|
+
/** The child's tool string, which decides whether its edits can persist. */
|
|
432
|
+
tools: string;
|
|
433
|
+
restartBudgetSpent: number;
|
|
434
|
+
connRetries: number;
|
|
435
|
+
connectionRetries: number;
|
|
436
|
+
leakRetries: number;
|
|
437
|
+
}
|
|
438
|
+
/** What a rule does to the budgets when it fires. */
|
|
439
|
+
interface RestartCounters {
|
|
440
|
+
/** Consume one of the shared loop/timeout/connection restarts. */
|
|
441
|
+
shared?: boolean;
|
|
442
|
+
/** Consume one of the leaked-tool-call retries (a separate budget). */
|
|
443
|
+
leak?: boolean;
|
|
444
|
+
/** Count a WATCHDOG kill specifically — drives the command-ceiling halving. */
|
|
445
|
+
hang?: boolean;
|
|
446
|
+
/** Count a CONNECTION restart specifically — drives the backoff schedule. */
|
|
447
|
+
connection?: boolean;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* One restartable failure: how to spot it, what to tell the fresh child, which
|
|
451
|
+
* budget it spends, and how long to wait first.
|
|
452
|
+
*/
|
|
453
|
+
interface RestartRule {
|
|
454
|
+
reason: WorkerRestartReason;
|
|
455
|
+
/**
|
|
456
|
+
* Does this rule apply to the attempt, and is its budget unspent? Returns
|
|
457
|
+
* the restart's detail line, or null to fall through to the next rule.
|
|
458
|
+
*
|
|
459
|
+
* Detection and budget are ONE test on purpose. An out-of-budget failure must
|
|
460
|
+
* fall through to the return path, not stop the ladder — a loop kill with the
|
|
461
|
+
* shared budget spent still has to let the plain-abort return happen.
|
|
462
|
+
*/
|
|
463
|
+
detect: (s: RestartState) => {
|
|
464
|
+
detail: string;
|
|
465
|
+
} | null;
|
|
466
|
+
/**
|
|
467
|
+
* The corrective preamble prepended to the next attempt's prompt. Omitted by
|
|
468
|
+
* `connection-error` alone: nothing the model did caused a dropped socket, so
|
|
469
|
+
* there is nothing to correct — and any hint already in flight from an
|
|
470
|
+
* earlier restart must survive the retry rather than be cleared by it.
|
|
471
|
+
*/
|
|
472
|
+
hint?: (s: RestartState) => string;
|
|
473
|
+
counters: RestartCounters;
|
|
474
|
+
/** Backoff before re-spawning, in ms. Only the connection rule waits. */
|
|
475
|
+
backoffMs?: (s: RestartState) => number;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* The restart ladder, in precedence order. FIRST MATCH WINS.
|
|
479
|
+
*
|
|
480
|
+
* Read the `!loopHit` guards as "a loop kill outranks me even when it has no
|
|
481
|
+
* budget left". They are not redundant with row order: when a loop is detected
|
|
482
|
+
* but the shared budget is spent, row 1 declines, and without those guards row 2
|
|
483
|
+
* or 4 would then restart the same runaway child under a hint that does not
|
|
484
|
+
* describe why it died.
|
|
485
|
+
*
|
|
486
|
+
* The whole ritual — check the budget, set the hint, spend the counters, record
|
|
487
|
+
* and announce the discarded attempt, sleep, re-spawn — belongs to the loop in
|
|
488
|
+
* `runWorker`, so a new failure mode is one row here and cannot be added without
|
|
489
|
+
* becoming visible in `restarts`.
|
|
490
|
+
*/
|
|
491
|
+
export declare const RESTART_RULES: readonly RestartRule[];
|
|
492
|
+
/** What the command watchdog recorded when it killed an attempt. */
|
|
493
|
+
interface CommandKill {
|
|
494
|
+
toolName: string;
|
|
495
|
+
timeoutMs: number;
|
|
496
|
+
/** The command line itself, when the tool carried one — quoted into the hint
|
|
497
|
+
* so the fresh child knows which call it must not repeat unbounded. */
|
|
498
|
+
detail?: string;
|
|
499
|
+
}
|
|
414
500
|
export declare function runWorker(input: RunWorkerInput): Promise<RunWorkerResult>;
|
|
@@ -10,6 +10,7 @@ import { detectLeakedToolCall, leakedToolCallHint, MAX_LEAK_RETRIES } from '../s
|
|
|
10
10
|
import { discoverModelEndpoints, probeModelEndpoints } from '../shared/model-endpoint.js';
|
|
11
11
|
import { streamStallHint } from '../shared/stream-watchdog.js';
|
|
12
12
|
import { classifyWorkerFailure } from './worker-failure.js';
|
|
13
|
+
import { CARRY_FORWARD_IDS } from './worker-kill.js';
|
|
13
14
|
// `--mode json` makes pi emit structured events as they happen instead of
|
|
14
15
|
// buffering the assistant text and flushing on exit. That matters for the
|
|
15
16
|
// wait/work timing split: in text mode the first stdout chunk only arrives at
|
|
@@ -89,12 +90,7 @@ const CARRY_FORWARD_LIMIT = 24_000;
|
|
|
89
90
|
* first is by definition the same call repeated, the second is malformed
|
|
90
91
|
* protocol text, and replaying either would feed the failure back to itself.
|
|
91
92
|
*/
|
|
92
|
-
const CARRY_FORWARD_REASONS =
|
|
93
|
-
'worker-timeout',
|
|
94
|
-
'command-timeout',
|
|
95
|
-
'stream-stall',
|
|
96
|
-
'connection-error'
|
|
97
|
-
]);
|
|
93
|
+
const CARRY_FORWARD_REASONS = CARRY_FORWARD_IDS;
|
|
98
94
|
/**
|
|
99
95
|
* Does this partial output carry ANSWER CONTENT, or is it the model clearing its
|
|
100
96
|
* throat?
|
|
@@ -276,7 +272,7 @@ export function commandCeilingForAttempt(baseMs, priorHangs) {
|
|
|
276
272
|
* `runWorker`, so a new failure mode is one row here and cannot be added without
|
|
277
273
|
* becoming visible in `restarts`.
|
|
278
274
|
*/
|
|
279
|
-
const RESTART_RULES = [
|
|
275
|
+
export const RESTART_RULES = [
|
|
280
276
|
{
|
|
281
277
|
// A loop-kill gets the same restart-with-hint treatment every other phase
|
|
282
278
|
// already gets (runPhaseChild) — name the offending call so the
|
|
@@ -3,15 +3,15 @@ import { Type } from '@sinclair/typebox';
|
|
|
3
3
|
import { Text } from '@earendil-works/pi-tui';
|
|
4
4
|
import { openCache as defaultOpenCache } from './docs-cache.js';
|
|
5
5
|
import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { docsLookup } from './docs-lookup.js';
|
|
7
|
+
import { projectCorpus } from './docs-project.js';
|
|
8
|
+
import { docsRaw, packageCorpus, buildVersionBanner } from './docs-core.js';
|
|
8
9
|
import { formatNpmVersionSection } from './npm-version.js';
|
|
9
|
-
import { runFocusedExtraction } from './focused-extractor.js';
|
|
10
10
|
import { childFailureReason, makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
|
|
11
11
|
import { isTypeOnlyAnswer } from '../task/type-only-answer.js';
|
|
12
12
|
import { logDocsAnswer } from './typeonly-log.js';
|
|
13
13
|
import { normalizeQuery } from './research-cache.js';
|
|
14
|
-
import { projectDocsRaw
|
|
14
|
+
import { projectDocsRaw } from './docs-project.js';
|
|
15
15
|
import { projectDocsBudget, projectDocsBudgetExhausted } from '../task/research-fanout-budget.js';
|
|
16
16
|
import { isAbstention } from './abstention.js';
|
|
17
17
|
import { groupThinkingArgs } from '../config/reasoning-args.js';
|
|
@@ -128,20 +128,18 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
128
128
|
// It was DEAD in production (pi runs under node) and BYPASSED under bun test
|
|
129
129
|
// (internals.spawn is always injected), i.e. untested, unreachable, and wrong.
|
|
130
130
|
const spawn = internals.spawn ?? defaultSpawn;
|
|
131
|
-
// Both
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
// Both arms below run the SAME tail — concatenate, extract, verify,
|
|
132
|
+
// format — through `docsLookup`; only the CORPUS differs. The
|
|
133
|
+
// `extraction` group's level is resolved here so neither the lookup
|
|
134
|
+
// nor the extractor reads ambient config.
|
|
135
|
+
const lookup = (corpus, chunks) => docsLookup({
|
|
136
|
+
corpus,
|
|
137
|
+
chunks,
|
|
138
|
+
query: params.query,
|
|
138
139
|
cwd: ctx.cwd,
|
|
139
140
|
signal,
|
|
140
141
|
spawn,
|
|
141
|
-
|
|
142
|
-
// extractor itself never reads ambient config.
|
|
143
|
-
thinking: groupThinkingArgs('extraction'),
|
|
144
|
-
abortedMessage
|
|
142
|
+
thinking: groupThinkingArgs('extraction')
|
|
145
143
|
});
|
|
146
144
|
// ── Project source lookup ───────────────────────────────────────
|
|
147
145
|
if (params.module === '.') {
|
|
@@ -182,12 +180,10 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
182
180
|
indexedFiles: filesIngested,
|
|
183
181
|
indexingMs
|
|
184
182
|
};
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const verified = extraction.excerptVerified;
|
|
190
|
-
const text = formatResultText(`Per ${projectName} (project source):`, extraction, verified);
|
|
183
|
+
const r = await lookup(projectCorpus(projectName), chunks);
|
|
184
|
+
if (r.kind === 'failed')
|
|
185
|
+
return docsFailureResult(r.extraction, baseDetails, '');
|
|
186
|
+
const { extraction, excerptVerified: verified, body: text } = r;
|
|
191
187
|
// SAME instrumentation channel as the package path below, extended to the
|
|
192
188
|
// project-source branch because that branch is the MAJORITY of what
|
|
193
189
|
// worker:apis asks — 13 of 17 docs calls in run 15's fatal task, 7 of 12 in
|
|
@@ -243,14 +239,16 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
243
239
|
hitCache: rawResult.hitCache,
|
|
244
240
|
cacheError: rawResult.cacheError,
|
|
245
241
|
autoInstalled: rawResult.autoInstalled,
|
|
242
|
+
// BUG FIX. Both sibling arms carry the pin and this one dropped
|
|
243
|
+
// it, so a package that WAS auto-installed and then failed to
|
|
244
|
+
// re-resolve lost its `versionSource`/`declaredRange` — the
|
|
245
|
+
// provenance the last defect in this area was about. `docsRaw`
|
|
246
|
+
// sets `autoInstallPin` on three of its five error returns.
|
|
247
|
+
...pinDetails(rawResult.autoInstallPin),
|
|
246
248
|
...npmDetails
|
|
247
249
|
};
|
|
248
250
|
return workerUnavailable(npmHeader + rawResult.message, details, 'docs-error');
|
|
249
251
|
}
|
|
250
|
-
if (rawResult.kind === 'not_installed') {
|
|
251
|
-
return workerUnavailable(npmHeader
|
|
252
|
-
+ `Package "${rawResult.pkg}" is not installed and auto-install failed.`, { resolveError: 'not_installed', ...npmDetails }, 'not-installed');
|
|
253
|
-
}
|
|
254
252
|
if (rawResult.kind === 'no_chunks') {
|
|
255
253
|
const banner = buildVersionBanner(rawResult.autoInstallPin, rawResult.pkg.name, rawResult.pkg.version, ctx.cwd);
|
|
256
254
|
// The package resolved and genuinely ships nothing to read — an
|
|
@@ -280,13 +278,11 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
280
278
|
...pinDetails(rawResult.autoInstallPin),
|
|
281
279
|
...npmDetails
|
|
282
280
|
};
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return docsFailureResult(extraction, baseDetails, versionBanner + npmHeader);
|
|
281
|
+
const r = await lookup(packageCorpus(pkg), chunks);
|
|
282
|
+
if (r.kind === 'failed') {
|
|
283
|
+
return docsFailureResult(r.extraction, baseDetails, versionBanner + npmHeader);
|
|
287
284
|
}
|
|
288
|
-
const verified =
|
|
289
|
-
const body = formatResultText(packageHeader(pkg), extraction, verified);
|
|
285
|
+
const { extraction, excerptVerified: verified, body, content: concatenated } = r;
|
|
290
286
|
// F-2: a TYPE-ONLY answer is the dangerous failure. "unclear from this package"
|
|
291
287
|
// is honest and already escalates; a signature is a well-formed, confident,
|
|
292
288
|
// on-topic answer that names the very parameter asked about, so the worker
|
|
@@ -20,22 +20,9 @@
|
|
|
20
20
|
* model and nothing renders at all.
|
|
21
21
|
*/
|
|
22
22
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
23
|
-
import { type
|
|
24
|
-
import { REASONING_GROUPS, resolveReasoning } from '../config/reasoning.js';
|
|
23
|
+
import { type GroupSetting, type ReasoningGroup } from '../config/reasoning.js';
|
|
25
24
|
import { type ReasoningMismatch } from '../shared/reasoning-capability.js';
|
|
26
|
-
|
|
27
|
-
export type GroupSettings = Array<{
|
|
28
|
-
group: (typeof REASONING_GROUPS)[number];
|
|
29
|
-
setting: ReturnType<typeof resolveReasoning>;
|
|
30
|
-
}>;
|
|
31
|
-
/**
|
|
32
|
-
* Read every group's effective setting from a config.
|
|
33
|
-
*
|
|
34
|
-
* Takes the config rather than calling `getConfig()` so the caller decides where
|
|
35
|
-
* it comes from. A test that has to mutate the live singleton to drive this is a
|
|
36
|
-
* test whose result depends on whatever the developer had saved before it ran.
|
|
37
|
-
*/
|
|
38
|
-
export declare function settingsFrom(cfg: PiTaskConfig): GroupSettings;
|
|
25
|
+
import { type ChatTemplateCaps } from '../shared/model-endpoint.js';
|
|
39
26
|
/**
|
|
40
27
|
* The warning line for a set of mismatches.
|
|
41
28
|
*
|
|
@@ -61,4 +48,11 @@ export declare function registerReasoningWarning(pi: ExtensionAPI,
|
|
|
61
48
|
* `session_start` so a /task-config change since the last session counts.
|
|
62
49
|
* Injected by tests, which must not depend on the developer's saved config.
|
|
63
50
|
*/
|
|
64
|
-
readSettings?: () =>
|
|
51
|
+
readSettings?: () => Readonly<Record<ReasoningGroup, GroupSetting>>,
|
|
52
|
+
/**
|
|
53
|
+
* The server-side chat-template probe. Injected so the REFINE path — the
|
|
54
|
+
* only half of this hint that talks to a network — is drivable at all; with
|
|
55
|
+
* the real probe it is reachable only from a model entry carrying a
|
|
56
|
+
* `baseUrl`, which no test model has.
|
|
57
|
+
*/
|
|
58
|
+
probe?: (baseUrl: string) => Promise<ChatTemplateCaps | null>): void;
|
|
@@ -20,20 +20,11 @@
|
|
|
20
20
|
* model and nothing renders at all.
|
|
21
21
|
*/
|
|
22
22
|
import { getConfig } from '../config/config.js';
|
|
23
|
-
import {
|
|
23
|
+
import { effectiveReasoning } from '../config/reasoning.js';
|
|
24
24
|
import { reasoningMismatches } from '../shared/reasoning-capability.js';
|
|
25
25
|
import { probeChatTemplateCaps } from '../shared/model-endpoint.js';
|
|
26
|
+
import { registerSessionHint } from './session-hint.js';
|
|
26
27
|
const WIDGET_KEY = 'pi-task-reasoning-warning';
|
|
27
|
-
/**
|
|
28
|
-
* Read every group's effective setting from a config.
|
|
29
|
-
*
|
|
30
|
-
* Takes the config rather than calling `getConfig()` so the caller decides where
|
|
31
|
-
* it comes from. A test that has to mutate the live singleton to drive this is a
|
|
32
|
-
* test whose result depends on whatever the developer had saved before it ran.
|
|
33
|
-
*/
|
|
34
|
-
export function settingsFrom(cfg) {
|
|
35
|
-
return REASONING_GROUPS.map(group => ({ group, setting: resolveReasoning(group, cfg) }));
|
|
36
|
-
}
|
|
37
28
|
/**
|
|
38
29
|
* The warning line for a set of mismatches.
|
|
39
30
|
*
|
|
@@ -83,60 +74,37 @@ export function registerReasoningWarning(pi,
|
|
|
83
74
|
* `session_start` so a /task-config change since the last session counts.
|
|
84
75
|
* Injected by tests, which must not depend on the developer's saved config.
|
|
85
76
|
*/
|
|
86
|
-
readSettings = () =>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
77
|
+
readSettings = () => effectiveReasoning(getConfig()),
|
|
78
|
+
/**
|
|
79
|
+
* The server-side chat-template probe. Injected so the REFINE path — the
|
|
80
|
+
* only half of this hint that talks to a network — is drivable at all; with
|
|
81
|
+
* the real probe it is reachable only from a model entry carrying a
|
|
82
|
+
* `baseUrl`, which no test model has.
|
|
83
|
+
*/
|
|
84
|
+
probe = probeChatTemplateCaps) {
|
|
85
|
+
registerSessionHint(pi, WIDGET_KEY, ctx => {
|
|
92
86
|
const model = ctx.model;
|
|
93
87
|
const mismatches = reasoningMismatches(model, readSettings());
|
|
94
88
|
if (mismatches.length === 0)
|
|
95
|
-
return;
|
|
89
|
+
return null;
|
|
96
90
|
const base = formatReasoningWarning(model?.name ?? model?.id ?? 'unknown', mismatches);
|
|
97
91
|
if (base === null)
|
|
98
|
-
return;
|
|
99
|
-
let unsubscribe = null;
|
|
100
|
-
let cleared = false;
|
|
101
|
-
const clear = () => {
|
|
102
|
-
cleared = true;
|
|
103
|
-
try {
|
|
104
|
-
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
/* stale ctx after a session switch — nothing to clear */
|
|
108
|
-
}
|
|
109
|
-
unsubscribe?.();
|
|
110
|
-
unsubscribe = null;
|
|
111
|
-
};
|
|
112
|
-
const render = (text) => {
|
|
113
|
-
try {
|
|
114
|
-
ctx.ui.setWidget(WIDGET_KEY, [ctx.ui.theme.fg('warning', text)]);
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
if (!render(base))
|
|
122
|
-
return;
|
|
123
|
-
unsubscribe = ctx.ui.onTerminalInput(() => {
|
|
124
|
-
clear();
|
|
125
|
-
return undefined;
|
|
126
|
-
});
|
|
92
|
+
return null;
|
|
127
93
|
// Fire-and-forget: the server probe only ever REFINES the cause line, so
|
|
128
94
|
// it must not delay the warning or be able to prevent it. A 2s budget and
|
|
129
95
|
// a swallowed failure mean a non-llama.cpp backend costs nothing.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
96
|
+
const baseUrl = model?.baseUrl;
|
|
97
|
+
if (model === undefined || baseUrl === undefined || baseUrl === '')
|
|
98
|
+
return { text: base };
|
|
99
|
+
const declares = model.reasoning;
|
|
100
|
+
return {
|
|
101
|
+
text: base,
|
|
102
|
+
refine: probe(baseUrl).then(caps => {
|
|
103
|
+
if (caps === null)
|
|
104
|
+
return null;
|
|
105
|
+
const extra = formatCapabilityConflict(caps.supportsReasoningEffort, declares);
|
|
106
|
+
return extra === null ? null : base + extra;
|
|
138
107
|
})
|
|
139
|
-
|
|
140
|
-
}
|
|
108
|
+
};
|
|
141
109
|
});
|
|
142
110
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A one-line startup hint in the TUI, and the whole widget lifetime around it.
|
|
3
|
+
*
|
|
4
|
+
* WHY IT IS ONE MODULE. Two hints exist (brave-warning, reasoning-warning) and
|
|
5
|
+
* both had written the same ritual out: the `session_start` subscription, the
|
|
6
|
+
* TUI gate, the `setWidget` in a try/catch, the `onTerminalInput` that clears on
|
|
7
|
+
* the first keystroke, the unsubscribe, and the swallow for a stale ctx — down
|
|
8
|
+
* to a byte-identical comment. Two adapters is a real seam, so the ritual lives
|
|
9
|
+
* here once and each hint supplies only its `compose`.
|
|
10
|
+
*
|
|
11
|
+
* The REFINE half is why this is not just deduplication. A hint may learn
|
|
12
|
+
* something after it has painted (the reasoning hint probes the model's server),
|
|
13
|
+
* and the rule that a refinement must never repaint a widget the user already
|
|
14
|
+
* dismissed lived in one closure variable in one of the two files. It is now a
|
|
15
|
+
* property of this module, asserted once.
|
|
16
|
+
*/
|
|
17
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
18
|
+
export interface SessionHint {
|
|
19
|
+
/** The line to paint now. */
|
|
20
|
+
text: string;
|
|
21
|
+
/**
|
|
22
|
+
* A later refinement of that line, if this hint has one. Resolving to null —
|
|
23
|
+
* or rejecting — leaves the first line standing, so a refinement can never
|
|
24
|
+
* remove a warning it was only meant to sharpen. It is fire-and-forget: it
|
|
25
|
+
* cannot delay or prevent the first paint, and it is dropped if the user has
|
|
26
|
+
* already cleared the hint.
|
|
27
|
+
*/
|
|
28
|
+
refine?: Promise<string | null>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Register one startup hint.
|
|
32
|
+
*
|
|
33
|
+
* `compose` is the seam: it runs at `session_start` inside the TUI gate and
|
|
34
|
+
* returns the hint, or null to say nothing at all. Everything it returns is
|
|
35
|
+
* text; nothing about widgets, keystrokes or teardown reaches it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function registerSessionHint(pi: ExtensionAPI, key: string, compose: (ctx: ExtensionContext) => SessionHint | null): void;
|