import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { SUBAGENT_FANOUT_CHILD_ENV } from "./pi-args.ts"; const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEXT"; const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS"; export const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME"; export const CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS = [ "You are a child subagent, not the parent orchestrator.", "The parent session owns delegation, orchestration, review fanout, and follow-up worker launches.", "Ignore prior parent-only orchestration instructions in inherited conversation history.", "Do not propose or run subagents. Complete only your assigned role-specific task with the tools available to you.", "If you need to edit files, call the actual edit/write tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.", ].join("\n"); export const CHILD_FANOUT_BOUNDARY_INSTRUCTIONS = [ "You are a child subagent with explicit fanout responsibility for this assigned task.", "The parent session owns final orchestration, acceptance, and follow-up implementation launches.", "You may use the `subagent` tool only for the fanout work explicitly requested in this task.", "Do not broaden yourself into general parent orchestration. Do not launch follow-up workers unless the task explicitly asks for that.", "The maxSubagentDepth cap still applies and may block further fanout.", "If you need to edit files, call the actual edit/write tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.", ].join("\n"); const PARENT_ONLY_CUSTOM_MESSAGE_TYPES = new Set([ "subagent-orchestration-instructions", "subagent-slash-result", "subagent-notify", "subagent_control_notice", "subagent-control", "subagent-control-notice", ]); const SUBAGENT_ORCHESTRATION_SKILL_NAME_PATTERN = /\s*pi-subagents\s*<\/name>/; const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n"; const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks."; const DATE_HEADER = "\nCurrent date:"; function readBooleanEnv(name: string): boolean | undefined { const value = process.env[name]; if (value === undefined) return undefined; return value !== "0"; } function findSectionEnd(prompt: string, startIndex: number, nextHeaders: string[]): number { let endIndex = prompt.length; for (const header of nextHeaders) { const index = prompt.indexOf(header, startIndex); if (index !== -1 && index < endIndex) { endIndex = index; } } return endIndex; } export function stripProjectContext(prompt: string): string { const startIndex = prompt.indexOf(PROJECT_CONTEXT_HEADER); if (startIndex === -1) return prompt; const endIndex = findSectionEnd(prompt, startIndex + PROJECT_CONTEXT_HEADER.length, [SKILLS_HEADER, DATE_HEADER]); return `${prompt.slice(0, startIndex)}${prompt.slice(endIndex)}`; } export function stripInheritedSkills(prompt: string): string { const startIndex = prompt.indexOf(SKILLS_HEADER); if (startIndex === -1) return prompt; const endIndex = findSectionEnd(prompt, startIndex + SKILLS_HEADER.length, [DATE_HEADER]); return `${prompt.slice(0, startIndex)}${prompt.slice(endIndex)}`; } export function stripSubagentOrchestrationSkill(prompt: string): string { return prompt .replace(/\n{0,2}]*>[\s\S]*?<\/skill>\n{0,2}/g, "\n\n") .replace(/[ \t]*\s*[\s\S]*?<\/skill>\s*/g, (block) => SUBAGENT_ORCHESTRATION_SKILL_NAME_PATTERN.test(block) ? "" : block); } function stripChildBoundaryInstructions(prompt: string): string { let rewritten = prompt; for (const boundary of [CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS, CHILD_FANOUT_BOUNDARY_INSTRUCTIONS]) { rewritten = rewritten.split(boundary).join(""); } return rewritten.replace(/^(?:[ \t]*\r?\n)+/, ""); } export function rewriteSubagentPrompt( prompt: string, options: { inheritProjectContext: boolean; inheritSkills: boolean; fanoutChild?: boolean }, ): string { let rewritten = prompt; if (!options.inheritProjectContext) { rewritten = stripProjectContext(rewritten); } if (!options.inheritSkills) { rewritten = stripInheritedSkills(rewritten); } rewritten = stripSubagentOrchestrationSkill(rewritten); rewritten = stripChildBoundaryInstructions(rewritten); const boundary = options.fanoutChild ? CHILD_FANOUT_BOUNDARY_INSTRUCTIONS : CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS; return `${boundary}\n\n${rewritten}`; } function isParentOnlySubagentMessage(message: unknown): boolean { const m = message as { role?: string; customType?: string }; return m?.role === "custom" && typeof m.customType === "string" && PARENT_ONLY_CUSTOM_MESSAGE_TYPES.has(m.customType); } function isSubagentToolResultMessage(message: unknown): boolean { const m = message as { role?: string; toolName?: string }; return m?.role === "toolResult" && m.toolName === "subagent"; } function isSubagentToolCallBlock(block: unknown): boolean { const b = block as { type?: string; name?: string }; return b?.type === "toolCall" && b.name === "subagent"; } function stripAssistantSubagentToolCallBlocks(message: unknown): unknown | undefined { const m = message as { role?: string; content?: unknown }; if (m?.role !== "assistant" || !Array.isArray(m.content)) return message; const filteredContent = m.content.filter((block) => !isSubagentToolCallBlock(block)); if (filteredContent.length === m.content.length) return message; if (filteredContent.length === 0) return undefined; return { ...m, content: filteredContent }; } export function stripParentOnlySubagentMessages(messages: unknown[]): unknown[] { let changed = false; const filtered: unknown[] = []; for (const message of messages) { if (isParentOnlySubagentMessage(message) || isSubagentToolResultMessage(message)) { changed = true; continue; } const stripped = stripAssistantSubagentToolCallBlocks(message); if (stripped === undefined) { changed = true; continue; } if (stripped !== message) changed = true; filtered.push(stripped); } return changed ? filtered : messages; } export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void { pi.on("context", (event) => { const messages = stripParentOnlySubagentMessages(event.messages); if (messages === event.messages) return undefined; return { messages }; }); pi.on("before_agent_start", async (event) => { const intercomSessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim(); if (intercomSessionName && typeof pi.setSessionName === "function") { pi.setSessionName(intercomSessionName); } const inheritProjectContext = readBooleanEnv(SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV); const inheritSkills = readBooleanEnv(SUBAGENT_INHERIT_SKILLS_ENV); const fanoutChild = readBooleanEnv(SUBAGENT_FANOUT_CHILD_ENV); if (inheritProjectContext === undefined && inheritSkills === undefined && fanoutChild === undefined) return; const rewritten = rewriteSubagentPrompt(event.systemPrompt, { inheritProjectContext: inheritProjectContext ?? true, inheritSkills: inheritSkills ?? true, fanoutChild: fanoutChild === true, }); if (rewritten === event.systemPrompt) return; return { systemPrompt: rewritten }; }); }