@ian-pascoe/pi-minimal-subagents 0.16.0 → 0.17.0
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/package.json +6 -6
- package/src/minimal-subagents-config.ts +65 -53
- package/src/minimal-subagents-coordinator.ts +1 -1
- package/src/minimal-subagents-extension.ts +58 -19
- package/src/minimal-subagents-rendering.ts +214 -193
- package/src/minimal-subagents-settings-writer.ts +22 -36
- package/src/minimal-subagents-status-panel.ts +93 -69
- package/src/minimal-subagents-tools.ts +22 -7
- package/src/minimal-subagents-transcript.ts +16 -11
- package/src/minimal-subagents-ui.ts +51 -154
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ filter in `~/.pi/agent/settings.json` using the repository-relative path:
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
From this package checkout, load the source directly with
|
|
32
|
-
`pi -e ./src/index.ts`. Requires Node `>=22.19.0` and Pi `>=
|
|
32
|
+
`pi -e ./src/index.ts`. Requires Node `>=22.19.0` and Pi `>=1.1.0`.
|
|
33
33
|
|
|
34
34
|
## Typical workflow
|
|
35
35
|
|
|
@@ -437,7 +437,7 @@ with Pi's tool-expansion hint (normally Ctrl+O), which reveals the rest and the
|
|
|
437
437
|
full detail. Automatic agent results and messages preview the same way.
|
|
438
438
|
|
|
439
439
|
In TUI mode, `/subagents` or `/subagents status` opens a large, centered,
|
|
440
|
-
|
|
440
|
+
overlay in Pi's selector frame. Up/Down selects a Child Agent; Enter opens its Child Session
|
|
441
441
|
Transcript. Escape returns to the tree, then Escape closes the overlay. The
|
|
442
442
|
viewer is read-only: Root Agent input and ongoing agent work remain intact.
|
|
443
443
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-minimal-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Persistent nested subagents with bounded delegation for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
"provenance": true
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ian-pascoe/pi-utils": "^0.
|
|
34
|
+
"@ian-pascoe/pi-utils": "^0.5.0",
|
|
35
35
|
"minimatch": "^10.2.6"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@earendil-works/pi-agent-core": ">=
|
|
39
|
-
"@earendil-works/pi-ai": ">=
|
|
40
|
-
"@earendil-works/pi-coding-agent": ">=
|
|
41
|
-
"@earendil-works/pi-tui": ">=
|
|
38
|
+
"@earendil-works/pi-agent-core": ">=1.1.0",
|
|
39
|
+
"@earendil-works/pi-ai": ">=1.1.0",
|
|
40
|
+
"@earendil-works/pi-coding-agent": ">=1.1.0",
|
|
41
|
+
"@earendil-works/pi-tui": ">=1.1.0",
|
|
42
42
|
"typebox": "*"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
type LayeredSettingScope,
|
|
4
|
+
resolveLayeredOptions,
|
|
5
|
+
} from "@ian-pascoe/pi-utils/layered-settings";
|
|
2
6
|
import { Minimatch } from "minimatch";
|
|
3
7
|
import { type Static, Type } from "typebox";
|
|
4
8
|
import { Value } from "typebox/value";
|
|
@@ -224,29 +228,28 @@ function readMinimalSubagentsSettings(
|
|
|
224
228
|
return {};
|
|
225
229
|
}
|
|
226
230
|
|
|
231
|
+
/** Merge one scope's model roles into those of lower scopes, entry by entry. */
|
|
227
232
|
function mergeModelRoleEntries(
|
|
228
|
-
|
|
229
|
-
|
|
233
|
+
current: ReadonlyMap<string, ScopedSettingValue>,
|
|
234
|
+
value: ModelRolesWireValue,
|
|
235
|
+
source: LayeredSettingScope,
|
|
230
236
|
warnings: string[],
|
|
231
237
|
): Map<string, ScopedSettingValue> {
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
} else if (globalValue?.kind === "invalid") {
|
|
238
|
-
warnings.push("global minimalSubagents.modelRoles: expected an object or null");
|
|
238
|
+
if (value.kind === "reset") return new Map();
|
|
239
|
+
if (value.kind === "invalid") {
|
|
240
|
+
warnings.push(`${source} minimalSubagents.modelRoles: expected an object or null`);
|
|
241
|
+
return new Map(current);
|
|
239
242
|
}
|
|
240
|
-
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
243
|
+
const entries = new Map(current);
|
|
244
|
+
if (source === "global") {
|
|
245
|
+
for (const [name, roleValue] of value.entries) {
|
|
246
|
+
entries.set(name, { scope: "global", value: roleValue });
|
|
247
|
+
}
|
|
244
248
|
return entries;
|
|
245
249
|
}
|
|
246
|
-
if (projectValue.kind !== "entries") return entries;
|
|
247
250
|
|
|
248
|
-
for (const [name,
|
|
249
|
-
if (
|
|
251
|
+
for (const [name, roleValue] of value.entries) {
|
|
252
|
+
if (roleValue.kind === "delete") {
|
|
250
253
|
entries.delete(name);
|
|
251
254
|
continue;
|
|
252
255
|
}
|
|
@@ -254,9 +257,9 @@ function mergeModelRoleEntries(
|
|
|
254
257
|
const mergedValue =
|
|
255
258
|
inherited !== undefined &&
|
|
256
259
|
isExpandedModelRoleWireValue(inherited) &&
|
|
257
|
-
isExpandedModelRoleWireValue(
|
|
258
|
-
? parseModelRoleWireValue({ ...inherited.fields, ...
|
|
259
|
-
:
|
|
260
|
+
isExpandedModelRoleWireValue(roleValue)
|
|
261
|
+
? parseModelRoleWireValue({ ...inherited.fields, ...roleValue.fields })
|
|
262
|
+
: roleValue;
|
|
260
263
|
entries.set(name, { scope: "project", value: mergedValue });
|
|
261
264
|
}
|
|
262
265
|
return entries;
|
|
@@ -380,26 +383,24 @@ function resolveSubagentAccessSettings(
|
|
|
380
383
|
};
|
|
381
384
|
}
|
|
382
385
|
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
|
|
386
|
+
function mergeMaxSubagentDepth(
|
|
387
|
+
current: number,
|
|
388
|
+
value: MaxSubagentDepthWireValue,
|
|
389
|
+
source: LayeredSettingScope,
|
|
386
390
|
warnings: string[],
|
|
387
391
|
): number {
|
|
388
|
-
|
|
389
|
-
if (
|
|
390
|
-
resolvedDepth = globalValue.value;
|
|
391
|
-
} else if (globalValue?.kind === "invalid") {
|
|
392
|
-
warnings.push(
|
|
393
|
-
"global minimalSubagents.maxSubagentDepth: expected a positive safe integer or null",
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
if (projectValue === undefined) return resolvedDepth;
|
|
397
|
-
if (projectValue.kind === "reset") return DEFAULT_MAX_SUBAGENT_DEPTH;
|
|
398
|
-
if (projectValue.kind === "depth") return projectValue.value;
|
|
392
|
+
if (value.kind === "reset") return DEFAULT_MAX_SUBAGENT_DEPTH;
|
|
393
|
+
if (value.kind === "depth") return value.value;
|
|
399
394
|
warnings.push(
|
|
400
|
-
|
|
395
|
+
`${source} minimalSubagents.maxSubagentDepth: expected a positive safe integer or null`,
|
|
401
396
|
);
|
|
402
|
-
return
|
|
397
|
+
return current;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Settings resolved across scopes, before model role references are checked. */
|
|
401
|
+
interface MergedMinimalSubagentsSettings extends MinimalSubagentsToolsets {
|
|
402
|
+
maxSubagentDepth: number;
|
|
403
|
+
modelRoles: Map<string, ScopedSettingValue>;
|
|
403
404
|
}
|
|
404
405
|
|
|
405
406
|
/** Resolve trusted global and project settings into validated subagent guidance and limits. */
|
|
@@ -412,28 +413,39 @@ export function resolveMinimalSubagentsConfig(
|
|
|
412
413
|
input.projectTrusted === false
|
|
413
414
|
? {}
|
|
414
415
|
: readMinimalSubagentsSettings(input.projectSettings, "project", warnings);
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
416
|
+
const { settings } = resolveLayeredOptions<
|
|
417
|
+
ParsedMinimalSubagentsSettings,
|
|
418
|
+
MergedMinimalSubagentsSettings
|
|
419
|
+
>({
|
|
420
|
+
defaults: {
|
|
421
|
+
maxSubagentDepth: DEFAULT_MAX_SUBAGENT_DEPTH,
|
|
422
|
+
modelRoles: new Map(),
|
|
423
|
+
baseToolset: [...DEFAULT_TOOLSETS.baseToolset],
|
|
424
|
+
readToolset: [...DEFAULT_TOOLSETS.readToolset],
|
|
425
|
+
modifyToolset: [...DEFAULT_TOOLSETS.modifyToolset],
|
|
426
|
+
},
|
|
427
|
+
// Depth warnings precede model role warnings, as each key merges across scopes in turn.
|
|
428
|
+
optionKeys: ["maxSubagentDepth", "modelRoles", "baseToolset", "readToolset", "modifyToolset"],
|
|
429
|
+
layers: [
|
|
430
|
+
["global", globalConfig],
|
|
431
|
+
["project", projectConfig],
|
|
432
|
+
],
|
|
433
|
+
merge: {
|
|
434
|
+
maxSubagentDepth: (current, value, source) =>
|
|
435
|
+
mergeMaxSubagentDepth(current, value, source, warnings),
|
|
436
|
+
modelRoles: (current, value, source) =>
|
|
437
|
+
mergeModelRoleEntries(current, value, source, warnings),
|
|
438
|
+
},
|
|
439
|
+
});
|
|
425
440
|
|
|
426
441
|
return {
|
|
427
|
-
maxSubagentDepth,
|
|
442
|
+
maxSubagentDepth: settings.maxSubagentDepth,
|
|
428
443
|
subagentAccess: resolveSubagentAccessSettings(globalConfig.enabled, projectConfig.enabled),
|
|
429
|
-
modelRoles: parseModelRoles(
|
|
444
|
+
modelRoles: parseModelRoles(settings.modelRoles, input.eligibleModelIds, warnings),
|
|
430
445
|
toolsets: {
|
|
431
|
-
baseToolset:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
globalConfig.readToolset ?? [...DEFAULT_TOOLSETS.readToolset],
|
|
435
|
-
modifyToolset: projectConfig.modifyToolset ??
|
|
436
|
-
globalConfig.modifyToolset ?? [...DEFAULT_TOOLSETS.modifyToolset],
|
|
446
|
+
baseToolset: settings.baseToolset,
|
|
447
|
+
readToolset: settings.readToolset,
|
|
448
|
+
modifyToolset: settings.modifyToolset,
|
|
437
449
|
},
|
|
438
450
|
warnings,
|
|
439
451
|
};
|
|
@@ -370,7 +370,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
370
370
|
this.dependencies.notify?.({
|
|
371
371
|
type: "tool-warning",
|
|
372
372
|
agentId,
|
|
373
|
-
message: `
|
|
373
|
+
message: `Tool warnings for ${agentId}:\n- ${warnings.join("\n- ")}`,
|
|
374
374
|
});
|
|
375
375
|
}
|
|
376
376
|
this.dependencies.notify?.({
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type TurnEndEvent,
|
|
21
21
|
} from "@earendil-works/pi-coding-agent";
|
|
22
22
|
import { isKeyRelease, isKeyRepeat, matchesKey } from "@earendil-works/pi-tui";
|
|
23
|
+
import { expandMessageOnClick, noticeText } from "@ian-pascoe/pi-utils/ui";
|
|
23
24
|
import {
|
|
24
25
|
createSubagentAccessBranchRecord,
|
|
25
26
|
reconcileCoordinatorToolAccess,
|
|
@@ -247,7 +248,10 @@ function reportInvalidRegistryRecords(
|
|
|
247
248
|
.map(({ entry_index: entryIndex, code }) => `entry ${entryIndex}: ${code}`)
|
|
248
249
|
.join(", ");
|
|
249
250
|
context.ui.notify(
|
|
250
|
-
|
|
251
|
+
noticeText(
|
|
252
|
+
"Subagents",
|
|
253
|
+
`Registry ignored ${diagnostics.length} invalid active-branch record${diagnostics.length === 1 ? "" : "s"} (${summaries}${diagnostics.length > 3 ? ", ..." : ""}).`,
|
|
254
|
+
),
|
|
251
255
|
"warning",
|
|
252
256
|
);
|
|
253
257
|
};
|
|
@@ -342,7 +346,10 @@ async function rebindForkAgents(
|
|
|
342
346
|
const message = error instanceof Error ? error.message : String(error);
|
|
343
347
|
failedSubtrees.add(agent.agent_id);
|
|
344
348
|
agents.push(unavailableForkAgent(agent, message));
|
|
345
|
-
context.ui.notify(
|
|
349
|
+
context.ui.notify(
|
|
350
|
+
noticeText("Subagents", `${options.notifyLabel} for ${agent.agent_id}: ${message}`),
|
|
351
|
+
"error",
|
|
352
|
+
);
|
|
346
353
|
}
|
|
347
354
|
}
|
|
348
355
|
return agents;
|
|
@@ -410,7 +417,10 @@ function reportInvalidSubagentAccessRecords(
|
|
|
410
417
|
): (diagnostics: SubagentAccessReplayDiagnostic[]) => void {
|
|
411
418
|
return (diagnostics) => {
|
|
412
419
|
context.ui.notify(
|
|
413
|
-
|
|
420
|
+
noticeText(
|
|
421
|
+
"Subagents",
|
|
422
|
+
`Ignored ${diagnostics.length} invalid Subagent Access branch record${diagnostics.length === 1 ? "" : "s"}.`,
|
|
423
|
+
),
|
|
414
424
|
"warning",
|
|
415
425
|
);
|
|
416
426
|
};
|
|
@@ -545,11 +555,18 @@ export class MinimalSubagentsLifecycleController {
|
|
|
545
555
|
schemas: createCoordinatorToolSchemas([]),
|
|
546
556
|
captureCaller: (context) => rootCallerSnapshot(this.pi, context),
|
|
547
557
|
onActivity: () => this.uiController?.refresh(),
|
|
548
|
-
onAttention: (message) =>
|
|
558
|
+
onAttention: (message) =>
|
|
559
|
+
this.accessSession?.context.ui.notify(noticeText("Subagents", message), "error"),
|
|
549
560
|
});
|
|
550
561
|
for (const tool of rootTools) this.pi.registerTool(tool);
|
|
551
|
-
this.pi.registerMessageRenderer(
|
|
552
|
-
|
|
562
|
+
this.pi.registerMessageRenderer(
|
|
563
|
+
"minimal-subagents.message",
|
|
564
|
+
expandMessageOnClick(renderMinimalSubagentsMessage),
|
|
565
|
+
);
|
|
566
|
+
this.pi.registerMessageRenderer(
|
|
567
|
+
"minimal-subagents.result",
|
|
568
|
+
expandMessageOnClick(renderMinimalSubagentsResult),
|
|
569
|
+
);
|
|
553
570
|
this.pi.registerCommand("subagents", {
|
|
554
571
|
description: "Control Subagent Access and inspect Child Agents",
|
|
555
572
|
getArgumentCompletions: completeSubagentsCommandArguments,
|
|
@@ -587,7 +604,10 @@ export class MinimalSubagentsLifecycleController {
|
|
|
587
604
|
).override;
|
|
588
605
|
if (minimalSubagentsConfig.warnings.length > 0) {
|
|
589
606
|
context.ui.notify(
|
|
590
|
-
|
|
607
|
+
noticeText(
|
|
608
|
+
"Subagents",
|
|
609
|
+
`Configuration warnings:\n- ${minimalSubagentsConfig.warnings.join("\n- ")}`,
|
|
610
|
+
),
|
|
591
611
|
"warning",
|
|
592
612
|
);
|
|
593
613
|
}
|
|
@@ -648,7 +668,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
648
668
|
callerId,
|
|
649
669
|
childContext.sessionManager.getLeafId() ?? callerId,
|
|
650
670
|
),
|
|
651
|
-
onAttention: (message) => context.ui.notify(message, "error"),
|
|
671
|
+
onAttention: (message) => context.ui.notify(noticeText("Subagents", message), "error"),
|
|
652
672
|
});
|
|
653
673
|
},
|
|
654
674
|
});
|
|
@@ -665,7 +685,11 @@ export class MinimalSubagentsLifecycleController {
|
|
|
665
685
|
notify: (notification) => {
|
|
666
686
|
this.uiController?.refresh();
|
|
667
687
|
if (shouldSurfaceNotification(notification)) {
|
|
668
|
-
|
|
688
|
+
const level = notificationLevel(notification);
|
|
689
|
+
context.ui.notify(
|
|
690
|
+
level === "info" ? notification.message : noticeText("Subagents", notification.message),
|
|
691
|
+
level,
|
|
692
|
+
);
|
|
669
693
|
}
|
|
670
694
|
},
|
|
671
695
|
});
|
|
@@ -679,7 +703,10 @@ export class MinimalSubagentsLifecycleController {
|
|
|
679
703
|
if (forkSnapshot?.source_root_session_id !== sourceRootSessionId) {
|
|
680
704
|
if (forkSnapshot) {
|
|
681
705
|
context.ui.notify(
|
|
682
|
-
|
|
706
|
+
noticeText(
|
|
707
|
+
"Subagents",
|
|
708
|
+
"Fork handoff rejected because its source root identity did not match.",
|
|
709
|
+
),
|
|
683
710
|
"error",
|
|
684
711
|
);
|
|
685
712
|
}
|
|
@@ -705,7 +732,10 @@ export class MinimalSubagentsLifecycleController {
|
|
|
705
732
|
);
|
|
706
733
|
} else {
|
|
707
734
|
context.ui.notify(
|
|
708
|
-
|
|
735
|
+
noticeText(
|
|
736
|
+
"Subagents",
|
|
737
|
+
"Fork recovery skipped because the destination selected branch could not be proven from parentSession provenance.",
|
|
738
|
+
),
|
|
709
739
|
"warning",
|
|
710
740
|
);
|
|
711
741
|
}
|
|
@@ -733,7 +763,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
733
763
|
schemas,
|
|
734
764
|
captureCaller: (toolContext) => rootCallerSnapshot(this.pi, toolContext),
|
|
735
765
|
onActivity: () => this.uiController?.refresh(),
|
|
736
|
-
onAttention: (message) => context.ui.notify(message, "error"),
|
|
766
|
+
onAttention: (message) => context.ui.notify(noticeText("Subagents", message), "error"),
|
|
737
767
|
});
|
|
738
768
|
for (const tool of rootTools) this.pi.registerTool(tool);
|
|
739
769
|
this.accessSession = {
|
|
@@ -760,7 +790,10 @@ export class MinimalSubagentsLifecycleController {
|
|
|
760
790
|
|
|
761
791
|
if (hasHistoricalChildIdentity(context.sessionManager.getBranch())) {
|
|
762
792
|
context.ui.notify(
|
|
763
|
-
|
|
793
|
+
noticeText(
|
|
794
|
+
"Subagents",
|
|
795
|
+
"Opened a former subagent session directly. It is now an independent root; former descendants and parent messaging were not restored. Concurrent ownership by its original root is unsupported.",
|
|
796
|
+
),
|
|
764
797
|
"warning",
|
|
765
798
|
);
|
|
766
799
|
}
|
|
@@ -828,11 +861,11 @@ export class MinimalSubagentsLifecycleController {
|
|
|
828
861
|
private async runSubagentsCommand(args: string, context: ExtensionCommandContext): Promise<void> {
|
|
829
862
|
const parsed = parseSubagentsCommandArguments(args);
|
|
830
863
|
if (!parsed.ok) {
|
|
831
|
-
context.ui.notify(parsed.message, "error");
|
|
864
|
+
context.ui.notify(noticeText("Subagents", parsed.message), "error");
|
|
832
865
|
return;
|
|
833
866
|
}
|
|
834
867
|
if (!this.accessSession || !this.coordinator) {
|
|
835
|
-
context.ui.notify("
|
|
868
|
+
context.ui.notify(noticeText("Subagents", "Not active for this session."), "error");
|
|
836
869
|
return;
|
|
837
870
|
}
|
|
838
871
|
if (parsed.command.action === "status") {
|
|
@@ -861,7 +894,10 @@ export class MinimalSubagentsLifecycleController {
|
|
|
861
894
|
command.action === "reset" ? undefined : command.action === "enable",
|
|
862
895
|
);
|
|
863
896
|
} catch (error) {
|
|
864
|
-
context.ui.notify(
|
|
897
|
+
context.ui.notify(
|
|
898
|
+
noticeText("Subagents", error instanceof Error ? error.message : String(error)),
|
|
899
|
+
"error",
|
|
900
|
+
);
|
|
865
901
|
return;
|
|
866
902
|
}
|
|
867
903
|
persistedScope = command.scope;
|
|
@@ -885,9 +921,12 @@ export class MinimalSubagentsLifecycleController {
|
|
|
885
921
|
} catch (error) {
|
|
886
922
|
const detail = error instanceof Error ? error.message : String(error);
|
|
887
923
|
context.ui.notify(
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
924
|
+
noticeText(
|
|
925
|
+
"Subagents",
|
|
926
|
+
persistedScope
|
|
927
|
+
? `${persistedScope} default changed, but the current session did not: ${detail}`
|
|
928
|
+
: `Could not change the current session: ${detail}`,
|
|
929
|
+
),
|
|
891
930
|
"error",
|
|
892
931
|
);
|
|
893
932
|
return;
|