@ian-pascoe/pi-minimal-subagents 0.10.1 → 0.11.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 CHANGED
@@ -206,15 +206,23 @@ 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
- 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
209
+ Child Agents load Pi's built-in `llama.cpp`, `codemode`, `tool-search`, and `mcp`
210
+ extensions unless settings disable them (`-builtin:<name>`); each child connects
211
+ its own MCP servers. Pi does not export the llama.cpp factory, so children load it
212
+ from Pi's installed `dist/extensions/llama/index.js`; when that file is absent, a
213
+ llama.cpp launch model is reported as a missing dependency. Child `codemode`
214
+ scripts get no `models` API, so they cannot call models outside the Launch
215
+ Contract. A tool's exposure still decides whether it is declared or reachable only
212
216
  through `codemode` or `tool_search`. Granted `codemode`- or `deferred`-exposed
213
217
  tools are callable but not declared, so a child that needs them also needs
214
218
  `codemode` or `tool_search` in its grant. Ungranted tools are unreachable in the
215
219
  child. MCP tools are checked at launch only by name, because children register
216
220
  them after connecting; a tool whose server disappeared fails when called. Closing
217
- a child runtime shuts its extensions down, closing its MCP connections.
221
+ a child runtime shuts its extensions down, closing its MCP connections. A reopened
222
+ child re-declares the tools it last declared itself, including tools loaded
223
+ through `tool_search`; as in Pi, a tool that registers only after the child opens,
224
+ such as an MCP tool, is not re-declared. Declarations inherited from the parent's
225
+ context are not restored.
218
226
 
219
227
  ## Capabilities and persistence
220
228
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -1,6 +1,14 @@
1
- import { dirname } from "node:path";
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { pathToFileURL } from "node:url";
2
4
  import * as piCodingAgent from "@earendil-works/pi-coding-agent";
3
- import { DefaultResourceLoader, SettingsManager } from "@earendil-works/pi-coding-agent";
5
+ import {
6
+ DefaultResourceLoader,
7
+ type ExtensionFactory,
8
+ SettingsManager,
9
+ } from "@earendil-works/pi-coding-agent";
10
+ import { Type } from "typebox";
11
+ import { Value } from "typebox/value";
4
12
  import { canonicalPath } from "./minimal-subagents-paths.js";
5
13
  import type { ProjectContextMode } from "./minimal-subagents-types.js";
6
14
 
@@ -17,19 +25,58 @@ type InlineExtension = NonNullable<
17
25
  ConstructorParameters<typeof DefaultResourceLoader>[0]["extensionFactories"]
18
26
  >[number];
19
27
 
20
- /** Pi's exported built-in extensions (Pi 0.99+); older hosts have none, so neither do children. */
28
+ // SAFETY: Older Pi hosts lack these exports; the namespace yields undefined rather than failing.
29
+ const sdk = piCodingAgent as Partial<typeof piCodingAgent>;
30
+
31
+ const ExtensionModuleSchema = Type.Object({
32
+ default: Type.Function([Type.Unknown()], Type.Unknown()),
33
+ });
34
+
35
+ /** Built-in extension name, and the provider ID it registers. */
36
+ const LLAMA = "llama.cpp";
37
+
38
+ /** Pi ships a file-backed llama.cpp factory but does not export it; undefined when it is absent. */
39
+ function piLlamaExtensionPath(): string | undefined {
40
+ if (!sdk.getPackageDir) return undefined;
41
+ const path = join(sdk.getPackageDir(), "dist", "extensions", "llama", "index.js");
42
+ return existsSync(path) ? path : undefined;
43
+ }
44
+
45
+ function llamaExtension(): InlineExtension[] {
46
+ const path = piLlamaExtensionPath();
47
+ if (!path) return [];
48
+ const factory: ExtensionFactory = async (pi) => {
49
+ const module: unknown = await import(pathToFileURL(path).href);
50
+ if (!Value.Check(ExtensionModuleSchema, module))
51
+ throw new Error(`Pi's built-in llama.cpp file has no extension factory (${path})`);
52
+ await module.default(pi);
53
+ };
54
+ return [{ name: LLAMA, factory, builtin: true }];
55
+ }
56
+
57
+ /** Whether a child can register a provider the root has; llama.cpp needs Pi's unexported factory. */
58
+ export function childProviderAvailable(provider: string): boolean {
59
+ return provider !== LLAMA || piLlamaExtensionPath() !== undefined;
60
+ }
61
+
62
+ /** Child scripts must not call models outside their Launch Contract. */
63
+ export const CHILD_CODEMODE_MODELS = false;
64
+
65
+ /** Pi's built-in extensions (Pi 0.99+) in Pi's order; older hosts have none, so neither do children. */
21
66
  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
- );
67
+ const createCodemode = sdk.createCodemodeExtension;
68
+ return [
69
+ ...llamaExtension(),
70
+ ...(
71
+ [
72
+ ["codemode", createCodemode && (() => createCodemode({ models: CHILD_CODEMODE_MODELS }))],
73
+ ["tool-search", sdk.createToolSearchExtension],
74
+ ["mcp", sdk.createMcpExtension],
75
+ ] as const
76
+ ).flatMap(([name, create]) =>
77
+ create ? [{ name, factory: create(), builtin: true, replaceable: true }] : [],
78
+ ),
79
+ ];
33
80
  }
34
81
 
35
82
  function createDefaultChildResourceLoaderOptions(
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
3
  import { unlink } from "node:fs/promises";
4
4
  import { resolve } from "node:path";
5
5
  import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
6
- import type { Model, Usage } from "@earendil-works/pi-ai";
6
+ import { getCurrentSystemMessage, type Model, type Usage } from "@earendil-works/pi-ai";
7
7
  import { clampThinkingLevel } from "@earendil-works/pi-ai/compat";
8
8
  import {
9
9
  AgentSession,
@@ -34,7 +34,11 @@ import {
34
34
  DEFAULT_MAX_SUBAGENT_DEPTH,
35
35
  getSubagentDepth,
36
36
  } from "./minimal-subagents-capabilities.js";
37
- import { createChildResourceLoader } from "./minimal-subagents-child-resources.js";
37
+ import {
38
+ CHILD_CODEMODE_MODELS,
39
+ childProviderAvailable,
40
+ createChildResourceLoader,
41
+ } from "./minimal-subagents-child-resources.js";
38
42
  import {
39
43
  CHILD_IDENTITY_ENTRY_TYPE,
40
44
  FORK_CLONE_ENTRY_TYPE,
@@ -164,6 +168,20 @@ function installChildToolCapabilityPolicy(
164
168
  session.setActiveToolsByName(session.getActiveToolNames().filter((name) => !isScriptOnly(name)));
165
169
  }
166
170
 
171
+ /**
172
+ * Passing `tools` skips Pi's transcript restore, so re-declare the tools a reopened child last
173
+ * declared itself (such as through `tool_search`); the capability policy still clamps them.
174
+ * Declarations inherited from the parent predate the child and are not restored.
175
+ */
176
+ function restoreChildDeclaredTools(session: AgentSession, agent: PersistedAgent): void {
177
+ const { messages } = session.sessionManager.buildSessionContext();
178
+ const createdAt = Date.parse(agent.created_at);
179
+ if (!messages.some((message) => message.role === "system" && message.timestamp >= createdAt))
180
+ return;
181
+ const declared = getCurrentSystemMessage(messages);
182
+ if (declared) session.setActiveToolsByName((declared.toolsAdded ?? []).map(({ name }) => name));
183
+ }
184
+
167
185
  /** Moves one verified child session file to trash and reports command unavailability. */
168
186
  export interface SessionFileTrashCapability {
169
187
  moveSessionFile(sessionFile: string): Promise<Error | undefined>;
@@ -201,6 +219,8 @@ export interface PiAgentSessionFactoryOptions {
201
219
  agentDir: string;
202
220
  extensions: readonly Pick<Extension, "path" | "resolvedPath" | "sourceInfo" | "hidden">[];
203
221
  flagValues: ReadonlyMap<string, boolean | string>;
222
+ /** Whether child codemode scripts get the `models` API, so observers can reproduce it. */
223
+ codemodeModels: boolean;
204
224
  },
205
225
  ) => ChildSessionObserver | undefined;
206
226
  }
@@ -1109,9 +1129,12 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
1109
1129
  requireEligibleModel: boolean,
1110
1130
  ): Promise<string[]> {
1111
1131
  const missing: string[] = [];
1112
- if (!this.modelById.has(agent.launch_contract.model)) missing.push(agent.launch_contract.model);
1132
+ const model = this.modelById.get(agent.launch_contract.model);
1133
+ if (!model) missing.push(agent.launch_contract.model);
1113
1134
  else if (requireEligibleModel && !this.eligibleModelIds.has(agent.launch_contract.model)) {
1114
1135
  missing.push(agent.launch_contract.model);
1136
+ } else if (!childProviderAvailable(model.provider)) {
1137
+ missing.push(agent.launch_contract.model);
1115
1138
  }
1116
1139
  const discoveredTools = await this.discoverChildToolNames(agent);
1117
1140
  for (const toolName of agent.launch_contract.ordinary_tools) {
@@ -1217,6 +1240,7 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
1217
1240
  throw new Error(`Minimal subagents child tool loading failed: ${missingTools.join(", ")}`);
1218
1241
  }
1219
1242
  installChildToolCapabilityPolicy(session, allowedToolNames, runtimeToolAdapters);
1243
+ restoreChildDeclaredTools(session, agent);
1220
1244
  await session.bindExtensions({ mode: "print" });
1221
1245
  const registeredNames = new Set(session.getAllTools().map((tool) => tool.name));
1222
1246
  const missingCoordinatorTools = coordinatorTools
@@ -1244,6 +1268,7 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
1244
1268
  hidden,
1245
1269
  })),
1246
1270
  flagValues: new Map(resourceLoader.getExtensions().runtime.flagValues),
1271
+ codemodeModels: CHILD_CODEMODE_MODELS,
1247
1272
  }),
1248
1273
  );
1249
1274
  }