@ian-pascoe/pi-minimal-subagents 0.7.1 → 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 +71 -9
- package/package.json +2 -1
- package/src/minimal-subagents-access.ts +4 -3
- package/src/minimal-subagents-capabilities.ts +39 -15
- package/src/minimal-subagents-config.ts +48 -2
- package/src/minimal-subagents-coordinator.ts +88 -39
- package/src/minimal-subagents-extension.ts +4 -2
- package/src/minimal-subagents-sessions.ts +49 -11
- package/src/minimal-subagents-tool-schemas.ts +2 -2
- package/src/minimal-subagents-types.ts +17 -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
|
|
@@ -226,8 +282,14 @@ Deleting a child first verifies its session header and persistent identity,
|
|
|
226
282
|
then uses the optional `trash` command when available and falls back to
|
|
227
283
|
unlinking its session file. Deletion prunes pending delivery state and retained
|
|
228
284
|
recent-message projections sourced from the complete deleted subtree. Restore
|
|
229
|
-
and clone perform the same ownership check
|
|
230
|
-
leaf.
|
|
285
|
+
and clone perform the same ownership check against the recorded child-session
|
|
286
|
+
leaf. Restoration loads metadata and validates saved sessions without starting
|
|
287
|
+
child runtimes or their extension services (including MCP Servers). Status,
|
|
288
|
+
saved transcripts, settled-result waits, cancellation of idle children, and
|
|
289
|
+
session management do not start runtimes. A message or an undelivered child-bound
|
|
290
|
+
result opens only its recipient's runtime on demand; saved Delivery Evidence
|
|
291
|
+
prevents already-delivered work from reopening it. Runtime initialization errors
|
|
292
|
+
are reported when that child is first needed.
|
|
231
293
|
|
|
232
294
|
Registry replay and Delivery Evidence are scoped to the Root Agent's active
|
|
233
295
|
session-tree branch. Registry writes use V2 records with complete field,
|
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": {
|
|
@@ -126,10 +126,11 @@ export function reconcileCoordinatorToolAccess(
|
|
|
126
126
|
activeToolNames: readonly string[],
|
|
127
127
|
enabled: boolean,
|
|
128
128
|
): string[] {
|
|
129
|
-
const
|
|
130
|
-
|
|
129
|
+
const missing = new Set<string>(COORDINATOR_TOOL_NAMES);
|
|
130
|
+
const retained = activeToolNames.filter(
|
|
131
|
+
(toolName) => !COORDINATOR_TOOL_NAME_SET.has(toolName) || (enabled && missing.delete(toolName)),
|
|
131
132
|
);
|
|
132
|
-
return enabled ? [...
|
|
133
|
+
return enabled ? [...retained, ...missing] : retained;
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
/** Resolve branch state over settings and include read-only Coordinator Tool activation. */
|
|
@@ -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,
|
|
@@ -655,19 +682,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
655
682
|
});
|
|
656
683
|
continue;
|
|
657
684
|
}
|
|
658
|
-
const runtime = await this.dependencies.sessions.openRuntime(agent);
|
|
659
|
-
if (restoreEpoch !== this.lifecycleEpoch) {
|
|
660
|
-
runtime.dispose();
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
if (!runtime.sessionLeafId) {
|
|
664
|
-
runtime.dispose();
|
|
665
|
-
throw new Error(
|
|
666
|
-
`Minimal subagents session restoration: no selected session leaf for ${agent.agent_id}`,
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
this.runtimes.set(agent.agent_id, runtime);
|
|
670
|
-
agent.session_leaf_id = runtime.sessionLeafId;
|
|
671
685
|
agent.availability = "available";
|
|
672
686
|
if (previousAvailability !== "available")
|
|
673
687
|
agent.latest_activity_at = this.now().toISOString();
|
|
@@ -926,6 +940,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
926
940
|
new Error(agent.clone_error ?? `No persistent session exists for ${agent.agent_id}`),
|
|
927
941
|
);
|
|
928
942
|
}
|
|
943
|
+
const openingForTurn = agent.active_turn_id !== undefined;
|
|
929
944
|
const initialization = this.dependencies.sessions
|
|
930
945
|
.openRuntime(agent)
|
|
931
946
|
.then((runtime) => {
|
|
@@ -933,9 +948,29 @@ export class MinimalSubagentsCoordinator {
|
|
|
933
948
|
runtime.dispose();
|
|
934
949
|
throw new Error(`Minimal subagents runtime replaced while opening ${agent.agent_id}`);
|
|
935
950
|
}
|
|
951
|
+
if (!runtime.sessionLeafId) {
|
|
952
|
+
runtime.dispose();
|
|
953
|
+
throw new Error(
|
|
954
|
+
`Minimal subagents session restoration: no selected session leaf for ${agent.agent_id}`,
|
|
955
|
+
);
|
|
956
|
+
}
|
|
936
957
|
this.runtimes.set(agent.agent_id, runtime);
|
|
958
|
+
agent.session_leaf_id = runtime.sessionLeafId;
|
|
937
959
|
return runtime;
|
|
938
960
|
})
|
|
961
|
+
.catch((error) => {
|
|
962
|
+
if (!openingForTurn && this.agents.get(agent.agent_id) === agent) {
|
|
963
|
+
agent.availability = "unavailable";
|
|
964
|
+
agent.latest_activity_at = this.now().toISOString();
|
|
965
|
+
agent.unavailable_reason = error instanceof Error ? error.message : String(error);
|
|
966
|
+
this.dependencies.notify?.({
|
|
967
|
+
type: "unavailable",
|
|
968
|
+
agentId: agent.agent_id,
|
|
969
|
+
message: `${agent.agent_id} unavailable: ${agent.unavailable_reason}`,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
throw error;
|
|
973
|
+
})
|
|
939
974
|
.finally(() => {
|
|
940
975
|
if (this.runtimeInitializations.get(agent.agent_id) === initialization) {
|
|
941
976
|
this.runtimeInitializations.delete(agent.agent_id);
|
|
@@ -1307,6 +1342,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1307
1342
|
}
|
|
1308
1343
|
const target = this.requireUsableAgent(targetId, "message");
|
|
1309
1344
|
const runtime = this.runtimes.get(targetId) ?? (await this.ensureRuntime(target));
|
|
1345
|
+
this.assertAccepting();
|
|
1310
1346
|
if (!isCurrentDelivery()) {
|
|
1311
1347
|
throw new Error("Minimal subagents delivery abandoned after session branch change");
|
|
1312
1348
|
}
|
|
@@ -1430,34 +1466,45 @@ export class MinimalSubagentsCoordinator {
|
|
|
1430
1466
|
}
|
|
1431
1467
|
|
|
1432
1468
|
private hasDeliveryEvidence(delivery: PersistedDelivery): boolean {
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
);
|
|
1438
|
-
}
|
|
1439
|
-
return (
|
|
1440
|
-
this.runtimes
|
|
1441
|
-
.get(delivery.destination_agent_id)
|
|
1442
|
-
?.hasDeliveryEvidence(delivery.source_agent_id, delivery.source_turn_id) ?? false
|
|
1469
|
+
return this.hasRecipientDeliveryEvidence(
|
|
1470
|
+
delivery.destination_agent_id,
|
|
1471
|
+
delivery.source_agent_id,
|
|
1472
|
+
delivery.source_turn_id,
|
|
1443
1473
|
);
|
|
1444
1474
|
}
|
|
1445
1475
|
|
|
1446
1476
|
private hasCoordinationDeliveryEvidence(delivery: PersistedCoordinationDelivery): boolean {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1477
|
+
return this.hasRecipientDeliveryEvidence(
|
|
1478
|
+
delivery.destination_agent_id,
|
|
1479
|
+
delivery.message.details.source_agent_id,
|
|
1480
|
+
delivery.message.details.source_turn_id,
|
|
1481
|
+
delivery.delivery_id,
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
private hasRecipientDeliveryEvidence(
|
|
1486
|
+
targetId: string,
|
|
1487
|
+
sourceAgentId: string,
|
|
1488
|
+
sourceTurnId: string,
|
|
1489
|
+
deliveryId?: string,
|
|
1490
|
+
): boolean {
|
|
1491
|
+
const endpoint = targetId === "root" ? this.dependencies.root : this.runtimes.get(targetId);
|
|
1492
|
+
if (endpoint) return endpoint.hasDeliveryEvidence(sourceAgentId, sourceTurnId, deliveryId);
|
|
1493
|
+
const agent = this.agents.get(targetId);
|
|
1494
|
+
if (!agent) return false;
|
|
1495
|
+
try {
|
|
1496
|
+
return (
|
|
1497
|
+
this.dependencies.sessions.hasDeliveryEvidence?.(
|
|
1498
|
+
agent,
|
|
1499
|
+
sourceAgentId,
|
|
1500
|
+
sourceTurnId,
|
|
1501
|
+
deliveryId,
|
|
1502
|
+
) ?? false
|
|
1454
1503
|
);
|
|
1504
|
+
} catch {
|
|
1505
|
+
// Unverified sessions cannot prove delivery; runtime opening reports their failure.
|
|
1506
|
+
return false;
|
|
1455
1507
|
}
|
|
1456
|
-
return (
|
|
1457
|
-
this.runtimes
|
|
1458
|
-
.get(delivery.destination_agent_id)
|
|
1459
|
-
?.hasDeliveryEvidence(sourceAgentId, sourceTurnId, delivery.delivery_id) ?? false
|
|
1460
|
-
);
|
|
1461
1508
|
}
|
|
1462
1509
|
|
|
1463
1510
|
private settleDelivery(delivery: PersistedDelivery): void {
|
|
@@ -1518,7 +1565,9 @@ export class MinimalSubagentsCoordinator {
|
|
|
1518
1565
|
capability_ceiling: [...agent.capability_ceiling],
|
|
1519
1566
|
spawn_entry_id: agent.spawn_entry_id,
|
|
1520
1567
|
recent_messages: structuredClone(agent.recent_messages),
|
|
1521
|
-
recent_activity: buildRecentAgentActivity(
|
|
1568
|
+
recent_activity: buildRecentAgentActivity(
|
|
1569
|
+
runtime?.snapshotActivityMessages() ?? this.inspectTranscript(agent.agent_id).messages,
|
|
1570
|
+
),
|
|
1522
1571
|
latest_result: agent.latest_result ? structuredClone(agent.latest_result) : undefined,
|
|
1523
1572
|
missing_dependencies: [...agent.missing_dependencies],
|
|
1524
1573
|
unavailable_reason: agent.unavailable_reason,
|
|
@@ -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());
|
|
@@ -826,24 +837,49 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
826
837
|
stat.ctimeMs,
|
|
827
838
|
]);
|
|
828
839
|
if (this.savedTranscript?.key === key) return this.savedTranscript.snapshot;
|
|
840
|
+
const manager = this.readSession(agent);
|
|
841
|
+
const snapshot = selectChildAgentTranscript(
|
|
842
|
+
manager.getBranch(agent.session_leaf_id).flatMap(sessionEntryToContextMessages),
|
|
843
|
+
);
|
|
844
|
+
this.savedTranscript = { key, snapshot };
|
|
845
|
+
return snapshot;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** Inspect durable evidence without starting child extensions or changing the saved branch. */
|
|
849
|
+
hasDeliveryEvidence(
|
|
850
|
+
agent: PersistedAgent,
|
|
851
|
+
sourceAgentId: string,
|
|
852
|
+
sourceTurnId: string,
|
|
853
|
+
deliveryId?: string,
|
|
854
|
+
): boolean {
|
|
855
|
+
return findDeliveryEvidence(
|
|
856
|
+
this.readSession(agent).getBranch(agent.session_leaf_id),
|
|
857
|
+
sourceAgentId,
|
|
858
|
+
sourceTurnId,
|
|
859
|
+
deliveryId,
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private readSession(agent: PersistedAgent): SessionManager {
|
|
864
|
+
if (!agent.session_file || !agent.session_id || !agent.session_leaf_id) {
|
|
865
|
+
throw new Error(`Child Session Position is unavailable for ${agent.agent_id}.`);
|
|
866
|
+
}
|
|
867
|
+
const sessionFile = canonicalPath(agent.session_file);
|
|
829
868
|
const entries = parseSessionEntries(readFileSync(sessionFile, "utf8"));
|
|
830
869
|
if (entries[0]?.type !== "session")
|
|
831
870
|
throw new Error(`Invalid child session file: ${sessionFile}`);
|
|
832
871
|
// SessionManager.open can migrate/rewrite files; an in-memory reader cannot write them.
|
|
833
872
|
const manager = SessionManager.inMemory(this.options.cwd, undefined, entries);
|
|
834
873
|
verifyChildSessionIdentity(manager, agent, this.options.rootSessionId);
|
|
835
|
-
|
|
836
|
-
manager.getBranch(agent.session_leaf_id).flatMap(sessionEntryToContextMessages),
|
|
837
|
-
);
|
|
838
|
-
this.savedTranscript = { key, snapshot };
|
|
839
|
-
return snapshot;
|
|
874
|
+
return manager;
|
|
840
875
|
}
|
|
841
876
|
|
|
842
877
|
resolveLaunchMissingDependencies(agent: PersistedAgent): Promise<string[]> {
|
|
843
878
|
return this.findMissingDependencies(agent, false);
|
|
844
879
|
}
|
|
845
880
|
|
|
846
|
-
resolveRestorationMissingDependencies(agent: PersistedAgent): Promise<string[]> {
|
|
881
|
+
async resolveRestorationMissingDependencies(agent: PersistedAgent): Promise<string[]> {
|
|
882
|
+
this.readSession(agent);
|
|
847
883
|
return this.findMissingDependencies(agent, this.options.modelScopeRestricted);
|
|
848
884
|
}
|
|
849
885
|
|
|
@@ -1129,15 +1165,17 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
1129
1165
|
session.dispose();
|
|
1130
1166
|
throw new Error(`Minimal subagents child tool loading failed: ${missingTools.join(", ")}`);
|
|
1131
1167
|
}
|
|
1132
|
-
// The inner policy
|
|
1133
|
-
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters);
|
|
1168
|
+
// The inner policy bounds extension-selected exposure without restoring hidden ordinary tools.
|
|
1169
|
+
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters, false);
|
|
1134
1170
|
await session.bindExtensions({ mode: "print" });
|
|
1135
1171
|
// The outer policy filters names before extension wrappers build their own tool catalogues.
|
|
1136
1172
|
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters);
|
|
1137
|
-
|
|
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));
|
|
1138
1176
|
const missingCoordinatorTools = coordinatorTools
|
|
1139
1177
|
.map((tool) => tool.name)
|
|
1140
|
-
.filter((toolName) => !
|
|
1178
|
+
.filter((toolName) => !registeredNames.has(toolName));
|
|
1141
1179
|
if (missingCoordinatorTools.length > 0) {
|
|
1142
1180
|
session.dispose();
|
|
1143
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. */
|
|
@@ -261,6 +267,13 @@ export interface AgentSessionFactory {
|
|
|
261
267
|
openRuntime(agent: PersistedAgent): Promise<ChildAgentRuntime>;
|
|
262
268
|
/** Read verified saved history independently of runtime restoration dependencies. */
|
|
263
269
|
readTranscript?(agent: PersistedAgent): ChildAgentTranscriptSnapshot;
|
|
270
|
+
/** Read verified selected-branch evidence without opening a runtime. */
|
|
271
|
+
hasDeliveryEvidence?(
|
|
272
|
+
agent: PersistedAgent,
|
|
273
|
+
sourceAgentId: string,
|
|
274
|
+
sourceTurnId: string,
|
|
275
|
+
deliveryId?: string,
|
|
276
|
+
): boolean;
|
|
264
277
|
resolveLaunchMissingDependencies(agent: PersistedAgent): Promise<string[]>;
|
|
265
278
|
resolveRestorationMissingDependencies(agent: PersistedAgent): Promise<string[]>;
|
|
266
279
|
resolveThinkingLevel(modelId: string, requested: ThinkingLevel): ThinkingLevel;
|
|
@@ -373,7 +386,8 @@ export interface CoordinatorNotification {
|
|
|
373
386
|
| "interruption"
|
|
374
387
|
| "restoration"
|
|
375
388
|
| "unavailable"
|
|
376
|
-
| "fork-clone-failure"
|
|
389
|
+
| "fork-clone-failure"
|
|
390
|
+
| "tool-warning";
|
|
377
391
|
agentId: string;
|
|
378
392
|
message: string;
|
|
379
393
|
}
|
|
@@ -384,6 +398,7 @@ export interface CoordinatorDependencies {
|
|
|
384
398
|
sessions: AgentSessionFactory;
|
|
385
399
|
root: RootConversationEndpoint;
|
|
386
400
|
maxSubagentDepth?: number;
|
|
401
|
+
toolsets?: MinimalSubagentsToolsets;
|
|
387
402
|
now?: () => Date;
|
|
388
403
|
automaticDeliveryGraceMs?: number;
|
|
389
404
|
notify?: (notification: CoordinatorNotification) => void;
|