@ian-pascoe/pi-minimal-subagents 0.7.2 → 0.9.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 +37 -10
- package/src/minimal-subagents-extension.ts +18 -2
- package/src/minimal-subagents-sessions.ts +64 -10
- package/src/minimal-subagents-tool-schemas.ts +2 -2
- package/src/minimal-subagents-types.ts +19 -3
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.9.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,
|
|
@@ -543,7 +570,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
543
570
|
const runtime = this.runtimes.get(agent.agent_id);
|
|
544
571
|
try {
|
|
545
572
|
if (agent.active_turn_id) await this.cancelActiveTurn(agent);
|
|
546
|
-
runtime?.dispose();
|
|
573
|
+
await runtime?.dispose();
|
|
547
574
|
this.runtimes.delete(agent.agent_id);
|
|
548
575
|
if (agent.session_file) {
|
|
549
576
|
await this.dependencies.sessions.trashSession(agent);
|
|
@@ -595,7 +622,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
595
622
|
await Promise.allSettled(
|
|
596
623
|
abandonedRuntimes.map((runtime) => (runtime.isRunning ? runtime.abort() : Promise.resolve())),
|
|
597
624
|
);
|
|
598
|
-
for (const runtime of abandonedRuntimes) runtime.dispose();
|
|
625
|
+
for (const runtime of abandonedRuntimes) await runtime.dispose();
|
|
599
626
|
this.runtimes.clear();
|
|
600
627
|
this.runtimeInitializations.clear();
|
|
601
628
|
this.pendingAgentIds.clear();
|
|
@@ -865,7 +892,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
865
892
|
await Promise.allSettled(this.runtimeInitializations.values());
|
|
866
893
|
await Promise.allSettled(this.backgroundOperations);
|
|
867
894
|
await Promise.allSettled(this.recipientQueues.values());
|
|
868
|
-
for (const runtime of this.runtimes.values()) runtime.dispose();
|
|
895
|
+
for (const runtime of this.runtimes.values()) await runtime.dispose();
|
|
869
896
|
this.runtimes.clear();
|
|
870
897
|
}
|
|
871
898
|
|
|
@@ -883,7 +910,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
883
910
|
const runtime = await this.ensureRuntime(agent);
|
|
884
911
|
if (this.agents.get(agentId) !== agent || agent.active_turn_id !== turnId) {
|
|
885
912
|
if (!this.agents.has(agentId) || !this.acceptingOperations) {
|
|
886
|
-
runtime.dispose();
|
|
913
|
+
await runtime.dispose();
|
|
887
914
|
this.runtimes.delete(agentId);
|
|
888
915
|
}
|
|
889
916
|
return;
|
|
@@ -916,13 +943,13 @@ export class MinimalSubagentsCoordinator {
|
|
|
916
943
|
const openingForTurn = agent.active_turn_id !== undefined;
|
|
917
944
|
const initialization = this.dependencies.sessions
|
|
918
945
|
.openRuntime(agent)
|
|
919
|
-
.then((runtime) => {
|
|
946
|
+
.then(async (runtime) => {
|
|
920
947
|
if (this.agents.get(agent.agent_id) !== agent) {
|
|
921
|
-
runtime.dispose();
|
|
948
|
+
await runtime.dispose();
|
|
922
949
|
throw new Error(`Minimal subagents runtime replaced while opening ${agent.agent_id}`);
|
|
923
950
|
}
|
|
924
951
|
if (!runtime.sessionLeafId) {
|
|
925
|
-
runtime.dispose();
|
|
952
|
+
await runtime.dispose();
|
|
926
953
|
throw new Error(
|
|
927
954
|
`Minimal subagents session restoration: no selected session leaf for ${agent.agent_id}`,
|
|
928
955
|
);
|
|
@@ -80,6 +80,7 @@ import { MinimalSubagentsUiController } from "./minimal-subagents-ui.js";
|
|
|
80
80
|
import type {
|
|
81
81
|
AgentSessionFactory,
|
|
82
82
|
CallerSnapshot,
|
|
83
|
+
ChildSessionObserver,
|
|
83
84
|
CoordinatorNotification,
|
|
84
85
|
ForkSnapshot,
|
|
85
86
|
PersistedAgent,
|
|
@@ -182,7 +183,7 @@ function createRootConversationEndpoint(
|
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
function shouldSurfaceNotification(notification: CoordinatorNotification): boolean {
|
|
185
|
-
return ["failure", "interruption", "unavailable", "fork-clone-failure"].includes(
|
|
186
|
+
return ["failure", "interruption", "unavailable", "fork-clone-failure", "tool-warning"].includes(
|
|
186
187
|
notification.type,
|
|
187
188
|
);
|
|
188
189
|
}
|
|
@@ -192,7 +193,8 @@ function notificationLevel(notification: CoordinatorNotification): "info" | "war
|
|
|
192
193
|
if (
|
|
193
194
|
notification.type === "cancellation" ||
|
|
194
195
|
notification.type === "interruption" ||
|
|
195
|
-
notification.type === "unavailable"
|
|
196
|
+
notification.type === "unavailable" ||
|
|
197
|
+
notification.type === "tool-warning"
|
|
196
198
|
) {
|
|
197
199
|
return "warning";
|
|
198
200
|
}
|
|
@@ -584,6 +586,19 @@ export class MinimalSubagentsLifecycleController {
|
|
|
584
586
|
projectTrusted: context.isProjectTrusted(),
|
|
585
587
|
maxSubagentDepth: minimalSubagentsConfig.maxSubagentDepth,
|
|
586
588
|
onChildSessionActivity: () => activeCoordinator?.scheduleDeliveryReconciliation(),
|
|
589
|
+
observeSession: (session, agentId, resourceInputs) => {
|
|
590
|
+
let observer: ChildSessionObserver | undefined;
|
|
591
|
+
this.pi.events.emit("pi-minimal-subagents:observe-session", {
|
|
592
|
+
rootSessionId,
|
|
593
|
+
agentId,
|
|
594
|
+
session,
|
|
595
|
+
resourceInputs,
|
|
596
|
+
attach: (attached: ChildSessionObserver) => {
|
|
597
|
+
observer = attached;
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
return observer;
|
|
601
|
+
},
|
|
587
602
|
getCoordinatorTools: (callerId) => {
|
|
588
603
|
const coordinator = requireActiveCoordinator();
|
|
589
604
|
return createCoordinatorToolDefinitions({
|
|
@@ -605,6 +620,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
605
620
|
sessions: sessionFactory,
|
|
606
621
|
root: createRootConversationEndpoint(this.pi, context),
|
|
607
622
|
maxSubagentDepth: minimalSubagentsConfig.maxSubagentDepth,
|
|
623
|
+
toolsets: minimalSubagentsConfig.toolsets,
|
|
608
624
|
registry: {
|
|
609
625
|
rootSessionId,
|
|
610
626
|
append: (registryEvent) => this.pi.appendEntry(REGISTRY_ENTRY_TYPE, registryEvent),
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
SettingsManager,
|
|
18
18
|
sessionEntryToContextMessages,
|
|
19
19
|
type AgentSessionEvent,
|
|
20
|
+
type Extension,
|
|
20
21
|
type SessionEntry,
|
|
21
22
|
type ToolDefinition,
|
|
22
23
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -53,6 +54,7 @@ import { addMinimalSubagentsUsage } from "./minimal-subagents-usage.js";
|
|
|
53
54
|
import type {
|
|
54
55
|
AgentSessionFactory,
|
|
55
56
|
ChildAgentRuntime,
|
|
57
|
+
ChildSessionObserver,
|
|
56
58
|
ChildAgentTranscriptSnapshot,
|
|
57
59
|
CoordinatorMessage,
|
|
58
60
|
PersistedAgent,
|
|
@@ -67,6 +69,7 @@ const PI_BUILTIN_ORDINARY_TOOL_NAMES = new Set([
|
|
|
67
69
|
"find",
|
|
68
70
|
"ls",
|
|
69
71
|
"bash",
|
|
72
|
+
"powershell",
|
|
70
73
|
"edit",
|
|
71
74
|
"write",
|
|
72
75
|
]);
|
|
@@ -129,11 +132,21 @@ function installChildToolCapabilityPolicy(
|
|
|
129
132
|
session: AgentSession,
|
|
130
133
|
allowedToolNames: readonly string[],
|
|
131
134
|
runtimeToolAdapters: readonly RuntimeToolAdapter[],
|
|
135
|
+
preserveGrantedTools = true,
|
|
132
136
|
): void {
|
|
133
137
|
const applyActiveTools = session.setActiveToolsByName.bind(session);
|
|
134
138
|
session.setActiveToolsByName = (requestedToolNames) => {
|
|
139
|
+
const permitted = resolveChildActiveToolNames(
|
|
140
|
+
allowedToolNames,
|
|
141
|
+
requestedToolNames,
|
|
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.
|
|
135
146
|
applyActiveTools(
|
|
136
|
-
|
|
147
|
+
preserveGrantedTools
|
|
148
|
+
? permitted
|
|
149
|
+
: permitted.filter((name) => requestedToolNames.includes(name)),
|
|
137
150
|
);
|
|
138
151
|
};
|
|
139
152
|
session.setActiveToolsByName(session.getActiveToolNames());
|
|
@@ -169,6 +182,15 @@ export interface PiAgentSessionFactoryOptions {
|
|
|
169
182
|
sessionFileTrash?: SessionFileTrashCapability;
|
|
170
183
|
getCoordinatorTools: (callerId: string) => ToolDefinition[];
|
|
171
184
|
onChildSessionActivity?: () => void;
|
|
185
|
+
observeSession?: (
|
|
186
|
+
session: AgentSession,
|
|
187
|
+
agentId: string,
|
|
188
|
+
resourceInputs: {
|
|
189
|
+
agentDir: string;
|
|
190
|
+
extensions: readonly Pick<Extension, "path" | "resolvedPath" | "sourceInfo" | "hidden">[];
|
|
191
|
+
flagValues: ReadonlyMap<string, boolean | string>;
|
|
192
|
+
},
|
|
193
|
+
) => ChildSessionObserver | undefined;
|
|
172
194
|
}
|
|
173
195
|
|
|
174
196
|
/** Build one child prompt using the active delegation depth rather than persisted launch state. */
|
|
@@ -483,7 +505,7 @@ function collectChildTurnOutcome(
|
|
|
483
505
|
error: "No terminal assistant response",
|
|
484
506
|
};
|
|
485
507
|
}
|
|
486
|
-
if (finalAssistant.stopReason === "aborted") {
|
|
508
|
+
if (aborted || finalAssistant.stopReason === "aborted") {
|
|
487
509
|
return {
|
|
488
510
|
status: "cancelled",
|
|
489
511
|
output: assistantText(finalAssistant),
|
|
@@ -543,6 +565,7 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
543
565
|
private readonly modelRuntime: ModelRuntime,
|
|
544
566
|
private readonly modelById: ReadonlyMap<string, Model<any>>,
|
|
545
567
|
onSessionActivity?: () => void,
|
|
568
|
+
private readonly observer?: ChildSessionObserver,
|
|
546
569
|
) {
|
|
547
570
|
// Keep this child-only; AgentSession.setSteeringMode would overwrite the user's global setting.
|
|
548
571
|
session.agent.steeringMode = "all";
|
|
@@ -602,12 +625,16 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
602
625
|
|
|
603
626
|
async abort(): Promise<void> {
|
|
604
627
|
this.aborted = true;
|
|
605
|
-
await this.session.abort();
|
|
628
|
+
await Promise.all([this.observer?.abort(), this.session.abort()]);
|
|
606
629
|
}
|
|
607
630
|
|
|
608
|
-
dispose(): void {
|
|
631
|
+
async dispose(): Promise<void> {
|
|
609
632
|
this.unsubscribe();
|
|
610
|
-
|
|
633
|
+
try {
|
|
634
|
+
await this.observer?.dispose();
|
|
635
|
+
} finally {
|
|
636
|
+
this.session.dispose();
|
|
637
|
+
}
|
|
611
638
|
}
|
|
612
639
|
|
|
613
640
|
getRuntimeProfile(): RuntimeProfile | undefined {
|
|
@@ -704,7 +731,20 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
704
731
|
|
|
705
732
|
private async captureTurn(operation: () => Promise<void>): Promise<RuntimeTurnOutcome> {
|
|
706
733
|
this.aborted = false;
|
|
707
|
-
|
|
734
|
+
this.observer?.beginTurn();
|
|
735
|
+
return captureChildTurnOutcome(
|
|
736
|
+
this.session,
|
|
737
|
+
async () => {
|
|
738
|
+
try {
|
|
739
|
+
await operation();
|
|
740
|
+
if (!this.aborted) await this.observer?.finishTurn();
|
|
741
|
+
} catch (error) {
|
|
742
|
+
await this.observer?.abort();
|
|
743
|
+
throw error;
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
() => this.aborted,
|
|
747
|
+
);
|
|
708
748
|
}
|
|
709
749
|
|
|
710
750
|
private async compactImportedContext(
|
|
@@ -1154,15 +1194,17 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
1154
1194
|
session.dispose();
|
|
1155
1195
|
throw new Error(`Minimal subagents child tool loading failed: ${missingTools.join(", ")}`);
|
|
1156
1196
|
}
|
|
1157
|
-
// The inner policy
|
|
1158
|
-
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters);
|
|
1197
|
+
// The inner policy bounds extension-selected exposure without restoring hidden ordinary tools.
|
|
1198
|
+
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters, false);
|
|
1159
1199
|
await session.bindExtensions({ mode: "print" });
|
|
1160
1200
|
// The outer policy filters names before extension wrappers build their own tool catalogues.
|
|
1161
1201
|
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters);
|
|
1162
|
-
|
|
1202
|
+
// Exposure policy may route granted Coordinator Tools through another tool;
|
|
1203
|
+
// require their definitions to remain registered, not necessarily direct.
|
|
1204
|
+
const registeredNames = new Set(session.getAllTools().map((tool) => tool.name));
|
|
1163
1205
|
const missingCoordinatorTools = coordinatorTools
|
|
1164
1206
|
.map((tool) => tool.name)
|
|
1165
|
-
.filter((toolName) => !
|
|
1207
|
+
.filter((toolName) => !registeredNames.has(toolName));
|
|
1166
1208
|
if (missingCoordinatorTools.length > 0) {
|
|
1167
1209
|
session.dispose();
|
|
1168
1210
|
throw new Error(
|
|
@@ -1174,6 +1216,18 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
1174
1216
|
modelRuntime,
|
|
1175
1217
|
this.modelById,
|
|
1176
1218
|
this.options.onChildSessionActivity,
|
|
1219
|
+
this.options.observeSession?.(session, agent.agent_id, {
|
|
1220
|
+
agentDir: this.options.agentDir,
|
|
1221
|
+
extensions: resourceLoader
|
|
1222
|
+
.getExtensions()
|
|
1223
|
+
.extensions.map(({ path, resolvedPath, sourceInfo, hidden }) => ({
|
|
1224
|
+
path,
|
|
1225
|
+
resolvedPath,
|
|
1226
|
+
sourceInfo,
|
|
1227
|
+
hidden,
|
|
1228
|
+
})),
|
|
1229
|
+
flagValues: new Map(resourceLoader.getExtensions().runtime.flagValues),
|
|
1230
|
+
}),
|
|
1177
1231
|
);
|
|
1178
1232
|
}
|
|
1179
1233
|
|
|
@@ -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. */
|
|
@@ -218,6 +224,14 @@ export interface CoordinatorMessage {
|
|
|
218
224
|
};
|
|
219
225
|
}
|
|
220
226
|
|
|
227
|
+
/** Optional review work attached by the root and awaited by the existing child owner. */
|
|
228
|
+
export interface ChildSessionObserver {
|
|
229
|
+
beginTurn(): void;
|
|
230
|
+
finishTurn(): Promise<void>;
|
|
231
|
+
abort(): Promise<void>;
|
|
232
|
+
dispose(): Promise<void>;
|
|
233
|
+
}
|
|
234
|
+
|
|
221
235
|
/** Process-local adapter around one SDK-created Pi child session. */
|
|
222
236
|
export interface ChildAgentRuntime {
|
|
223
237
|
readonly sessionLeafId: string | undefined;
|
|
@@ -232,7 +246,7 @@ export interface ChildAgentRuntime {
|
|
|
232
246
|
/** Queue one typed coordinator message into the child session. */
|
|
233
247
|
queueCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
|
|
234
248
|
abort(): Promise<void>;
|
|
235
|
-
dispose(): void
|
|
249
|
+
dispose(): void | Promise<void>;
|
|
236
250
|
/** Return the live Runtime Profile, or undefined when the SDK session has no model. */
|
|
237
251
|
getRuntimeProfile(): RuntimeProfile | undefined;
|
|
238
252
|
/** Return the effective ordinary tools after child extensions apply runtime adapters. */
|
|
@@ -380,7 +394,8 @@ export interface CoordinatorNotification {
|
|
|
380
394
|
| "interruption"
|
|
381
395
|
| "restoration"
|
|
382
396
|
| "unavailable"
|
|
383
|
-
| "fork-clone-failure"
|
|
397
|
+
| "fork-clone-failure"
|
|
398
|
+
| "tool-warning";
|
|
384
399
|
agentId: string;
|
|
385
400
|
message: string;
|
|
386
401
|
}
|
|
@@ -391,6 +406,7 @@ export interface CoordinatorDependencies {
|
|
|
391
406
|
sessions: AgentSessionFactory;
|
|
392
407
|
root: RootConversationEndpoint;
|
|
393
408
|
maxSubagentDepth?: number;
|
|
409
|
+
toolsets?: MinimalSubagentsToolsets;
|
|
394
410
|
now?: () => Date;
|
|
395
411
|
automaticDeliveryGraceMs?: number;
|
|
396
412
|
notify?: (notification: CoordinatorNotification) => void;
|