@mjasnikovs/pi-task 0.38.23 → 0.38.25
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/gate-child.js +11 -11
- 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 +48 -464
- package/dist/task/question-dialog.d.ts +56 -0
- package/dist/task/question-dialog.js +53 -0
- package/dist/task/research-fanout-budget.d.ts +20 -0
- package/dist/task/research-fanout-budget.js +29 -0
- package/dist/task/research-worker.d.ts +183 -0
- package/dist/task/research-worker.js +429 -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 +112 -109
- package/dist/workers/pi-worker-core.js +33 -48
- package/dist/workers/pi-worker-docs.js +27 -31
- package/dist/workers/pi-worker.js +6 -0
- 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/dist/workers/worker-profiles.d.ts +314 -0
- package/dist/workers/worker-profiles.js +220 -0
- package/package.json +1 -1
- package/dist/task/reasoning-groups.d.ts +0 -36
- package/dist/task/reasoning-groups.js +0 -36
|
@@ -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
|
|
@@ -47,6 +47,12 @@ export function registerPiWorker(pi) {
|
|
|
47
47
|
prompt: params.prompt,
|
|
48
48
|
cwd: ctx.cwd,
|
|
49
49
|
signal,
|
|
50
|
+
// `adhoc` is every guard at its default — which is what this call
|
|
51
|
+
// site already got by naming none of them. Named now so it is a
|
|
52
|
+
// decision, and so the one asymmetry it carries (a FIXED 240s cap
|
|
53
|
+
// where a research worker gets 240s without progress) is written
|
|
54
|
+
// down. See the `adhoc` row in workers/worker-profiles.ts.
|
|
55
|
+
profile: 'adhoc',
|
|
50
56
|
thinking: groupThinkingArgs('research')
|
|
51
57
|
});
|
|
52
58
|
const details = { exitCode: result.exitCode };
|
|
@@ -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;
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Register one startup hint.
|
|
19
|
+
*
|
|
20
|
+
* `compose` is the seam: it runs at `session_start` inside the TUI gate and
|
|
21
|
+
* returns the hint, or null to say nothing at all. Everything it returns is
|
|
22
|
+
* text; nothing about widgets, keystrokes or teardown reaches it.
|
|
23
|
+
*/
|
|
24
|
+
export function registerSessionHint(pi, key, compose) {
|
|
25
|
+
pi.on('session_start', (_event, ctx) => {
|
|
26
|
+
// Terminal-only hint: needs an interactive TUI to render and to catch the
|
|
27
|
+
// keystroke that dismisses it.
|
|
28
|
+
if (ctx.mode !== 'tui')
|
|
29
|
+
return;
|
|
30
|
+
const hint = compose(ctx);
|
|
31
|
+
if (hint === null)
|
|
32
|
+
return;
|
|
33
|
+
let unsubscribe = null;
|
|
34
|
+
let cleared = false;
|
|
35
|
+
let painted = false;
|
|
36
|
+
const clear = () => {
|
|
37
|
+
cleared = true;
|
|
38
|
+
try {
|
|
39
|
+
ctx.ui.setWidget(key, undefined);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* stale ctx after a session switch — nothing to clear */
|
|
43
|
+
}
|
|
44
|
+
unsubscribe?.();
|
|
45
|
+
unsubscribe = null;
|
|
46
|
+
};
|
|
47
|
+
const render = (text) => {
|
|
48
|
+
try {
|
|
49
|
+
ctx.ui.setWidget(key, [ctx.ui.theme.fg('warning', text)]);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
// Handled BEFORE the paint can bail. `compose` builds this promise
|
|
57
|
+
// eagerly — the reasoning hint kicks its probe off inside it — so a
|
|
58
|
+
// rejection with the handler attached later is an unhandled rejection on
|
|
59
|
+
// exactly the path the try/catch around `setWidget` exists for: a stale
|
|
60
|
+
// ctx after a session switch. `refine`'s contract says a rejection leaves
|
|
61
|
+
// the first line standing, and that has to hold when there is no first
|
|
62
|
+
// line either.
|
|
63
|
+
if (hint.refine !== undefined) {
|
|
64
|
+
void hint.refine
|
|
65
|
+
.then(text => {
|
|
66
|
+
if (cleared || !painted || text === null)
|
|
67
|
+
return;
|
|
68
|
+
render(text);
|
|
69
|
+
})
|
|
70
|
+
.catch(() => { });
|
|
71
|
+
}
|
|
72
|
+
painted = render(hint.text);
|
|
73
|
+
if (!painted)
|
|
74
|
+
return;
|
|
75
|
+
// Disappear on any interaction — the first raw keystroke clears it.
|
|
76
|
+
// Returning undefined leaves the input untouched (we only observe it).
|
|
77
|
+
unsubscribe = ctx.ui.onTerminalInput(() => {
|
|
78
|
+
clear();
|
|
79
|
+
return undefined;
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* caused the bug impossible rather than merely fixed.
|
|
27
27
|
*/
|
|
28
28
|
import type { LoopHit } from '../task/loop-detector.js';
|
|
29
|
+
import type { WorkerKillId } from './worker-kill.js';
|
|
29
30
|
/**
|
|
30
31
|
* The subset of a finished child result this classification reads.
|
|
31
32
|
*
|
|
@@ -83,6 +84,39 @@ export type WorkerFailure = {
|
|
|
83
84
|
};
|
|
84
85
|
/** The `kind` of every row, for exhaustiveness checks in consumers. */
|
|
85
86
|
export type WorkerFailureKind = WorkerFailure['kind'];
|
|
87
|
+
/**
|
|
88
|
+
* The ordered ladder. FIRST MATCH WINS — row order IS the precedence, and it is
|
|
89
|
+
* the only statement of it in the codebase.
|
|
90
|
+
*
|
|
91
|
+
* The order, and why:
|
|
92
|
+
*
|
|
93
|
+
* 1. `stalled` — no output AND the model endpoint did not answer a probe. The
|
|
94
|
+
* most specific diagnosis there is, and the one most easily lost: the kill
|
|
95
|
+
* aborts, so anything checked after `aborted` never sees it.
|
|
96
|
+
* 2. `command-timeout` — a watchdog kill naming the tool call that hung. Also
|
|
97
|
+
* aborts. Before the wall-clock timeout because it is the narrower cause
|
|
98
|
+
* (the two cannot be confused: a watchdog kill leaves the worker's own
|
|
99
|
+
* timeout flag false).
|
|
100
|
+
* 3. `stream-stall` — a watchdog kill for a model stream that went silent while
|
|
101
|
+
* no tool was running. Sits next to `command-timeout` because it is the same
|
|
102
|
+
* class of event: a watchdog, not the model, ended the attempt.
|
|
103
|
+
* 4. `worker-timeout` — the wall-clock backstop.
|
|
104
|
+
* 5. `loop` — killed for repeating one tool call past threshold. After the
|
|
105
|
+
* timeouts, matching the enforce ladder this replaces; in practice the two
|
|
106
|
+
* cannot both fire, since a loop kill stops the attempt before its own timer
|
|
107
|
+
* can expire.
|
|
108
|
+
* 6. `leaked-tool-call` — the model wrote a call as prose instead of invoking
|
|
109
|
+
* it. Only ever set on an otherwise clean run.
|
|
110
|
+
* 7. `aborted` — no specific cause survived, so this really is a cancel.
|
|
111
|
+
* 8. `exit` — a plain non-zero exit with no kill behind it: a crash.
|
|
112
|
+
*/
|
|
113
|
+
export declare const FAILURE_RULES: ReadonlyArray<{
|
|
114
|
+
/** The roster id this row matches. `worker-kill.test.ts` checks the sequence
|
|
115
|
+
* against `FAILURE_ORDER`, so a reordering or an omission is a test failure
|
|
116
|
+
* rather than a silently different precedence. */
|
|
117
|
+
id: WorkerKillId;
|
|
118
|
+
match: (r: WorkerFailureInput) => WorkerFailure | null;
|
|
119
|
+
}>;
|
|
86
120
|
/**
|
|
87
121
|
* Classify a finished child. Returns `undefined` when nothing killed it —
|
|
88
122
|
* which is not the same as "it answered": the text may still be empty, and that
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* in every consumer that has not handled it — which is what makes the drift that
|
|
26
26
|
* caused the bug impossible rather than merely fixed.
|
|
27
27
|
*/
|
|
28
|
+
const _kindsAreKills = true;
|
|
29
|
+
void _kindsAreKills;
|
|
28
30
|
/**
|
|
29
31
|
* The ordered ladder. FIRST MATCH WINS — row order IS the precedence, and it is
|
|
30
32
|
* the only statement of it in the codebase.
|
|
@@ -51,21 +53,30 @@
|
|
|
51
53
|
* 7. `aborted` — no specific cause survived, so this really is a cancel.
|
|
52
54
|
* 8. `exit` — a plain non-zero exit with no kill behind it: a crash.
|
|
53
55
|
*/
|
|
54
|
-
const FAILURE_RULES = [
|
|
55
|
-
r => (r.stalled === true ? { kind: 'stalled' } : null),
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
export const FAILURE_RULES = [
|
|
57
|
+
{ id: 'stalled', match: r => (r.stalled === true ? { kind: 'stalled' } : null) },
|
|
58
|
+
{
|
|
59
|
+
id: 'command-timeout',
|
|
60
|
+
match: r => r.commandTimedOut ?
|
|
61
|
+
{
|
|
62
|
+
kind: 'command-timeout',
|
|
63
|
+
toolName: r.commandTimedOut.toolName,
|
|
64
|
+
timeoutMs: r.commandTimedOut.timeoutMs
|
|
65
|
+
}
|
|
66
|
+
: null
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'stream-stall',
|
|
70
|
+
match: r => r.streamStalled ? { kind: 'stream-stall', idleMs: r.streamStalled.idleMs } : null
|
|
71
|
+
},
|
|
72
|
+
{ id: 'worker-timeout', match: r => (r.timedOut === true ? { kind: 'worker-timeout' } : null) },
|
|
73
|
+
{ id: 'loop', match: r => (r.loopHit ? { kind: 'loop', hit: r.loopHit } : null) },
|
|
74
|
+
{
|
|
75
|
+
id: 'leaked-tool-call',
|
|
76
|
+
match: r => r.leakedToolCall ? { kind: 'leaked-tool-call', text: String(r.leakedToolCall) } : null
|
|
77
|
+
},
|
|
78
|
+
{ id: 'aborted', match: r => (r.aborted ? { kind: 'aborted' } : null) },
|
|
79
|
+
{ id: 'exit', match: r => (r.exitCode !== 0 ? { kind: 'exit', code: r.exitCode } : null) }
|
|
69
80
|
];
|
|
70
81
|
/**
|
|
71
82
|
* Classify a finished child. Returns `undefined` when nothing killed it —
|
|
@@ -74,7 +85,7 @@ const FAILURE_RULES = [
|
|
|
74
85
|
*/
|
|
75
86
|
export function classifyWorkerFailure(r) {
|
|
76
87
|
for (const rule of FAILURE_RULES) {
|
|
77
|
-
const hit = rule(r);
|
|
88
|
+
const hit = rule.match(r);
|
|
78
89
|
if (hit)
|
|
79
90
|
return hit;
|
|
80
91
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ROSTER of ways a worker child can die, and what each one implies.
|
|
3
|
+
*
|
|
4
|
+
* WHY IT EXISTS. One kill cause was named in six unlinked places: a
|
|
5
|
+
* `RunWorkerInput` guard option, a `RunWorkerResult` field, the
|
|
6
|
+
* `WorkerRestartReason` union, a `RESTART_RULES` row, the `CARRY_FORWARD_REASONS`
|
|
7
|
+
* set, and a `FAILURE_RULES` row. Adding one meant six coordinated edits, and
|
|
8
|
+
* only two of them failed to compile if you skipped one. That is not
|
|
9
|
+
* hypothetical: `worker-failure.ts`'s own header records the bug it cost —
|
|
10
|
+
* *"`streamStalled` was added to the result and to `finalAttemptFailed`, but the
|
|
11
|
+
* enforce ladder never grew an arm for it, so an enforcement child killed for a
|
|
12
|
+
* hung model stream fell all the way through to `if (aborted) return
|
|
13
|
+
* USER_CANCELLED`"*. That fix closed the READER side. This closes the author
|
|
14
|
+
* side.
|
|
15
|
+
*
|
|
16
|
+
* WHAT IS AND IS NOT UNIFIED. The roster is one table. The two ORDERINGS stay
|
|
17
|
+
* two, because they genuinely disagree and each says so in its own prose: the
|
|
18
|
+
* restart ladder puts `loop` first (its hint is the most specific thing to tell a
|
|
19
|
+
* re-spawn), and the failure ladder puts `stalled` first (the diagnosis most
|
|
20
|
+
* easily lost behind the `aborted` every kill path sets). Folding two
|
|
21
|
+
* precedences into one row type would need an escape hatch per row — the same
|
|
22
|
+
* objection that got a `WriteGuard` row table rejected. What the orderings gain
|
|
23
|
+
* here is that neither can name a cause with no row, nor silently omit one.
|
|
24
|
+
*
|
|
25
|
+
* Not every cause appears in both ladders, and that asymmetry is real:
|
|
26
|
+
* `connection-error` is restartable but is reported as a `modelError`, never as a
|
|
27
|
+
* kill; `stalled`, `aborted` and `exit` end an attempt outright and no hint would
|
|
28
|
+
* help.
|
|
29
|
+
*/
|
|
30
|
+
/** Every way a worker attempt can end other than by answering. */
|
|
31
|
+
export type WorkerKillId = 'stalled' | 'command-timeout' | 'stream-stall' | 'worker-timeout' | 'connection-error' | 'loop' | 'leaked-tool-call' | 'aborted' | 'exit';
|
|
32
|
+
export interface WorkerKill {
|
|
33
|
+
id: WorkerKillId;
|
|
34
|
+
/**
|
|
35
|
+
* The `RunWorkerResult` field that reports this cause on the FINAL attempt,
|
|
36
|
+
* or null when the cause never reaches the result under its own name
|
|
37
|
+
* (`connection-error` arrives as `modelError`; `aborted` and `exit` are the
|
|
38
|
+
* generic fields every kill path also sets).
|
|
39
|
+
*
|
|
40
|
+
* A string rather than `keyof RunWorkerResult` so this module stays free of
|
|
41
|
+
* pi-worker-core's import graph; `worker-kill.test.ts` checks it against the
|
|
42
|
+
* real interface.
|
|
43
|
+
*/
|
|
44
|
+
resultField: string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Is a killed attempt's partial output worth carrying into the next one?
|
|
47
|
+
*
|
|
48
|
+
* A clock kill, a hung tool, an idle stream and a dropped socket all discard
|
|
49
|
+
* work the model genuinely did. A loop kill and a leaked tool call do not —
|
|
50
|
+
* the first is by definition the same call repeated, the second is malformed
|
|
51
|
+
* protocol text, and replaying either would feed the failure back to itself.
|
|
52
|
+
*/
|
|
53
|
+
carryForward: boolean;
|
|
54
|
+
/** Does the restart ladder have a rule for this cause? */
|
|
55
|
+
restartable: boolean;
|
|
56
|
+
/** Does this cause reach a consumer as a `WorkerFailure`? */
|
|
57
|
+
reported: boolean;
|
|
58
|
+
}
|
|
59
|
+
export declare const WORKER_KILLS: readonly WorkerKill[];
|
|
60
|
+
/** Look one cause up. `undefined` only for an id with no row, which the suite forbids. */
|
|
61
|
+
export declare function workerKill(id: WorkerKillId): WorkerKill | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* The restart ladder's precedence, as ids. `RESTART_RULES` must be exactly this,
|
|
64
|
+
* in this order.
|
|
65
|
+
*
|
|
66
|
+
* `loop` leads: its hint names the offending call, which is the most useful thing
|
|
67
|
+
* to tell a re-spawn. The two watchdogs come before the wall clock because each
|
|
68
|
+
* is the narrower diagnosis, and they cannot be confused with it — a watchdog
|
|
69
|
+
* kill leaves the worker's own timeout flag false.
|
|
70
|
+
*/
|
|
71
|
+
export declare const RESTART_ORDER: readonly ["loop", "command-timeout", "stream-stall", "worker-timeout", "connection-error", "leaked-tool-call"];
|
|
72
|
+
/**
|
|
73
|
+
* The failure ladder's precedence, as ids. `FAILURE_RULES` must be exactly this,
|
|
74
|
+
* in this order.
|
|
75
|
+
*
|
|
76
|
+
* DIFFERENT from `RESTART_ORDER`, deliberately. Every kill path also sets
|
|
77
|
+
* `aborted` and a non-zero exit, so the specific causes must all be matched
|
|
78
|
+
* before the two generic ones or a dead backend is reported as "you cancelled".
|
|
79
|
+
* `stalled` leads because it is both the most specific diagnosis and the one most
|
|
80
|
+
* easily lost.
|
|
81
|
+
*/
|
|
82
|
+
export declare const FAILURE_ORDER: readonly ["stalled", "command-timeout", "stream-stall", "worker-timeout", "loop", "leaked-tool-call", "aborted", "exit"];
|
|
83
|
+
/** The causes whose partial output is worth keeping. Derived, never hand-kept. */
|
|
84
|
+
export declare const CARRY_FORWARD_IDS: ReadonlySet<WorkerKillId>;
|