@ian-pascoe/pi-minimal-subagents 0.9.0 → 0.10.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 +15 -11
- package/package.json +3 -6
- package/skills/pi-minimal-subagents/SKILL.md +1 -0
- package/src/minimal-subagents-child-resources.ts +21 -0
- package/src/minimal-subagents-coordinator.ts +13 -6
- package/src/minimal-subagents-extension.ts +43 -19
- package/src/minimal-subagents-sessions.ts +39 -22
- package/src/minimal-subagents-settings-writer.ts +39 -275
- package/src/minimal-subagents-tools.ts +10 -6
- package/src/troubleshooting-skill.ts +14 -0
package/README.md
CHANGED
|
@@ -156,9 +156,8 @@ spawn attempts; the root retains recursive hierarchy management.
|
|
|
156
156
|
|
|
157
157
|
## Toolsets
|
|
158
158
|
|
|
159
|
-
Configure ordinary tools using case-sensitive minimatch
|
|
160
|
-
|
|
161
|
-
CodeMode exposure-rule objects. For example:
|
|
159
|
+
Configure ordinary tools using case-sensitive minimatch pattern strings. For
|
|
160
|
+
example:
|
|
162
161
|
|
|
163
162
|
```json
|
|
164
163
|
{
|
|
@@ -183,14 +182,15 @@ Tool Presets are cumulative:
|
|
|
183
182
|
- `tools: "none"` or `tools: []`: Base Toolset only.
|
|
184
183
|
- `tools: ["read"]`: Base Toolset + exactly `read`; arrays do not expand patterns
|
|
185
184
|
or presets.
|
|
186
|
-
- Omitted `tools`: Base Toolset + the caller's
|
|
185
|
+
- Omitted `tools`: Base Toolset + the caller's Reachable Tools: the Root Agent's
|
|
186
|
+
active tools plus tools scripts can call without declaring them (`codemode` or
|
|
187
|
+
`deferred` exposure, such as MCP tools), or a Child Agent caller's own grant.
|
|
187
188
|
|
|
188
189
|
Patterns select from permitted ordinary tool names, including inactive tools
|
|
189
190
|
registered at the root. Their matches are unioned in pattern order, retaining
|
|
190
191
|
registry order within each pattern and removing duplicates at first occurrence.
|
|
191
192
|
Each pattern is independent: a negated minimatch pattern matches its complement;
|
|
192
|
-
it does not subtract earlier matches
|
|
193
|
-
exposure policy. Coordinator Tools remain separately controlled by delegation.
|
|
193
|
+
it does not subtract earlier matches. Coordinator Tools remain separately controlled by delegation.
|
|
194
194
|
|
|
195
195
|
Invalid configuration entries and patterns matching no permitted ordinary tools
|
|
196
196
|
warn and are skipped. Configured tools unavailable in child resources are also
|
|
@@ -206,11 +206,15 @@ still apply; the saved grant is not silently rewritten. Toolsets configure names
|
|
|
206
206
|
not tool operations: granting `lsp`, `dap`, or another multifunction tool grants
|
|
207
207
|
that tool's available operations, regardless of preset name.
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
209
|
+
Child Agents load Pi's built-in `codemode`, `tool-search`, and `mcp` extensions
|
|
210
|
+
unless settings disable them (`-builtin:<name>`); each child connects its own MCP
|
|
211
|
+
servers. A tool's exposure still decides whether it is declared or reachable only
|
|
212
|
+
through `codemode` or `tool_search`. Granted `codemode`- or `deferred`-exposed
|
|
213
|
+
tools are callable but not declared, so a child that needs them also needs
|
|
214
|
+
`codemode` or `tool_search` in its grant. Ungranted tools are unreachable in the
|
|
215
|
+
child. MCP tools are checked at launch only by name, because children register
|
|
216
|
+
them after connecting; a tool whose server disappeared fails when called. Closing
|
|
217
|
+
a child runtime shuts its extensions down, closing its MCP connections.
|
|
214
218
|
|
|
215
219
|
## Capabilities and persistence
|
|
216
220
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-minimal-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Persistent nested subagents with bounded delegation for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -31,11 +31,8 @@
|
|
|
31
31
|
"provenance": true
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@types/proper-lockfile": "^4.1.4"
|
|
34
|
+
"@ian-pascoe/pi-utils": "^0.3.0",
|
|
35
|
+
"minimatch": "^10.2.6"
|
|
39
36
|
},
|
|
40
37
|
"peerDependencies": {
|
|
41
38
|
"@earendil-works/pi-agent-core": "*",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: pi-minimal-subagents
|
|
3
3
|
description: Configure or diagnose Pi Minimal Subagents for Subagent Access, missing Coordinator Tools, spawn, capability, delivery, wait, restore, reload, or fork failures.
|
|
4
4
|
license: MIT
|
|
5
|
+
disable-model-invocation: true
|
|
5
6
|
---
|
|
6
7
|
|
|
7
8
|
# Pi Minimal Subagents
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
|
+
import * as piCodingAgent from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { DefaultResourceLoader, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { canonicalPath } from "./minimal-subagents-paths.js";
|
|
4
5
|
import type { ProjectContextMode } from "./minimal-subagents-types.js";
|
|
@@ -12,6 +13,25 @@ export interface ChildResourceLoaderOptions {
|
|
|
12
13
|
settingsManager?: SettingsManager;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
type InlineExtension = NonNullable<
|
|
17
|
+
ConstructorParameters<typeof DefaultResourceLoader>[0]["extensionFactories"]
|
|
18
|
+
>[number];
|
|
19
|
+
|
|
20
|
+
/** Pi's exported built-in extensions (Pi 0.99+); older hosts have none, so neither do children. */
|
|
21
|
+
export function childBuiltinExtensions(): InlineExtension[] {
|
|
22
|
+
// SAFETY: Older Pi hosts lack these exports; the namespace yields undefined rather than failing.
|
|
23
|
+
const sdk = piCodingAgent as Partial<typeof piCodingAgent>;
|
|
24
|
+
return (
|
|
25
|
+
[
|
|
26
|
+
["codemode", sdk.createCodemodeExtension],
|
|
27
|
+
["tool-search", sdk.createToolSearchExtension],
|
|
28
|
+
["mcp", sdk.createMcpExtension],
|
|
29
|
+
] as const
|
|
30
|
+
).flatMap(([name, create]) =>
|
|
31
|
+
create ? [{ name, factory: create(), builtin: true, replaceable: true }] : [],
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
function createDefaultChildResourceLoaderOptions(
|
|
16
36
|
input: ChildResourceLoaderOptions,
|
|
17
37
|
): ConstructorParameters<typeof DefaultResourceLoader>[0] {
|
|
@@ -24,6 +44,7 @@ function createDefaultChildResourceLoaderOptions(
|
|
|
24
44
|
noContextFiles: false,
|
|
25
45
|
noSkills: false,
|
|
26
46
|
noPromptTemplates: false,
|
|
47
|
+
extensionFactories: childBuiltinExtensions(),
|
|
27
48
|
extensionsOverride: (base) => ({
|
|
28
49
|
...base,
|
|
29
50
|
extensions: base.extensions.filter(
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type DeliveryLedger,
|
|
35
35
|
type DeliveryLedgerTransition,
|
|
36
36
|
} from "./minimal-subagents-delivery-ledger.js";
|
|
37
|
+
import { withTroubleshootingHint } from "./troubleshooting-skill.js";
|
|
37
38
|
import { addCoordinatorMessageEnvelope } from "./minimal-subagents-message-envelope.js";
|
|
38
39
|
import { unavailableAgent } from "./minimal-subagents-sessions.js";
|
|
39
40
|
import { createRegistryEvent } from "./minimal-subagents-registry.js";
|
|
@@ -264,7 +265,9 @@ export class MinimalSubagentsCoordinator {
|
|
|
264
265
|
const requiredMissing = missingDependencies.filter((name) => !optionalMissing.has(name));
|
|
265
266
|
if (requiredMissing.length > 0) {
|
|
266
267
|
throw new Error(
|
|
267
|
-
|
|
268
|
+
withTroubleshootingHint(
|
|
269
|
+
`Minimal subagents launch dependencies unavailable: ${requiredMissing.join(", ")}`,
|
|
270
|
+
),
|
|
268
271
|
);
|
|
269
272
|
}
|
|
270
273
|
if (optionalMissing.size > 0) {
|
|
@@ -381,7 +384,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
381
384
|
agent_id: targetId,
|
|
382
385
|
message_id: messageId,
|
|
383
386
|
disposition: "failed",
|
|
384
|
-
error: deliveryError,
|
|
387
|
+
error: withTroubleshootingHint(deliveryError),
|
|
385
388
|
};
|
|
386
389
|
}
|
|
387
390
|
}
|
|
@@ -925,7 +928,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
925
928
|
turn_id: turnId,
|
|
926
929
|
status: "failed",
|
|
927
930
|
output: "",
|
|
928
|
-
error: error instanceof Error ? error.message : String(error),
|
|
931
|
+
error: withTroubleshootingHint(error instanceof Error ? error.message : String(error)),
|
|
929
932
|
});
|
|
930
933
|
}
|
|
931
934
|
}
|
|
@@ -1366,7 +1369,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1366
1369
|
turn_id: turnId,
|
|
1367
1370
|
status: "failed",
|
|
1368
1371
|
output: "",
|
|
1369
|
-
error: cause instanceof Error ? cause.message : String(cause),
|
|
1372
|
+
error: withTroubleshootingHint(cause instanceof Error ? cause.message : String(cause)),
|
|
1370
1373
|
});
|
|
1371
1374
|
}
|
|
1372
1375
|
});
|
|
@@ -1634,12 +1637,16 @@ export class MinimalSubagentsCoordinator {
|
|
|
1634
1637
|
const agent = this.requireAgent(agentId);
|
|
1635
1638
|
if (agent.availability === "unavailable") {
|
|
1636
1639
|
throw new Error(
|
|
1637
|
-
|
|
1640
|
+
withTroubleshootingHint(
|
|
1641
|
+
agent.unavailable_reason ?? `Minimal subagents ${operation}: ${agentId} is unavailable`,
|
|
1642
|
+
),
|
|
1638
1643
|
);
|
|
1639
1644
|
}
|
|
1640
1645
|
if (agent.clone_error || !agent.session_file) {
|
|
1641
1646
|
throw new Error(
|
|
1642
|
-
|
|
1647
|
+
withTroubleshootingHint(
|
|
1648
|
+
`Minimal subagents ${operation}: ${agent.clone_error ?? `${agentId} has no child session`}`,
|
|
1649
|
+
),
|
|
1643
1650
|
);
|
|
1644
1651
|
}
|
|
1645
1652
|
return agent;
|
|
@@ -62,8 +62,9 @@ import {
|
|
|
62
62
|
type RuntimeToolAdapter,
|
|
63
63
|
unavailableAgent,
|
|
64
64
|
} from "./minimal-subagents-sessions.js";
|
|
65
|
+
import { withTroubleshootingHint } from "./troubleshooting-skill.js";
|
|
65
66
|
import { shutdownMinimalSubagentsSession } from "./minimal-subagents-shutdown.js";
|
|
66
|
-
import {
|
|
67
|
+
import { writeMinimalSubagentsEnabled } from "./minimal-subagents-settings-writer.js";
|
|
67
68
|
import {
|
|
68
69
|
MinimalSubagentsStatusPanelController,
|
|
69
70
|
type MinimalSubagentsStatusAccess,
|
|
@@ -138,16 +139,42 @@ function currentConversationMessages(context: ExtensionContext): AgentMessage[]
|
|
|
138
139
|
return snapshotCommittedContext(messages, !context.isIdle());
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
export function availableToolNames(pi: Pick<ExtensionAPI, "getAllTools">): string[] {
|
|
143
|
+
return excludeCoordinatorTools(
|
|
144
|
+
pi
|
|
145
|
+
.getAllTools()
|
|
146
|
+
.filter((tool) => tool.exposure !== "hidden")
|
|
147
|
+
.map((tool) => tool.name),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Reachable Tools: active tools plus tools scripts can call without declaring them. */
|
|
152
|
+
export function reachableToolNames(
|
|
153
|
+
pi: Pick<ExtensionAPI, "getActiveTools" | "getAllTools">,
|
|
154
|
+
): string[] {
|
|
155
|
+
const active = new Set(pi.getActiveTools());
|
|
156
|
+
return excludeCoordinatorTools(
|
|
157
|
+
pi
|
|
158
|
+
.getAllTools()
|
|
159
|
+
.filter(
|
|
160
|
+
(tool) =>
|
|
161
|
+
active.has(tool.name) || tool.exposure === "codemode" || tool.exposure === "deferred",
|
|
162
|
+
)
|
|
163
|
+
.map((tool) => tool.name),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
141
167
|
function rootCallerSnapshot(pi: ExtensionAPI, context: ExtensionContext): CallerSnapshot {
|
|
142
|
-
if (!context.model)
|
|
143
|
-
|
|
144
|
-
|
|
168
|
+
if (!context.model)
|
|
169
|
+
throw new Error(
|
|
170
|
+
withTroubleshootingHint("Minimal subagents spawn: root has no effective model"),
|
|
171
|
+
);
|
|
145
172
|
return {
|
|
146
173
|
messages: currentConversationMessages(context),
|
|
147
174
|
model: `${context.model.provider}/${context.model.id}`,
|
|
148
175
|
thinkingLevel: context.thinkingLevel ?? pi.getThinkingLevel(),
|
|
149
|
-
ordinaryTools:
|
|
150
|
-
capabilityCeiling:
|
|
176
|
+
ordinaryTools: reachableToolNames(pi),
|
|
177
|
+
capabilityCeiling: availableToolNames(pi),
|
|
151
178
|
spawnEntryId: context.sessionManager.getLeafId() ?? "root",
|
|
152
179
|
};
|
|
153
180
|
}
|
|
@@ -561,9 +588,6 @@ export class MinimalSubagentsLifecycleController {
|
|
|
561
588
|
) {
|
|
562
589
|
models.push(context.model);
|
|
563
590
|
}
|
|
564
|
-
const availableToolNames = excludeCoordinatorTools(
|
|
565
|
-
this.pi.getAllTools().map((tool) => tool.name),
|
|
566
|
-
);
|
|
567
591
|
const schemas = createCoordinatorToolSchemas(eligibleModelIds);
|
|
568
592
|
let activeCoordinator: MinimalSubagentsCoordinator | undefined;
|
|
569
593
|
const requireActiveCoordinator = (): MinimalSubagentsCoordinator => {
|
|
@@ -581,7 +605,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
581
605
|
models,
|
|
582
606
|
eligibleModelIds,
|
|
583
607
|
modelScopeRestricted: enabledModelPatterns !== undefined,
|
|
584
|
-
availableToolNames,
|
|
608
|
+
availableToolNames: availableToolNames(this.pi),
|
|
585
609
|
getRuntimeToolAdapters: () => codexConversionRuntimeToolAdapters(this.pi),
|
|
586
610
|
projectTrusted: context.isProjectTrusted(),
|
|
587
611
|
maxSubagentDepth: minimalSubagentsConfig.maxSubagentDepth,
|
|
@@ -816,15 +840,15 @@ export class MinimalSubagentsLifecycleController {
|
|
|
816
840
|
|
|
817
841
|
let persistedScope: "global" | "project" | undefined;
|
|
818
842
|
if (command.scope !== "session") {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
context.ui.notify(
|
|
843
|
+
try {
|
|
844
|
+
await writeMinimalSubagentsEnabled(
|
|
845
|
+
context,
|
|
846
|
+
accessSession.agentDir,
|
|
847
|
+
command.scope,
|
|
848
|
+
command.action === "reset" ? undefined : command.action === "enable",
|
|
849
|
+
);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
context.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
828
852
|
return;
|
|
829
853
|
}
|
|
830
854
|
persistedScope = command.scope;
|
|
@@ -98,6 +98,18 @@ export interface RuntimeToolAdapter {
|
|
|
98
98
|
readonly replacements: readonly RuntimeToolReplacement[];
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
const MCP_RESOURCE_TOOL_NAMES = new Set([
|
|
102
|
+
"list_mcp_resources",
|
|
103
|
+
"list_mcp_resource_templates",
|
|
104
|
+
"read_mcp_resource",
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
// ponytail: Pi's built-in MCP registers tools only after connecting, so launch checks accept its
|
|
108
|
+
// tool names unverified; a vanished server's tools fail when called instead of at launch.
|
|
109
|
+
function isMcpToolName(toolName: string): boolean {
|
|
110
|
+
return toolName.startsWith("mcp__") || MCP_RESOURCE_TOOL_NAMES.has(toolName);
|
|
111
|
+
}
|
|
112
|
+
|
|
101
113
|
/** Resolve the exact active tools permitted by one Child Agent Launch Contract. */
|
|
102
114
|
export function resolveChildActiveToolNames(
|
|
103
115
|
allowedToolNames: readonly string[],
|
|
@@ -132,24 +144,24 @@ function installChildToolCapabilityPolicy(
|
|
|
132
144
|
session: AgentSession,
|
|
133
145
|
allowedToolNames: readonly string[],
|
|
134
146
|
runtimeToolAdapters: readonly RuntimeToolAdapter[],
|
|
135
|
-
preserveGrantedTools = true,
|
|
136
147
|
): void {
|
|
148
|
+
const isScriptOnly = (name: string) => {
|
|
149
|
+
const exposure = session.getAllTools().find((tool) => tool.name === name)?.exposure;
|
|
150
|
+
return exposure === "codemode" || exposure === "deferred";
|
|
151
|
+
};
|
|
137
152
|
const applyActiveTools = session.setActiveToolsByName.bind(session);
|
|
138
153
|
session.setActiveToolsByName = (requestedToolNames) => {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
runtimeToolAdapters,
|
|
143
|
-
);
|
|
144
|
-
// Inside an exposure wrapper, ordinary tools may intentionally be hidden. The
|
|
145
|
-
// outer policy restores grants before that wrapper applies its own selection.
|
|
154
|
+
const requested = new Set(requestedToolNames);
|
|
155
|
+
// Granted declared tools stay active; `codemode` and `deferred` grants stay
|
|
156
|
+
// callable without being declared unless an extension activates them.
|
|
146
157
|
applyActiveTools(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
resolveChildActiveToolNames(allowedToolNames, requestedToolNames, runtimeToolAdapters).filter(
|
|
159
|
+
(name) => requested.has(name) || !isScriptOnly(name),
|
|
160
|
+
),
|
|
150
161
|
);
|
|
151
162
|
};
|
|
152
|
-
|
|
163
|
+
// Pi's `tools` option also activates script-only grants; drop them before extensions bind.
|
|
164
|
+
session.setActiveToolsByName(session.getActiveToolNames().filter((name) => !isScriptOnly(name)));
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
/** Moves one verified child session file to trash and reports command unavailability. */
|
|
@@ -632,6 +644,11 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
632
644
|
this.unsubscribe();
|
|
633
645
|
try {
|
|
634
646
|
await this.observer?.dispose();
|
|
647
|
+
// Let child extensions release services such as MCP server connections.
|
|
648
|
+
const runner = this.session.extensionRunner;
|
|
649
|
+
if (runner.hasHandlers("session_shutdown")) {
|
|
650
|
+
await runner.emit({ type: "session_shutdown", reason: "quit" });
|
|
651
|
+
}
|
|
635
652
|
} finally {
|
|
636
653
|
this.session.dispose();
|
|
637
654
|
}
|
|
@@ -1127,6 +1144,11 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
1127
1144
|
await resourceLoader.reload();
|
|
1128
1145
|
for (const extension of resourceLoader.getExtensions().extensions) {
|
|
1129
1146
|
for (const toolName of extension.tools.keys()) names.add(toolName);
|
|
1147
|
+
if (extension.path === "builtin:mcp") {
|
|
1148
|
+
for (const toolName of agent.launch_contract.ordinary_tools) {
|
|
1149
|
+
if (isMcpToolName(toolName)) names.add(toolName);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1130
1152
|
}
|
|
1131
1153
|
return names;
|
|
1132
1154
|
})();
|
|
@@ -1174,7 +1196,6 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
1174
1196
|
];
|
|
1175
1197
|
const runtimeToolAdapters = this.options.getRuntimeToolAdapters?.() ?? [];
|
|
1176
1198
|
const adapterToolNames = new Set(runtimeToolAdapters.flatMap((adapter) => adapter.toolNames));
|
|
1177
|
-
const adaptRuntimeTools = adapterToolNames.size > 0;
|
|
1178
1199
|
const { session } = await createAgentSession({
|
|
1179
1200
|
cwd: this.options.cwd,
|
|
1180
1201
|
agentDir: this.options.agentDir,
|
|
@@ -1187,20 +1208,16 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
1187
1208
|
settingsManager,
|
|
1188
1209
|
modelRuntime,
|
|
1189
1210
|
});
|
|
1190
|
-
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1211
|
+
const loadedNames = new Set(session.getAllTools().map((tool) => tool.name));
|
|
1212
|
+
const missingTools = allowedToolNames.filter(
|
|
1213
|
+
(toolName) => !loadedNames.has(toolName) && !isMcpToolName(toolName),
|
|
1214
|
+
);
|
|
1193
1215
|
if (missingTools.length > 0) {
|
|
1194
1216
|
session.dispose();
|
|
1195
1217
|
throw new Error(`Minimal subagents child tool loading failed: ${missingTools.join(", ")}`);
|
|
1196
1218
|
}
|
|
1197
|
-
// The inner policy bounds extension-selected exposure without restoring hidden ordinary tools.
|
|
1198
|
-
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters, false);
|
|
1199
|
-
await session.bindExtensions({ mode: "print" });
|
|
1200
|
-
// The outer policy filters names before extension wrappers build their own tool catalogues.
|
|
1201
1219
|
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters);
|
|
1202
|
-
|
|
1203
|
-
// require their definitions to remain registered, not necessarily direct.
|
|
1220
|
+
await session.bindExtensions({ mode: "print" });
|
|
1204
1221
|
const registeredNames = new Set(session.getAllTools().map((tool) => tool.name));
|
|
1205
1222
|
const missingCoordinatorTools = coordinatorTools
|
|
1206
1223
|
.map((tool) => tool.name)
|
|
@@ -1,308 +1,72 @@
|
|
|
1
1
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
|
-
import { CONFIG_DIR_NAME
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
-
import lockfile from "proper-lockfile";
|
|
2
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { updateFileLocked } from "@ian-pascoe/pi-utils/locked-file-update";
|
|
4
|
+
import { resolve } from "node:path";
|
|
7
5
|
|
|
8
6
|
/** Identifies the standard Pi settings file changed by a Subagent Access command. */
|
|
9
7
|
export type MinimalSubagentsSettingsScope = "global" | "project";
|
|
10
8
|
|
|
11
|
-
/** Describes why a scoped minimal subagents settings write could not complete. */
|
|
12
|
-
export type MinimalSubagentsSettingsWriteFailureReason =
|
|
13
|
-
| "project-untrusted"
|
|
14
|
-
| "malformed-json"
|
|
15
|
-
| "incompatible-shape"
|
|
16
|
-
| "filesystem";
|
|
17
|
-
|
|
18
|
-
/** Reports an expected scoped settings failure without throwing through the command boundary. */
|
|
19
|
-
export class MinimalSubagentsSettingsWriteError extends Error {
|
|
20
|
-
readonly _tag = "MinimalSubagentsSettingsWriteError" as const;
|
|
21
|
-
|
|
22
|
-
/** Create a settings write failure carrying its exact scope and path. */
|
|
23
|
-
constructor(
|
|
24
|
-
readonly scope: MinimalSubagentsSettingsScope,
|
|
25
|
-
readonly path: string,
|
|
26
|
-
readonly reason: MinimalSubagentsSettingsWriteFailureReason,
|
|
27
|
-
message: string,
|
|
28
|
-
readonly operation?: SettingsFilesystemOperation,
|
|
29
|
-
cause?: unknown,
|
|
30
|
-
) {
|
|
31
|
-
super(message, cause === undefined ? undefined : { cause });
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Returns the path changed by a successful write or a typed expected settings failure. */
|
|
36
|
-
export type MinimalSubagentsSettingsWriteResult =
|
|
37
|
-
| {
|
|
38
|
-
readonly ok: true;
|
|
39
|
-
readonly scope: MinimalSubagentsSettingsScope;
|
|
40
|
-
readonly path: string;
|
|
41
|
-
}
|
|
42
|
-
| {
|
|
43
|
-
readonly ok: false;
|
|
44
|
-
readonly error: MinimalSubagentsSettingsWriteError;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
9
|
/** Supplies only the Root Agent context needed to select and authorize a settings file. */
|
|
48
10
|
export interface MinimalSubagentsSettingsWriteContext {
|
|
49
11
|
readonly cwd: string;
|
|
50
12
|
isProjectTrusted(): boolean;
|
|
51
13
|
}
|
|
52
14
|
|
|
53
|
-
type SettingsFilesystemOperation = "prepare" | "lock" | "read" | "write" | "release";
|
|
54
15
|
type SettingsJsonObject = Record<string, JsonValue>;
|
|
55
16
|
|
|
56
|
-
type ParsedSettingsDocument =
|
|
57
|
-
| { readonly ok: true; readonly settings: SettingsJsonObject }
|
|
58
|
-
| { readonly ok: false; readonly error: MinimalSubagentsSettingsWriteError };
|
|
59
|
-
|
|
60
|
-
interface ExistingSettingsDocument {
|
|
61
|
-
readonly settings: SettingsJsonObject;
|
|
62
|
-
readonly mode: number;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
17
|
function isSettingsJsonObject(value: JsonValue | undefined): value is SettingsJsonObject {
|
|
66
18
|
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- JSON.parse already established JSON data; distinguish object roots and settings blocks from primitives and arrays.
|
|
67
19
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
68
20
|
}
|
|
69
|
-
const SETTINGS_LOCK_RETRY_DELAY_MS = 20;
|
|
70
|
-
const SETTINGS_LOCK_RETRIES = 100;
|
|
71
|
-
const NEW_SETTINGS_FILE_MODE = 0o600;
|
|
72
|
-
|
|
73
|
-
function stripUtf8Bom(content: string): string {
|
|
74
|
-
return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
|
|
75
|
-
}
|
|
76
21
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
path: string,
|
|
80
|
-
detail: string,
|
|
81
|
-
): MinimalSubagentsSettingsWriteError {
|
|
82
|
-
return new MinimalSubagentsSettingsWriteError(
|
|
83
|
-
scope,
|
|
84
|
-
path,
|
|
85
|
-
"incompatible-shape",
|
|
86
|
-
`Minimal subagents settings shape is incompatible for ${scope} settings at ${path}: ${detail}`,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function parseSettingsDocument(
|
|
91
|
-
content: string,
|
|
92
|
-
scope: MinimalSubagentsSettingsScope,
|
|
93
|
-
path: string,
|
|
94
|
-
): ParsedSettingsDocument {
|
|
22
|
+
function parseSettings(content: string | undefined, path: string): SettingsJsonObject {
|
|
23
|
+
if (content === undefined) return {};
|
|
95
24
|
// JSON.parse is the provenance for this JSON type; the object shape is checked below.
|
|
96
25
|
let parsed: JsonValue;
|
|
97
26
|
try {
|
|
98
|
-
parsed = JSON.parse(
|
|
27
|
+
parsed = JSON.parse(content.charCodeAt(0) === 0xfeff ? content.slice(1) : content);
|
|
99
28
|
} catch (cause) {
|
|
100
|
-
|
|
101
|
-
ok: false,
|
|
102
|
-
error: new MinimalSubagentsSettingsWriteError(
|
|
103
|
-
scope,
|
|
104
|
-
path,
|
|
105
|
-
"malformed-json",
|
|
106
|
-
`Minimal subagents settings JSON is malformed for ${scope} settings at ${path}`,
|
|
107
|
-
"read",
|
|
108
|
-
cause,
|
|
109
|
-
),
|
|
110
|
-
};
|
|
29
|
+
throw new Error(`Minimal subagents settings JSON is malformed at ${path}`, { cause });
|
|
111
30
|
}
|
|
112
|
-
|
|
113
31
|
if (!isSettingsJsonObject(parsed)) {
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
const minimalSubagents = parsed.minimalSubagents;
|
|
117
|
-
if (minimalSubagents !== undefined && !isSettingsJsonObject(minimalSubagents)) {
|
|
118
|
-
return {
|
|
119
|
-
ok: false,
|
|
120
|
-
error: settingsContractError(
|
|
121
|
-
scope,
|
|
122
|
-
path,
|
|
123
|
-
"expected minimalSubagents to be an object or absent",
|
|
124
|
-
),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
return { ok: true, settings: parsed };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function mutateMinimalSubagentsEnabled(
|
|
131
|
-
settings: SettingsJsonObject,
|
|
132
|
-
enabled: boolean | undefined,
|
|
133
|
-
): void {
|
|
134
|
-
const currentMinimalSubagents = settings.minimalSubagents;
|
|
135
|
-
const minimalSubagents = isSettingsJsonObject(currentMinimalSubagents)
|
|
136
|
-
? currentMinimalSubagents
|
|
137
|
-
: {};
|
|
138
|
-
|
|
139
|
-
if (enabled === undefined) {
|
|
140
|
-
delete minimalSubagents.enabled;
|
|
141
|
-
} else {
|
|
142
|
-
minimalSubagents.enabled = enabled;
|
|
32
|
+
throw new Error(`Minimal subagents settings at ${path} must have an object root`);
|
|
143
33
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
delete settings.minimalSubagents;
|
|
147
|
-
} else {
|
|
148
|
-
settings.minimalSubagents = minimalSubagents;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function filesystemWriteError(
|
|
153
|
-
scope: MinimalSubagentsSettingsScope,
|
|
154
|
-
path: string,
|
|
155
|
-
operation: SettingsFilesystemOperation,
|
|
156
|
-
cause: unknown,
|
|
157
|
-
): MinimalSubagentsSettingsWriteError {
|
|
158
|
-
return new MinimalSubagentsSettingsWriteError(
|
|
159
|
-
scope,
|
|
160
|
-
path,
|
|
161
|
-
"filesystem",
|
|
162
|
-
`Minimal subagents settings ${operation} failed for ${scope} settings at ${path}`,
|
|
163
|
-
operation,
|
|
164
|
-
cause,
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function readSettingsDocumentUnderLock(
|
|
169
|
-
scope: MinimalSubagentsSettingsScope,
|
|
170
|
-
path: string,
|
|
171
|
-
): Promise<ExistingSettingsDocument | MinimalSubagentsSettingsWriteError> {
|
|
172
|
-
let fileMode = NEW_SETTINGS_FILE_MODE;
|
|
173
|
-
let content: string;
|
|
174
|
-
try {
|
|
175
|
-
const fileStat = await stat(path);
|
|
176
|
-
fileMode = fileStat.mode & 0o7777;
|
|
177
|
-
content = await readFile(path, "utf8");
|
|
178
|
-
} catch (cause) {
|
|
179
|
-
if (isNodeErrorWithCode(cause, "ENOENT")) return { settings: {}, mode: fileMode };
|
|
180
|
-
return filesystemWriteError(scope, path, "read", cause);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const parsed = parseSettingsDocument(content, scope, path);
|
|
184
|
-
return parsed.ok ? { settings: parsed.settings, mode: fileMode } : parsed.error;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function isNodeErrorWithCode(cause: unknown, code: string): boolean {
|
|
188
|
-
return cause instanceof Error && "code" in cause && cause.code === code;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function replaceSettingsFileAtomically(
|
|
192
|
-
path: string,
|
|
193
|
-
settings: SettingsJsonObject,
|
|
194
|
-
mode: number,
|
|
195
|
-
temporaryId: string,
|
|
196
|
-
): Promise<void> {
|
|
197
|
-
const temporaryPath = join(dirname(path), `.${basename(path)}.${process.pid}.${temporaryId}.tmp`);
|
|
198
|
-
let temporaryFile: Awaited<ReturnType<typeof open>> | undefined;
|
|
199
|
-
try {
|
|
200
|
-
temporaryFile = await open(temporaryPath, "wx", mode);
|
|
201
|
-
await temporaryFile.chmod(mode);
|
|
202
|
-
await temporaryFile.writeFile(`${JSON.stringify(settings, undefined, 2)}\n`, "utf8");
|
|
203
|
-
await temporaryFile.sync();
|
|
204
|
-
await temporaryFile.close();
|
|
205
|
-
temporaryFile = undefined;
|
|
206
|
-
await rename(temporaryPath, path);
|
|
207
|
-
} catch (cause) {
|
|
208
|
-
if (temporaryFile !== undefined) await temporaryFile.close().catch(() => undefined);
|
|
209
|
-
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
210
|
-
throw cause;
|
|
34
|
+
if (parsed.minimalSubagents !== undefined && !isSettingsJsonObject(parsed.minimalSubagents)) {
|
|
35
|
+
throw new Error(`Minimal subagents settings at ${path} must have an object minimalSubagents`);
|
|
211
36
|
}
|
|
37
|
+
return parsed;
|
|
212
38
|
}
|
|
213
39
|
|
|
214
40
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
41
|
+
* Set or remove `minimalSubagents.enabled` in global or trusted-project Pi settings,
|
|
42
|
+
* preserving every unrelated setting. Writes re-read the file under Pi's settings lock.
|
|
43
|
+
* @returns The changed settings path.
|
|
217
44
|
*/
|
|
218
|
-
export
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
/** Set or remove one authored Subagent Access default, preserving every unrelated setting. */
|
|
233
|
-
async writeMinimalSubagentsEnabled(
|
|
234
|
-
scope: MinimalSubagentsSettingsScope,
|
|
235
|
-
enabled: boolean | undefined,
|
|
236
|
-
): Promise<MinimalSubagentsSettingsWriteResult> {
|
|
237
|
-
const path = scope === "global" ? this.globalSettingsPath : this.projectSettingsPath;
|
|
238
|
-
if (scope === "project" && !this.context.isProjectTrusted()) {
|
|
239
|
-
return {
|
|
240
|
-
ok: false,
|
|
241
|
-
error: new MinimalSubagentsSettingsWriteError(
|
|
242
|
-
scope,
|
|
243
|
-
path,
|
|
244
|
-
"project-untrusted",
|
|
245
|
-
`Minimal subagents project settings write refused because the project is not trusted: ${path}`,
|
|
246
|
-
),
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return this.writeEnabledUnderLock(scope, path, enabled);
|
|
45
|
+
export async function writeMinimalSubagentsEnabled(
|
|
46
|
+
context: MinimalSubagentsSettingsWriteContext,
|
|
47
|
+
agentDirectory: string,
|
|
48
|
+
scope: MinimalSubagentsSettingsScope,
|
|
49
|
+
enabled: boolean | undefined,
|
|
50
|
+
): Promise<string> {
|
|
51
|
+
const path =
|
|
52
|
+
scope === "global"
|
|
53
|
+
? resolve(agentDirectory, "settings.json")
|
|
54
|
+
: resolve(context.cwd, CONFIG_DIR_NAME, "settings.json");
|
|
55
|
+
if (scope === "project" && !context.isProjectTrusted()) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Minimal subagents project settings write refused because the project is not trusted: ${path}`,
|
|
58
|
+
);
|
|
251
59
|
}
|
|
252
60
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
let release: (() => Promise<void>) | undefined;
|
|
265
|
-
try {
|
|
266
|
-
release = await lockfile.lock(path, {
|
|
267
|
-
realpath: false,
|
|
268
|
-
retries: {
|
|
269
|
-
retries: SETTINGS_LOCK_RETRIES,
|
|
270
|
-
factor: 1,
|
|
271
|
-
minTimeout: SETTINGS_LOCK_RETRY_DELAY_MS,
|
|
272
|
-
maxTimeout: SETTINGS_LOCK_RETRY_DELAY_MS,
|
|
273
|
-
randomize: false,
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
} catch (cause) {
|
|
277
|
-
return { ok: false, error: filesystemWriteError(scope, path, "lock", cause) };
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
let result: MinimalSubagentsSettingsWriteResult;
|
|
281
|
-
try {
|
|
282
|
-
const existing = await readSettingsDocumentUnderLock(scope, path);
|
|
283
|
-
if (existing instanceof MinimalSubagentsSettingsWriteError) {
|
|
284
|
-
result = { ok: false, error: existing };
|
|
285
|
-
} else {
|
|
286
|
-
try {
|
|
287
|
-
mutateMinimalSubagentsEnabled(existing.settings, enabled);
|
|
288
|
-
await replaceSettingsFileAtomically(
|
|
289
|
-
path,
|
|
290
|
-
existing.settings,
|
|
291
|
-
existing.mode,
|
|
292
|
-
this.createTemporaryId(),
|
|
293
|
-
);
|
|
294
|
-
result = { ok: true, scope, path };
|
|
295
|
-
} catch (cause) {
|
|
296
|
-
result = { ok: false, error: filesystemWriteError(scope, path, "write", cause) };
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
} finally {
|
|
300
|
-
try {
|
|
301
|
-
await release();
|
|
302
|
-
} catch (cause) {
|
|
303
|
-
result = { ok: false, error: filesystemWriteError(scope, path, "release", cause) };
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return result;
|
|
307
|
-
}
|
|
61
|
+
await updateFileLocked(path, (content) => {
|
|
62
|
+
const settings = parseSettings(content, path);
|
|
63
|
+
const current = settings.minimalSubagents;
|
|
64
|
+
const minimalSubagents = isSettingsJsonObject(current) ? { ...current } : {};
|
|
65
|
+
if (enabled === undefined) delete minimalSubagents.enabled;
|
|
66
|
+
else minimalSubagents.enabled = enabled;
|
|
67
|
+
if (Object.keys(minimalSubagents).length === 0) delete settings.minimalSubagents;
|
|
68
|
+
else settings.minimalSubagents = minimalSubagents;
|
|
69
|
+
return `${JSON.stringify(settings, undefined, 2)}\n`;
|
|
70
|
+
});
|
|
71
|
+
return path;
|
|
308
72
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ToolRenderResultOptions,
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
|
|
13
|
+
import { withTroubleshootingHint } from "./troubleshooting-skill.js";
|
|
13
14
|
import type { MinimalSubagentsModelRole } from "./minimal-subagents-config.js";
|
|
14
15
|
import {
|
|
15
16
|
renderCoordinatorToolCall,
|
|
@@ -125,6 +126,7 @@ function structuredToolResult<TDetails extends CoordinatorToolResultDetails>(
|
|
|
125
126
|
return {
|
|
126
127
|
content: [{ type: "text" as const, text: truncated.content }],
|
|
127
128
|
details: result,
|
|
129
|
+
structuredContent: JSON.parse(json),
|
|
128
130
|
};
|
|
129
131
|
}
|
|
130
132
|
|
|
@@ -134,7 +136,7 @@ function failedStructuredOperation(prefix: string, result: DeleteResult): never
|
|
|
134
136
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
135
137
|
maxLines: DEFAULT_MAX_LINES,
|
|
136
138
|
});
|
|
137
|
-
throw new Error(`${prefix}: ${truncated.content}`);
|
|
139
|
+
throw new Error(withTroubleshootingHint(`${prefix}: ${truncated.content}`));
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
function callerSourceTurnId(
|
|
@@ -235,13 +237,15 @@ export function createCoordinatorToolDefinitions(
|
|
|
235
237
|
signal,
|
|
236
238
|
parameters.turn_id,
|
|
237
239
|
);
|
|
240
|
+
const details = {
|
|
241
|
+
...result,
|
|
242
|
+
source_agent_id: result.agent_id,
|
|
243
|
+
source_turn_id: result.turn_id,
|
|
244
|
+
};
|
|
238
245
|
return {
|
|
239
246
|
...structuredToolResult(result),
|
|
240
|
-
details
|
|
241
|
-
|
|
242
|
-
source_agent_id: result.agent_id,
|
|
243
|
-
source_turn_id: result.turn_id,
|
|
244
|
-
},
|
|
247
|
+
details,
|
|
248
|
+
structuredContent: JSON.parse(JSON.stringify(details)),
|
|
245
249
|
};
|
|
246
250
|
});
|
|
247
251
|
} finally {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
|
|
3
|
+
/** Absolute path of this package's user-invoked troubleshooting Skill. */
|
|
4
|
+
export const TROUBLESHOOTING_SKILL_PATH = fileURLToPath(
|
|
5
|
+
new URL("../skills/pi-minimal-subagents/SKILL.md", import.meta.url),
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
/** Model-facing pointer appended to failures this package's Skill diagnoses. */
|
|
9
|
+
export const TROUBLESHOOTING_HINT = `For diagnosis, read the pi-minimal-subagents troubleshooting Skill at ${TROUBLESHOOTING_SKILL_PATH}.`;
|
|
10
|
+
|
|
11
|
+
/** Append the troubleshooting Skill pointer to a model-facing configuration or runtime failure. */
|
|
12
|
+
export function withTroubleshootingHint(message: string): string {
|
|
13
|
+
return `${message}\n\n${TROUBLESHOOTING_HINT}`;
|
|
14
|
+
}
|