/** * Jev-assisted setup for a public single-child launch. Opt-in: * `jevAdvisory.routes.subagent.enabled: true` in the agent settings.json. * * Jev only fills what the parent left open, and every answer can only narrow: * 1. Agent: chosen by function when `agent` is omitted or generic (default "delegate"), * from native, enabled agents the capability ceiling allows. * 2. Tools: Jev may drop tools from the agent's declared allowlist for this task; it never adds one. * 3. Model tier: simple | complex | reasoning, mapped through the user's * `jevAdvisory.routes.subagent.tiers` (provider/id values), only when the parent passed no `model`. * * Every failure (no credential, timeout, malformed or low-confidence answer) leaves the launch * exactly as requested. Launch validation, ceilings, and preflight remain authoritative. */ import * as fs from "node:fs"; import * as path from "node:path"; import type { AgentConfig } from "../../agents/agents.ts"; import { getAgentDir } from "../../shared/utils.ts"; import { isAgentAllowedByCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "./capability-ceiling.ts"; import { askJev, buildConversation, buildJevPayload, emitJevTelemetry, jevConnection, readJevAdvisoryConfig, type JevAbstainReason, type JevConnection, type JevQuestion, type JevRuntime, } from "../../../../jev/decisions.ts"; export { warnJevUnavailableOnce } from "../../../../jev/decisions.ts"; export const MODEL_TIERS = ["simple", "complex", "reasoning"] as const; export type ModelTier = (typeof MODEL_TIERS)[number]; const KEEP = "keep"; const DEFAULT_GENERIC_AGENTS = ["delegate"]; /** Never offered for removal: skills load through read, and the rest are supervision plumbing. */ const PROTECTED_TOOLS = new Set(["read", "contact_supervisor", "intercom", "structured_output", "subagent", "subagent_supervisor"]); // ponytail: tools past this cap are simply kept; raise it if agents declare long allowlists. const MAX_TOOL_QUESTIONS = 10; const DESCRIPTION_CHARS = 300; const TIER_CRITERIA: Record = { simple: "Mechanical or lookup work: find, list, summarize, rename, run a command and report.", complex: "Multi-step implementation or investigation across several files with ordinary judgment.", reasoning: "Hard judgment: subtle debugging, architecture or security review, tricky algorithms, ambiguous trade-offs.", }; export interface SubagentRoutingSettings { connection: JevConnection; maxBytes: number; contextChars: number; genericAgents: string[]; tiers: Partial>; } export interface SubagentRoutingInput { task: string; agent?: string; model?: string; agents: AgentConfig[]; ceiling?: ResolvedSubagentCapabilityCeiling; } export interface SubagentRoutingResult { /** The agent to launch: Jev's pick, or the requested one. */ agent?: string; /** The run's agent list, with the launched agent narrowed (tools) or re-modelled (tier). */ agents: AgentConfig[]; routedAgent?: string; tier?: ModelTier; droppedTools: string[]; /** First reason a question went unanswered, for telemetry and the missing-agent error. */ abstained?: JevAbstainReason; elapsedMs: number; questions: number; } type JevAsk = typeof askJev; /** The route's settings, or undefined while it is off. Never throws. */ export function readSubagentRoutingSettings(settingsPath = path.join(getAgentDir(), "settings.json")): SubagentRoutingSettings | undefined { const config = readJevAdvisoryConfig(settingsPath); const route = config.routes.subagent; if (!route.enabled) return undefined; let raw: Record = {}; try { const parsed = JSON.parse(fs.readFileSync(settingsPath, "utf-8")) as { jevAdvisory?: { routes?: { subagent?: Record } } }; raw = parsed?.jevAdvisory?.routes?.subagent ?? {}; } catch { // readJevAdvisoryConfig already defaulted; extra fields just stay unset. } const tiers: Partial> = {}; const rawTiers = raw.tiers && typeof raw.tiers === "object" ? (raw.tiers as Record) : {}; for (const tier of MODEL_TIERS) { const model = rawTiers[tier]; if (typeof model === "string" && model.trim()) tiers[tier] = model.trim(); } const generic = Array.isArray(raw.genericAgents) ? raw.genericAgents.filter((name): name is string => typeof name === "string" && name.trim() !== "") : DEFAULT_GENERIC_AGENTS; return { connection: jevConnection(config, route), maxBytes: route.payloadBytes, contextChars: route.contextChars, genericAgents: generic, tiers, }; } /** Agents Jev may pick: native, enabled, file-defined, and inside the capability ceiling. */ export function routableAgents(agents: readonly AgentConfig[], ceiling: ResolvedSubagentCapabilityCeiling | undefined): AgentConfig[] { return agents.filter( (agent) => agent.source !== "runtime" && agent.disabled !== true && agent.runner?.type !== "external-cli" && agent.runner?.type !== "external-job" && isAgentAllowedByCapabilityCeiling(agent.name, ceiling), ); } function droppableTools(agent: AgentConfig): string[] { const excluded = new Set(agent.excludeTools ?? []); return (agent.tools ?? []) .filter((tool) => !PROTECTED_TOOLS.has(tool) && !excluded.has(tool) && !/[/\\]|\.(?:ts|js)$/.test(tool)) .slice(0, MAX_TOOL_QUESTIONS); } function toolKey(index: number): string { return `tool_${index}`; } export async function routeSubagentLaunch( input: SubagentRoutingInput, ctx: JevRuntime, settings: SubagentRoutingSettings, ask: JevAsk = askJev, ): Promise { const result: SubagentRoutingResult = { agent: input.agent, agents: input.agents, droppedTools: [], elapsedMs: 0, questions: 0 }; const conversation = buildConversation(input.task.slice(0, settings.contextChars), [], { contextTurns: 1, contextChars: settings.contextChars }); const noteAbstain = (reason: JevAbstainReason | undefined) => { if (reason && !result.abstained) result.abstained = reason; }; // 1. Agent by function, only when the parent left it open. const generic = input.agent !== undefined && settings.genericAgents.includes(input.agent); if (input.agent === undefined || generic) { const candidates = routableAgents(input.agents, input.ceiling).filter((agent) => agent.name !== input.agent); if (candidates.length > 0) { const criteria: Record = {}; if (generic) criteria[KEEP] = `Keep the generic "${input.agent}" agent: no listed specialist fits this task better.`; for (const agent of candidates) criteria[agent.name] = agent.description.slice(0, DESCRIPTION_CHARS); const questions: Record = { agent: { question: "Which subagent's specialization best fits this delegated task?", focus: "Judge by what the task needs done (read-only review, research, investigation, implementation), not by wording that names an agent.", criteria, }, }; const decision = await ask(ctx, settings.connection, { payload: buildJevPayload(conversation, questions), maxBytes: settings.maxBytes, allowed: { agent: Object.keys(criteria) }, }); result.elapsedMs += decision.elapsedMs; result.questions += 1; const pick = decision.choices.agent?.choice; if (pick && pick !== KEEP) { result.agent = pick; result.routedAgent = pick; } noteAbstain(decision.failure ?? decision.rejected.agent); } } const config = result.agent === undefined ? undefined : input.agents.find((agent) => agent.name === result.agent); if (!config) return result; // 2 + 3. Tool narrowing and model tier, for the agent that will actually launch. const tools = droppableTools(config); const tierChoices = input.model === undefined ? MODEL_TIERS.filter((tier) => settings.tiers[tier] !== undefined) : []; const questions: Record = {}; const allowed: Record = {}; if (tierChoices.length > 1) { questions.tier = { question: "How demanding is this delegated task for the model that runs it?", criteria: Object.fromEntries(tierChoices.map((tier) => [tier, TIER_CRITERIA[tier]])), }; allowed.tier = [...tierChoices]; } tools.forEach((tool, index) => { questions[toolKey(index)] = { question: `Does this delegated task need the "${tool}" tool?`, focus: "Answer unneeded only when the task clearly cannot require it; when in doubt, it is needed.", criteria: { needed: `The task may need ${tool}.`, unneeded: `The task clearly never needs ${tool}.` }, }; allowed[toolKey(index)] = ["needed", "unneeded"]; }); if (Object.keys(questions).length === 0) return result; const decision = await ask(ctx, settings.connection, { payload: buildJevPayload(conversation, questions), maxBytes: settings.maxBytes, allowed, }); result.elapsedMs += decision.elapsedMs; result.questions += Object.keys(questions).length; noteAbstain(decision.failure); const tier = decision.choices.tier?.choice as ModelTier | undefined; const tierModel = tier ? settings.tiers[tier] : undefined; if (tierModel) result.tier = tier; // Dropping a tool needs a stated confidence: an unquantified "unneeded" keeps the tool. result.droppedTools = tools.filter((_tool, index) => { const choice = decision.choices[toolKey(index)]; return choice?.choice === "unneeded" && typeof choice.confidence === "number"; }); if (!tierModel && result.droppedTools.length === 0) return result; const { modelProvider: _modelProvider, modelSource: _modelSource, ...base } = config; const routed: AgentConfig = { ...(tierModel ? base : config), ...(tierModel ? { model: tierModel } : {}), ...(result.droppedTools.length > 0 ? { excludeTools: [...(config.excludeTools ?? []), ...result.droppedTools] } : {}), }; result.agents = input.agents.map((agent) => (agent === config ? routed : agent)); return result; } /** Content-free: shape and outcome only, never the task, descriptions, or answers. */ export function emitSubagentRoutingTelemetry(events: { emit(channel: string, data: unknown): void } | undefined, result: SubagentRoutingResult): void { if (result.questions === 0) return; const changed = Boolean(result.routedAgent || result.tier || result.droppedTools.length > 0); emitJevTelemetry(events, "decision", { route: "subagent", outcome: changed ? "jev" : "fallback", candidates: result.questions, elapsedMs: result.elapsedMs, ...(result.routedAgent ? { agent: result.routedAgent } : {}), ...(result.tier ? { tier: result.tier } : {}), dropped: result.droppedTools.length, ...(result.abstained ? { reason: result.abstained } : {}), }); } /** One line for the user and the parent: what Jev set up, or nothing when it changed nothing. */ export function describeSubagentRouting(result: SubagentRoutingResult, tiers: Partial>): string | undefined { const parts: string[] = []; if (result.routedAgent) parts.push(`agent ${result.routedAgent}`); if (result.tier) parts.push(`${result.tier} tier (${tiers[result.tier]})`); if (result.droppedTools.length > 0) parts.push(`without ${result.droppedTools.join(", ")}`); return parts.length > 0 ? `Jev set up this subagent: ${parts.join("; ")}.` : undefined; }