@ian-pascoe/pi-minimal-subagents 0.8.0 → 0.10.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 +15 -11
- package/package.json +3 -6
- package/src/minimal-subagents-child-resources.ts +21 -0
- package/src/minimal-subagents-coordinator.ts +7 -7
- package/src/minimal-subagents-extension.ts +52 -18
- package/src/minimal-subagents-sessions.ts +85 -27
- package/src/minimal-subagents-settings-writer.ts +39 -275
- package/src/minimal-subagents-tools.ts +8 -5
- package/src/minimal-subagents-types.ts +9 -1
package/README.md
CHANGED
|
@@ -156,9 +156,8 @@ spawn attempts; the root retains recursive hierarchy management.
|
|
|
156
156
|
|
|
157
157
|
## Toolsets
|
|
158
158
|
|
|
159
|
-
Configure ordinary tools using case-sensitive minimatch
|
|
160
|
-
|
|
161
|
-
CodeMode exposure-rule objects. For example:
|
|
159
|
+
Configure ordinary tools using case-sensitive minimatch pattern strings. For
|
|
160
|
+
example:
|
|
162
161
|
|
|
163
162
|
```json
|
|
164
163
|
{
|
|
@@ -183,14 +182,15 @@ Tool Presets are cumulative:
|
|
|
183
182
|
- `tools: "none"` or `tools: []`: Base Toolset only.
|
|
184
183
|
- `tools: ["read"]`: Base Toolset + exactly `read`; arrays do not expand patterns
|
|
185
184
|
or presets.
|
|
186
|
-
- Omitted `tools`: Base Toolset + the caller's
|
|
185
|
+
- Omitted `tools`: Base Toolset + the caller's Reachable Tools: the Root Agent's
|
|
186
|
+
active tools plus tools scripts can call without declaring them (`codemode` or
|
|
187
|
+
`deferred` exposure, such as MCP tools), or a Child Agent caller's own grant.
|
|
187
188
|
|
|
188
189
|
Patterns select from permitted ordinary tool names, including inactive tools
|
|
189
190
|
registered at the root. Their matches are unioned in pattern order, retaining
|
|
190
191
|
registry order within each pattern and removing duplicates at first occurrence.
|
|
191
192
|
Each pattern is independent: a negated minimatch pattern matches its complement;
|
|
192
|
-
it does not subtract earlier matches
|
|
193
|
-
exposure policy. Coordinator Tools remain separately controlled by delegation.
|
|
193
|
+
it does not subtract earlier matches. Coordinator Tools remain separately controlled by delegation.
|
|
194
194
|
|
|
195
195
|
Invalid configuration entries and patterns matching no permitted ordinary tools
|
|
196
196
|
warn and are skipped. Configured tools unavailable in child resources are also
|
|
@@ -206,11 +206,15 @@ still apply; the saved grant is not silently rewritten. Toolsets configure names
|
|
|
206
206
|
not tool operations: granting `lsp`, `dap`, or another multifunction tool grants
|
|
207
207
|
that tool's available operations, regardless of preset name.
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
209
|
+
Child Agents load Pi's built-in `codemode`, `tool-search`, and `mcp` extensions
|
|
210
|
+
unless settings disable them (`-builtin:<name>`); each child connects its own MCP
|
|
211
|
+
servers. A tool's exposure still decides whether it is declared or reachable only
|
|
212
|
+
through `codemode` or `tool_search`. Granted `codemode`- or `deferred`-exposed
|
|
213
|
+
tools are callable but not declared, so a child that needs them also needs
|
|
214
|
+
`codemode` or `tool_search` in its grant. Ungranted tools are unreachable in the
|
|
215
|
+
child. MCP tools are checked at launch only by name, because children register
|
|
216
|
+
them after connecting; a tool whose server disappeared fails when called. Closing
|
|
217
|
+
a child runtime shuts its extensions down, closing its MCP connections.
|
|
214
218
|
|
|
215
219
|
## Capabilities and persistence
|
|
216
220
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-minimal-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Persistent nested subagents with bounded delegation for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -31,11 +31,8 @@
|
|
|
31
31
|
"provenance": true
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@types/proper-lockfile": "^4.1.4"
|
|
34
|
+
"@ian-pascoe/pi-utils": "^0.3.0",
|
|
35
|
+
"minimatch": "^10.2.6"
|
|
39
36
|
},
|
|
40
37
|
"peerDependencies": {
|
|
41
38
|
"@earendil-works/pi-agent-core": "*",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
|
+
import * as piCodingAgent from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { DefaultResourceLoader, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { canonicalPath } from "./minimal-subagents-paths.js";
|
|
4
5
|
import type { ProjectContextMode } from "./minimal-subagents-types.js";
|
|
@@ -12,6 +13,25 @@ export interface ChildResourceLoaderOptions {
|
|
|
12
13
|
settingsManager?: SettingsManager;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
type InlineExtension = NonNullable<
|
|
17
|
+
ConstructorParameters<typeof DefaultResourceLoader>[0]["extensionFactories"]
|
|
18
|
+
>[number];
|
|
19
|
+
|
|
20
|
+
/** Pi's exported built-in extensions (Pi 0.99+); older hosts have none, so neither do children. */
|
|
21
|
+
export function childBuiltinExtensions(): InlineExtension[] {
|
|
22
|
+
// SAFETY: Older Pi hosts lack these exports; the namespace yields undefined rather than failing.
|
|
23
|
+
const sdk = piCodingAgent as Partial<typeof piCodingAgent>;
|
|
24
|
+
return (
|
|
25
|
+
[
|
|
26
|
+
["codemode", sdk.createCodemodeExtension],
|
|
27
|
+
["tool-search", sdk.createToolSearchExtension],
|
|
28
|
+
["mcp", sdk.createMcpExtension],
|
|
29
|
+
] as const
|
|
30
|
+
).flatMap(([name, create]) =>
|
|
31
|
+
create ? [{ name, factory: create(), builtin: true, replaceable: true }] : [],
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
function createDefaultChildResourceLoaderOptions(
|
|
16
36
|
input: ChildResourceLoaderOptions,
|
|
17
37
|
): ConstructorParameters<typeof DefaultResourceLoader>[0] {
|
|
@@ -24,6 +44,7 @@ function createDefaultChildResourceLoaderOptions(
|
|
|
24
44
|
noContextFiles: false,
|
|
25
45
|
noSkills: false,
|
|
26
46
|
noPromptTemplates: false,
|
|
47
|
+
extensionFactories: childBuiltinExtensions(),
|
|
27
48
|
extensionsOverride: (base) => ({
|
|
28
49
|
...base,
|
|
29
50
|
extensions: base.extensions.filter(
|
|
@@ -570,7 +570,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
570
570
|
const runtime = this.runtimes.get(agent.agent_id);
|
|
571
571
|
try {
|
|
572
572
|
if (agent.active_turn_id) await this.cancelActiveTurn(agent);
|
|
573
|
-
runtime?.dispose();
|
|
573
|
+
await runtime?.dispose();
|
|
574
574
|
this.runtimes.delete(agent.agent_id);
|
|
575
575
|
if (agent.session_file) {
|
|
576
576
|
await this.dependencies.sessions.trashSession(agent);
|
|
@@ -622,7 +622,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
622
622
|
await Promise.allSettled(
|
|
623
623
|
abandonedRuntimes.map((runtime) => (runtime.isRunning ? runtime.abort() : Promise.resolve())),
|
|
624
624
|
);
|
|
625
|
-
for (const runtime of abandonedRuntimes) runtime.dispose();
|
|
625
|
+
for (const runtime of abandonedRuntimes) await runtime.dispose();
|
|
626
626
|
this.runtimes.clear();
|
|
627
627
|
this.runtimeInitializations.clear();
|
|
628
628
|
this.pendingAgentIds.clear();
|
|
@@ -892,7 +892,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
892
892
|
await Promise.allSettled(this.runtimeInitializations.values());
|
|
893
893
|
await Promise.allSettled(this.backgroundOperations);
|
|
894
894
|
await Promise.allSettled(this.recipientQueues.values());
|
|
895
|
-
for (const runtime of this.runtimes.values()) runtime.dispose();
|
|
895
|
+
for (const runtime of this.runtimes.values()) await runtime.dispose();
|
|
896
896
|
this.runtimes.clear();
|
|
897
897
|
}
|
|
898
898
|
|
|
@@ -910,7 +910,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
910
910
|
const runtime = await this.ensureRuntime(agent);
|
|
911
911
|
if (this.agents.get(agentId) !== agent || agent.active_turn_id !== turnId) {
|
|
912
912
|
if (!this.agents.has(agentId) || !this.acceptingOperations) {
|
|
913
|
-
runtime.dispose();
|
|
913
|
+
await runtime.dispose();
|
|
914
914
|
this.runtimes.delete(agentId);
|
|
915
915
|
}
|
|
916
916
|
return;
|
|
@@ -943,13 +943,13 @@ export class MinimalSubagentsCoordinator {
|
|
|
943
943
|
const openingForTurn = agent.active_turn_id !== undefined;
|
|
944
944
|
const initialization = this.dependencies.sessions
|
|
945
945
|
.openRuntime(agent)
|
|
946
|
-
.then((runtime) => {
|
|
946
|
+
.then(async (runtime) => {
|
|
947
947
|
if (this.agents.get(agent.agent_id) !== agent) {
|
|
948
|
-
runtime.dispose();
|
|
948
|
+
await runtime.dispose();
|
|
949
949
|
throw new Error(`Minimal subagents runtime replaced while opening ${agent.agent_id}`);
|
|
950
950
|
}
|
|
951
951
|
if (!runtime.sessionLeafId) {
|
|
952
|
-
runtime.dispose();
|
|
952
|
+
await runtime.dispose();
|
|
953
953
|
throw new Error(
|
|
954
954
|
`Minimal subagents session restoration: no selected session leaf for ${agent.agent_id}`,
|
|
955
955
|
);
|
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
unavailableAgent,
|
|
64
64
|
} from "./minimal-subagents-sessions.js";
|
|
65
65
|
import { shutdownMinimalSubagentsSession } from "./minimal-subagents-shutdown.js";
|
|
66
|
-
import {
|
|
66
|
+
import { writeMinimalSubagentsEnabled } from "./minimal-subagents-settings-writer.js";
|
|
67
67
|
import {
|
|
68
68
|
MinimalSubagentsStatusPanelController,
|
|
69
69
|
type MinimalSubagentsStatusAccess,
|
|
@@ -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,
|
|
@@ -137,16 +138,39 @@ function currentConversationMessages(context: ExtensionContext): AgentMessage[]
|
|
|
137
138
|
return snapshotCommittedContext(messages, !context.isIdle());
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
export function availableToolNames(pi: Pick<ExtensionAPI, "getAllTools">): string[] {
|
|
142
|
+
return excludeCoordinatorTools(
|
|
143
|
+
pi
|
|
144
|
+
.getAllTools()
|
|
145
|
+
.filter((tool) => tool.exposure !== "hidden")
|
|
146
|
+
.map((tool) => tool.name),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Reachable Tools: active tools plus tools scripts can call without declaring them. */
|
|
151
|
+
export function reachableToolNames(
|
|
152
|
+
pi: Pick<ExtensionAPI, "getActiveTools" | "getAllTools">,
|
|
153
|
+
): string[] {
|
|
154
|
+
const active = new Set(pi.getActiveTools());
|
|
155
|
+
return excludeCoordinatorTools(
|
|
156
|
+
pi
|
|
157
|
+
.getAllTools()
|
|
158
|
+
.filter(
|
|
159
|
+
(tool) =>
|
|
160
|
+
active.has(tool.name) || tool.exposure === "codemode" || tool.exposure === "deferred",
|
|
161
|
+
)
|
|
162
|
+
.map((tool) => tool.name),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
140
166
|
function rootCallerSnapshot(pi: ExtensionAPI, context: ExtensionContext): CallerSnapshot {
|
|
141
167
|
if (!context.model) throw new Error("Minimal subagents spawn: root has no effective model");
|
|
142
|
-
const activeTools = excludeCoordinatorTools(pi.getActiveTools());
|
|
143
|
-
const availableTools = excludeCoordinatorTools(pi.getAllTools().map((tool) => tool.name));
|
|
144
168
|
return {
|
|
145
169
|
messages: currentConversationMessages(context),
|
|
146
170
|
model: `${context.model.provider}/${context.model.id}`,
|
|
147
171
|
thinkingLevel: context.thinkingLevel ?? pi.getThinkingLevel(),
|
|
148
|
-
ordinaryTools:
|
|
149
|
-
capabilityCeiling:
|
|
172
|
+
ordinaryTools: reachableToolNames(pi),
|
|
173
|
+
capabilityCeiling: availableToolNames(pi),
|
|
150
174
|
spawnEntryId: context.sessionManager.getLeafId() ?? "root",
|
|
151
175
|
};
|
|
152
176
|
}
|
|
@@ -560,9 +584,6 @@ export class MinimalSubagentsLifecycleController {
|
|
|
560
584
|
) {
|
|
561
585
|
models.push(context.model);
|
|
562
586
|
}
|
|
563
|
-
const availableToolNames = excludeCoordinatorTools(
|
|
564
|
-
this.pi.getAllTools().map((tool) => tool.name),
|
|
565
|
-
);
|
|
566
587
|
const schemas = createCoordinatorToolSchemas(eligibleModelIds);
|
|
567
588
|
let activeCoordinator: MinimalSubagentsCoordinator | undefined;
|
|
568
589
|
const requireActiveCoordinator = (): MinimalSubagentsCoordinator => {
|
|
@@ -580,11 +601,24 @@ export class MinimalSubagentsLifecycleController {
|
|
|
580
601
|
models,
|
|
581
602
|
eligibleModelIds,
|
|
582
603
|
modelScopeRestricted: enabledModelPatterns !== undefined,
|
|
583
|
-
availableToolNames,
|
|
604
|
+
availableToolNames: availableToolNames(this.pi),
|
|
584
605
|
getRuntimeToolAdapters: () => codexConversionRuntimeToolAdapters(this.pi),
|
|
585
606
|
projectTrusted: context.isProjectTrusted(),
|
|
586
607
|
maxSubagentDepth: minimalSubagentsConfig.maxSubagentDepth,
|
|
587
608
|
onChildSessionActivity: () => activeCoordinator?.scheduleDeliveryReconciliation(),
|
|
609
|
+
observeSession: (session, agentId, resourceInputs) => {
|
|
610
|
+
let observer: ChildSessionObserver | undefined;
|
|
611
|
+
this.pi.events.emit("pi-minimal-subagents:observe-session", {
|
|
612
|
+
rootSessionId,
|
|
613
|
+
agentId,
|
|
614
|
+
session,
|
|
615
|
+
resourceInputs,
|
|
616
|
+
attach: (attached: ChildSessionObserver) => {
|
|
617
|
+
observer = attached;
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
return observer;
|
|
621
|
+
},
|
|
588
622
|
getCoordinatorTools: (callerId) => {
|
|
589
623
|
const coordinator = requireActiveCoordinator();
|
|
590
624
|
return createCoordinatorToolDefinitions({
|
|
@@ -802,15 +836,15 @@ export class MinimalSubagentsLifecycleController {
|
|
|
802
836
|
|
|
803
837
|
let persistedScope: "global" | "project" | undefined;
|
|
804
838
|
if (command.scope !== "session") {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
context.ui.notify(
|
|
839
|
+
try {
|
|
840
|
+
await writeMinimalSubagentsEnabled(
|
|
841
|
+
context,
|
|
842
|
+
accessSession.agentDir,
|
|
843
|
+
command.scope,
|
|
844
|
+
command.action === "reset" ? undefined : command.action === "enable",
|
|
845
|
+
);
|
|
846
|
+
} catch (error) {
|
|
847
|
+
context.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
814
848
|
return;
|
|
815
849
|
}
|
|
816
850
|
persistedScope = command.scope;
|
|
@@ -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,
|
|
@@ -96,6 +98,18 @@ export interface RuntimeToolAdapter {
|
|
|
96
98
|
readonly replacements: readonly RuntimeToolReplacement[];
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
const MCP_RESOURCE_TOOL_NAMES = new Set([
|
|
102
|
+
"list_mcp_resources",
|
|
103
|
+
"list_mcp_resource_templates",
|
|
104
|
+
"read_mcp_resource",
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
// ponytail: Pi's built-in MCP registers tools only after connecting, so launch checks accept its
|
|
108
|
+
// tool names unverified; a vanished server's tools fail when called instead of at launch.
|
|
109
|
+
function isMcpToolName(toolName: string): boolean {
|
|
110
|
+
return toolName.startsWith("mcp__") || MCP_RESOURCE_TOOL_NAMES.has(toolName);
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
/** Resolve the exact active tools permitted by one Child Agent Launch Contract. */
|
|
100
114
|
export function resolveChildActiveToolNames(
|
|
101
115
|
allowedToolNames: readonly string[],
|
|
@@ -130,24 +144,24 @@ function installChildToolCapabilityPolicy(
|
|
|
130
144
|
session: AgentSession,
|
|
131
145
|
allowedToolNames: readonly string[],
|
|
132
146
|
runtimeToolAdapters: readonly RuntimeToolAdapter[],
|
|
133
|
-
preserveGrantedTools = true,
|
|
134
147
|
): void {
|
|
148
|
+
const isScriptOnly = (name: string) => {
|
|
149
|
+
const exposure = session.getAllTools().find((tool) => tool.name === name)?.exposure;
|
|
150
|
+
return exposure === "codemode" || exposure === "deferred";
|
|
151
|
+
};
|
|
135
152
|
const applyActiveTools = session.setActiveToolsByName.bind(session);
|
|
136
153
|
session.setActiveToolsByName = (requestedToolNames) => {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
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.
|
|
154
|
+
const requested = new Set(requestedToolNames);
|
|
155
|
+
// Granted declared tools stay active; `codemode` and `deferred` grants stay
|
|
156
|
+
// callable without being declared unless an extension activates them.
|
|
144
157
|
applyActiveTools(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
resolveChildActiveToolNames(allowedToolNames, requestedToolNames, runtimeToolAdapters).filter(
|
|
159
|
+
(name) => requested.has(name) || !isScriptOnly(name),
|
|
160
|
+
),
|
|
148
161
|
);
|
|
149
162
|
};
|
|
150
|
-
|
|
163
|
+
// Pi's `tools` option also activates script-only grants; drop them before extensions bind.
|
|
164
|
+
session.setActiveToolsByName(session.getActiveToolNames().filter((name) => !isScriptOnly(name)));
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
/** Moves one verified child session file to trash and reports command unavailability. */
|
|
@@ -180,6 +194,15 @@ export interface PiAgentSessionFactoryOptions {
|
|
|
180
194
|
sessionFileTrash?: SessionFileTrashCapability;
|
|
181
195
|
getCoordinatorTools: (callerId: string) => ToolDefinition[];
|
|
182
196
|
onChildSessionActivity?: () => void;
|
|
197
|
+
observeSession?: (
|
|
198
|
+
session: AgentSession,
|
|
199
|
+
agentId: string,
|
|
200
|
+
resourceInputs: {
|
|
201
|
+
agentDir: string;
|
|
202
|
+
extensions: readonly Pick<Extension, "path" | "resolvedPath" | "sourceInfo" | "hidden">[];
|
|
203
|
+
flagValues: ReadonlyMap<string, boolean | string>;
|
|
204
|
+
},
|
|
205
|
+
) => ChildSessionObserver | undefined;
|
|
183
206
|
}
|
|
184
207
|
|
|
185
208
|
/** Build one child prompt using the active delegation depth rather than persisted launch state. */
|
|
@@ -494,7 +517,7 @@ function collectChildTurnOutcome(
|
|
|
494
517
|
error: "No terminal assistant response",
|
|
495
518
|
};
|
|
496
519
|
}
|
|
497
|
-
if (finalAssistant.stopReason === "aborted") {
|
|
520
|
+
if (aborted || finalAssistant.stopReason === "aborted") {
|
|
498
521
|
return {
|
|
499
522
|
status: "cancelled",
|
|
500
523
|
output: assistantText(finalAssistant),
|
|
@@ -554,6 +577,7 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
554
577
|
private readonly modelRuntime: ModelRuntime,
|
|
555
578
|
private readonly modelById: ReadonlyMap<string, Model<any>>,
|
|
556
579
|
onSessionActivity?: () => void,
|
|
580
|
+
private readonly observer?: ChildSessionObserver,
|
|
557
581
|
) {
|
|
558
582
|
// Keep this child-only; AgentSession.setSteeringMode would overwrite the user's global setting.
|
|
559
583
|
session.agent.steeringMode = "all";
|
|
@@ -613,12 +637,21 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
613
637
|
|
|
614
638
|
async abort(): Promise<void> {
|
|
615
639
|
this.aborted = true;
|
|
616
|
-
await this.session.abort();
|
|
640
|
+
await Promise.all([this.observer?.abort(), this.session.abort()]);
|
|
617
641
|
}
|
|
618
642
|
|
|
619
|
-
dispose(): void {
|
|
643
|
+
async dispose(): Promise<void> {
|
|
620
644
|
this.unsubscribe();
|
|
621
|
-
|
|
645
|
+
try {
|
|
646
|
+
await this.observer?.dispose();
|
|
647
|
+
// Let child extensions release services such as MCP server connections.
|
|
648
|
+
const runner = this.session.extensionRunner;
|
|
649
|
+
if (runner.hasHandlers("session_shutdown")) {
|
|
650
|
+
await runner.emit({ type: "session_shutdown", reason: "quit" });
|
|
651
|
+
}
|
|
652
|
+
} finally {
|
|
653
|
+
this.session.dispose();
|
|
654
|
+
}
|
|
622
655
|
}
|
|
623
656
|
|
|
624
657
|
getRuntimeProfile(): RuntimeProfile | undefined {
|
|
@@ -715,7 +748,20 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
715
748
|
|
|
716
749
|
private async captureTurn(operation: () => Promise<void>): Promise<RuntimeTurnOutcome> {
|
|
717
750
|
this.aborted = false;
|
|
718
|
-
|
|
751
|
+
this.observer?.beginTurn();
|
|
752
|
+
return captureChildTurnOutcome(
|
|
753
|
+
this.session,
|
|
754
|
+
async () => {
|
|
755
|
+
try {
|
|
756
|
+
await operation();
|
|
757
|
+
if (!this.aborted) await this.observer?.finishTurn();
|
|
758
|
+
} catch (error) {
|
|
759
|
+
await this.observer?.abort();
|
|
760
|
+
throw error;
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
() => this.aborted,
|
|
764
|
+
);
|
|
719
765
|
}
|
|
720
766
|
|
|
721
767
|
private async compactImportedContext(
|
|
@@ -1098,6 +1144,11 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
1098
1144
|
await resourceLoader.reload();
|
|
1099
1145
|
for (const extension of resourceLoader.getExtensions().extensions) {
|
|
1100
1146
|
for (const toolName of extension.tools.keys()) names.add(toolName);
|
|
1147
|
+
if (extension.path === "builtin:mcp") {
|
|
1148
|
+
for (const toolName of agent.launch_contract.ordinary_tools) {
|
|
1149
|
+
if (isMcpToolName(toolName)) names.add(toolName);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1101
1152
|
}
|
|
1102
1153
|
return names;
|
|
1103
1154
|
})();
|
|
@@ -1145,7 +1196,6 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
1145
1196
|
];
|
|
1146
1197
|
const runtimeToolAdapters = this.options.getRuntimeToolAdapters?.() ?? [];
|
|
1147
1198
|
const adapterToolNames = new Set(runtimeToolAdapters.flatMap((adapter) => adapter.toolNames));
|
|
1148
|
-
const adaptRuntimeTools = adapterToolNames.size > 0;
|
|
1149
1199
|
const { session } = await createAgentSession({
|
|
1150
1200
|
cwd: this.options.cwd,
|
|
1151
1201
|
agentDir: this.options.agentDir,
|
|
@@ -1158,20 +1208,16 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
1158
1208
|
settingsManager,
|
|
1159
1209
|
modelRuntime,
|
|
1160
1210
|
});
|
|
1161
|
-
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1211
|
+
const loadedNames = new Set(session.getAllTools().map((tool) => tool.name));
|
|
1212
|
+
const missingTools = allowedToolNames.filter(
|
|
1213
|
+
(toolName) => !loadedNames.has(toolName) && !isMcpToolName(toolName),
|
|
1214
|
+
);
|
|
1164
1215
|
if (missingTools.length > 0) {
|
|
1165
1216
|
session.dispose();
|
|
1166
1217
|
throw new Error(`Minimal subagents child tool loading failed: ${missingTools.join(", ")}`);
|
|
1167
1218
|
}
|
|
1168
|
-
// The inner policy bounds extension-selected exposure without restoring hidden ordinary tools.
|
|
1169
|
-
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters, false);
|
|
1170
|
-
await session.bindExtensions({ mode: "print" });
|
|
1171
|
-
// The outer policy filters names before extension wrappers build their own tool catalogues.
|
|
1172
1219
|
installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters);
|
|
1173
|
-
|
|
1174
|
-
// require their definitions to remain registered, not necessarily direct.
|
|
1220
|
+
await session.bindExtensions({ mode: "print" });
|
|
1175
1221
|
const registeredNames = new Set(session.getAllTools().map((tool) => tool.name));
|
|
1176
1222
|
const missingCoordinatorTools = coordinatorTools
|
|
1177
1223
|
.map((tool) => tool.name)
|
|
@@ -1187,6 +1233,18 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
1187
1233
|
modelRuntime,
|
|
1188
1234
|
this.modelById,
|
|
1189
1235
|
this.options.onChildSessionActivity,
|
|
1236
|
+
this.options.observeSession?.(session, agent.agent_id, {
|
|
1237
|
+
agentDir: this.options.agentDir,
|
|
1238
|
+
extensions: resourceLoader
|
|
1239
|
+
.getExtensions()
|
|
1240
|
+
.extensions.map(({ path, resolvedPath, sourceInfo, hidden }) => ({
|
|
1241
|
+
path,
|
|
1242
|
+
resolvedPath,
|
|
1243
|
+
sourceInfo,
|
|
1244
|
+
hidden,
|
|
1245
|
+
})),
|
|
1246
|
+
flagValues: new Map(resourceLoader.getExtensions().runtime.flagValues),
|
|
1247
|
+
}),
|
|
1190
1248
|
);
|
|
1191
1249
|
}
|
|
1192
1250
|
|
|
@@ -1,308 +1,72 @@
|
|
|
1
1
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
|
-
import { CONFIG_DIR_NAME
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
-
import lockfile from "proper-lockfile";
|
|
2
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { updateFileLocked } from "@ian-pascoe/pi-utils/locked-file-update";
|
|
4
|
+
import { resolve } from "node:path";
|
|
7
5
|
|
|
8
6
|
/** Identifies the standard Pi settings file changed by a Subagent Access command. */
|
|
9
7
|
export type MinimalSubagentsSettingsScope = "global" | "project";
|
|
10
8
|
|
|
11
|
-
/** Describes why a scoped minimal subagents settings write could not complete. */
|
|
12
|
-
export type MinimalSubagentsSettingsWriteFailureReason =
|
|
13
|
-
| "project-untrusted"
|
|
14
|
-
| "malformed-json"
|
|
15
|
-
| "incompatible-shape"
|
|
16
|
-
| "filesystem";
|
|
17
|
-
|
|
18
|
-
/** Reports an expected scoped settings failure without throwing through the command boundary. */
|
|
19
|
-
export class MinimalSubagentsSettingsWriteError extends Error {
|
|
20
|
-
readonly _tag = "MinimalSubagentsSettingsWriteError" as const;
|
|
21
|
-
|
|
22
|
-
/** Create a settings write failure carrying its exact scope and path. */
|
|
23
|
-
constructor(
|
|
24
|
-
readonly scope: MinimalSubagentsSettingsScope,
|
|
25
|
-
readonly path: string,
|
|
26
|
-
readonly reason: MinimalSubagentsSettingsWriteFailureReason,
|
|
27
|
-
message: string,
|
|
28
|
-
readonly operation?: SettingsFilesystemOperation,
|
|
29
|
-
cause?: unknown,
|
|
30
|
-
) {
|
|
31
|
-
super(message, cause === undefined ? undefined : { cause });
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Returns the path changed by a successful write or a typed expected settings failure. */
|
|
36
|
-
export type MinimalSubagentsSettingsWriteResult =
|
|
37
|
-
| {
|
|
38
|
-
readonly ok: true;
|
|
39
|
-
readonly scope: MinimalSubagentsSettingsScope;
|
|
40
|
-
readonly path: string;
|
|
41
|
-
}
|
|
42
|
-
| {
|
|
43
|
-
readonly ok: false;
|
|
44
|
-
readonly error: MinimalSubagentsSettingsWriteError;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
9
|
/** Supplies only the Root Agent context needed to select and authorize a settings file. */
|
|
48
10
|
export interface MinimalSubagentsSettingsWriteContext {
|
|
49
11
|
readonly cwd: string;
|
|
50
12
|
isProjectTrusted(): boolean;
|
|
51
13
|
}
|
|
52
14
|
|
|
53
|
-
type SettingsFilesystemOperation = "prepare" | "lock" | "read" | "write" | "release";
|
|
54
15
|
type SettingsJsonObject = Record<string, JsonValue>;
|
|
55
16
|
|
|
56
|
-
type ParsedSettingsDocument =
|
|
57
|
-
| { readonly ok: true; readonly settings: SettingsJsonObject }
|
|
58
|
-
| { readonly ok: false; readonly error: MinimalSubagentsSettingsWriteError };
|
|
59
|
-
|
|
60
|
-
interface ExistingSettingsDocument {
|
|
61
|
-
readonly settings: SettingsJsonObject;
|
|
62
|
-
readonly mode: number;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
17
|
function isSettingsJsonObject(value: JsonValue | undefined): value is SettingsJsonObject {
|
|
66
18
|
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- JSON.parse already established JSON data; distinguish object roots and settings blocks from primitives and arrays.
|
|
67
19
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
68
20
|
}
|
|
69
|
-
const SETTINGS_LOCK_RETRY_DELAY_MS = 20;
|
|
70
|
-
const SETTINGS_LOCK_RETRIES = 100;
|
|
71
|
-
const NEW_SETTINGS_FILE_MODE = 0o600;
|
|
72
|
-
|
|
73
|
-
function stripUtf8Bom(content: string): string {
|
|
74
|
-
return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
|
|
75
|
-
}
|
|
76
21
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
path: string,
|
|
80
|
-
detail: string,
|
|
81
|
-
): MinimalSubagentsSettingsWriteError {
|
|
82
|
-
return new MinimalSubagentsSettingsWriteError(
|
|
83
|
-
scope,
|
|
84
|
-
path,
|
|
85
|
-
"incompatible-shape",
|
|
86
|
-
`Minimal subagents settings shape is incompatible for ${scope} settings at ${path}: ${detail}`,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function parseSettingsDocument(
|
|
91
|
-
content: string,
|
|
92
|
-
scope: MinimalSubagentsSettingsScope,
|
|
93
|
-
path: string,
|
|
94
|
-
): ParsedSettingsDocument {
|
|
22
|
+
function parseSettings(content: string | undefined, path: string): SettingsJsonObject {
|
|
23
|
+
if (content === undefined) return {};
|
|
95
24
|
// JSON.parse is the provenance for this JSON type; the object shape is checked below.
|
|
96
25
|
let parsed: JsonValue;
|
|
97
26
|
try {
|
|
98
|
-
parsed = JSON.parse(
|
|
27
|
+
parsed = JSON.parse(content.charCodeAt(0) === 0xfeff ? content.slice(1) : content);
|
|
99
28
|
} catch (cause) {
|
|
100
|
-
|
|
101
|
-
ok: false,
|
|
102
|
-
error: new MinimalSubagentsSettingsWriteError(
|
|
103
|
-
scope,
|
|
104
|
-
path,
|
|
105
|
-
"malformed-json",
|
|
106
|
-
`Minimal subagents settings JSON is malformed for ${scope} settings at ${path}`,
|
|
107
|
-
"read",
|
|
108
|
-
cause,
|
|
109
|
-
),
|
|
110
|
-
};
|
|
29
|
+
throw new Error(`Minimal subagents settings JSON is malformed at ${path}`, { cause });
|
|
111
30
|
}
|
|
112
|
-
|
|
113
31
|
if (!isSettingsJsonObject(parsed)) {
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
const minimalSubagents = parsed.minimalSubagents;
|
|
117
|
-
if (minimalSubagents !== undefined && !isSettingsJsonObject(minimalSubagents)) {
|
|
118
|
-
return {
|
|
119
|
-
ok: false,
|
|
120
|
-
error: settingsContractError(
|
|
121
|
-
scope,
|
|
122
|
-
path,
|
|
123
|
-
"expected minimalSubagents to be an object or absent",
|
|
124
|
-
),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
return { ok: true, settings: parsed };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function mutateMinimalSubagentsEnabled(
|
|
131
|
-
settings: SettingsJsonObject,
|
|
132
|
-
enabled: boolean | undefined,
|
|
133
|
-
): void {
|
|
134
|
-
const currentMinimalSubagents = settings.minimalSubagents;
|
|
135
|
-
const minimalSubagents = isSettingsJsonObject(currentMinimalSubagents)
|
|
136
|
-
? currentMinimalSubagents
|
|
137
|
-
: {};
|
|
138
|
-
|
|
139
|
-
if (enabled === undefined) {
|
|
140
|
-
delete minimalSubagents.enabled;
|
|
141
|
-
} else {
|
|
142
|
-
minimalSubagents.enabled = enabled;
|
|
32
|
+
throw new Error(`Minimal subagents settings at ${path} must have an object root`);
|
|
143
33
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
delete settings.minimalSubagents;
|
|
147
|
-
} else {
|
|
148
|
-
settings.minimalSubagents = minimalSubagents;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function filesystemWriteError(
|
|
153
|
-
scope: MinimalSubagentsSettingsScope,
|
|
154
|
-
path: string,
|
|
155
|
-
operation: SettingsFilesystemOperation,
|
|
156
|
-
cause: unknown,
|
|
157
|
-
): MinimalSubagentsSettingsWriteError {
|
|
158
|
-
return new MinimalSubagentsSettingsWriteError(
|
|
159
|
-
scope,
|
|
160
|
-
path,
|
|
161
|
-
"filesystem",
|
|
162
|
-
`Minimal subagents settings ${operation} failed for ${scope} settings at ${path}`,
|
|
163
|
-
operation,
|
|
164
|
-
cause,
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function readSettingsDocumentUnderLock(
|
|
169
|
-
scope: MinimalSubagentsSettingsScope,
|
|
170
|
-
path: string,
|
|
171
|
-
): Promise<ExistingSettingsDocument | MinimalSubagentsSettingsWriteError> {
|
|
172
|
-
let fileMode = NEW_SETTINGS_FILE_MODE;
|
|
173
|
-
let content: string;
|
|
174
|
-
try {
|
|
175
|
-
const fileStat = await stat(path);
|
|
176
|
-
fileMode = fileStat.mode & 0o7777;
|
|
177
|
-
content = await readFile(path, "utf8");
|
|
178
|
-
} catch (cause) {
|
|
179
|
-
if (isNodeErrorWithCode(cause, "ENOENT")) return { settings: {}, mode: fileMode };
|
|
180
|
-
return filesystemWriteError(scope, path, "read", cause);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const parsed = parseSettingsDocument(content, scope, path);
|
|
184
|
-
return parsed.ok ? { settings: parsed.settings, mode: fileMode } : parsed.error;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function isNodeErrorWithCode(cause: unknown, code: string): boolean {
|
|
188
|
-
return cause instanceof Error && "code" in cause && cause.code === code;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function replaceSettingsFileAtomically(
|
|
192
|
-
path: string,
|
|
193
|
-
settings: SettingsJsonObject,
|
|
194
|
-
mode: number,
|
|
195
|
-
temporaryId: string,
|
|
196
|
-
): Promise<void> {
|
|
197
|
-
const temporaryPath = join(dirname(path), `.${basename(path)}.${process.pid}.${temporaryId}.tmp`);
|
|
198
|
-
let temporaryFile: Awaited<ReturnType<typeof open>> | undefined;
|
|
199
|
-
try {
|
|
200
|
-
temporaryFile = await open(temporaryPath, "wx", mode);
|
|
201
|
-
await temporaryFile.chmod(mode);
|
|
202
|
-
await temporaryFile.writeFile(`${JSON.stringify(settings, undefined, 2)}\n`, "utf8");
|
|
203
|
-
await temporaryFile.sync();
|
|
204
|
-
await temporaryFile.close();
|
|
205
|
-
temporaryFile = undefined;
|
|
206
|
-
await rename(temporaryPath, path);
|
|
207
|
-
} catch (cause) {
|
|
208
|
-
if (temporaryFile !== undefined) await temporaryFile.close().catch(() => undefined);
|
|
209
|
-
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
210
|
-
throw cause;
|
|
34
|
+
if (parsed.minimalSubagents !== undefined && !isSettingsJsonObject(parsed.minimalSubagents)) {
|
|
35
|
+
throw new Error(`Minimal subagents settings at ${path} must have an object minimalSubagents`);
|
|
211
36
|
}
|
|
37
|
+
return parsed;
|
|
212
38
|
}
|
|
213
39
|
|
|
214
40
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
41
|
+
* Set or remove `minimalSubagents.enabled` in global or trusted-project Pi settings,
|
|
42
|
+
* preserving every unrelated setting. Writes re-read the file under Pi's settings lock.
|
|
43
|
+
* @returns The changed settings path.
|
|
217
44
|
*/
|
|
218
|
-
export
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
/** Set or remove one authored Subagent Access default, preserving every unrelated setting. */
|
|
233
|
-
async writeMinimalSubagentsEnabled(
|
|
234
|
-
scope: MinimalSubagentsSettingsScope,
|
|
235
|
-
enabled: boolean | undefined,
|
|
236
|
-
): Promise<MinimalSubagentsSettingsWriteResult> {
|
|
237
|
-
const path = scope === "global" ? this.globalSettingsPath : this.projectSettingsPath;
|
|
238
|
-
if (scope === "project" && !this.context.isProjectTrusted()) {
|
|
239
|
-
return {
|
|
240
|
-
ok: false,
|
|
241
|
-
error: new MinimalSubagentsSettingsWriteError(
|
|
242
|
-
scope,
|
|
243
|
-
path,
|
|
244
|
-
"project-untrusted",
|
|
245
|
-
`Minimal subagents project settings write refused because the project is not trusted: ${path}`,
|
|
246
|
-
),
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return this.writeEnabledUnderLock(scope, path, enabled);
|
|
45
|
+
export async function writeMinimalSubagentsEnabled(
|
|
46
|
+
context: MinimalSubagentsSettingsWriteContext,
|
|
47
|
+
agentDirectory: string,
|
|
48
|
+
scope: MinimalSubagentsSettingsScope,
|
|
49
|
+
enabled: boolean | undefined,
|
|
50
|
+
): Promise<string> {
|
|
51
|
+
const path =
|
|
52
|
+
scope === "global"
|
|
53
|
+
? resolve(agentDirectory, "settings.json")
|
|
54
|
+
: resolve(context.cwd, CONFIG_DIR_NAME, "settings.json");
|
|
55
|
+
if (scope === "project" && !context.isProjectTrusted()) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Minimal subagents project settings write refused because the project is not trusted: ${path}`,
|
|
58
|
+
);
|
|
251
59
|
}
|
|
252
60
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
let release: (() => Promise<void>) | undefined;
|
|
265
|
-
try {
|
|
266
|
-
release = await lockfile.lock(path, {
|
|
267
|
-
realpath: false,
|
|
268
|
-
retries: {
|
|
269
|
-
retries: SETTINGS_LOCK_RETRIES,
|
|
270
|
-
factor: 1,
|
|
271
|
-
minTimeout: SETTINGS_LOCK_RETRY_DELAY_MS,
|
|
272
|
-
maxTimeout: SETTINGS_LOCK_RETRY_DELAY_MS,
|
|
273
|
-
randomize: false,
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
} catch (cause) {
|
|
277
|
-
return { ok: false, error: filesystemWriteError(scope, path, "lock", cause) };
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
let result: MinimalSubagentsSettingsWriteResult;
|
|
281
|
-
try {
|
|
282
|
-
const existing = await readSettingsDocumentUnderLock(scope, path);
|
|
283
|
-
if (existing instanceof MinimalSubagentsSettingsWriteError) {
|
|
284
|
-
result = { ok: false, error: existing };
|
|
285
|
-
} else {
|
|
286
|
-
try {
|
|
287
|
-
mutateMinimalSubagentsEnabled(existing.settings, enabled);
|
|
288
|
-
await replaceSettingsFileAtomically(
|
|
289
|
-
path,
|
|
290
|
-
existing.settings,
|
|
291
|
-
existing.mode,
|
|
292
|
-
this.createTemporaryId(),
|
|
293
|
-
);
|
|
294
|
-
result = { ok: true, scope, path };
|
|
295
|
-
} catch (cause) {
|
|
296
|
-
result = { ok: false, error: filesystemWriteError(scope, path, "write", cause) };
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
} finally {
|
|
300
|
-
try {
|
|
301
|
-
await release();
|
|
302
|
-
} catch (cause) {
|
|
303
|
-
result = { ok: false, error: filesystemWriteError(scope, path, "release", cause) };
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return result;
|
|
307
|
-
}
|
|
61
|
+
await updateFileLocked(path, (content) => {
|
|
62
|
+
const settings = parseSettings(content, path);
|
|
63
|
+
const current = settings.minimalSubagents;
|
|
64
|
+
const minimalSubagents = isSettingsJsonObject(current) ? { ...current } : {};
|
|
65
|
+
if (enabled === undefined) delete minimalSubagents.enabled;
|
|
66
|
+
else minimalSubagents.enabled = enabled;
|
|
67
|
+
if (Object.keys(minimalSubagents).length === 0) delete settings.minimalSubagents;
|
|
68
|
+
else settings.minimalSubagents = minimalSubagents;
|
|
69
|
+
return `${JSON.stringify(settings, undefined, 2)}\n`;
|
|
70
|
+
});
|
|
71
|
+
return path;
|
|
308
72
|
}
|
|
@@ -125,6 +125,7 @@ function structuredToolResult<TDetails extends CoordinatorToolResultDetails>(
|
|
|
125
125
|
return {
|
|
126
126
|
content: [{ type: "text" as const, text: truncated.content }],
|
|
127
127
|
details: result,
|
|
128
|
+
structuredContent: JSON.parse(json),
|
|
128
129
|
};
|
|
129
130
|
}
|
|
130
131
|
|
|
@@ -235,13 +236,15 @@ export function createCoordinatorToolDefinitions(
|
|
|
235
236
|
signal,
|
|
236
237
|
parameters.turn_id,
|
|
237
238
|
);
|
|
239
|
+
const details = {
|
|
240
|
+
...result,
|
|
241
|
+
source_agent_id: result.agent_id,
|
|
242
|
+
source_turn_id: result.turn_id,
|
|
243
|
+
};
|
|
238
244
|
return {
|
|
239
245
|
...structuredToolResult(result),
|
|
240
|
-
details
|
|
241
|
-
|
|
242
|
-
source_agent_id: result.agent_id,
|
|
243
|
-
source_turn_id: result.turn_id,
|
|
244
|
-
},
|
|
246
|
+
details,
|
|
247
|
+
structuredContent: JSON.parse(JSON.stringify(details)),
|
|
245
248
|
};
|
|
246
249
|
});
|
|
247
250
|
} finally {
|
|
@@ -224,6 +224,14 @@ export interface CoordinatorMessage {
|
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
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
|
+
|
|
227
235
|
/** Process-local adapter around one SDK-created Pi child session. */
|
|
228
236
|
export interface ChildAgentRuntime {
|
|
229
237
|
readonly sessionLeafId: string | undefined;
|
|
@@ -238,7 +246,7 @@ export interface ChildAgentRuntime {
|
|
|
238
246
|
/** Queue one typed coordinator message into the child session. */
|
|
239
247
|
queueCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
|
|
240
248
|
abort(): Promise<void>;
|
|
241
|
-
dispose(): void
|
|
249
|
+
dispose(): void | Promise<void>;
|
|
242
250
|
/** Return the live Runtime Profile, or undefined when the SDK session has no model. */
|
|
243
251
|
getRuntimeProfile(): RuntimeProfile | undefined;
|
|
244
252
|
/** Return the effective ordinary tools after child extensions apply runtime adapters. */
|