feat(pi): add subagent config and agents

This commit is contained in:
2026-08-01 14:27:36 -04:00
parent 4e6baed5f0
commit 418d9c986d
7 changed files with 472 additions and 8 deletions

View File

@@ -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<string, AgentDefinition> {
const user = loadTier(join(agentDir, "agents"), "user", diagnostics);
const project = projectTrusted ? loadTier(join(cwd, ".pi", "agents"), "project", diagnostics) : new Map<string, AgentDefinition>();
return new Map([...user, ...project]);
}
function loadTier(dir: string, tier: string, diagnostics: Diagnostics): Map<string, AgentDefinition> {
const agents = new Map<string, AgentDefinition>();
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<string, unknown> {
const result: Record<string, unknown> = {};
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<string, unknown>, key: string): string | undefined {
const value = record[key];
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function booleanField(record: Record<string, unknown>, 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");
}

View File

@@ -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'")));
});

View File

@@ -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<string, ToolProfile>;
}
export interface ResolvedSpawnRequest extends SpawnRequest {
prompt: string;
context: ContextMode;
tools: string;
toolProfile: ToolProfile;
agentBody?: string;
}
export const BUILT_IN_TOOL_PROFILES: Record<string, ToolProfile> = {
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<string, AgentDefinition>): 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<SubagentsConfig> | 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<SubagentsConfig> | 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<string, unknown>;
const config: Partial<SubagentsConfig> = {};
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<string, ToolProfile> {
const profiles: Record<string, ToolProfile> = {};
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<string, unknown>)) {
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<SubagentsConfig> | 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");
}

View File

@@ -1,10 +1,13 @@
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox"; import { Type } from "typebox";
import { loadAgents } from "./agents.ts";
import { loadConfig, resolveSpawn, type Diagnostics } from "./config.ts";
import { SubprocessRpcRunner } from "./runner.ts"; import { SubprocessRpcRunner } from "./runner.ts";
import { Supervisor } from "./supervisor.ts"; import { Supervisor } from "./supervisor.ts";
import type { SpawnRequest } from "./types.ts"; import type { SpawnRequest } from "./types.ts";
let supervisor: Supervisor | undefined; let supervisor: Supervisor | undefined;
let lastDiagnostics: Diagnostics = { warnings: [] };
export default function subagents(pi: ExtensionAPI) { export default function subagents(pi: ExtensionAPI) {
const getSupervisor = (ctx: ExtensionContext): Supervisor => { const getSupervisor = (ctx: ExtensionContext): Supervisor => {
@@ -12,18 +15,30 @@ export default function subagents(pi: ExtensionAPI) {
return supervisor; 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({ pi.registerTool({
name: "subagent_spawn", name: "subagent_spawn",
label: "Spawn subagent", label: "Spawn subagent",
description: "Start one ad hoc independent subagent and return immediately with its child id", description: "Start one ad hoc independent subagent and return immediately with its child id",
parameters: Type.Object({ parameters: Type.Object({
prompt: Type.String({ description: "Prompt for the delegated subagent" }), 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")), context: Type.Optional(Type.Literal("independent")),
model: Type.Optional(Type.String({ description: "Optional model selector 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 status metadata" })), 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) { 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"); ctx.ui?.notify?.(`Started subagent ${accepted.id}`, "info");
return textResult(accepted); return textResult(accepted);
}, },
@@ -78,7 +93,7 @@ export default function subagents(pi: ExtensionAPI) {
pi.registerCommand("subagent-spawn", { pi.registerCommand("subagent-spawn", {
description: "Start an ad hoc independent subagent", description: "Start an ad hoc independent subagent",
handler: async (args, ctx) => { 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"); 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", { pi.registerCommand("subagent-cancel", {
description: "Cancel a running subagent by id", description: "Cancel a running subagent by id",
handler: async (args, ctx) => { 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 { function cwdOf(ctx: ExtensionContext): string {
const sessionCwd = (ctx as unknown as { sessionManager?: { getCwd?: () => string }; cwd?: string }).sessionManager?.getCwd?.(); const sessionCwd = (ctx as unknown as { sessionManager?: { getCwd?: () => string }; cwd?: string }).sessionManager?.getCwd?.();
return sessionCwd ?? (ctx as unknown as { cwd?: string }).cwd ?? process.cwd(); return sessionCwd ?? (ctx as unknown as { cwd?: string }).cwd ?? process.cwd();

View File

@@ -156,7 +156,7 @@ class RpcChildHandle implements ChildHandle {
export class SubprocessRpcRunner implements ChildRunner { export class SubprocessRpcRunner implements ChildRunner {
async start(id: string, request: SpawnRequest, cwd: string, events: RunnerEvents): Promise<ChildHandle> { async start(id: string, request: SpawnRequest, cwd: string, events: RunnerEvents): Promise<ChildHandle> {
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, { const child = spawn(process.execPath, args, {
cwd, cwd,
env: childEnvironment(), env: childEnvironment(),
@@ -174,6 +174,20 @@ function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); 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 { function childEnvironment(): NodeJS.ProcessEnv {
const env = { ...process.env }; const env = { ...process.env };
delete env.PI_SESSION_ID; delete env.PI_SESSION_ID;
@@ -185,5 +199,6 @@ function childEnvironment(): NodeJS.ProcessEnv {
} }
function independentPrompt(request: SpawnRequest): string { 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}`;
} }

View File

@@ -46,7 +46,7 @@ export class Supervisor {
cwd: this.cwd, cwd: this.cwd,
model: request.model, model: request.model,
thinking: request.thinking, thinking: request.thinking,
tools: "pi-default", tools: request.tools ?? "read-only",
startedAt: now, startedAt: now,
elapsedMs: 0, elapsedMs: 0,
lastEvent: "queued", lastEvent: "queued",
@@ -216,7 +216,7 @@ export class Supervisor {
private resolveContext(context: ContextMode | undefined): ContextMode { private resolveContext(context: ContextMode | undefined): ContextMode {
if (context === undefined) return "independent"; 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; return context;
} }

View File

@@ -11,6 +11,10 @@ export type SubagentState =
| "timed_out" | "timed_out"
| "orphaned"; | "orphaned";
export interface ToolProfile {
activeTools: string[] | null;
}
export interface SpawnRequest { export interface SpawnRequest {
prompt: string; prompt: string;
context?: ContextMode; context?: ContextMode;
@@ -18,6 +22,8 @@ export interface SpawnRequest {
model?: string; model?: string;
thinking?: string; thinking?: string;
tools?: string; tools?: string;
toolProfile?: ToolProfile;
agentBody?: string;
} }
export interface SpawnAccepted { export interface SpawnAccepted {