import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { encodeNestedPathEnv, parseNestedPathEnv, type NestedPathEntry } from "./nested-path.ts"; import { resolveMcpDirectToolNames } from "./mcp-direct-tool-allowlist.ts"; const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"]; const TASK_ARG_LIMIT = 8000; const PROMPT_RUNTIME_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-prompt-runtime.ts"); const FANOUT_CHILD_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "extension", "fanout-child.ts"); export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD"; export const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "PI_SUBAGENT_ORCHESTRATOR_TARGET"; export const SUBAGENT_RUN_ID_ENV = "PI_SUBAGENT_RUN_ID"; export const SUBAGENT_CHILD_AGENT_ENV = "PI_SUBAGENT_CHILD_AGENT"; export const SUBAGENT_CHILD_INDEX_ENV = "PI_SUBAGENT_CHILD_INDEX"; export const SUBAGENT_FANOUT_CHILD_ENV = "PI_SUBAGENT_FANOUT_CHILD"; export const SUBAGENT_PARENT_EVENT_SINK_ENV = "PI_SUBAGENT_PARENT_EVENT_SINK"; export const SUBAGENT_PARENT_CONTROL_INBOX_ENV = "PI_SUBAGENT_PARENT_CONTROL_INBOX"; export const SUBAGENT_PARENT_ROOT_RUN_ID_ENV = "PI_SUBAGENT_PARENT_ROOT_RUN_ID"; export const SUBAGENT_PARENT_RUN_ID_ENV = "PI_SUBAGENT_PARENT_RUN_ID"; export const SUBAGENT_PARENT_CHILD_INDEX_ENV = "PI_SUBAGENT_PARENT_CHILD_INDEX"; export const SUBAGENT_PARENT_DEPTH_ENV = "PI_SUBAGENT_PARENT_DEPTH"; export const SUBAGENT_PARENT_PATH_ENV = "PI_SUBAGENT_PARENT_PATH"; export const SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV = "PI_SUBAGENT_PARENT_CAPABILITY_TOKEN"; interface BuildPiArgsInput { baseArgs: string[]; task: string; sessionEnabled: boolean; sessionDir?: string; sessionFile?: string; model?: string; thinking?: string; systemPromptMode?: "append" | "replace"; inheritProjectContext: boolean; inheritSkills: boolean; tools?: string[]; extensions?: string[]; systemPrompt?: string | null; mcpDirectTools?: string[]; cwd?: string; promptFileStem?: string; intercomSessionName?: string; orchestratorIntercomTarget?: string; runId?: string; childAgentName?: string; childIndex?: number; parentEventSink?: string; parentControlInbox?: string; parentRootRunId?: string; parentRunId?: string; parentChildIndex?: number; parentDepth?: number; parentPath?: NestedPathEntry[]; parentCapabilityToken?: string; } interface BuildPiArgsResult { args: string[]; env: Record; tempDir?: string; } export function applyThinkingSuffix(model: string | undefined, thinking: string | undefined): string | undefined { if (!model || !thinking || thinking === "off") return model; const colonIdx = model.lastIndexOf(":"); if (colonIdx !== -1 && THINKING_LEVELS.includes(model.substring(colonIdx + 1))) return model; return `${model}:${thinking}`; } export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult { const args = [...input.baseArgs]; if (input.sessionFile) { fs.mkdirSync(path.dirname(input.sessionFile), { recursive: true }); args.push("--session", input.sessionFile); } else { if (!input.sessionEnabled) { args.push("--no-session"); } if (input.sessionDir) { fs.mkdirSync(input.sessionDir, { recursive: true }); args.push("--session-dir", input.sessionDir); } } const modelArg = applyThinkingSuffix(input.model, input.thinking); if (modelArg) { args.push("--model", modelArg); } const declaredBuiltinTools = input.tools?.filter((tool) => !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) ?? []; const fanoutAuthorized = declaredBuiltinTools.includes("subagent"); const toolExtensionPaths: string[] = []; if (input.tools?.length) { const builtinTools = [...declaredBuiltinTools]; for (const tool of input.tools) { if (!declaredBuiltinTools.includes(tool) && (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) { toolExtensionPaths.push(tool); } } if (builtinTools.length > 0) { if (input.mcpDirectTools?.length) { builtinTools.push(...resolveMcpDirectToolNames(input.mcpDirectTools, input.cwd)); } args.push("--tools", builtinTools.join(",")); } } const runtimeExtensions = fanoutAuthorized ? [PROMPT_RUNTIME_EXTENSION_PATH, FANOUT_CHILD_EXTENSION_PATH] : [PROMPT_RUNTIME_EXTENSION_PATH]; if (input.extensions !== undefined) { args.push("--no-extensions"); for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions])]) { args.push("--extension", extPath); } } else { for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths])]) { args.push("--extension", extPath); } } if (!input.inheritSkills) { args.push("--no-skills"); } let tempDir: string | undefined; if (input.systemPrompt !== undefined && input.systemPrompt !== null) { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-")); const stem = (input.promptFileStem ?? "prompt").replace(/[^\w.-]/g, "_"); const promptPath = path.join(tempDir, `${stem}.md`); fs.writeFileSync(promptPath, input.systemPrompt, { mode: 0o600 }); args.push(input.systemPromptMode === "replace" ? "--system-prompt" : "--append-system-prompt", promptPath); } if (input.task.length > TASK_ARG_LIMIT) { if (!tempDir) { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-")); } const taskFilePath = path.join(tempDir, "task.md"); fs.writeFileSync(taskFilePath, `Task: ${input.task}`, { mode: 0o600 }); args.push(`@${taskFilePath}`); } else { args.push(`Task: ${input.task}`); } const env: Record = {}; env[SUBAGENT_CHILD_ENV] = "1"; env[SUBAGENT_FANOUT_CHILD_ENV] = fanoutAuthorized ? "1" : "0"; const inheritedNestedRoute = Boolean(process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] && process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] && process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV]); const parentRunId = input.parentRunId ?? input.runId ?? (inheritedNestedRoute ? process.env[SUBAGENT_RUN_ID_ENV] : undefined) ?? process.env[SUBAGENT_PARENT_RUN_ID_ENV] ?? ""; const parentChildIndex = input.parentChildIndex !== undefined ? String(input.parentChildIndex) : input.childIndex !== undefined ? String(input.childIndex) : process.env[SUBAGENT_PARENT_CHILD_INDEX_ENV] ?? ""; const inheritedDepth = Number(process.env[SUBAGENT_PARENT_DEPTH_ENV]); const parentDepth = input.parentDepth ?? (inheritedNestedRoute && Number.isFinite(inheritedDepth) ? inheritedDepth + 1 : 1); const parentPath = input.parentPath ?? [ ...parseNestedPathEnv(process.env[SUBAGENT_PARENT_PATH_ENV]), ...(parentRunId ? [{ runId: parentRunId, ...(parentChildIndex && /^\d+$/.test(parentChildIndex) ? { stepIndex: Number(parentChildIndex) } : {}), ...(input.childAgentName ? { agent: input.childAgentName } : {}), }] : []), ]; env[SUBAGENT_PARENT_EVENT_SINK_ENV] = fanoutAuthorized ? input.parentEventSink ?? process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] ?? "" : ""; env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] = fanoutAuthorized ? input.parentControlInbox ?? process.env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] ?? "" : ""; env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] = fanoutAuthorized ? input.parentRootRunId ?? process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] ?? input.runId ?? "" : ""; env[SUBAGENT_PARENT_RUN_ID_ENV] = fanoutAuthorized ? parentRunId : ""; env[SUBAGENT_PARENT_CHILD_INDEX_ENV] = fanoutAuthorized ? parentChildIndex : ""; env[SUBAGENT_PARENT_DEPTH_ENV] = fanoutAuthorized ? String(parentDepth) : ""; env[SUBAGENT_PARENT_PATH_ENV] = fanoutAuthorized ? encodeNestedPathEnv(parentPath) : ""; env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] = fanoutAuthorized ? input.parentCapabilityToken ?? process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] ?? "" : ""; env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext ? "1" : "0"; env.PI_SUBAGENT_INHERIT_SKILLS = input.inheritSkills ? "1" : "0"; if (input.intercomSessionName) { env.PI_SUBAGENT_INTERCOM_SESSION_NAME = input.intercomSessionName; } if (input.orchestratorIntercomTarget) { env[SUBAGENT_ORCHESTRATOR_TARGET_ENV] = input.orchestratorIntercomTarget; } if (input.runId) { env[SUBAGENT_RUN_ID_ENV] = input.runId; } if (input.childAgentName) { env[SUBAGENT_CHILD_AGENT_ENV] = input.childAgentName; } if (input.childIndex !== undefined) { env[SUBAGENT_CHILD_INDEX_ENV] = String(input.childIndex); } if (input.mcpDirectTools?.length) { env.MCP_DIRECT_TOOLS = input.mcpDirectTools.join(","); } else { env.MCP_DIRECT_TOOLS = "__none__"; } return { args, env, tempDir }; } export const parseParentPathEnv = parseNestedPathEnv; export function cleanupTempDir(tempDir: string | null | undefined): void { if (!tempDir) return; try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { // Temp cleanup is best effort. } }