import { createHash } from "node:crypto"; import * as fs from "node:fs"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { discoverAgents, discoverAgentsAll, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts"; import { resolveExecutionAgentScope } from "../agents/agent-scope.ts"; import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts"; import { buildAgentMemoryInjection } from "../agents/agent-memory.ts"; import { buildModelCandidates, resolveEffectiveSubagentModel, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-fallback.ts"; import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/pi-args.ts"; import { injectOutputPathSystemPrompt, normalizeSingleOutputOverride, resolveSingleOutputPath } from "../runs/shared/single-output.ts"; import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts"; import { resolveEffectiveThinking } from "../shared/model-info.ts"; import { SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, type ArtifactDirPreference, type ArtifactPaths, type JsonSchemaObject, type OutputMode } from "../shared/types.ts"; import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts"; import { appendTurnBudgetSystemPrompt } from "../runs/shared/turn-budget.ts"; import type { ResolvedTurnBudget } from "../shared/types.ts"; import type { ResolvedMcpDirectToolSelection } from "../runs/shared/mcp-direct-tool-allowlist.ts"; import { resolveStepBehavior } from "../shared/settings.ts"; import { agentDefinitionDigest, AGENT_DEFINITION_PROJECTION_VERSION, launchBindingDigest } from "../shared/launch-contract.ts"; import { ASYNC_DIR, RESULTS_DIR, TEMP_ROOT_DIR } from "../shared/types.ts"; import { processTerminalCandidatePath, processTerminalPath } from "../runs/background/process-terminal.ts"; import { nestedResultsPath } from "../runs/shared/nested-events.ts"; export const SUBAGENT_LAUNCH_CONTRACT_VERSION = 2 as const; export type SubagentLaunchContractReasonCode = | "missing_agent" | "ambiguous_agent" | "missing_skill" | "denied_required_tool" | "invalid_artifact_dir" | "invalid_cwd" | "unsupported_mode"; export interface SubagentLaunchContractDiagnostic { code: SubagentLaunchContractReasonCode | "host_required" | "snapshot_warning"; severity: "error" | "warning" | "host-required"; message: string; } export interface SubagentLaunchContractInput { agent: string; cwd: string; task?: string; agentScope?: AgentScope; context?: "fresh" | "fork"; model?: string; thinking?: string | false; parentModel?: ParentModel; availableModels?: ReadonlyArray; preferredProvider?: string; skill?: string | string[] | boolean; output?: string | boolean; outputMode?: OutputMode; outputSchema?: JsonSchemaObject; turnBudget?: ResolvedTurnBudget; artifacts?: boolean; artifactDir?: ArtifactDirPreference; parentSessionFile?: string | null; sessionRoot?: string; sessionDir?: string; runId?: string; /** Root run id supplied by a host when projecting nested async lifecycle paths. */ nestedRootRunId?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling; inheritedCapabilityCeiling?: ResolvedSubagentCapabilityCeiling; } export interface SubagentLaunchContractAgentCandidate { name: string; localName?: string; packageName?: string; source: AgentSource; filePath: string; disabled?: boolean; selected: boolean; } export interface SubagentLaunchContractAgent { name: string; localName?: string; packageName?: string; source: AgentSource; filePath: string; definitionProjectionVersion: typeof AGENT_DEFINITION_PROJECTION_VERSION; definitionDigest: string; shadowedCandidates: SubagentLaunchContractAgentCandidate[]; } export interface SubagentLaunchContractSkills { requested: string[]; resolved: Array<{ name: string; path: string; source: string }>; missing: string[]; } export interface SubagentLaunchContractTools { requestedBuiltin: string[]; declaredBuiltin: string[]; effectiveAllowlist: string[]; explicitAllowlist: boolean; requiredChildTools: string[]; internalTools: string[]; mcp: ResolvedMcpDirectToolSelection[]; effectiveMcpTools: string[]; toolExtensionPaths: string[]; runtimeExtensions: string[]; configuredExtensions: string[]; extensionArgs: string[]; disableAmbientExtensions: boolean; fanoutAuthorized: boolean; capabilityCeiling?: ResolvedSubagentCapabilityCeiling; capabilityAudit?: SubagentCapabilityAudit; } export interface SubagentLaunchContractRoots { cwd: string; sessionRoot?: string; sessionDir?: string; sessionFile?: string; artifactsDir?: string; artifactPaths?: ArtifactPaths; outputPath?: string; lifecycle?: { asyncDir: string; resultPath: string; statusPath: string; eventsPath: string; processTerminalPath: string; processTerminalCandidatePath: string; }; } export interface SubagentLaunchContract { version: typeof SUBAGENT_LAUNCH_CONTRACT_VERSION; runId: string; agent: SubagentLaunchContractAgent; context: "fresh" | "fork"; model?: string; modelCandidates: string[]; thinking?: string; systemPromptMode: AgentConfig["systemPromptMode"]; inheritProjectContext: boolean; inheritSkills: boolean; skills: SubagentLaunchContractSkills; tools: SubagentLaunchContractTools; roots: SubagentLaunchContractRoots; protocol: { lifecycleArtifactVersion: number; packageVersion: string; }; diagnostics: SubagentLaunchContractDiagnostic[]; /** Digest of the resolved child inputs, recomputed by execution paths. */ launchContractDigest: string; digest: string; } export type SubagentLaunchContractResult = | { ok: true; contract: SubagentLaunchContract } | { ok: false; code: SubagentLaunchContractReasonCode; message: string; diagnostics: SubagentLaunchContractDiagnostic[] }; function packageVersion(): string { const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json"); const parsed = JSON.parse(fs.readFileSync(packagePath, "utf-8")) as { version?: unknown }; if (typeof parsed.version !== "string" || !parsed.version.trim()) { throw new Error(`Invalid package version in '${packagePath}'.`); } return parsed.version; } function stableJson(value: unknown): string { if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; if (value && typeof value === "object") { return `{${Object.entries(value as Record) .filter(([, entry]) => entry !== undefined) .sort(([a], [b]) => a.localeCompare(b)) .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`) .join(",")}}`; } return JSON.stringify(value); } function digestContract(contract: Omit): string { return createHash("sha256").update(stableJson(contract)).digest("hex"); } function normalizeAvailableModels(models: SubagentLaunchContractInput["availableModels"]): AvailableModelInfo[] { return (models ?? []).map((model) => ({ ...model, fullId: model.fullId ?? `${model.provider}/${model.id}` })); } function candidateList(inputAgent: string, selected: AgentConfig | undefined, cwd: string): SubagentLaunchContractAgentCandidate[] { const all = discoverAgentsAll(cwd); return [...all.builtin, ...all.package, ...all.user, ...all.project] .filter((agent) => agent.name === inputAgent || agent.localName === inputAgent) .map((agent) => ({ name: agent.name, ...(agent.localName ? { localName: agent.localName } : {}), ...(agent.packageName ? { packageName: agent.packageName } : {}), source: agent.source, filePath: agent.filePath, ...(agent.disabled === true ? { disabled: true } : {}), selected: Boolean(selected && agent.filePath === selected.filePath && agent.name === selected.name), })); } export async function resolveSubagentLaunchContract(input: SubagentLaunchContractInput): Promise { const diagnostics: SubagentLaunchContractDiagnostic[] = []; const effectiveCwd = path.resolve(input.cwd); try { if (!fs.statSync(effectiveCwd).isDirectory()) { return { ok: false, code: "invalid_cwd", message: `cwd '${effectiveCwd}' is not a directory.`, diagnostics }; } } catch (error) { const detail = error instanceof Error ? ` ${error.message}` : ""; return { ok: false, code: "invalid_cwd", message: `cwd '${effectiveCwd}' is not a directory.${detail}`, diagnostics }; } if (input.context !== undefined && input.context !== "fresh" && input.context !== "fork") { return { ok: false, code: "unsupported_mode", message: `Unsupported context '${String(input.context)}'; expected 'fresh' or 'fork'.`, diagnostics }; } if (input.artifactDir !== undefined && input.artifactDir !== "project" && input.artifactDir !== "session" && input.artifactDir !== "temp") { return { ok: false, code: "invalid_artifact_dir", message: `Unsupported artifactDir '${String(input.artifactDir)}'; expected 'project', 'session', or 'temp'.`, diagnostics }; } if (input.context === "fork") { diagnostics.push({ code: "host_required", severity: "host-required", message: "Exact fork session branching and fork-thinking downgrade checks require Pi host session and model-registry snapshots." }); } const scope = resolveExecutionAgentScope(input.agentScope); const discovered = discoverAgents(effectiveCwd, scope); const matches = discovered.agents.filter((agent) => agent.name === input.agent || agent.localName === input.agent); if (matches.length === 0) { return { ok: false, code: "missing_agent", message: `Unknown agent: ${input.agent}`, diagnostics }; } if (matches.length > 1) { return { ok: false, code: "ambiguous_agent", message: `Ambiguous agent: ${input.agent}`, diagnostics }; } const agent = matches[0]!; const runId = input.runId ?? "preflight"; const skillInput = normalizeSkillInput(input.skill); const outputOverride = normalizeSingleOutputOverride(input.output, agent.output); const behavior = resolveStepBehavior(agent, { ...(outputOverride !== undefined ? { output: outputOverride } : {}), ...(input.outputMode !== undefined ? { outputMode: input.outputMode } : {}), ...(skillInput !== undefined ? { skills: skillInput } : {}), ...(input.model !== undefined ? { model: input.model } : {}), }); const requestedSkills = behavior.skills === false ? [] : behavior.skills; const resolvedSkills = resolveSkillsWithFallback( requestedSkills, effectiveCwd, effectiveCwd, agent.skillPath, agent.filePath ? path.dirname(agent.filePath) : effectiveCwd, ); if (resolvedSkills.missing.includes("pi-subagents")) { return { ok: false, code: "missing_skill", message: "The pi-subagents orchestration skill is not child-injectable.", diagnostics }; } if (resolvedSkills.missing.length > 0) diagnostics.push({ code: "missing_skill", severity: "error", message: `Missing skills: ${resolvedSkills.missing.join(", ")}` }); const availableModels = normalizeAvailableModels(input.availableModels); const preferredProvider = input.preferredProvider ?? input.parentModel?.provider; const primaryModel = resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope: discovered.modelScope }); const effectiveThinkingConfig = input.thinking !== undefined ? input.thinking : agent.thinking; const model = applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined); const modelCandidates = buildModelCandidates(primaryModel, agent.fallbackModels, availableModels, preferredProvider, { scope: discovered.modelScope }) .map((candidate) => applyThinkingSuffix(candidate, effectiveThinkingConfig, input.thinking !== undefined) ?? candidate); let toolPlan: PiLaunchToolPlan; try { toolPlan = resolvePiLaunchToolPlan({ tools: agent.tools, extensions: agent.extensions, subagentOnlyExtensions: agent.subagentOnlyExtensions, mcpDirectTools: agent.mcpDirectTools, cwd: effectiveCwd, requireReadTool: resolvedSkills.resolved.length > 0, structuredOutput: Boolean(input.outputSchema), capabilityCeiling: input.capabilityCeiling, inheritedCapabilityCeiling: input.inheritedCapabilityCeiling, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); diagnostics.push({ code: "denied_required_tool", severity: "error", message }); return { ok: false, code: "denied_required_tool", message, diagnostics }; } const artifactsEnabled = input.artifacts !== false; const artifactsDir = artifactsEnabled ? getArtifactsDir(input.parentSessionFile ?? null, effectiveCwd, input.artifactDir ?? "project") : undefined; const artifactPaths = artifactsDir ? getArtifactPaths(artifactsDir, runId, agent.name, 0) : undefined; const outputPath = resolveSingleOutputPath(behavior.output, effectiveCwd, effectiveCwd, artifactsDir ? path.join(artifactsDir, "outputs", runId) : undefined); const sessionRoot = input.sessionDir ? path.resolve(input.sessionDir) : input.sessionRoot ? path.join(path.resolve(input.sessionRoot), runId) : undefined; const sessionDir = sessionRoot ? path.join(sessionRoot, "run-0") : undefined; const lifecycleAsyncDir = input.nestedRootRunId ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", input.nestedRootRunId, runId) : path.join(ASYNC_DIR, runId); const lifecycleResultPath = input.nestedRootRunId ? nestedResultsPath(input.nestedRootRunId, runId) : path.join(RESULTS_DIR, `${runId}.json`); if (!sessionDir) diagnostics.push({ code: "host_required", severity: "host-required", message: "No sessionRoot/sessionDir was supplied; exact child session paths require the Pi host session-root policy." }); if (input.availableModels === undefined && (input.model || agent.model || input.parentModel)) { diagnostics.push({ code: "host_required", severity: "host-required", message: "No availableModels snapshot was supplied; model resolution may differ from the active Pi host registry." }); } if (resolvedSkills.missing.length > 0) { return { ok: false, code: "missing_skill", message: `Missing skills: ${resolvedSkills.missing.join(", ")}`, diagnostics }; } let effectiveSystemPrompt = agent.systemPrompt?.trim() ?? ""; if (resolvedSkills.resolved.length > 0) { const skillInjection = buildSkillInjection(resolvedSkills.resolved); effectiveSystemPrompt = effectiveSystemPrompt ? `${effectiveSystemPrompt}\n\n${skillInjection}` : skillInjection; } const memoryInjection = buildAgentMemoryInjection(agent, effectiveCwd); if (memoryInjection) effectiveSystemPrompt = effectiveSystemPrompt ? `${effectiveSystemPrompt}\n\n${memoryInjection}` : memoryInjection; effectiveSystemPrompt = injectOutputPathSystemPrompt(effectiveSystemPrompt, outputPath, agent); const turnBudget = input.turnBudget ?? agent.defaultTurnBudget; effectiveSystemPrompt = appendTurnBudgetSystemPrompt(effectiveSystemPrompt, turnBudget); const candidates = candidateList(input.agent, agent, effectiveCwd); const shadowedCandidates = candidates.filter((candidate) => !candidate.selected); const definitionDigest = agentDefinitionDigest(agent); const contractBase: Omit = { version: SUBAGENT_LAUNCH_CONTRACT_VERSION, runId, agent: { name: agent.name, ...(agent.localName ? { localName: agent.localName } : {}), ...(agent.packageName ? { packageName: agent.packageName } : {}), source: agent.source, filePath: agent.filePath, definitionProjectionVersion: AGENT_DEFINITION_PROJECTION_VERSION, definitionDigest, shadowedCandidates, }, context: input.context ?? agent.defaultContext ?? "fresh", ...(model ? { model } : {}), modelCandidates, ...(resolveEffectiveThinking(model, effectiveThinkingConfig) ? { thinking: resolveEffectiveThinking(model, effectiveThinkingConfig) } : {}), systemPromptMode: agent.systemPromptMode, inheritProjectContext: agent.inheritProjectContext, inheritSkills: agent.inheritSkills, skills: { requested: requestedSkills, resolved: resolvedSkills.resolved.map((skill) => ({ name: skill.name, path: skill.path, source: skill.source })), missing: resolvedSkills.missing, }, tools: { requestedBuiltin: toolPlan.requestedBuiltinTools, declaredBuiltin: toolPlan.declaredBuiltinTools, effectiveAllowlist: toolPlan.effectiveToolAllowlist, explicitAllowlist: toolPlan.explicitToolAllowlist, requiredChildTools: toolPlan.requiredChildTools, internalTools: toolPlan.internalTools, mcp: toolPlan.effectiveMcpSelections, effectiveMcpTools: toolPlan.effectiveMcpTools, toolExtensionPaths: toolPlan.toolExtensionPaths, runtimeExtensions: toolPlan.runtimeExtensions, configuredExtensions: toolPlan.configuredExtensions, extensionArgs: toolPlan.extensionArgs, disableAmbientExtensions: toolPlan.disableAmbientExtensions, fanoutAuthorized: toolPlan.fanoutAuthorized, ...(toolPlan.capabilityCeiling ? { capabilityCeiling: toolPlan.capabilityCeiling } : {}), ...(toolPlan.capabilityAudit ? { capabilityAudit: toolPlan.capabilityAudit } : {}), }, roots: { cwd: effectiveCwd, ...(sessionRoot ? { sessionRoot } : {}), ...(sessionDir ? { sessionDir, sessionFile: path.join(sessionDir, "session.jsonl") } : {}), ...(artifactsDir ? { artifactsDir } : {}), ...(artifactPaths ? { artifactPaths } : {}), ...(outputPath ? { outputPath } : {}), lifecycle: { asyncDir: lifecycleAsyncDir, resultPath: lifecycleResultPath, statusPath: path.join(lifecycleAsyncDir, "status.json"), eventsPath: path.join(lifecycleAsyncDir, "events.jsonl"), processTerminalPath: processTerminalPath(lifecycleAsyncDir), processTerminalCandidatePath: processTerminalCandidatePath(lifecycleAsyncDir), }, }, protocol: { lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, packageVersion: packageVersion(), }, diagnostics, launchContractDigest: launchBindingDigest({ task: input.task ?? "", definitionDigest, ...(model ? { model } : {}), modelCandidates, ...(resolveEffectiveThinking(model, effectiveThinkingConfig) ? { thinking: resolveEffectiveThinking(model, effectiveThinkingConfig) } : {}), systemPrompt: effectiveSystemPrompt, systemPromptMode: agent.systemPromptMode, inheritProjectContext: agent.inheritProjectContext, inheritSkills: agent.inheritSkills, skills: requestedSkills, tools: toolPlan.effectiveToolAllowlist, extensions: toolPlan.extensionArgs, mcpDirectTools: toolPlan.effectiveMcpTools, ...(outputPath ? { outputPath } : {}), outputMode: input.outputMode ?? "inline", ...(input.outputSchema ? { structuredOutputSchema: input.outputSchema } : {}), }), }; return { ok: true, contract: { ...contractBase, digest: digestContract(contractBase) } }; }