@ian-pascoe/pi-minimal-subagents 0.7.2 → 0.8.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 +63 -7
- package/package.json +2 -1
- package/src/minimal-subagents-capabilities.ts +39 -15
- package/src/minimal-subagents-config.ts +48 -2
- package/src/minimal-subagents-coordinator.ts +30 -3
- package/src/minimal-subagents-extension.ts +4 -2
- package/src/minimal-subagents-sessions.ts +18 -5
- package/src/minimal-subagents-tool-schemas.ts +2 -2
- package/src/minimal-subagents-types.ts +10 -2
package/README.md
CHANGED
|
@@ -154,6 +154,64 @@ child runtimes. A deliberately non-settling agent can therefore delay reload
|
|
|
154
154
|
indefinitely. The new limit controls restored tool availability and future
|
|
155
155
|
spawn attempts; the root retains recursive hierarchy management.
|
|
156
156
|
|
|
157
|
+
## Toolsets
|
|
158
|
+
|
|
159
|
+
Configure ordinary tools using case-sensitive minimatch patterns, with the same
|
|
160
|
+
syntax as CodeMode Exposure Patterns. These are lists of pattern strings, not
|
|
161
|
+
CodeMode exposure-rule objects. For example:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"minimalSubagents": {
|
|
166
|
+
"baseToolset": ["context_*"],
|
|
167
|
+
"readToolset": ["read", "grep", "find", "ls"],
|
|
168
|
+
"modifyToolset": ["bash", "edit", "write", "lsp", "dap"]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The defaults are `baseToolset: []`,
|
|
174
|
+
`readToolset: ["read", "grep", "find", "ls"]`, and
|
|
175
|
+
`modifyToolset: ["bash", "edit", "write"]`. Each configured array replaces that
|
|
176
|
+
key's inherited value; `[]` clears it. Omitted keys inherit the global value or
|
|
177
|
+
built-in default. Only trusted project settings apply.
|
|
178
|
+
|
|
179
|
+
Tool Presets are cumulative:
|
|
180
|
+
|
|
181
|
+
- `tools: "read"`: Base Toolset + `readToolset`.
|
|
182
|
+
- `tools: "modify"`: Base Toolset + `readToolset` + `modifyToolset`.
|
|
183
|
+
- `tools: "none"` or `tools: []`: Base Toolset only.
|
|
184
|
+
- `tools: ["read"]`: Base Toolset + exactly `read`; arrays do not expand patterns
|
|
185
|
+
or presets.
|
|
186
|
+
- Omitted `tools`: Base Toolset + the caller's inherited ordinary tools.
|
|
187
|
+
|
|
188
|
+
Patterns select from permitted ordinary tool names, including inactive tools
|
|
189
|
+
registered at the root. Their matches are unioned in pattern order, retaining
|
|
190
|
+
registry order within each pattern and removing duplicates at first occurrence.
|
|
191
|
+
Each pattern is independent: a negated minimatch pattern matches its complement;
|
|
192
|
+
it does not subtract earlier matches or implement CodeMode's last-rule-wins
|
|
193
|
+
exposure policy. Coordinator Tools remain separately controlled by delegation.
|
|
194
|
+
|
|
195
|
+
Invalid configuration entries and patterns matching no permitted ordinary tools
|
|
196
|
+
warn and are skipped. Configured tools unavailable in child resources are also
|
|
197
|
+
skipped with a warning, rather than blocking launch. Explicit and inherited tool
|
|
198
|
+
requests remain strict. A child never gains capabilities beyond its parent's
|
|
199
|
+
ceiling, including when a restored parent predates a newly configured Base
|
|
200
|
+
Toolset. Use status to inspect the concrete grant if an optional plugin is absent.
|
|
201
|
+
|
|
202
|
+
Pattern expansion happens when a Child Agent is created. `/reload` applies
|
|
203
|
+
settings to future launches without changing existing Launch Contracts. If a tool
|
|
204
|
+
in an existing contract later disappears, normal restoration dependency checks
|
|
205
|
+
still apply; the saved grant is not silently rewritten. Toolsets configure names,
|
|
206
|
+
not tool operations: granting `lsp`, `dap`, or another multifunction tool grants
|
|
207
|
+
that tool's available operations, regardless of preset name.
|
|
208
|
+
|
|
209
|
+
CodeMode still controls whether a granted tool is direct, CodeMode-only, or both.
|
|
210
|
+
If a child needs CodeMode-only tools, also grant `codemode_*` in its toolsets so
|
|
211
|
+
it has the tools needed to discover and call them. This does not bypass CodeMode's
|
|
212
|
+
own restrictions on nested calls. Exposure rules also apply to injected
|
|
213
|
+
Coordinator Tools; delegation still determines which Coordinator Tools are granted.
|
|
214
|
+
|
|
157
215
|
## Capabilities and persistence
|
|
158
216
|
|
|
159
217
|
Child sessions are persistent Pi sessions. Their launch contracts bound model,
|
|
@@ -172,13 +230,11 @@ text, reasoning, tool calls, and tool results. It includes the current streaming
|
|
|
172
230
|
assistant message but omits image data. Timeout Wait Events include the same
|
|
173
231
|
detailed status snapshot.
|
|
174
232
|
|
|
175
|
-
The `subagent` `tools` argument distinguishes
|
|
176
|
-
lists
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
misuse returns an actionable error. Use the string preset when a child needs
|
|
181
|
-
the complete discovery bundle. Child sessions load the Root Agent's
|
|
233
|
+
The `subagent` `tools` argument distinguishes configurable Tool Presets from
|
|
234
|
+
exact lists; every selection also receives the permitted Base Toolset described
|
|
235
|
+
above. Coordinator tools are injected separately according to delegation and
|
|
236
|
+
must not appear in explicit `tools` arrays; misuse returns an actionable error.
|
|
237
|
+
Child sessions load the Root Agent's
|
|
182
238
|
configured settings and extensions, excluding the recursive
|
|
183
239
|
`pi-minimal-subagents` entrypoint. Project Context controls only project-scoped
|
|
184
240
|
AGENTS instructions and skills; omitting it retains user instructions and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-minimal-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Persistent nested subagents with bounded delegation for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"provenance": true
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"minimatch": "^10.2.6",
|
|
34
35
|
"proper-lockfile": "^4.1.2"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Minimatch } from "minimatch";
|
|
2
|
+
import type {
|
|
3
|
+
DelegationMode,
|
|
4
|
+
MinimalSubagentsToolsets,
|
|
5
|
+
ToolSelection,
|
|
6
|
+
} from "./minimal-subagents-types.js";
|
|
2
7
|
|
|
3
8
|
/** Lists Pi thinking levels in increasing effort order for schema validation and clamping. */
|
|
4
9
|
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
@@ -14,8 +19,12 @@ export const COORDINATOR_TOOL_NAMES = [
|
|
|
14
19
|
"subagent_delete",
|
|
15
20
|
] as const;
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
const
|
|
22
|
+
/** Preserves the built-in presets when their settings are omitted. */
|
|
23
|
+
export const DEFAULT_TOOLSETS: MinimalSubagentsToolsets = {
|
|
24
|
+
baseToolset: [],
|
|
25
|
+
readToolset: ["read", "grep", "find", "ls"],
|
|
26
|
+
modifyToolset: ["bash", "edit", "write"],
|
|
27
|
+
};
|
|
19
28
|
interface ModelReference {
|
|
20
29
|
provider: string;
|
|
21
30
|
id: string;
|
|
@@ -43,23 +52,16 @@ export function buildEligibleModelIds(input: {
|
|
|
43
52
|
export interface ToolResolutionContext {
|
|
44
53
|
ordinaryTools: readonly string[];
|
|
45
54
|
capabilityCeiling: readonly string[];
|
|
55
|
+
toolsets?: MinimalSubagentsToolsets;
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
/**
|
|
58
|
+
/** Expand configured presets within the ceiling while keeping explicit requests strict. */
|
|
49
59
|
export function resolveOrdinaryToolSelection(
|
|
50
60
|
selection: ToolSelection | undefined,
|
|
51
61
|
context: ToolResolutionContext,
|
|
52
|
-
)
|
|
62
|
+
) {
|
|
53
63
|
const requested =
|
|
54
|
-
selection === undefined
|
|
55
|
-
? [...context.ordinaryTools]
|
|
56
|
-
: selection === "none"
|
|
57
|
-
? []
|
|
58
|
-
: selection === "read"
|
|
59
|
-
? READ_TOOL_BUNDLE
|
|
60
|
-
: selection === "modify"
|
|
61
|
-
? MODIFY_TOOL_BUNDLE
|
|
62
|
-
: selection;
|
|
64
|
+
selection === undefined ? context.ordinaryTools : Array.isArray(selection) ? selection : [];
|
|
63
65
|
const uniqueRequested = [...new Set(requested)];
|
|
64
66
|
const coordinatorTools = new Set<string>(COORDINATOR_TOOL_NAMES);
|
|
65
67
|
const requestedCoordinatorTools = uniqueRequested.filter((name) => coordinatorTools.has(name));
|
|
@@ -76,7 +78,29 @@ export function resolveOrdinaryToolSelection(
|
|
|
76
78
|
throw new Error(`Minimal subagents capability ceiling exceeded: ${exceeded.join(", ")}`);
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
const toolsets = context.toolsets ?? DEFAULT_TOOLSETS;
|
|
82
|
+
const keys: (keyof MinimalSubagentsToolsets)[] = ["baseToolset"];
|
|
83
|
+
if (selection === "read" || selection === "modify") keys.push("readToolset");
|
|
84
|
+
if (selection === "modify") keys.push("modifyToolset");
|
|
85
|
+
const permitted = excludeCoordinatorTools(context.capabilityCeiling);
|
|
86
|
+
const warnings: string[] = [];
|
|
87
|
+
const configured = keys.flatMap((key) =>
|
|
88
|
+
toolsets[key].flatMap((pattern) => {
|
|
89
|
+
const matcher = new Minimatch(pattern);
|
|
90
|
+
const matches = permitted.filter((name) => matcher.match(name));
|
|
91
|
+
if (matches.length === 0) {
|
|
92
|
+
warnings.push(
|
|
93
|
+
`minimalSubagents.${key}: ${JSON.stringify(pattern)} matched no permitted ordinary tools (unavailable, outside the caller's capability ceiling, or Coordinator Tools); skipped`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return matches;
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
return {
|
|
100
|
+
ordinaryTools: [...new Set([...configured, ...uniqueRequested])],
|
|
101
|
+
requiredTools: uniqueRequested,
|
|
102
|
+
warnings,
|
|
103
|
+
};
|
|
80
104
|
}
|
|
81
105
|
|
|
82
106
|
/** Return an agent's hierarchy depth where the interactive root is depth zero. */
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Minimatch } from "minimatch";
|
|
2
3
|
import { type Static, Type } from "typebox";
|
|
3
4
|
import { Value } from "typebox/value";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_MAX_SUBAGENT_DEPTH,
|
|
7
|
+
DEFAULT_TOOLSETS,
|
|
8
|
+
THINKING_LEVELS,
|
|
9
|
+
} from "./minimal-subagents-capabilities.js";
|
|
10
|
+
import type { MinimalSubagentsToolsets } from "./minimal-subagents-types.js";
|
|
5
11
|
|
|
6
12
|
const MODEL_ROLE_NAME_MAX_LENGTH = 64;
|
|
7
13
|
const MODEL_ROLE_HINT_MAX_LENGTH = 500;
|
|
@@ -13,12 +19,17 @@ const MinimalSubagentsSettingsSchema = Type.Object({
|
|
|
13
19
|
enabled: Type.Optional(Type.Unknown()),
|
|
14
20
|
maxSubagentDepth: Type.Optional(Type.Unknown()),
|
|
15
21
|
modelRoles: Type.Optional(Type.Unknown()),
|
|
22
|
+
baseToolset: Type.Optional(Type.Unknown()),
|
|
23
|
+
readToolset: Type.Optional(Type.Unknown()),
|
|
24
|
+
modifyToolset: Type.Optional(Type.Unknown()),
|
|
16
25
|
});
|
|
17
26
|
const ModelRoleObjectSchema = Type.Object({
|
|
18
27
|
model: Type.Optional(Type.Unknown()),
|
|
19
28
|
hint: Type.Optional(Type.Unknown()),
|
|
20
29
|
});
|
|
21
30
|
const EnabledSettingSchema = Type.Boolean();
|
|
31
|
+
const ToolsetSchema = Type.Array(Type.Unknown());
|
|
32
|
+
const ToolPatternSchema = Type.String({ minLength: 1 });
|
|
22
33
|
const PositiveSafeIntegerSchema = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
|
|
23
34
|
const MaxSubagentDepthSettingSchema = Type.Union([PositiveSafeIntegerSchema, Type.Null()]);
|
|
24
35
|
const ShorthandModelRoleSchema = Type.String();
|
|
@@ -58,6 +69,7 @@ export interface ResolvedMinimalSubagentsConfig {
|
|
|
58
69
|
maxSubagentDepth: number;
|
|
59
70
|
subagentAccess: ResolvedSubagentAccessSettings;
|
|
60
71
|
modelRoles: MinimalSubagentsModelRole[];
|
|
72
|
+
toolsets: MinimalSubagentsToolsets;
|
|
61
73
|
warnings: string[];
|
|
62
74
|
}
|
|
63
75
|
|
|
@@ -88,7 +100,7 @@ interface ScopedSettingValue {
|
|
|
88
100
|
value: ModelRoleWireValue;
|
|
89
101
|
}
|
|
90
102
|
|
|
91
|
-
interface ParsedMinimalSubagentsSettings {
|
|
103
|
+
interface ParsedMinimalSubagentsSettings extends Partial<MinimalSubagentsToolsets> {
|
|
92
104
|
enabled?: boolean;
|
|
93
105
|
maxSubagentDepth?: MaxSubagentDepthWireValue;
|
|
94
106
|
modelRoles?: ModelRolesWireValue;
|
|
@@ -170,6 +182,32 @@ function readMinimalSubagentsSettings(
|
|
|
170
182
|
if (minimalSubagents.modelRoles !== undefined) {
|
|
171
183
|
parsed.modelRoles = parseModelRolesWireValue(minimalSubagents.modelRoles);
|
|
172
184
|
}
|
|
185
|
+
for (const key of ["baseToolset", "readToolset", "modifyToolset"] as const) {
|
|
186
|
+
const value = minimalSubagents[key];
|
|
187
|
+
if (value === undefined) continue;
|
|
188
|
+
const path = `${scope} minimalSubagents.${key}`;
|
|
189
|
+
if (!Value.Check(ToolsetSchema, value)) {
|
|
190
|
+
warnings.push(`${path}: expected an array of patterns`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const patterns: string[] = [];
|
|
194
|
+
for (const [index, pattern] of value.entries()) {
|
|
195
|
+
if (!Value.Check(ToolPatternSchema, pattern)) {
|
|
196
|
+
warnings.push(`${path}[${index}]: expected a non-empty string`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
if (new Minimatch(pattern).makeRe() !== false) {
|
|
201
|
+
patterns.push(pattern);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// Invalid minimatch patterns are nonblocking configuration warnings.
|
|
206
|
+
}
|
|
207
|
+
warnings.push(`${path}[${index}]: invalid minimatch pattern`);
|
|
208
|
+
}
|
|
209
|
+
parsed[key] = patterns;
|
|
210
|
+
}
|
|
173
211
|
return parsed;
|
|
174
212
|
}
|
|
175
213
|
warnings.push(`${scope} minimalSubagents: expected an object`);
|
|
@@ -384,6 +422,14 @@ export function resolveMinimalSubagentsConfig(
|
|
|
384
422
|
maxSubagentDepth,
|
|
385
423
|
subagentAccess: resolveSubagentAccessSettings(globalConfig.enabled, projectConfig.enabled),
|
|
386
424
|
modelRoles: parseModelRoles(modelRoleEntries, input.eligibleModelIds, warnings),
|
|
425
|
+
toolsets: {
|
|
426
|
+
baseToolset: projectConfig.baseToolset ??
|
|
427
|
+
globalConfig.baseToolset ?? [...DEFAULT_TOOLSETS.baseToolset],
|
|
428
|
+
readToolset: projectConfig.readToolset ??
|
|
429
|
+
globalConfig.readToolset ?? [...DEFAULT_TOOLSETS.readToolset],
|
|
430
|
+
modifyToolset: projectConfig.modifyToolset ??
|
|
431
|
+
globalConfig.modifyToolset ?? [...DEFAULT_TOOLSETS.modifyToolset],
|
|
432
|
+
},
|
|
387
433
|
warnings,
|
|
388
434
|
};
|
|
389
435
|
}
|
|
@@ -207,9 +207,14 @@ export class MinimalSubagentsCoordinator {
|
|
|
207
207
|
const model = parameters.model ?? caller.model;
|
|
208
208
|
const requestedThinking = parameters.thinking_level ?? caller.thinkingLevel;
|
|
209
209
|
const thinkingLevel = this.dependencies.sessions.resolveThinkingLevel(model, requestedThinking);
|
|
210
|
-
const
|
|
210
|
+
const {
|
|
211
|
+
ordinaryTools,
|
|
212
|
+
requiredTools,
|
|
213
|
+
warnings: toolWarnings,
|
|
214
|
+
} = resolveOrdinaryToolSelection(parameters.tools, {
|
|
211
215
|
ordinaryTools: excludeCoordinatorTools(caller.ordinaryTools),
|
|
212
216
|
capabilityCeiling: excludeCoordinatorTools(caller.capabilityCeiling),
|
|
217
|
+
toolsets: this.dependencies.toolsets,
|
|
213
218
|
});
|
|
214
219
|
const committedMessages = structuredClone(caller.messages);
|
|
215
220
|
const imported = assembleImportedContext(sessionContext, committedMessages);
|
|
@@ -251,9 +256,24 @@ export class MinimalSubagentsCoordinator {
|
|
|
251
256
|
const missingDependencies =
|
|
252
257
|
await this.dependencies.sessions.resolveLaunchMissingDependencies(agent);
|
|
253
258
|
this.assertAccepting();
|
|
254
|
-
|
|
259
|
+
const optionalMissing = new Set(
|
|
260
|
+
missingDependencies.filter(
|
|
261
|
+
(name) => name !== model && ordinaryTools.includes(name) && !requiredTools.includes(name),
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
const requiredMissing = missingDependencies.filter((name) => !optionalMissing.has(name));
|
|
265
|
+
if (requiredMissing.length > 0) {
|
|
255
266
|
throw new Error(
|
|
256
|
-
`Minimal subagents launch dependencies unavailable: ${
|
|
267
|
+
`Minimal subagents launch dependencies unavailable: ${requiredMissing.join(", ")}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (optionalMissing.size > 0) {
|
|
271
|
+
agent.launch_contract.ordinary_tools = ordinaryTools.filter(
|
|
272
|
+
(name) => !optionalMissing.has(name),
|
|
273
|
+
);
|
|
274
|
+
agent.capability_ceiling = [...agent.launch_contract.ordinary_tools];
|
|
275
|
+
toolWarnings.push(
|
|
276
|
+
`Configured tools unavailable in child resources; skipped: ${[...optionalMissing].join(", ")}`,
|
|
257
277
|
);
|
|
258
278
|
}
|
|
259
279
|
identity = this.dependencies.sessions.createIdentity(agent, imported.messages);
|
|
@@ -273,6 +293,13 @@ export class MinimalSubagentsCoordinator {
|
|
|
273
293
|
createRegistryEvent(this.dependencies.registry.rootSessionId, "agent-created", { agent }),
|
|
274
294
|
);
|
|
275
295
|
const turnId = this.beginTurn(agent);
|
|
296
|
+
if (toolWarnings.length > 0) {
|
|
297
|
+
this.dependencies.notify?.({
|
|
298
|
+
type: "tool-warning",
|
|
299
|
+
agentId,
|
|
300
|
+
message: `Minimal subagents tool warnings for ${agentId}:\n- ${[...new Set(toolWarnings)].join("\n- ")}`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
276
303
|
this.dependencies.notify?.({
|
|
277
304
|
type: "spawn",
|
|
278
305
|
agentId,
|
|
@@ -182,7 +182,7 @@ function createRootConversationEndpoint(
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
function shouldSurfaceNotification(notification: CoordinatorNotification): boolean {
|
|
185
|
-
return ["failure", "interruption", "unavailable", "fork-clone-failure"].includes(
|
|
185
|
+
return ["failure", "interruption", "unavailable", "fork-clone-failure", "tool-warning"].includes(
|
|
186
186
|
notification.type,
|
|
187
187
|
);
|
|
188
188
|
}
|
|
@@ -192,7 +192,8 @@ function notificationLevel(notification: CoordinatorNotification): "info" | "war
|
|
|
192
192
|
if (
|
|
193
193
|
notification.type === "cancellation" ||
|
|
194
194
|
notification.type === "interruption" ||
|
|
195
|
-
notification.type === "unavailable"
|
|
195
|
+
notification.type === "unavailable" ||
|
|
196
|
+
notification.type === "tool-warning"
|
|
196
197
|
) {
|
|
197
198
|
return "warning";
|
|
198
199
|
}
|
|
@@ -605,6 +606,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
605
606
|
sessions: sessionFactory,
|
|
606
607
|
root: createRootConversationEndpoint(this.pi, context),
|
|
607
608
|
maxSubagentDepth: minimalSubagentsConfig.maxSubagentDepth,
|
|
609
|
+
toolsets: minimalSubagentsConfig.toolsets,
|
|
608
610
|
registry: {
|
|
609
611
|
rootSessionId,
|
|
610
612
|
append: (registryEvent) => this.pi.appendEntry(REGISTRY_ENTRY_TYPE, registryEvent),
|
|
@@ -67,6 +67,7 @@ const PI_BUILTIN_ORDINARY_TOOL_NAMES = new Set([
|
|
|
67
67
|
"find",
|
|
68
68
|
"ls",
|
|
69
69
|
"bash",
|
|
70
|
+
"powershell",
|
|
70
71
|
"edit",
|
|
71
72
|
"write",
|
|
72
73
|
]);
|
|
@@ -129,11 +130,21 @@ function installChildToolCapabilityPolicy(
|
|
|
129
130
|
session: AgentSession,
|
|
130
131
|
allowedToolNames: readonly string[],
|
|
131
132
|
runtimeToolAdapters: readonly RuntimeToolAdapter[],
|
|
133
|
+
preserveGrantedTools = true,
|
|
132
134
|
): void {
|
|
133
135
|
const applyActiveTools = session.setActiveToolsByName.bind(session);
|
|
134
136
|
session.setActiveToolsByName = (requestedToolNames) => {
|
|
137
|
+
const permitted = resolveChildActiveToolNames(
|
|
138
|
+
allowedToolNames,
|
|
139
|
+
requestedToolNames,
|
|
140
|
+
runtimeToolAdapters,
|
|
141
|
+
);
|
|
142
|
+
// Inside an exposure wrapper, ordinary tools may intentionally be hidden. The
|
|
143
|
+
// outer policy restores grants before that wrapper applies its own selection.
|
|
135
144
|
applyActiveTools(
|
|
136
|
-
|
|
145
|
+
preserveGrantedTools
|
|
146
|
+
? permitted
|
|
147
|
+
: permitted.filter((name) => requestedToolNames.includes(name)),
|
|
137
148
|
);
|
|
138
149
|
};
|
|
139
150
|
session.setActiveToolsByName(session.getActiveToolNames());
|
|
@@ -1154,15 +1165,17 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
1154
1165
|
session.dispose();
|
|
1155
1166
|
throw new Error(`Minimal subagents child tool loading failed: ${missingTools.join(", ")}`);
|
|
1156
1167
|
}
|
|
1157
|
-
// The inner policy
|
|
1158
|
-
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters);
|
|
1168
|
+
// The inner policy bounds extension-selected exposure without restoring hidden ordinary tools.
|
|
1169
|
+
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters, false);
|
|
1159
1170
|
await session.bindExtensions({ mode: "print" });
|
|
1160
1171
|
// The outer policy filters names before extension wrappers build their own tool catalogues.
|
|
1161
1172
|
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters);
|
|
1162
|
-
|
|
1173
|
+
// Exposure policy may route granted Coordinator Tools through another tool;
|
|
1174
|
+
// require their definitions to remain registered, not necessarily direct.
|
|
1175
|
+
const registeredNames = new Set(session.getAllTools().map((tool) => tool.name));
|
|
1163
1176
|
const missingCoordinatorTools = coordinatorTools
|
|
1164
1177
|
.map((tool) => tool.name)
|
|
1165
|
-
.filter((toolName) => !
|
|
1178
|
+
.filter((toolName) => !registeredNames.has(toolName));
|
|
1166
1179
|
if (missingCoordinatorTools.length > 0) {
|
|
1167
1180
|
session.dispose();
|
|
1168
1181
|
throw new Error(
|
|
@@ -15,12 +15,12 @@ const ToolSelectionSchema = Type.Union(
|
|
|
15
15
|
Type.Array(Type.String({ minLength: 1 }), {
|
|
16
16
|
uniqueItems: true,
|
|
17
17
|
description:
|
|
18
|
-
"Exact ordinary tool names
|
|
18
|
+
"Exact ordinary tool names, plus configured base tools. No pattern or preset expansion in arrays; use the string preset `read` or `modify`. Coordinator tools are injected separately and must not appear here.",
|
|
19
19
|
}),
|
|
20
20
|
],
|
|
21
21
|
{
|
|
22
22
|
description:
|
|
23
|
-
'
|
|
23
|
+
'Configurable presets: "read" adds readToolset; "modify" adds readToolset plus modifyToolset. Defaults: read, grep, find, ls; modify additionally grants bash, edit, write. Configured baseToolset applies to all selections, including "none" and exact arrays. Omit to inherit the caller\'s active ordinary tools plus base. All grants stay within the caller\'s capability ceiling; coordinator tools are injected separately.',
|
|
24
24
|
},
|
|
25
25
|
);
|
|
26
26
|
const FRIENDLY_AGENT_ID_PATTERN = "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$";
|
|
@@ -6,8 +6,14 @@ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
|
6
6
|
export type SessionContextMode = "inherit" | "compact" | "omit";
|
|
7
7
|
/** Controls whether child resource discovery includes project instructions, skills, and prompts. */
|
|
8
8
|
export type ProjectContextMode = "inherit" | "omit";
|
|
9
|
-
/** Selects inherited,
|
|
9
|
+
/** Selects inherited, preset, base-only, or explicitly named ordinary child tools. */
|
|
10
10
|
export type ToolSelection = "none" | "read" | "modify" | string[];
|
|
11
|
+
/** Configures additive ordinary-tool patterns for the base and cumulative presets. */
|
|
12
|
+
export interface MinimalSubagentsToolsets {
|
|
13
|
+
baseToolset: readonly string[];
|
|
14
|
+
readToolset: readonly string[];
|
|
15
|
+
modifyToolset: readonly string[];
|
|
16
|
+
}
|
|
11
17
|
/** Controls whether a child must work directly or may explicitly fan out one bounded level. */
|
|
12
18
|
export type DelegationMode = "none" | "fanout";
|
|
13
19
|
/** Reports whether a persistent agent currently owns an active turn. */
|
|
@@ -380,7 +386,8 @@ export interface CoordinatorNotification {
|
|
|
380
386
|
| "interruption"
|
|
381
387
|
| "restoration"
|
|
382
388
|
| "unavailable"
|
|
383
|
-
| "fork-clone-failure"
|
|
389
|
+
| "fork-clone-failure"
|
|
390
|
+
| "tool-warning";
|
|
384
391
|
agentId: string;
|
|
385
392
|
message: string;
|
|
386
393
|
}
|
|
@@ -391,6 +398,7 @@ export interface CoordinatorDependencies {
|
|
|
391
398
|
sessions: AgentSessionFactory;
|
|
392
399
|
root: RootConversationEndpoint;
|
|
393
400
|
maxSubagentDepth?: number;
|
|
401
|
+
toolsets?: MinimalSubagentsToolsets;
|
|
394
402
|
now?: () => Date;
|
|
395
403
|
automaticDeliveryGraceMs?: number;
|
|
396
404
|
notify?: (notification: CoordinatorNotification) => void;
|