diff --git a/modules/agents/pi/extensions/subagents/agents.ts b/modules/agents/pi/extensions/subagents/agents.ts new file mode 100644 index 0000000..f56c1c8 --- /dev/null +++ b/modules/agents/pi/extensions/subagents/agents.ts @@ -0,0 +1,141 @@ +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; +import type { ContextMode } from "./types.ts"; +import type { Diagnostics } from "./config.ts"; + +export interface AgentDefinition { + name: string; + description: string; + body: string; + context?: ContextMode; + model?: string; + thinking?: string; + tools?: string; + allowedContexts?: ContextMode[]; + hidden?: boolean; + source: string; +} + +export function loadAgents(cwd: string, projectTrusted: boolean, diagnostics: Diagnostics, agentDir = defaultAgentDir()): Map { + const user = loadTier(join(agentDir, "agents"), "user", diagnostics); + const project = projectTrusted ? loadTier(join(cwd, ".pi", "agents"), "project", diagnostics) : new Map(); + return new Map([...user, ...project]); +} + +function loadTier(dir: string, tier: string, diagnostics: Diagnostics): Map { + const agents = new Map(); + if (!existsSync(dir)) return agents; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith(".md")) continue; + const path = join(dir, entry.name); + const parsed = parseAgent(path, diagnostics); + if (!parsed) continue; + if (agents.has(parsed.name)) { + diagnostics.warnings.push(`Duplicate ${tier} agent '${parsed.name}' ignored at ${path}`); + continue; + } + const stem = basename(entry.name, ".md"); + if (stem !== parsed.name) diagnostics.warnings.push(`${tier} agent file '${entry.name}' name '${parsed.name}' does not match filename`); + agents.set(parsed.name, parsed); + } + return agents; +} + +export function parseAgent(path: string, diagnostics: Diagnostics): AgentDefinition | undefined { + try { + const text = readFileSync(path, "utf8"); + const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/u.exec(text); + if (!match) { + diagnostics.warnings.push(`Agent ${path} missing YAML frontmatter`); + return undefined; + } + const frontmatter = parseFrontmatter(match[1]); + const name = stringField(frontmatter, "name"); + const description = stringField(frontmatter, "description"); + if (!name || !/^[a-z0-9-]+$/.test(name)) { + diagnostics.warnings.push(`Agent ${path} has invalid name`); + return undefined; + } + if (!description) { + diagnostics.warnings.push(`Agent ${path} has invalid description`); + return undefined; + } + const context = contextField(frontmatter.context); + const allowedContexts = contextsField(frontmatter.allowedContexts); + if (frontmatter.context !== undefined && !context) diagnostics.warnings.push(`Agent ${path} has invalid context`); + if (frontmatter.allowedContexts !== undefined && !allowedContexts) diagnostics.warnings.push(`Agent ${path} has invalid allowedContexts`); + if (context && allowedContexts && !allowedContexts.includes(context)) diagnostics.warnings.push(`Agent ${path} context is outside allowedContexts`); + return { + name, + description, + body: match[2].trim(), + context, + model: stringField(frontmatter, "model"), + thinking: stringField(frontmatter, "thinking"), + tools: stringField(frontmatter, "tools"), + allowedContexts, + hidden: booleanField(frontmatter, "hidden"), + source: path, + }; + } catch (error) { + diagnostics.warnings.push(`Failed to load agent ${path}: ${error instanceof Error ? error.message : String(error)}`); + return undefined; + } +} + +function parseFrontmatter(text: string): Record { + const result: Record = {}; + const lines = text.split(/\r?\n/u); + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + if (!line.trim() || line.trimStart().startsWith("#")) continue; + const scalar = /^(\w+):\s*(.*?)\s*$/u.exec(line); + if (!scalar) continue; + const [, key, raw] = scalar; + if (raw !== "") { + result[key] = parseScalar(raw); + continue; + } + const values: string[] = []; + while (i + 1 < lines.length) { + const item = /^\s+-\s*(.*?)\s*$/u.exec(lines[i + 1]); + if (!item) break; + values.push(String(parseScalar(item[1]))); + i += 1; + } + result[key] = values; + } + return result; +} + +function parseScalar(raw: string): string | boolean { + const unquoted = raw.replace(/^['"]|['"]$/gu, ""); + if (unquoted === "true") return true; + if (unquoted === "false") return false; + return unquoted; +} + +function stringField(record: Record, key: string): string | undefined { + const value = record[key]; + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function booleanField(record: Record, key: string): boolean | undefined { + const value = record[key]; + return typeof value === "boolean" ? value : undefined; +} + +function contextField(value: unknown): ContextMode | undefined { + return value === "independent" || value === "fork" ? value : undefined; +} + +function contextsField(value: unknown): ContextMode[] | undefined { + if (!Array.isArray(value)) return undefined; + const contexts = value.map(contextField); + return contexts.every(Boolean) ? (contexts as ContextMode[]) : undefined; +} + +function defaultAgentDir(): string { + return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"); +} diff --git a/modules/agents/pi/extensions/subagents/config.test.ts b/modules/agents/pi/extensions/subagents/config.test.ts new file mode 100644 index 0000000..e292df3 --- /dev/null +++ b/modules/agents/pi/extensions/subagents/config.test.ts @@ -0,0 +1,117 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { loadAgents } from "./agents.ts"; +import { BUILT_IN_TOOL_PROFILES, loadConfig, resolveSpawn, type Diagnostics } from "./config.ts"; + +function fixture() { + const root = mkdtempSync(join(tmpdir(), "subagents-config-")); + const agentDir = join(root, "agent"); + const cwd = join(root, "project"); + mkdirSync(agentDir, { recursive: true }); + mkdirSync(cwd, { recursive: true }); + return { root, agentDir, cwd }; +} + +function diagnostics(): Diagnostics { + return { warnings: [] }; +} + +test("missing config files and agent directories are normal", () => { + const { cwd, agentDir } = fixture(); + const diag = diagnostics(); + + const config = loadConfig(cwd, true, diag, agentDir); + const agents = loadAgents(cwd, true, diag, agentDir); + + assert.equal(config.defaultContext, "independent"); + assert.equal(config.defaultTools, "read-only"); + assert.equal(agents.size, 0); + assert.deepEqual(diag.warnings, []); +}); + +test("global and trusted project config merge in order", () => { + const { cwd, agentDir } = fixture(); + mkdirSync(join(cwd, ".pi"), { recursive: true }); + writeFileSync(join(agentDir, "subagents.json"), JSON.stringify({ defaultTools: "global-profile", toolProfiles: { "global-profile": { activeTools: ["read"] } } })); + writeFileSync(join(cwd, ".pi", "subagents.json"), JSON.stringify({ defaultTools: "project-profile", toolProfiles: { "project-profile": { activeTools: ["ls"] } } })); + + const config = loadConfig(cwd, true, diagnostics(), agentDir); + + assert.equal(config.defaultTools, "project-profile"); + assert.deepEqual(config.toolProfiles["global-profile"].activeTools, ["read"]); + assert.deepEqual(config.toolProfiles["project-profile"].activeTools, ["ls"]); +}); + +test("project config is ignored when project is untrusted", () => { + const { cwd, agentDir } = fixture(); + mkdirSync(join(cwd, ".pi"), { recursive: true }); + writeFileSync(join(cwd, ".pi", "subagents.json"), JSON.stringify({ defaultTools: "project-profile", toolProfiles: { "project-profile": { activeTools: ["ls"] } } })); + + const config = loadConfig(cwd, false, diagnostics(), agentDir); + + assert.equal(config.defaultTools, "read-only"); + assert.equal(config.toolProfiles["project-profile"], undefined); +}); + +test("agents load with project precedence over user", () => { + const { cwd, agentDir } = fixture(); + mkdirSync(join(agentDir, "agents"), { recursive: true }); + mkdirSync(join(cwd, ".pi", "agents"), { recursive: true }); + writeFileSync(join(agentDir, "agents", "review.md"), "---\nname: review\ndescription: User review\ntools: read-only\n---\nuser body\n"); + writeFileSync(join(cwd, ".pi", "agents", "review.md"), "---\nname: review\ndescription: Project review\ntools: full-tools\n---\nproject body\n"); + + const agents = loadAgents(cwd, true, diagnostics(), agentDir); + + assert.equal(agents.get("review")?.description, "Project review"); + assert.equal(agents.get("review")?.body, "project body"); +}); + +test("duplicate same-tier definitions and invalid frontmatter produce diagnostics", () => { + const { cwd, agentDir } = fixture(); + const dir = join(agentDir, "agents"); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, "one.md"), "---\nname: same\ndescription: One\n---\none\n"); + writeFileSync(join(dir, "two.md"), "---\nname: same\ndescription: Two\n---\ntwo\n"); + writeFileSync(join(dir, "bad.md"), "---\nname: Bad Name\n---\nbad\n"); + const diag = diagnostics(); + + const agents = loadAgents(cwd, true, diag, agentDir); + + assert.equal(agents.size, 1); + assert.ok(diag.warnings.some((warning) => warning.includes("Duplicate user agent 'same'"))); + assert.ok(diag.warnings.some((warning) => warning.includes("invalid name"))); +}); + +test("named spawn resolves overrides, frontmatter, config, and defaults", () => { + const { cwd, agentDir } = fixture(); + mkdirSync(join(agentDir, "agents"), { recursive: true }); + writeFileSync(join(agentDir, "subagents.json"), JSON.stringify({ defaultTools: "local-review", toolProfiles: { "local-review": { activeTools: ["read"] } } })); + writeFileSync(join(agentDir, "agents", "review.md"), "---\nname: review\ndescription: Review\ncontext: independent\nmodel: inherit\nthinking: high\ntools: local-review\n---\nagent body\n"); + const diag = diagnostics(); + const config = loadConfig(cwd, true, diag, agentDir); + const agents = loadAgents(cwd, true, diag, agentDir); + + const resolved = resolveSpawn({ agent: "review", prompt: "check this", thinking: "low" }, config, agents); + + assert.equal(resolved.prompt, "check this"); + assert.equal(resolved.context, "independent"); + assert.equal(resolved.model, "inherit"); + assert.equal(resolved.thinking, "low"); + assert.equal(resolved.tools, "local-review"); + assert.deepEqual(resolved.toolProfile.activeTools, ["read"]); + assert.equal(resolved.agentBody, "agent body"); +}); + +test("built-in tool profile names cannot be overridden", () => { + const { cwd, agentDir } = fixture(); + writeFileSync(join(agentDir, "subagents.json"), JSON.stringify({ toolProfiles: { "read-only": { activeTools: ["bash"] } } })); + const diag = diagnostics(); + + const config = loadConfig(cwd, true, diag, agentDir); + + assert.deepEqual(config.toolProfiles["read-only"], BUILT_IN_TOOL_PROFILES["read-only"]); + assert.ok(diag.warnings.some((warning) => warning.includes("Ignoring global override for built-in tool profile 'read-only'"))); +}); diff --git a/modules/agents/pi/extensions/subagents/config.ts b/modules/agents/pi/extensions/subagents/config.ts new file mode 100644 index 0000000..836793a --- /dev/null +++ b/modules/agents/pi/extensions/subagents/config.ts @@ -0,0 +1,152 @@ +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import type { ContextMode, SpawnRequest, ToolProfile } from "./types.ts"; +import type { AgentDefinition } from "./agents.ts"; + +export interface Diagnostics { + warnings: string[]; +} + +export interface SubagentsConfig { + defaultContext: ContextMode; + defaultTools: string; + toolProfiles: Record; +} + +export interface ResolvedSpawnRequest extends SpawnRequest { + prompt: string; + context: ContextMode; + tools: string; + toolProfile: ToolProfile; + agentBody?: string; +} + +export const BUILT_IN_TOOL_PROFILES: Record = { + none: { activeTools: [] }, + "read-only": { activeTools: ["read", "grep", "find", "ls"] }, + "read-only-with-safe-bash": { activeTools: ["read", "grep", "find", "ls", "bash"] }, + "full-tools": { activeTools: null }, +}; + +const DEFAULT_CONFIG: SubagentsConfig = { + defaultContext: "independent", + defaultTools: "read-only", + toolProfiles: { ...BUILT_IN_TOOL_PROFILES }, +}; + +export function loadConfig(cwd: string, projectTrusted: boolean, diagnostics: Diagnostics, agentDir = defaultAgentDir()): SubagentsConfig { + let config = cloneConfig(DEFAULT_CONFIG); + config = mergeConfig(config, readConfig(join(agentDir, "subagents.json"), diagnostics, "global"), diagnostics, "global"); + if (projectTrusted) { + config = mergeConfig(config, readConfig(join(cwd, ".pi", "subagents.json"), diagnostics, "project"), diagnostics, "project"); + } + if (!config.toolProfiles[config.defaultTools]) { + diagnostics.warnings.push(`Unknown defaultTools profile '${config.defaultTools}', using read-only`); + config.defaultTools = "read-only"; + } + return config; +} + +export function resolveSpawn(request: SpawnRequest, config: SubagentsConfig, agents: Map): ResolvedSpawnRequest { + const prompt = typeof request.prompt === "string" ? request.prompt.trim() : ""; + if (!prompt) throw new Error("prompt is required"); + const agent = request.agent ? agents.get(request.agent) : undefined; + if (request.agent && !agent) throw new Error(`unknown subagent agent: ${request.agent}`); + + const context = request.context ?? agent?.context ?? config.defaultContext; + if (context !== "independent" && context !== "fork") throw new Error(`unsupported context: ${context}`); + if (agent?.allowedContexts && !agent.allowedContexts.includes(context)) { + throw new Error(`agent '${agent.name}' does not allow ${context} context`); + } + + const tools = request.tools ?? agent?.tools ?? config.defaultTools; + const toolProfile = config.toolProfiles[tools]; + if (!toolProfile) throw new Error(`unknown tool profile: ${tools}`); + + return { + ...request, + prompt, + agent: agent?.name ?? request.agent, + context, + model: request.model ?? agent?.model, + thinking: request.thinking ?? agent?.thinking, + tools, + toolProfile, + agentBody: agent?.body, + }; +} + +function readConfig(path: string, diagnostics: Diagnostics, label: string): Partial | undefined { + if (!existsSync(path)) return undefined; + try { + const parsed = JSON.parse(readFileSync(path, "utf8")); + return normalizeConfig(parsed, diagnostics, label); + } catch (error) { + diagnostics.warnings.push(`Invalid ${label} subagents.json: ${error instanceof Error ? error.message : String(error)}`); + return undefined; + } +} + +function normalizeConfig(raw: unknown, diagnostics: Diagnostics, label: string): Partial | undefined { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + diagnostics.warnings.push(`Invalid ${label} subagents.json: root must be an object`); + return undefined; + } + const input = raw as Record; + const config: Partial = {}; + if (input.defaultContext === "independent" || input.defaultContext === "fork") config.defaultContext = input.defaultContext; + else if (input.defaultContext !== undefined) diagnostics.warnings.push(`Invalid ${label} defaultContext ignored`); + if (typeof input.defaultTools === "string") config.defaultTools = input.defaultTools; + else if (input.defaultTools !== undefined) diagnostics.warnings.push(`Invalid ${label} defaultTools ignored`); + if (input.toolProfiles !== undefined) config.toolProfiles = normalizeProfiles(input.toolProfiles, diagnostics, label); + return config; +} + +function normalizeProfiles(raw: unknown, diagnostics: Diagnostics, label: string): Record { + const profiles: Record = {}; + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + diagnostics.warnings.push(`Invalid ${label} toolProfiles ignored`); + return profiles; + } + for (const [name, value] of Object.entries(raw as Record)) { + if (name in BUILT_IN_TOOL_PROFILES) { + diagnostics.warnings.push(`Ignoring ${label} override for built-in tool profile '${name}'`); + continue; + } + const profile = normalizeProfile(value); + if (!profile) { + diagnostics.warnings.push(`Invalid ${label} tool profile '${name}' ignored`); + continue; + } + profiles[name] = profile; + } + return profiles; +} + +function normalizeProfile(raw: unknown): ToolProfile | undefined { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; + const activeTools = (raw as { activeTools?: unknown }).activeTools; + if (!Array.isArray(activeTools) || !activeTools.every((tool) => typeof tool === "string")) return undefined; + return { activeTools }; +} + +function mergeConfig(base: SubagentsConfig, override: Partial | undefined, diagnostics: Diagnostics, label: string): SubagentsConfig { + if (!override) return base; + const merged = cloneConfig(base); + if (override.defaultContext) merged.defaultContext = override.defaultContext; + if (override.defaultTools) merged.defaultTools = override.defaultTools; + if (override.toolProfiles) merged.toolProfiles = { ...merged.toolProfiles, ...override.toolProfiles }; + for (const key of Object.keys(merged.toolProfiles)) { + if (key in BUILT_IN_TOOL_PROFILES) merged.toolProfiles[key] = BUILT_IN_TOOL_PROFILES[key]; + } + return merged; +} + +function cloneConfig(config: SubagentsConfig): SubagentsConfig { + return { ...config, toolProfiles: { ...config.toolProfiles } }; +} + +function defaultAgentDir(): string { + return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"); +} diff --git a/modules/agents/pi/extensions/subagents/index.ts b/modules/agents/pi/extensions/subagents/index.ts index 2be89dc..3643bc8 100644 --- a/modules/agents/pi/extensions/subagents/index.ts +++ b/modules/agents/pi/extensions/subagents/index.ts @@ -1,10 +1,13 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; +import { loadAgents } from "./agents.ts"; +import { loadConfig, resolveSpawn, type Diagnostics } from "./config.ts"; import { SubprocessRpcRunner } from "./runner.ts"; import { Supervisor } from "./supervisor.ts"; import type { SpawnRequest } from "./types.ts"; let supervisor: Supervisor | undefined; +let lastDiagnostics: Diagnostics = { warnings: [] }; export default function subagents(pi: ExtensionAPI) { const getSupervisor = (ctx: ExtensionContext): Supervisor => { @@ -12,18 +15,30 @@ export default function subagents(pi: ExtensionAPI) { return supervisor; }; + const resolve = (ctx: ExtensionContext, request: SpawnRequest): SpawnRequest => { + const diagnostics: Diagnostics = { warnings: [] }; + const cwd = cwdOf(ctx); + const trusted = isProjectTrusted(ctx); + const config = loadConfig(cwd, trusted, diagnostics); + const agents = loadAgents(cwd, trusted, diagnostics); + lastDiagnostics = diagnostics; + return resolveSpawn(request, config, agents); + }; + pi.registerTool({ name: "subagent_spawn", label: "Spawn subagent", description: "Start one ad hoc independent subagent and return immediately with its child id", parameters: Type.Object({ prompt: Type.String({ description: "Prompt for the delegated subagent" }), + agent: Type.Optional(Type.String({ description: "Named agent definition to use" })), context: Type.Optional(Type.Literal("independent")), - model: Type.Optional(Type.String({ description: "Optional model selector for status metadata" })), - thinking: Type.Optional(Type.String({ description: "Optional thinking level for status metadata" })), + model: Type.Optional(Type.String({ description: "Optional model selector for the child" })), + thinking: Type.Optional(Type.String({ description: "Optional thinking level for the child" })), + tools: Type.Optional(Type.String({ description: "Tool profile name" })), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { - const accepted = getSupervisor(ctx).spawn(params as SpawnRequest); + const accepted = getSupervisor(ctx).spawn(resolve(ctx, params as SpawnRequest)); ctx.ui?.notify?.(`Started subagent ${accepted.id}`, "info"); return textResult(accepted); }, @@ -78,7 +93,7 @@ export default function subagents(pi: ExtensionAPI) { pi.registerCommand("subagent-spawn", { description: "Start an ad hoc independent subagent", handler: async (args, ctx) => { - const accepted = getSupervisor(ctx).spawn({ prompt: args }); + const accepted = getSupervisor(ctx).spawn(resolve(ctx, parseSpawnArgs(args))); ctx.ui.notify(`Started subagent ${accepted.id}`, "info"); }, }); @@ -104,6 +119,13 @@ export default function subagents(pi: ExtensionAPI) { }, }); + pi.registerCommand("subagent-diagnostics", { + description: "Show subagent configuration diagnostics from the last load", + handler: async (_args, ctx) => { + ctx.ui.notify(JSON.stringify(lastDiagnostics, null, 2), "info"); + }, + }); + pi.registerCommand("subagent-cancel", { description: "Cancel a running subagent by id", handler: async (args, ctx) => { @@ -117,6 +139,17 @@ export default function subagents(pi: ExtensionAPI) { }); } +function parseSpawnArgs(args: string): SpawnRequest { + const match = /^--agent\s+(\S+)\s+([\s\S]+)$/u.exec(args.trim()); + if (!match) return { prompt: args }; + return { agent: match[1], prompt: match[2] }; +} + +function isProjectTrusted(ctx: ExtensionContext): boolean { + const value = (ctx as unknown as { isProjectTrusted?: () => boolean }).isProjectTrusted?.(); + return value === true; +} + function cwdOf(ctx: ExtensionContext): string { const sessionCwd = (ctx as unknown as { sessionManager?: { getCwd?: () => string }; cwd?: string }).sessionManager?.getCwd?.(); return sessionCwd ?? (ctx as unknown as { cwd?: string }).cwd ?? process.cwd(); diff --git a/modules/agents/pi/extensions/subagents/runner.ts b/modules/agents/pi/extensions/subagents/runner.ts index 8c88112..8edeaaa 100644 --- a/modules/agents/pi/extensions/subagents/runner.ts +++ b/modules/agents/pi/extensions/subagents/runner.ts @@ -156,7 +156,7 @@ class RpcChildHandle implements ChildHandle { export class SubprocessRpcRunner implements ChildRunner { async start(id: string, request: SpawnRequest, cwd: string, events: RunnerEvents): Promise { - const args = [process.argv[1], "--mode", "rpc", "--no-extensions", "--name", `subagent ${id}`]; + const args = [process.argv[1], "--mode", "rpc", "--no-extensions", "--name", `subagent ${id}`, ...toolArgs(request), ...modelArgs(request)]; const child = spawn(process.execPath, args, { cwd, env: childEnvironment(), @@ -174,6 +174,20 @@ function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } +function toolArgs(request: SpawnRequest): string[] { + const activeTools = request.toolProfile?.activeTools; + if (activeTools === undefined || activeTools === null) return []; + if (activeTools.length === 0) return ["--no-tools"]; + return ["--tools", activeTools.join(",")]; +} + +function modelArgs(request: SpawnRequest): string[] { + const args: string[] = []; + if (request.model && request.model !== "inherit") args.push("--model", request.model); + if (request.thinking) args.push("--thinking", request.thinking); + return args; +} + function childEnvironment(): NodeJS.ProcessEnv { const env = { ...process.env }; delete env.PI_SESSION_ID; @@ -185,5 +199,6 @@ function childEnvironment(): NodeJS.ProcessEnv { } function independentPrompt(request: SpawnRequest): string { - return `You are running as a delegated subagent in independent context.\nDo not assume access to the parent conversation transcript.\nReturn a concise final answer for the parent agent.\n\nTask:\n${request.prompt}`; + const base = request.agentBody ? `${request.agentBody}\n\n` : ""; + return `${base}You are running as a delegated subagent in independent context.\nDo not assume access to the parent conversation transcript.\nReturn a concise final answer for the parent agent.\n\nTask:\n${request.prompt}`; } diff --git a/modules/agents/pi/extensions/subagents/supervisor.ts b/modules/agents/pi/extensions/subagents/supervisor.ts index 2129536..fe42838 100644 --- a/modules/agents/pi/extensions/subagents/supervisor.ts +++ b/modules/agents/pi/extensions/subagents/supervisor.ts @@ -46,7 +46,7 @@ export class Supervisor { cwd: this.cwd, model: request.model, thinking: request.thinking, - tools: "pi-default", + tools: request.tools ?? "read-only", startedAt: now, elapsedMs: 0, lastEvent: "queued", @@ -216,7 +216,7 @@ export class Supervisor { private resolveContext(context: ContextMode | undefined): ContextMode { if (context === undefined) return "independent"; - if (context !== "independent") throw new Error("only independent context is implemented in this tracer bullet"); + if (context !== "independent") throw new Error("only independent context is implemented before fork mode lands"); return context; } diff --git a/modules/agents/pi/extensions/subagents/types.ts b/modules/agents/pi/extensions/subagents/types.ts index 4380099..209a098 100644 --- a/modules/agents/pi/extensions/subagents/types.ts +++ b/modules/agents/pi/extensions/subagents/types.ts @@ -11,6 +11,10 @@ export type SubagentState = | "timed_out" | "orphaned"; +export interface ToolProfile { + activeTools: string[] | null; +} + export interface SpawnRequest { prompt: string; context?: ContextMode; @@ -18,6 +22,8 @@ export interface SpawnRequest { model?: string; thinking?: string; tools?: string; + toolProfile?: ToolProfile; + agentBody?: string; } export interface SpawnAccepted {