import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { registerPromptSegment } from "../../vera-prompt-inspector/src/registry"; import { abortAllBackgroundJobs, createBackgroundState, formatBackgroundIndicator } from "./background"; import { type CardsMode, installSubagentCards } from "./cards"; import { discoverSubagents, filterVisibleSubagents } from "./definitions"; import { setLiveSubagentRuntimeState } from "./status"; import { cleanupOldRuns } from "./ttl"; import { registerSubagentTool, type SubagentRuntimeState } from "./tool"; function formatSubagentCatalog(state: SubagentRuntimeState): string { if (state.registry.definitions.length === 0) return ""; const lines = [ "## Available YAML-defined subagents", "Use the subagent tool with agent: \"name\" when one of these specialized child agents fits the task.", ]; for (const definition of state.registry.definitions) { const model = definition.model?.trim() ? definition.model : "inherit current"; const effort = definition.effort?.trim() ? definition.effort : "inherit current"; const tools = definition.tools && definition.tools.length > 0 ? definition.tools.join(", ") : "any built-in"; const extTools = definition.extensionTools && definition.extensionTools.length > 0 ? definition.extensionTools.join(", ") : "none"; const skills = definition.skills && definition.skills.length > 0 ? definition.skills.join(", ") : "any discovered"; lines.push(`- ${definition.name}: ${definition.description} (model: ${model}; effort: ${effort}; tools: ${tools}; extensionTools: ${extTools}; skills: ${skills})`); } return lines.join("\n"); } function refreshBackgroundStatus(state: SubagentRuntimeState): void { const ctx = state.sessionCtx; if (!ctx?.hasUI) return; ctx.ui.setStatus("subagents", formatBackgroundIndicator(state.background) ?? undefined); } function readCardsMode(pi: ExtensionAPI): CardsMode { // Config flag: VERA_SUBAGENT_CARDS = "all" | "background" | "off". Default "all". // Read via env var since vera-subagents has no settings.json schema yet; // upgrading to a real config layer is straightforward later. const raw = String(process.env.VERA_SUBAGENT_CARDS ?? "all").trim().toLowerCase(); if (raw === "off" || raw === "background" || raw === "all") return raw; return "all"; } export default function veraSubagents(pi: ExtensionAPI): void { const state: SubagentRuntimeState = { registry: { definitions: [], errors: [], projectDir: null }, background: createBackgroundState(), sessionCtx: undefined, foregroundRuns: new Map(), }; state.background.onChange = () => { refreshBackgroundStatus(state); }; // Do not publish the empty initial state to globalThis here. On a Pi // extension reload that would overwrite the previously populated state // with an empty one, leaving diagnostics blind until the next // before_agent_start refresh. The publish happens after refresh inside // session_start instead. const refresh = (cwd: string) => { state.registry = filterVisibleSubagents(discoverSubagents(cwd)); }; state.refresh = refresh; registerSubagentTool(pi, state); installSubagentCards(pi, state, { mode: readCardsMode(pi) }); pi.on("session_start", async (_event, ctx) => { state.sessionCtx = ctx; refresh(ctx.cwd); refreshBackgroundStatus(state); setLiveSubagentRuntimeState(state); void cleanupOldRuns().catch(() => undefined); if (ctx.hasUI && state.registry.errors.length > 0) { ctx.ui.notify(`subagents: ${state.registry.errors[0]}${state.registry.errors.length > 1 ? ` (+${state.registry.errors.length - 1} more)` : ""}`, "warning"); } }); pi.on("before_agent_start", async (_event, ctx) => { refresh(ctx.cwd); const catalog = formatSubagentCatalog(state); const agentCount = state.registry.definitions.length; registerPromptSegment({ id: "subagents", label: "subagent catalog", category: "catalog", kind: "cached-per-session", text: catalog, details: [{ label: "agents", value: String(agentCount) }], }); return undefined; }); pi.on("session_shutdown", async () => { abortAllBackgroundJobs(state.background); refreshBackgroundStatus(state); state.sessionCtx = undefined; setLiveSubagentRuntimeState(null); }); }