@mjasnikovs/pi-task 0.38.32 → 0.39.1
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/group-args.d.ts +24 -9
- package/dist/config/group-args.js +38 -28
- package/dist/config/option-picker.d.ts +39 -11
- package/dist/config/option-picker.js +52 -12
- package/dist/config/reasoning.d.ts +10 -7
- package/dist/config/reasoning.js +19 -32
- package/dist/config/register.d.ts +66 -41
- package/dist/config/register.js +201 -162
- package/dist/shared/child-process.d.ts +34 -32
- package/dist/shared/child-process.js +44 -58
- package/dist/shared/command-watchdog.d.ts +12 -4
- package/dist/shared/command-watchdog.js +6 -7
- package/dist/shared/connection-error.d.ts +7 -0
- package/dist/shared/connection-error.js +65 -0
- package/dist/shared/model-endpoint.d.ts +12 -24
- package/dist/shared/model-endpoint.js +32 -82
- package/dist/shared/model-resolve.d.ts +105 -0
- package/dist/shared/model-resolve.js +97 -0
- package/dist/shared/reasoning-capability.d.ts +20 -0
- package/dist/shared/reasoning-capability.js +32 -1
- package/dist/shared/stall-probe.d.ts +51 -0
- package/dist/shared/stall-probe.js +79 -0
- package/dist/task/child-runner.d.ts +76 -278
- package/dist/task/child-runner.js +186 -722
- package/dist/task/context-usage.js +2 -7
- package/dist/task/failure-classifier.js +53 -81
- package/dist/task/gate-child.js +1 -1
- package/dist/task/impl-widget.d.ts +2 -0
- package/dist/task/impl-widget.js +4 -0
- package/dist/task/implementation-hold.d.ts +11 -0
- package/dist/task/implementation-hold.js +20 -0
- package/dist/task/implementation-scope.d.ts +24 -0
- package/dist/task/implementation-scope.js +34 -0
- package/dist/task/loop-detector.d.ts +13 -5
- package/dist/task/loop-detector.js +11 -5
- package/dist/task/model-hold-stash.js +4 -14
- package/dist/task/orchestrator.d.ts +1 -8
- package/dist/task/orchestrator.js +11 -34
- package/dist/task/phases.js +2 -2
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +1 -1
- package/dist/workers/model-warning.d.ts +4 -16
- package/dist/workers/model-warning.js +14 -70
- package/dist/workers/pi-worker-core.d.ts +65 -20
- package/dist/workers/pi-worker-core.js +109 -50
- package/dist/workers/reasoning-warning.js +2 -24
- package/dist/workers/worker-failure.d.ts +2 -0
- package/dist/workers/worker-failure.js +2 -1
- package/dist/workers/worker-kill.d.ts +30 -11
- package/dist/workers/worker-kill.js +68 -20
- package/dist/workers/worker-profiles.d.ts +20 -0
- package/dist/workers/worker-profiles.js +22 -9
- package/package.json +1 -1
package/dist/config/register.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { getKeybindings, SettingsList, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { effectiveSetting, offeredLevels } from '../shared/reasoning-capability.js';
|
|
3
|
+
import { resolveModel, specOf } from '../shared/model-resolve.js';
|
|
4
|
+
import { MODEL_INHERIT } from './group-models.js';
|
|
5
|
+
import { PairPicker } from './option-picker.js';
|
|
5
6
|
import { registerBridgeCommand } from '../remote/bridge.js';
|
|
6
7
|
import { readPkgVersion } from '../shared/pkg-version.js';
|
|
7
8
|
import { SEARCH_PROVIDERS, SEARCH_PROVIDER_LABELS, providerForLabel } from '../workers/search-types.js';
|
|
8
9
|
import { COMMAND_TIMEOUT_OPTIONS, DEBUG_LOG_OPTIONS, getConfig, sanitizeDebugLogs, saveConfig, STREAM_INACTIVITY_OPTIONS } from './config.js';
|
|
9
10
|
import { listInstalledExtensions } from './extension-list.js';
|
|
10
11
|
import { listGuardableTools } from './tool-list.js';
|
|
11
|
-
import { CHILD_GROUPS, REASONING_MODES, sanitizeReasoningMode,
|
|
12
|
+
import { CHILD_GROUPS, REASONING_MODES, sanitizeReasoningMode, STEP_GROUP_HELP, REASONING_SETTINGS, effectiveReasoning, resolveReasoning } from './reasoning.js';
|
|
12
13
|
// Version in the title so a bug report or screenshot says which build it came
|
|
13
14
|
// from without anyone having to go look it up.
|
|
14
15
|
const CONFIG_TITLE = `pi-task ${readPkgVersion()} settings`;
|
|
@@ -76,12 +77,20 @@ export const SECTIONS = [
|
|
|
76
77
|
{ key: 'session', title: 'session' },
|
|
77
78
|
{ key: 'checks', title: 'after each task' },
|
|
78
79
|
{ key: 'research', title: 'research' },
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
// The global override gets its own heading, so it cannot be mistaken for a
|
|
81
|
+
// twelfth step. It is one row, and `renderRows` drops an empty section, so
|
|
82
|
+
// this costs a header and a blank line and nothing else.
|
|
83
|
+
//
|
|
84
|
+
// Both titles are SHORT on purpose. `SettingsList` sizes its label column
|
|
85
|
+
// from the widest item it holds — headers included — so a long heading is
|
|
86
|
+
// taken straight out of every row's value column, and the first thing to
|
|
87
|
+
// fall off the right is the thinking level.
|
|
88
|
+
{ key: 'profile', title: 'profile' },
|
|
89
|
+
// ONE block for both dials. They shipped as two parallel blocks of eleven,
|
|
90
|
+
// which put a step's two settings eleven rows apart and hid the coupling
|
|
91
|
+
// between them: choosing a model re-clamps that step's thinking level, and
|
|
92
|
+
// nobody could see it happen.
|
|
93
|
+
{ key: 'reasoning', title: 'steps' },
|
|
85
94
|
{ key: 'unattended', title: 'unattended' },
|
|
86
95
|
{ key: 'logging', title: 'logging' },
|
|
87
96
|
{ key: 'extensions', title: 'child extensions' },
|
|
@@ -229,15 +238,17 @@ export const ITEMS = [
|
|
|
229
238
|
+ 'For throwaway projects you are not watching'),
|
|
230
239
|
{
|
|
231
240
|
id: 'reasoningMode',
|
|
232
|
-
section
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
+ '
|
|
239
|
-
+ '
|
|
240
|
-
+ '
|
|
241
|
+
// Its OWN section, above the per-step block it governs. Sitting inside
|
|
242
|
+
// that block it read as a twelfth step called `reasoning` — a global
|
|
243
|
+
// override disguised as one more per-step row.
|
|
244
|
+
section: 'profile',
|
|
245
|
+
label: 'profile',
|
|
246
|
+
description: 'How much every step below thinks, in one word. "default" uses the per-step '
|
|
247
|
+
+ 'table pi-task has measured, "on" and "off" force one answer everywhere and '
|
|
248
|
+
+ 'IGNORE the rows below, and "custom" is whatever those rows say. They always '
|
|
249
|
+
+ 'show what each step actually runs at, so changing one switches this to '
|
|
250
|
+
+ 'custom. A step left on "inherit" uses whatever thinking level pi itself is '
|
|
251
|
+
+ 'set to, which is what every step did before this setting existed',
|
|
241
252
|
values: [...REASONING_MODES],
|
|
242
253
|
format: cfg => String(cfg.reasoningMode),
|
|
243
254
|
apply: (cfg, chosen) => {
|
|
@@ -340,9 +351,9 @@ export function applyToolToggle(exempt, toolName, watched) {
|
|
|
340
351
|
* though the custom table underneath is untouched, which is the honest
|
|
341
352
|
* answer to "what will my next child do".
|
|
342
353
|
*/
|
|
343
|
-
const
|
|
354
|
+
const STEP_ID_PREFIX = 'step:';
|
|
344
355
|
/**
|
|
345
|
-
* The label for one
|
|
356
|
+
* The label for one step row.
|
|
346
357
|
*
|
|
347
358
|
* A group whose name carries a colon is a CHILD of the group before the colon —
|
|
348
359
|
* `research:files` is one of the four workers `research` fans out to. Rendered
|
|
@@ -351,36 +362,120 @@ const REASON_ID_PREFIX = 'reason:';
|
|
|
351
362
|
* parent is also a row.
|
|
352
363
|
*
|
|
353
364
|
* So a child is drawn as a tree branch under its parent and loses the repeated
|
|
354
|
-
* `
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
365
|
+
* `research:` prefix, the same text at the head of four consecutive lines. `└─`
|
|
366
|
+
* on the last child, `├─` on the rest, decided from the group's position in
|
|
367
|
+
* {@link CHILD_GROUPS} rather than a hand-kept list — adding a fifth worker
|
|
368
|
+
* moves the corner on its own.
|
|
369
|
+
*
|
|
370
|
+
* A parentless group is its own bare name. It carried a `think: ` prefix while
|
|
371
|
+
* there were two families to tell apart; with one row per step there is nothing
|
|
372
|
+
* to disambiguate, and the prefix was the widest thing in the column.
|
|
358
373
|
*
|
|
359
374
|
* Leading spaces survive: SettingsList pads the label right, never trims it.
|
|
360
375
|
*/
|
|
361
|
-
export function
|
|
376
|
+
export function stepRowLabel(group) {
|
|
362
377
|
const colon = group.indexOf(':');
|
|
363
378
|
if (colon < 0)
|
|
364
|
-
return
|
|
379
|
+
return group;
|
|
365
380
|
const parent = group.slice(0, colon);
|
|
366
381
|
const nextIsSibling = CHILD_GROUPS[CHILD_GROUPS.indexOf(group) + 1]?.startsWith(`${parent}:`);
|
|
367
382
|
return ` ${nextIsSibling ? '├─' : '└─'} ${group.slice(colon + 1)}`;
|
|
368
383
|
}
|
|
369
|
-
const MODEL_ID_PREFIX = 'model:';
|
|
370
384
|
/** No registry reachable. Every row still renders; nothing narrows. */
|
|
371
385
|
export const EMPTY_CATALOG = { specs: [], facts: () => undefined };
|
|
372
|
-
|
|
386
|
+
/** The separator between a step row's two halves. */
|
|
387
|
+
const PAIR_SEP = ' \u00b7 ';
|
|
388
|
+
/**
|
|
389
|
+
* `level · provider/id`, the one string a step row shows and accepts.
|
|
390
|
+
*
|
|
391
|
+
* THE LEVEL COMES FIRST, and that is a display decision with teeth.
|
|
392
|
+
* `SettingsList` truncates a value from the RIGHT, so whatever is last is what
|
|
393
|
+
* silently disappears — and a real local model id (`local/Qwen3.8-27B-UD-Q4_K_XL
|
|
394
|
+
* .gguf`) is wide enough to consume the whole column on its own. Level-first
|
|
395
|
+
* means the half that falls off is the one still identifiable from its head, and
|
|
396
|
+
* the levels line up as a column you can read down.
|
|
397
|
+
*/
|
|
398
|
+
export function formatStepValue(spec, level) {
|
|
399
|
+
return `${level}${PAIR_SEP}${spec}`;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* The two halves back out, or `undefined` for anything not of that shape.
|
|
403
|
+
*
|
|
404
|
+
* Split on the FIRST separator, because the level leads and is a closed set,
|
|
405
|
+
* while a `provider/id` could conceivably contain one.
|
|
406
|
+
*/
|
|
407
|
+
export function parseStepValue(value) {
|
|
408
|
+
const at = value.indexOf(PAIR_SEP);
|
|
409
|
+
if (at <= 0)
|
|
410
|
+
return undefined;
|
|
411
|
+
const level = value.slice(0, at);
|
|
412
|
+
const spec = value.slice(at + PAIR_SEP.length);
|
|
413
|
+
if (spec === '' || !REASONING_SETTINGS.includes(level))
|
|
414
|
+
return undefined;
|
|
415
|
+
return { spec, level: level };
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* One row per step, carrying BOTH dials.
|
|
419
|
+
*
|
|
420
|
+
* They were two parallel blocks of eleven, and the coupling between them was
|
|
421
|
+
* invisible: choosing a model re-clamps that step's thinking level, but the row
|
|
422
|
+
* that moved was eleven rows away from the row you touched. One row shows the
|
|
423
|
+
* pair, and the two-step picker shows the clamp happening.
|
|
424
|
+
*
|
|
425
|
+
* `values` is the LEGAL cross product — every model against only the levels that
|
|
426
|
+
* model declares. Nothing renders it: the picker offers two short lists, and the
|
|
427
|
+
* round-trip property in config-items.test.ts is its only reader. Building it
|
|
428
|
+
* from `offeredLevels` rather than the full ladder is what makes the property
|
|
429
|
+
* true, because a pair the model cannot honour would be clamped by `apply` and
|
|
430
|
+
* would not round-trip.
|
|
431
|
+
*/
|
|
432
|
+
export function stepItems(catalog = EMPTY_CATALOG) {
|
|
433
|
+
const specs = [MODEL_INHERIT, ...catalog.specs];
|
|
373
434
|
return CHILD_GROUPS.map(group => ({
|
|
374
|
-
id:
|
|
375
|
-
section: '
|
|
376
|
-
label:
|
|
377
|
-
headlessLabel: `
|
|
378
|
-
description:
|
|
379
|
-
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
435
|
+
id: STEP_ID_PREFIX + group,
|
|
436
|
+
section: 'reasoning',
|
|
437
|
+
label: stepRowLabel(group),
|
|
438
|
+
headlessLabel: `step: ${group}`,
|
|
439
|
+
description: STEP_GROUP_HELP[group],
|
|
440
|
+
values: specs.flatMap(spec => offeredLevels(catalog.facts(spec)).map(level => formatStepValue(spec, level))),
|
|
441
|
+
// Built at ENTER-time from the LIVE draft, not when the rows were made:
|
|
442
|
+
// stage two narrows to the model chosen in stage one, and the user can
|
|
443
|
+
// have changed another row since the panel opened.
|
|
444
|
+
picker: cfg => stepPicker(group, cfg, catalog),
|
|
445
|
+
// The model half VERBATIM even when the catalog no longer offers it — the
|
|
446
|
+
// vanished-model case, where the row is the only place the user can see
|
|
447
|
+
// what their config actually holds. The thinking half is the EFFECTIVE
|
|
448
|
+
// level, not the stored cell: in mode default/on/off the stored table is
|
|
449
|
+
// not what runs, and a row showing a value the run does not use is worse
|
|
450
|
+
// than no row.
|
|
451
|
+
format: cfg => formatStepValue(cfg.groupModels[group], resolveReasoning(group, cfg)),
|
|
452
|
+
apply: (cfg, chosen) => applyStepValue(cfg, group, chosen, catalog)
|
|
453
|
+
}));
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* The two lists behind one step row.
|
|
457
|
+
*
|
|
458
|
+
* Stage two is where the coupling becomes visible. It offers only the levels the
|
|
459
|
+
* chosen model declares, and it OPENS on the level that will actually run — the
|
|
460
|
+
* current one when that model can honour it, otherwise the clamp, with the
|
|
461
|
+
* reason written beside it. Picking a model that cannot think is therefore not a
|
|
462
|
+
* silent downgrade discovered later; it is the option the cursor is already on.
|
|
463
|
+
*/
|
|
464
|
+
function stepPicker(group, cfg, catalog) {
|
|
465
|
+
const held = cfg.groupModels[group];
|
|
466
|
+
// The row's CURRENT model leads stage one when the catalog cannot offer it.
|
|
467
|
+
// Without this, opening the row to nudge only the level would silently
|
|
468
|
+
// rewrite the model to `inherit`: `FilterList` falls back to index 0 when
|
|
469
|
+
// the preselect matches nothing, and the two dials are one row now, so there
|
|
470
|
+
// is no way to touch the level without confirming a model. That would erase
|
|
471
|
+
// a spec set on the user's other machine — the one thing the loader, the
|
|
472
|
+
// format function and the startup hint all go out of their way to preserve.
|
|
473
|
+
const missing = held !== MODEL_INHERIT && !catalog.specs.includes(held);
|
|
474
|
+
return {
|
|
475
|
+
first: [
|
|
476
|
+
...(missing ?
|
|
477
|
+
[{ value: held, label: held, description: 'not available here — kept as-is' }]
|
|
478
|
+
: []),
|
|
384
479
|
{ value: MODEL_INHERIT, label: MODEL_INHERIT, description: "pi's own default" },
|
|
385
480
|
...catalog.specs.map(spec => ({
|
|
386
481
|
value: spec,
|
|
@@ -388,88 +483,58 @@ export function modelItems(catalog) {
|
|
|
388
483
|
...(catalog.note?.(spec) === undefined ? {} : { description: catalog.note(spec) })
|
|
389
484
|
}))
|
|
390
485
|
],
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
486
|
+
second: spec => {
|
|
487
|
+
const facts = catalog.facts(spec);
|
|
488
|
+
const wanted = resolveReasoning(group, cfg);
|
|
489
|
+
const runs = effectiveSetting(facts, wanted);
|
|
490
|
+
return {
|
|
491
|
+
options: offeredLevels(facts).map(level => ({
|
|
492
|
+
value: level,
|
|
493
|
+
label: level,
|
|
494
|
+
...(level === runs && runs !== wanted ?
|
|
495
|
+
{ description: `${spec} cannot do ${wanted}` }
|
|
496
|
+
: {})
|
|
497
|
+
})),
|
|
498
|
+
preselect: runs
|
|
499
|
+
};
|
|
500
|
+
},
|
|
501
|
+
firstOf: value => parseStepValue(value)?.spec ?? value,
|
|
502
|
+
join: (spec, level) => formatStepValue(spec, level)
|
|
503
|
+
};
|
|
397
504
|
}
|
|
398
505
|
/**
|
|
399
|
-
* Write
|
|
400
|
-
*
|
|
401
|
-
* Narrowing the picker does nothing about a level already stored from before the
|
|
402
|
-
* model was chosen: on its own it would freeze a lie into a cell it has just
|
|
403
|
-
* made unconfigurable. `syncRows` re-renders every row, so the user sees the
|
|
404
|
-
* level move.
|
|
506
|
+
* Write both halves of a step row, atomically.
|
|
405
507
|
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* effective `medium` the model will erase — and comparing the stored cell would
|
|
410
|
-
* see no clamp and stay silent about a real lie.
|
|
508
|
+
* Atomically matters for the round-trip property, which starts from a FRESH
|
|
509
|
+
* config every iteration: a value that wrote only one half would leave the other
|
|
510
|
+
* at its default and render as something else.
|
|
411
511
|
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
512
|
+
* The level is re-clamped even though the picker only ever offers legal pairs.
|
|
513
|
+
* The picker is not the only door — `values` is built when the panel opens, and
|
|
514
|
+
* a registry that moved underneath it would otherwise let an unhonourable level
|
|
515
|
+
* through.
|
|
415
516
|
*/
|
|
416
|
-
export function
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
// it may only ever ARRIVE here from the picker, which offers exactly these.
|
|
420
|
-
// Without this, the panel could write a spec this machine cannot resolve.
|
|
421
|
-
if (!isModelSpec(chosen))
|
|
517
|
+
export function applyStepValue(cfg, group, chosen, catalog) {
|
|
518
|
+
const pair = parseStepValue(chosen);
|
|
519
|
+
if (pair === undefined)
|
|
422
520
|
return;
|
|
423
|
-
|
|
521
|
+
// MEMBERSHIP, not just shape: the panel may only ever write what the picker
|
|
522
|
+
// showed. That is `inherit`, the catalog's own specs, and — when the catalog
|
|
523
|
+
// cannot offer it — the spec this cell ALREADY holds, which stage one keeps
|
|
524
|
+
// at its head precisely so the level can be changed without discarding it.
|
|
525
|
+
// Re-writing the value that is already there is not a new unresolvable spec.
|
|
526
|
+
if (pair.spec !== MODEL_INHERIT
|
|
527
|
+
&& pair.spec !== cfg.groupModels[group]
|
|
528
|
+
&& !catalog.specs.includes(pair.spec)) {
|
|
424
529
|
return;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (clamped !== wanted)
|
|
434
|
-
applyReasoningLevel(cfg, group, clamped);
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* The levels a row may offer, given the model that row's group will run on.
|
|
438
|
-
*
|
|
439
|
-
* The INTERSECTION with `REASONING_SETTINGS`, not `supportedThinkingLevels`
|
|
440
|
-
* directly: that returns the whole ladder including `xhigh` and `max`, which
|
|
441
|
-
* this menu excludes on purpose (see reasoning.ts) because pi's own UI may not
|
|
442
|
-
* offer them. A model declaring `xhigh` must not smuggle it in here.
|
|
443
|
-
*/
|
|
444
|
-
export function offeredLevels(facts) {
|
|
445
|
-
if (facts === undefined)
|
|
446
|
-
return [...REASONING_SETTINGS];
|
|
447
|
-
const supported = supportedThinkingLevels(facts);
|
|
448
|
-
return REASONING_SETTINGS.filter(s => s === 'inherit' || supported.includes(s));
|
|
449
|
-
}
|
|
450
|
-
export function reasoningItems(catalog = EMPTY_CATALOG) {
|
|
451
|
-
return CHILD_GROUPS.map(group => ({
|
|
452
|
-
id: REASON_ID_PREFIX + group,
|
|
453
|
-
section: 'reasoning',
|
|
454
|
-
label: reasoningRowLabel(group),
|
|
455
|
-
headlessLabel: `think: ${group}`,
|
|
456
|
-
description: REASONING_GROUP_HELP[group],
|
|
457
|
-
values: [...REASONING_SETTINGS],
|
|
458
|
-
// A submenu, not a cycle, because what this row may offer depends on the
|
|
459
|
-
// model row above it — which the user can change while the panel is
|
|
460
|
-
// open. `values` is static and computed when the rows are built, and
|
|
461
|
-
// `syncRows` can only re-ask `format`; a factory runs at Enter-time.
|
|
462
|
-
submenu: (cfg) => offeredLevels(catalog.facts(cfg.groupModels[group])).map(level => ({
|
|
463
|
-
value: level,
|
|
464
|
-
label: level
|
|
465
|
-
})),
|
|
466
|
-
// The EFFECTIVE level, not cfg.reasoningLevels[group]: in default/on/off
|
|
467
|
-
// the stored table is not what runs, and a row that shows a value the
|
|
468
|
-
// run does not use is worse than no row. As a FUNCTION rather than a
|
|
469
|
-
// snapshot, so `syncRows` can re-ask after any change.
|
|
470
|
-
format: cfg => resolveReasoning(group, cfg),
|
|
471
|
-
apply: (cfg, chosen) => applyReasoningLevel(cfg, group, chosen)
|
|
472
|
-
}));
|
|
530
|
+
}
|
|
531
|
+
cfg.groupModels = { ...cfg.groupModels, [group]: pair.spec };
|
|
532
|
+
const level = effectiveSetting(catalog.facts(pair.spec), pair.level);
|
|
533
|
+
// Only when it MOVES something: `applyReasoningLevel` flips the whole table
|
|
534
|
+
// to `custom`, and picking a pair the config already runs must not do that
|
|
535
|
+
// as a side effect.
|
|
536
|
+
if (level !== resolveReasoning(group, cfg))
|
|
537
|
+
applyReasoningLevel(cfg, group, level);
|
|
473
538
|
}
|
|
474
539
|
/**
|
|
475
540
|
* Apply one group row's new value.
|
|
@@ -621,10 +686,10 @@ export function createSettingsPanel(items, theme,
|
|
|
621
686
|
* Called with the row's id, its new value, and the LIST ITSELF.
|
|
622
687
|
*
|
|
623
688
|
* The list is handed back because some rows change what OTHER rows display:
|
|
624
|
-
* flipping `
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
689
|
+
* flipping `profile` to off means every `step:` row now runs at off, and a
|
|
690
|
+
* row's `currentValue` is a snapshot taken when the panel was built. Without
|
|
691
|
+
* a way to write the others back, the menu would show `profile off` beside
|
|
692
|
+
* rows still claiming `inherit`.
|
|
628
693
|
*/
|
|
629
694
|
onChange, onCancel) {
|
|
630
695
|
const headerLabels = new Set(items.filter(isSectionRow).map(i => i.label));
|
|
@@ -639,11 +704,11 @@ onChange, onCancel) {
|
|
|
639
704
|
* crosses a section boundary.
|
|
640
705
|
*/
|
|
641
706
|
let submenuOpen = false;
|
|
642
|
-
const settingItems = items.map(({
|
|
707
|
+
const settingItems = items.map(({ pickerOptions, ...row }) => pickerOptions === undefined ? row : ({
|
|
643
708
|
...row,
|
|
644
709
|
submenu: (currentValue, done) => {
|
|
645
710
|
submenuOpen = true;
|
|
646
|
-
return new
|
|
711
|
+
return new PairPicker(pickerOptions(), currentValue, theme, v => {
|
|
647
712
|
submenuOpen = false;
|
|
648
713
|
done(v);
|
|
649
714
|
});
|
|
@@ -651,7 +716,7 @@ onChange, onCancel) {
|
|
|
651
716
|
}));
|
|
652
717
|
const list = new SettingsList(settingItems, MAX_VISIBLE, makeTheme(theme, label => headerLabels.has(label.trimEnd())), (id, newValue) => onChange(id, newValue, list), onCancel);
|
|
653
718
|
return new BorderedBox(new SkipInertRows(list, items.map(i => !isSectionRow(i)
|
|
654
|
-
&& ((i.values?.length ?? 0) > 0 || i.
|
|
719
|
+
&& ((i.values?.length ?? 0) > 0 || i.pickerOptions !== undefined)), () => submenuOpen), CONFIG_TITLE, s => theme.fg('borderMuted', s), s => theme.fg('accent', theme.bold(s)), settingsBodyHeight(items.map(i => i.description), MAX_VISIBLE, OVERLAY_WIDTH - 8));
|
|
655
720
|
}
|
|
656
721
|
/**
|
|
657
722
|
* Every settings row for this session, fixed and DISCOVERED, in menu order.
|
|
@@ -671,13 +736,7 @@ catalog = EMPTY_CATALOG) {
|
|
|
671
736
|
// The discovered rows carry a section like every other row — the per-tool
|
|
672
737
|
// watchdog exemptions under `timeouts` (they are exemptions FROM that
|
|
673
738
|
// timeout), and the per-extension toggles under their own heading.
|
|
674
|
-
return [
|
|
675
|
-
...ITEMS,
|
|
676
|
-
...modelItems(catalog),
|
|
677
|
-
...reasoningItems(catalog),
|
|
678
|
-
...toolItems(tools),
|
|
679
|
-
...extensionItems(installed)
|
|
680
|
-
];
|
|
739
|
+
return [...ITEMS, ...stepItems(catalog), ...toolItems(tools), ...extensionItems(installed)];
|
|
681
740
|
}
|
|
682
741
|
/** Render `rows` for the current config, grouped under their section headers. */
|
|
683
742
|
export function renderRows(cfg, rows) {
|
|
@@ -691,7 +750,7 @@ export function renderRows(cfg, rows) {
|
|
|
691
750
|
description: i.description,
|
|
692
751
|
currentValue: i.format(cfg),
|
|
693
752
|
values: i.values ?? ['on', 'off'],
|
|
694
|
-
...(i.
|
|
753
|
+
...(i.picker === undefined ? {} : { pickerOptions: () => i.picker(cfg) }),
|
|
695
754
|
...(i.headlessLabel === undefined ? {} : { headlessLabel: i.headlessLabel })
|
|
696
755
|
}));
|
|
697
756
|
// An empty section prints no header. `extensions` has no fixed rows at
|
|
@@ -713,7 +772,7 @@ export function panelItems(cfg, installed, tools = [], catalog = EMPTY_CATALOG)
|
|
|
713
772
|
*
|
|
714
773
|
* A row's `currentValue` in the live list is a snapshot taken when the panel was
|
|
715
774
|
* built, and rows describe each other: cycling `reasoning` to `off` changes what
|
|
716
|
-
* every `
|
|
775
|
+
* every `step:` row runs at, and setting one step row flips the profile, which
|
|
717
776
|
* changes all the others.
|
|
718
777
|
*
|
|
719
778
|
* This runs after ANY change, over EVERY row. Re-reading a `format` costs
|
|
@@ -725,7 +784,7 @@ export function syncRows(cfg, rows, list) {
|
|
|
725
784
|
list.updateValue(row.id, row.format(cfg));
|
|
726
785
|
}
|
|
727
786
|
/**
|
|
728
|
-
* The
|
|
787
|
+
* The step rows' model offer list, read live when the menu opens.
|
|
729
788
|
*
|
|
730
789
|
* `getAvailable()`, never `getAll()`: an unauthed model would spawn a child that
|
|
731
790
|
* exits 1 on every phase of that group, and offering it would be offering a
|
|
@@ -747,47 +806,27 @@ function liveCatalog(ctx) {
|
|
|
747
806
|
// registry that cannot answer must cost the model rows, never the menu.
|
|
748
807
|
// Every row below still renders; `EMPTY_CATALOG` offers only `inherit` and
|
|
749
808
|
// narrows nothing, which is exactly the pre-feature panel.
|
|
750
|
-
let registry;
|
|
751
809
|
let available;
|
|
752
|
-
let fromExtension;
|
|
753
810
|
try {
|
|
754
|
-
|
|
755
|
-
available = registry.getAvailable();
|
|
756
|
-
fromExtension = new Set(registry.getRegisteredProviderIds());
|
|
811
|
+
available = ctx.modelRegistry.getAvailable();
|
|
757
812
|
}
|
|
758
813
|
catch {
|
|
759
814
|
return EMPTY_CATALOG;
|
|
760
815
|
}
|
|
761
816
|
return {
|
|
762
|
-
specs: available.map(
|
|
763
|
-
note: spec =>
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
: undefined;
|
|
769
|
-
},
|
|
770
|
-
facts: spec => {
|
|
771
|
-
// `inherit` means the session's own model, which is what a child
|
|
772
|
-
// resolves today. Its facts are what the thinking row must narrow to.
|
|
773
|
-
if (spec === MODEL_INHERIT)
|
|
774
|
-
return ctx.model;
|
|
775
|
-
const parts = splitSpec(spec);
|
|
776
|
-
return parts ? registry.find(parts.provider, parts.id) : undefined;
|
|
777
|
-
}
|
|
817
|
+
specs: available.map(specOf),
|
|
818
|
+
note: spec => resolveModel(ctx, spec)?.fromExtension ?
|
|
819
|
+
'provider comes from an extension — whitelist it under child extensions, '
|
|
820
|
+
+ "or this group's children exit 1"
|
|
821
|
+
: undefined,
|
|
822
|
+
facts: spec => resolveModel(ctx, spec)
|
|
778
823
|
};
|
|
779
824
|
}
|
|
780
825
|
async function handleTaskConfig(_args, ctx, getTools = () => []) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
// Copied for the same reason as the two arrays above: the panel mutates
|
|
786
|
-
// its own draft, and sharing the live object would apply half-made
|
|
787
|
-
// choices to running children before the user finished choosing.
|
|
788
|
-
reasoningLevels: { ...getConfig().reasoningLevels },
|
|
789
|
-
groupModels: { ...getConfig().groupModels }
|
|
790
|
-
};
|
|
826
|
+
// A deep copy: the panel mutates its own draft, and sharing the live object
|
|
827
|
+
// would apply half-made choices to running children before the user
|
|
828
|
+
// finished choosing.
|
|
829
|
+
const cfg = structuredClone(getConfig());
|
|
791
830
|
// Enumerated live at open so an installed extension appears and an
|
|
792
831
|
// uninstalled one vanishes without pi-task doing any bookkeeping. A failed
|
|
793
832
|
// enumeration only costs the extension toggles, never the whole menu.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { EventEmitter } from 'node:events';
|
|
2
|
+
import type { CommandKillReason } from './command-watchdog.js';
|
|
3
|
+
import type { LoopHit } from '../task/loop-detector.js';
|
|
2
4
|
/** Grace period between SIGTERM and SIGKILL (ms). */
|
|
3
5
|
export declare const KILL_GRACE_MS = 5000;
|
|
4
6
|
/** Base flags shared by all child pi invocations. */
|
|
@@ -36,11 +38,41 @@ export type SpawnFn = (command: string, args: ReadonlyArray<string>, options: {
|
|
|
36
38
|
* model children (json-events); plumbing stays in-group. */
|
|
37
39
|
detached?: boolean;
|
|
38
40
|
}) => ProcLike;
|
|
41
|
+
/**
|
|
42
|
+
* Why runChild killed the child. Five sources converge on one kill path, and
|
|
43
|
+
* each names itself here rather than in its own flag — so a consumer reads ONE
|
|
44
|
+
* field, and a sixth source is one more member, not a fourth boolean that every
|
|
45
|
+
* consumer has to remember to test before `aborted`.
|
|
46
|
+
*
|
|
47
|
+
* `aborted` is the caller's signal with no cause attached: a user cancel, or a
|
|
48
|
+
* wall clock that aborts without saying so. `command-timeout` also arrives
|
|
49
|
+
* through the signal, but the watchdog aborts WITH its kill as the reason
|
|
50
|
+
* (command-watchdog.ts), which is how it stays a member here instead of an
|
|
51
|
+
* out-of-band query.
|
|
52
|
+
*/
|
|
53
|
+
export type ChildKill = {
|
|
54
|
+
by: 'aborted';
|
|
55
|
+
} | {
|
|
56
|
+
by: 'loop';
|
|
57
|
+
hit: LoopHit;
|
|
58
|
+
} | {
|
|
59
|
+
by: 'stream-stall';
|
|
60
|
+
idleMs: number;
|
|
61
|
+
} | {
|
|
62
|
+
by: 'stalled';
|
|
63
|
+
} | CommandKillReason;
|
|
39
64
|
export interface ChildResult {
|
|
40
65
|
stdout: string;
|
|
41
66
|
stderr: string;
|
|
42
67
|
exitCode: number;
|
|
68
|
+
/** true exactly when `kill` is set. Kept as a flag for the text-mode callers. */
|
|
43
69
|
aborted: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Set when WE ended the child. Its exit status then describes our SIGTERM
|
|
72
|
+
* and says nothing about the child's verdict, so a consumer must read this
|
|
73
|
+
* before the exit code.
|
|
74
|
+
*/
|
|
75
|
+
kill?: ChildKill;
|
|
44
76
|
/** Extracted assistant text (only populated in json-events mode). */
|
|
45
77
|
text?: string;
|
|
46
78
|
/**
|
|
@@ -51,24 +83,6 @@ export interface ChildResult {
|
|
|
51
83
|
* Only populated in json-events mode.
|
|
52
84
|
*/
|
|
53
85
|
modelError?: string;
|
|
54
|
-
/**
|
|
55
|
-
* true when the stall guard killed the child: no output for the stall
|
|
56
|
-
* window AND the model endpoint probe found the backend unreachable.
|
|
57
|
-
* Callers must check this BEFORE `aborted` — the kill sets aborted too,
|
|
58
|
-
* and without the flag it would mislabel as a user cancel.
|
|
59
|
-
*/
|
|
60
|
-
stalled?: boolean;
|
|
61
|
-
/**
|
|
62
|
-
* true when the STREAM watchdog killed the child: no output at all for the
|
|
63
|
-
* configured inactivity window, regardless of whether the backend answers a
|
|
64
|
-
* probe. Distinct from `stalled`, which requires an UNREACHABLE endpoint —
|
|
65
|
-
* these hangs have a perfectly healthy server and a dead stream, so the
|
|
66
|
-
* probe path could never fire. Callers must check this BEFORE `aborted`
|
|
67
|
-
* (the kill sets aborted too) and route it into the connection-error retry.
|
|
68
|
-
*/
|
|
69
|
-
streamStalled?: {
|
|
70
|
-
idleMs: number;
|
|
71
|
-
};
|
|
72
86
|
}
|
|
73
87
|
export interface ToolCall {
|
|
74
88
|
name: string;
|
|
@@ -83,19 +97,7 @@ export interface ToolCall {
|
|
|
83
97
|
*/
|
|
84
98
|
toolCallId?: string;
|
|
85
99
|
}
|
|
86
|
-
export
|
|
87
|
-
call: ToolCall;
|
|
88
|
-
count: number;
|
|
89
|
-
windowSize: number;
|
|
90
|
-
/**
|
|
91
|
-
* Set when the kill came from the whole-run StallDetector rather than the
|
|
92
|
-
* short-window LoopDetector, naming which of its two rules tripped
|
|
93
|
-
* (task/stall-detector.ts). Absent for an ordinary loop hit. Carried here so
|
|
94
|
-
* a stall rides the kill/restart plumbing the loop hit already has instead of
|
|
95
|
-
* needing a second channel.
|
|
96
|
-
*/
|
|
97
|
-
stall?: 'no-new-ground' | 'context-churn';
|
|
98
|
-
}
|
|
100
|
+
export type { LoopHit } from '../task/loop-detector.js';
|
|
99
101
|
export interface ContextSnapshot {
|
|
100
102
|
tokens: number;
|
|
101
103
|
contextWindow: number;
|
|
@@ -215,7 +217,7 @@ export declare class JsonEventSink {
|
|
|
215
217
|
private buf;
|
|
216
218
|
constructor(opts: RunChildJsonEventsOptions,
|
|
217
219
|
/** Invoked when onToolCall reports a loop hit — runChild kills the child. */
|
|
218
|
-
onLoopKill: () => void);
|
|
220
|
+
onLoopKill: (hit: LoopHit) => void);
|
|
219
221
|
/** Feed a raw stdout chunk: parse every complete line, buffer the partial tail. */
|
|
220
222
|
feed(chunk: string): void;
|
|
221
223
|
/** Flush a trailing event that wasn't newline-terminated (call on close). */
|