import { Minimatch } from "minimatch"; import type { DelegationMode, MinimalSubagentsToolsets, ToolSelection, } from "./minimal-subagents-types.js"; /** Lists Pi thinking levels in increasing effort order for schema validation and clamping. */ export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const; /** Defaults explicit fanout to root → child → grandchild when settings omit a depth. */ export const DEFAULT_MAX_SUBAGENT_DEPTH = 2; /** Lists the six coordinator tools excluded from ordinary child capabilities. */ export const COORDINATOR_TOOL_NAMES = [ "subagent", "agent_message", "subagent_wait", "subagent_status", "subagent_cancel", "subagent_delete", ] as const; /** Preserves the built-in presets when their settings are omitted. */ export const DEFAULT_TOOLSETS: MinimalSubagentsToolsets = { baseToolset: [], readToolset: ["read", "grep", "find", "ls"], modifyToolset: ["bash", "edit", "write"], }; interface ModelReference { provider: string; id: string; } interface ScopedModelReference { model: ModelReference; thinkingLevel?: string; } /** Build the authenticated runtime model enum from Pi's already-resolved model scope. */ export function buildEligibleModelIds(input: { availableModels: readonly ModelReference[]; scopedModels: readonly ScopedModelReference[]; scopeConfigured?: boolean; }): string[] { const scopeConfigured = input.scopeConfigured ?? input.scopedModels.length > 0; const source = scopeConfigured ? input.scopedModels.map((entry) => entry.model) : input.availableModels; return [...new Set(source.map(({ provider, id }) => `${provider}/${id}`))]; } /** Supplies inherited tools and the ancestor ceiling for exact tool resolution. */ export interface ToolResolutionContext { ordinaryTools: readonly string[]; capabilityCeiling: readonly string[]; toolsets?: MinimalSubagentsToolsets; } /** Expand configured presets within the ceiling while keeping explicit requests strict. */ export function resolveOrdinaryToolSelection( selection: ToolSelection | undefined, context: ToolResolutionContext, ) { const requested = selection === undefined ? context.ordinaryTools : Array.isArray(selection) ? selection : []; const uniqueRequested = [...new Set(requested)]; const coordinatorTools = new Set(COORDINATOR_TOOL_NAMES); const requestedCoordinatorTools = uniqueRequested.filter((name) => coordinatorTools.has(name)); if (requestedCoordinatorTools.length > 0) { throw new Error( `Minimal subagents ordinary tool selection: coordinator tools are injected separately and must not appear in tools: ${requestedCoordinatorTools.join(", ")}`, ); } // ponytail: availableTools and capabilityCeiling are identical at every production site, // so availability and the ancestor ceiling are enforced by a single membership check. const ceiling = new Set(context.capabilityCeiling); const exceeded = uniqueRequested.filter((name) => !ceiling.has(name)); if (exceeded.length > 0) { throw new Error(`Minimal subagents capability ceiling exceeded: ${exceeded.join(", ")}`); } const toolsets = context.toolsets ?? DEFAULT_TOOLSETS; const keys: (keyof MinimalSubagentsToolsets)[] = ["baseToolset"]; if (selection === "read" || selection === "modify") keys.push("readToolset"); if (selection === "modify") keys.push("modifyToolset"); const permitted = excludeCoordinatorTools(context.capabilityCeiling); const warnings: string[] = []; const configured = keys.flatMap((key) => toolsets[key].flatMap((pattern) => { const matcher = new Minimatch(pattern); const matches = permitted.filter((name) => matcher.match(name)); if (matches.length === 0) { warnings.push( `minimalSubagents.${key}: ${JSON.stringify(pattern)} matched no permitted ordinary tools (unavailable, outside the caller's capability ceiling, or Coordinator Tools); skipped`, ); } return matches; }), ); return { ordinaryTools: [...new Set([...configured, ...uniqueRequested])], requiredTools: uniqueRequested, warnings, }; } /** Return an agent's hierarchy depth where the interactive root is depth zero. */ export function getSubagentDepth(agentId: string): number { if (agentId === "root") return 0; const segments = agentId.split(".").length; return agentId.startsWith("root.") ? segments - 1 : segments; } /** Report whether an explicit fanout contract remains below the configured delegation depth cap. */ export function canAgentContractSpawn( agentId: string, delegation?: DelegationMode, maxSubagentDepth = DEFAULT_MAX_SUBAGENT_DEPTH, ): boolean { return delegation === "fanout" && getSubagentDepth(agentId) < maxSubagentDepth; } /** Return ordinary tools only, excluding all six coordinator tools. */ export function excludeCoordinatorTools(toolNames: readonly string[]): string[] { const coordinatorNames = new Set(COORDINATOR_TOOL_NAMES); return toolNames.filter((name) => !coordinatorNames.has(name)); }