import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { Text } from "@mariozechner/pi-tui"; import { buildExpandedParams, ensurePiMarkdownLoaded, kylinFrameForCall, kylinFrameForResult, renderMarkdownLines } from "../../vera-theme/src/public"; import { type BackgroundState, cancelBackgroundJobs, createJobId, createQueuedJob, getBackgroundStatusDetails, startBackgroundJob, } from "./background"; import type { SubagentRegistry } from "./definitions"; import { inspectRun } from "./inspect"; import { resolveSubagentCwd, resumeSubagentTask, runSubagentTask } from "./session"; import { buildSubagentSystemReport, collectResumableSubagentRuns, collectSubagentSystemStatus } from "./status"; import type { BackgroundJobSnapshot, SubagentCancelDetails, SubagentNotificationDetails, SubagentRunDetails, SubagentStatusDetails, SubagentTimelineItem, SubagentUsage, } from "./types"; /** * Snapshot of one in-flight foreground subagent call. Background subagents * already live in `state.background.jobs`; this Map is the parallel store * for synchronous (non-background) calls so the cards widget can show both * kinds in one view. Entries linger for `FOREGROUND_RETAIN_MS` after the * call completes so users can briefly see the final state, then drop. */ export interface ForegroundRun { toolCallId: string; agentName: string; task: string; startedAt: number; completedAt?: number; details?: SubagentRunDetails; } export const FOREGROUND_RETAIN_MS = 30_000; export interface SubagentRuntimeState { registry: SubagentRegistry; background: BackgroundState; sessionCtx?: any; foregroundRuns: Map; /** Fired whenever foregroundRuns changes (start / update / complete / drop). */ foregroundOnChange?: () => void; /** Re-runs subagent discovery for the given cwd and assigns the result to state.registry. */ refresh?: (cwd: string) => void; } const SubagentParams = Type.Object({ agent: Type.Optional(Type.String({ description: "Optional named subagent loaded from YAML definitions in ~/.pi/agent/subagents or .pi/subagents." })), task: Type.Optional(Type.String({ description: "Task to delegate to the isolated subagent session." })), cwd: Type.Optional(Type.String({ description: "Optional working directory for the subagent. Relative paths are resolved from the current project cwd." })), systemPrompt: Type.Optional(Type.String({ description: "Optional extra system instructions appended to the selected subagent system prompt." })), background: Type.Optional(Type.Boolean({ description: "If true, queue the subagent as a background job and notify the main agent when it completes. Default false." })), model: Type.Optional(Type.String({ description: "Optional model override in 'provider/id' form (e.g. 'cpa/deepseek-v4-pro'). When set, supersedes the YAML definition's model and the inherited session model." })), thinkingLevel: Type.Optional(Type.String({ description: "Optional thinking-effort override. One of: off, minimal, low, medium, high, xhigh. When set, supersedes the YAML definition's effort and the inherited session level." })), resumeRunId: Type.Optional(Type.String({ description: "Resume an interrupted subagent run by its prior runId. Mutually exclusive with task/agent/model/thinkingLevel/systemPrompt/cwd." })), resumeArtifactsDir: Type.Optional(Type.String({ description: "Optional artifacts directory override when the default state/subagents// path does not apply." })), }); const SubagentStatusParams = Type.Object({ limit: Type.Optional(Type.Number({ minimum: 1, maximum: 50, description: "Maximum jobs to return. Default 10." })), }); const SubagentInspectParams = Type.Object({ runId: Type.String({ description: "Persisted subagent run id, e.g. subagent_1717945200000_001." }), artifactsDir: Type.Optional(Type.String({ description: "Optional override when the default state/subagents// path does not apply." })), }); const SubagentCancelParams = Type.Object({ jobId: Type.Optional(Type.String({ description: "Specific background job id to cancel." })), all: Type.Optional(Type.Boolean({ description: "If true, cancel all running or queued background jobs." })), }); function clip(text: string, max = 140): string { const normalized = String(text ?? "").trim(); if (!normalized) return ""; return normalized.length > max ? `${normalized.slice(0, max - 3)}...` : normalized; } export function formatResumeErrorResult(error: any): { content: Array<{ type: "text"; text: string }>; isError: true } | undefined { if (error?.name !== "ResumeError" || typeof error.reason !== "string") return undefined; const lines = [ `Resume failed: ${error.reason}`, `Detail: ${error.detail ?? error.message}`, `Suggested action: ${error.suggestedAction ?? "(none)"}`, ]; if (error.context?.runId) lines.push(`runId: ${error.context.runId}`); if (error.context?.artifactsDir) lines.push(`artifactsDir: ${error.context.artifactsDir}`); if (error.context?.sessionFile) lines.push(`sessionFile: ${error.context.sessionFile}`); return { content: [{ type: "text", text: lines.join("\n") }], isError: true, }; } function formatMs(ms?: number): string { if (!ms || ms < 0) return "0ms"; if (ms < 1000) return `${ms}ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; return `${(ms / 60_000).toFixed(1)}m`; } function formatCount(count: number): string { if (count < 1000) return String(count); if (count < 10_000) return `${(count / 1000).toFixed(1)}k`; if (count < 1_000_000) return `${Math.round(count / 1000)}k`; return `${(count / 1_000_000).toFixed(1)}M`; } export function formatUsage(usage: SubagentUsage, model?: string, durationMs?: number): string { const parts: string[] = []; if (usage.turns > 0) parts.push(`${usage.turns} turn${usage.turns === 1 ? "" : "s"}`); if (usage.input > 0) parts.push(`↑${formatCount(usage.input)}`); if (usage.output > 0) parts.push(`↓${formatCount(usage.output)}`); if (usage.cacheRead > 0) parts.push(`R${formatCount(usage.cacheRead)}`); if (usage.cacheWrite > 0) parts.push(`W${formatCount(usage.cacheWrite)}`); if (usage.cost > 0) parts.push(`$${usage.cost.toFixed(4)}`); if (durationMs && durationMs > 0) parts.push(formatMs(durationMs)); if (model) parts.push(model); return parts.join(" "); } export function iconFor(details: SubagentRunDetails, isPartial: boolean, theme: any): string { if (isPartial || details.phase === "running") return theme.fg("warning", "⏳"); if (details.phase === "error") return theme.fg("error", "✗"); return theme.fg("success", "✓"); } function statusIcon(status: BackgroundJobSnapshot["status"], theme: any): string { switch (status) { case "queued": return theme.fg("warning", "⌛"); case "running": return theme.fg("warning", "⏳"); case "done": return theme.fg("success", "✓"); case "aborted": return theme.fg("warning", "◌"); default: return theme.fg("error", "✗"); } } export function renderTimelineItem(item: SubagentTimelineItem, theme: any): string { const text = String(item.text ?? ""); if (item.kind === "assistant") { return theme.fg("muted", "» ") + theme.fg("toolOutput", clip(text, 220)); } if (item.kind === "error") { return theme.fg("error", "✗ ") + theme.fg("error", text); } if (item.kind === "tool") { const firstSpace = text.indexOf(" "); const name = firstSpace >= 0 ? text.slice(0, firstSpace) : text; const rest = firstSpace >= 0 ? text.slice(firstSpace) : ""; if (item.phase === "start") { return theme.fg("warning", "→ ") + theme.fg("accent", name) + theme.fg("dim", rest); } if (item.isError) { return theme.fg("error", "✗ ") + theme.fg("accent", name) + theme.fg("error", rest); } return theme.fg("success", "✓ ") + theme.fg("accent", name) + theme.fg("muted", rest); } return theme.fg("dim", "· ") + theme.fg("muted", text); } export function agentLabel(details: SubagentRunDetails): string { if (details.agent?.name) return details.agent.name; return "generic"; } function buildSubagentParamSummary(args: any, theme: any): string { if (args?.resumeRunId) { return theme.fg("text", "resume") + theme.fg("borderMuted", " · ") + theme.fg("dim", clip(String(args.resumeRunId), 40)); } const agent = String(args?.agent ?? "worker").trim() || "worker"; // Title shows only the first non-empty line of the task. Multi-line tasks // (markdown headers, numbered steps) would leak embedded \n into the // content row and break the frame's right-side `│` placement; tasks that // begin with blank lines should still surface their first real content // instead of falling through to "(no task)". const firstLine = String(args?.task ?? "").split(/\r?\n/).find((line) => line.trim()) ?? ""; const task = clip(firstLine, 40) || "(no task)"; const model = String(args?.model ?? "").trim(); const effort = String(args?.thinkingLevel ?? "").trim(); const overrideParts = [ model ? `model: ${clip(model, 42)}` : "", effort ? `effort: ${clip(effort, 24)}` : "", ].filter(Boolean); const mode = args?.background ? theme.fg("muted", "background") + theme.fg("borderMuted", " · ") : ""; const parts = [agent, ...overrideParts, task]; return mode + parts.map((part, index) => theme.fg(index === 0 ? "text" : "dim", part)).join(theme.fg("borderMuted", " · ")); } function toolUseCount(details: SubagentRunDetails): number { return details.timeline.filter((item) => item.kind === "tool" && item.phase === "start").length; } function subagentResultSummary(details: SubagentRunDetails, theme: any): string { if (details.phase === "running") return theme.fg("warning", details.background ? "queued" : "running"); const duration = formatMs(details.durationMs); const tools = toolUseCount(details); const parts = [duration, `${tools} tool${tools === 1 ? "" : "s"}`]; const usage = formatUsage(details.usage, details.model); if (usage) parts.push(usage); return theme.fg(details.phase === "error" ? "error" : "success", parts.join(" · ")); } /** * Body shown when the subagent frame in the chat stream is expanded * (Ctrl+O). Kept deliberately minimal: only the final report from the * child agent. Full timeline / session / usage detail lives in the cards * widget's overlay (Ctrl+Alt+1–9), which is the proper place for runtime * inspection. The chat-stream frame is a record of "what was returned". */ function buildSubagentExpandedBody(details: SubagentRunDetails, theme: any, _isPartial: boolean): string[] { const lines: string[] = []; if (details.phase === "error" && details.error) { for (const line of details.error.split(/\r?\n/)) lines.push(theme.fg("error", line)); return lines; } const output = (details.finalText || details.liveText || "").trim(); if (!output) { lines.push(theme.fg("dim", details.phase === "running" ? "(running, no output yet)" : "(no output)")); return lines; } return renderMarkdownLines(output, theme, 100); } function buildStatusExpandedBody(details: SubagentStatusDetails, theme: any, limit?: number): string[] { const lines: string[] = []; const jobs = typeof limit === "number" ? details.jobs.slice(0, limit) : details.jobs; for (const job of jobs) { const icon = statusIcon(job.status, theme); const label = job.details.agent?.name ?? "generic"; const summary = job.status === "done" ? job.details.finalText || job.details.lastEvent || "(done)" : job.details.error || job.details.liveText || job.details.lastEvent || `(${job.status})`; lines.push(`${icon} ${theme.fg("accent", job.jobId)} ${theme.fg("muted", `[${label}]`)} ${theme.fg("text", clip(summary, 140))}`); } if (details.jobs.length === 0) lines.push(theme.fg("dim", "(no background jobs)")); return lines; } function formatStatusText(details: SubagentStatusDetails, limit: number): string { const lines = [ `queued: ${details.queued}`, `running: ${details.running}`, `finished: ${details.finished}`, `resumable: ${details.resumable?.length ?? 0} runs`, "", ]; for (const run of details.resumable ?? []) { const staleSuffix = run.isStale ? " [stale]" : ""; lines.push(`[resumable] ${run.runId} | ${run.phase} | ${run.agent ?? "generic"} | ${run.startedAt ?? ""}${staleSuffix}`); } if ((details.resumable?.length ?? 0) > 0) lines.push(""); const jobs = details.jobs.slice(0, limit); for (const job of jobs) { const label = job.details.agent?.name ?? "generic"; const summary = job.status === "done" ? job.details.finalText || "(no output)" : job.details.error || job.details.liveText || job.details.lastEvent || `(${job.status})`; lines.push(`[${job.status}] ${job.jobId} (${label})`); lines.push(` task: ${clip(job.details.task, 180)}`); lines.push(` summary: ${clip(summary, 220)}`); lines.push(""); } return lines.join("\n").trim(); } function formatCancelText(details: SubagentCancelDetails): string { const lines: string[] = []; if (details.canceled.length > 0) { lines.push(`canceled: ${details.canceled.length}`); for (const job of details.canceled) { const label = job.details.agent?.name ?? "generic"; lines.push(`- ${job.jobId} (${label})`); } } if (details.alreadyFinished.length > 0) { if (lines.length > 0) lines.push(""); lines.push(`already finished: ${details.alreadyFinished.length}`); for (const job of details.alreadyFinished.slice(0, 10)) { const label = job.details.agent?.name ?? "generic"; lines.push(`- ${job.jobId} (${label}) [${job.status}]`); } } if (details.notFound.length > 0) { if (lines.length > 0) lines.push(""); lines.push(`not found: ${details.notFound.join(", ")}`); } return lines.join("\n") || "No background jobs were canceled."; } function notifyCommand(ctx: any, message: string, level: "info" | "warning" | "error" = "info"): void { if (ctx?.hasUI) { ctx.ui.notify(message, level); return; } if (level === "error") console.error(message); else console.log(message); } function showSubagentCommandReport(ctx: any, actionLines?: string[]): void { const status = collectSubagentSystemStatus(ctx.cwd); const report = buildSubagentSystemReport({ status, colorize: Boolean(ctx?.hasUI), actionLines, }); const level = status.summary.overallStatus === "degraded" ? "error" : status.summary.overallStatus === "partial" ? "warning" : "info"; notifyCommand(ctx, report, level); } function registerSubagentStatusTool(pi: ExtensionAPI, state: SubagentRuntimeState): void { pi.registerTool({ name: "subagent_status", label: "Subagent Status", description: "Inspect running and recently completed background subagent jobs.", promptSnippet: "Use subagent_status to inspect background subagent jobs and their latest state.", parameters: SubagentStatusParams, renderShell: "self" as const, renderCall(_args, theme, ctx) { const renderState = ctx.state as { startedAt?: number }; if (renderState.startedAt === undefined) renderState.startedAt = Date.now(); return kylinFrameForCall(ctx, { name: "subagent_status", theme, status: "pending", paramSummary: theme.fg("dim", "status"), startedAt: renderState.startedAt, }); }, renderResult(result, { expanded, isPartial }, theme, context) { const renderState = context.state as { startedAt?: number }; const startedAt = renderState.startedAt; const paramSummary = theme.fg("dim", "status"); const expandedParams = expanded ? buildExpandedParams([ { label: "limit", value: context.args?.limit }, ], theme) : undefined; if (isPartial) return kylinFrameForResult(context, { name: "subagent_status", theme, status: "pending", paramSummary, expanded, expandedParams, startedAt }); const details = result.details as SubagentStatusDetails | undefined; if (!details || result.isError) return kylinFrameForResult(context, { name: "subagent_status", theme, status: "error", paramSummary, errorMessage: "subagent status unavailable", expanded, expandedParams, startedAt }); const body = buildStatusExpandedBody(details, theme); return kylinFrameForResult(context, { name: "subagent_status", theme, status: "success", paramSummary, resultSummary: theme.fg("dim", `${details.jobs.length} jobs · ${details.queued} queued · ${details.running} running · ${details.finished} finished`), expanded, expandedParams, expandedBody: expanded ? body : undefined, expandedTotalLines: body.length, startedAt, }); }, async execute(_toolCallId, params, _signal, _onUpdate, _ctx) { const details = getBackgroundStatusDetails(state.background); details.resumable = collectResumableSubagentRuns(details.jobs); const limit = Number.isFinite(params.limit) ? Number(params.limit) : 10; return { content: [{ type: "text" as const, text: formatStatusText(details, limit) }], details, }; }, }); } function registerSubagentInspectTool(pi: ExtensionAPI): void { pi.registerTool({ name: "subagent_inspect", label: "Subagent Inspect", description: "Inspect a persisted subagent run by runId. Returns metadata, the most recent timeline events, salvaged or final text excerpt, and a structured assessment of whether the run can be resumed.", promptSnippet: "Use subagent_inspect to inspect a persisted subagent run before deciding whether to resume it.", parameters: SubagentInspectParams, renderShell: "self" as const, renderCall(args, theme, ctx) { const renderState = ctx.state as { startedAt?: number }; if (renderState.startedAt === undefined) renderState.startedAt = Date.now(); const runId = String(args?.runId ?? "").trim() || "(missing runId)"; return kylinFrameForCall(ctx, { name: "subagent_inspect", theme, status: "pending", paramSummary: theme.fg("text", clip(runId, 60)), startedAt: renderState.startedAt, }); }, renderResult(result, { expanded, isPartial }, theme, context) { const renderState = context.state as { startedAt?: number }; const startedAt = renderState.startedAt; const runId = String(context.args?.runId ?? "").trim() || "(missing runId)"; const paramSummary = theme.fg("text", clip(runId, 60)); const expandedParams = expanded ? buildExpandedParams([ { label: "run_id", value: context.args?.runId }, { label: "artifacts_dir", value: context.args?.artifactsDir }, ], theme) : undefined; if (isPartial) return kylinFrameForResult(context, { name: "subagent_inspect", theme, status: "pending", paramSummary, expanded, expandedParams, startedAt }); if (result.isError) return kylinFrameForResult(context, { name: "subagent_inspect", theme, status: "error", paramSummary, errorMessage: "subagent inspect failed", expanded, expandedParams, startedAt }); const details = result.details as { phase?: string; isStale?: boolean; resumable?: boolean } | undefined; const summaryParts = [ details?.phase ?? "unknown", details?.isStale ? "stale" : "fresh", details?.resumable ? "resumable" : "not resumable", ]; return kylinFrameForResult(context, { name: "subagent_inspect", theme, status: "success", paramSummary, resultSummary: theme.fg(details?.isStale ? "warning" : "dim", summaryParts.join(" · ")), expanded, expandedParams, startedAt, }); }, async execute(_toolCallId, params, _signal, _onUpdate, _ctx) { try { const report = inspectRun(String(params.runId), params.artifactsDir); return { content: [{ type: "text" as const, text: report.rendered }], details: { runId: report.runId, artifactsDir: report.artifactsDir, phase: report.phase, isStale: report.isStale, resumable: report.resumable, resumeBlockReason: report.resumeBlockReason, warnings: report.warnings, }, }; } catch (error: any) { if (error?.name === "ResumeError" && typeof error.reason === "string") { const lines = [ `Inspect failed: ${error.reason}`, `Detail: ${error.detail ?? error.message}`, `Suggested action: ${error.suggestedAction ?? "(none)"}`, ]; if (error.context?.runId) lines.push(`runId: ${error.context.runId}`); if (error.context?.artifactsDir) lines.push(`artifactsDir: ${error.context.artifactsDir}`); return { content: [{ type: "text" as const, text: lines.join("\n") }], isError: true, }; } return { content: [{ type: "text" as const, text: `subagent_inspect failed: ${error?.message ?? String(error)}` }], isError: true, }; } }, }); } function registerSubagentCancelTool(pi: ExtensionAPI, state: SubagentRuntimeState): void { pi.registerTool({ name: "subagent_cancel", label: "Subagent Cancel", description: "Cancel one running background subagent job or all running background jobs.", promptSnippet: "Use subagent_cancel to stop a queued or running background subagent job when it is no longer wanted.", parameters: SubagentCancelParams, renderShell: "self" as const, renderCall(args, theme, ctx) { const renderState = ctx.state as { startedAt?: number }; if (renderState.startedAt === undefined) renderState.startedAt = Date.now(); const target = args.all ? "all" : args.jobId ? clip(String(args.jobId), 40) : "(missing target)"; return kylinFrameForCall(ctx, { name: "subagent_cancel", theme, status: "pending", paramSummary: theme.fg("text", target), startedAt: renderState.startedAt, }); }, renderResult(result, { expanded, isPartial }, theme, context) { const renderState = context.state as { startedAt?: number }; const startedAt = renderState.startedAt; const target = context.args.all ? "all" : context.args.jobId ? clip(String(context.args.jobId), 40) : "(missing target)"; const paramSummary = theme.fg("text", target); const expandedParams = expanded ? buildExpandedParams([ { label: "job_id", value: context.args?.jobId }, { label: "all", value: context.args?.all }, ], theme) : undefined; if (isPartial) return kylinFrameForResult(context, { name: "subagent_cancel", theme, status: "pending", paramSummary, expanded, expandedParams, startedAt }); const details = result.details as SubagentCancelDetails | undefined; if (result.isError) return kylinFrameForResult(context, { name: "subagent_cancel", theme, status: "error", paramSummary, errorMessage: "subagent cancel failed", expanded, expandedParams, startedAt }); const canceled = details?.canceled?.length ?? 0; const alreadyFinished = details?.alreadyFinished?.length ?? 0; const notFound = details?.notFound?.length ?? 0; const summary = canceled > 0 ? "cancelled" : "nothing to cancel"; const extras = [alreadyFinished ? `${alreadyFinished} finished` : "", notFound ? `${notFound} not found` : ""].filter(Boolean).join(" · "); return kylinFrameForResult(context, { name: "subagent_cancel", theme, status: "success", paramSummary, resultSummary: theme.fg(canceled > 0 ? "success" : "dim", summary) + (extras ? theme.fg("borderMuted", " · ") + theme.fg("dim", extras) : ""), expanded, expandedParams, startedAt, }); }, async execute(_toolCallId, params) { if (!params.all && !String(params.jobId ?? "").trim()) { const text = "Provide jobId or set all=true."; return { content: [{ type: "text" as const, text }], details: { canceled: [], alreadyFinished: [], notFound: [] } satisfies SubagentCancelDetails, }; } const details = cancelBackgroundJobs({ state: state.background, jobId: String(params.jobId ?? "").trim() || undefined, all: Boolean(params.all), }); return { content: [{ type: "text" as const, text: formatCancelText(details) }], details, }; }, }); } function registerNotificationRenderer(pi: ExtensionAPI): void { pi.registerMessageRenderer("vera-subagent-notify", (message, options, theme) => { const details = message.details; const result = details?.result; if (!details || !result) { return new Text(typeof message.content === "string" ? message.content : "Background subagent notification", 0, 0); } const label = result.agent?.name ?? "generic"; let text = `${statusIcon(details.status, theme)} ${theme.fg("toolTitle", theme.bold("background subagent"))}`; text += theme.fg("muted", ` [${label}] {${details.jobId}}`); const summary = details.status === "done" ? result.finalText || "(no output)" : result.error || "Subagent failed."; text += `\n${theme.fg(details.status === "done" ? "text" : "error", clip(summary, 220))}`; if (options.expanded) { text += `\n${theme.fg("dim", `task: ${clip(result.task, 220)}`)}`; const usage = formatUsage(result.usage, result.model, result.durationMs); if (usage) text += `\n${theme.fg("dim", usage)}`; } return new Text(text, 0, 0); }); } function registerSubagentCommand(pi: ExtensionAPI, state: SubagentRuntimeState): void { pi.registerCommand("subagents", { description: "Show detailed background subagent system status, or cancel jobs with /subagents cancel .", handler: async (args, ctx) => { const input = String(args ?? "").trim(); if (!input || input === "status" || input === "list" || input === "ls") { showSubagentCommandReport(ctx); return; } const [command, ...rest] = input.split(/\s+/); if (command !== "cancel") { notifyCommand(ctx, 'Usage: /subagents [status] | /subagents cancel ', "warning"); return; } const target = rest.join(" ").trim(); if (!target) { notifyCommand(ctx, 'Usage: /subagents cancel ', "warning"); return; } const details = cancelBackgroundJobs({ state: state.background, all: target === "all", jobId: target === "all" ? undefined : target, }); const actionLines = formatCancelText(details).split("\n").filter(Boolean); showSubagentCommandReport(ctx, actionLines); }, }); } export function registerSubagentTool(pi: ExtensionAPI, state: SubagentRuntimeState): void { void ensurePiMarkdownLoaded(); registerSubagentStatusTool(pi, state); registerSubagentInspectTool(pi); registerSubagentCancelTool(pi, state); registerNotificationRenderer(pi); registerSubagentCommand(pi, state); pi.registerTool({ name: "subagent", label: "Subagent", description: "Run one delegated task in an isolated in-process Pi SDK session. Optionally select a named YAML subagent, run in background, or override the child model/thinking effort for this call. Stores compact execution details for expandable TUI inspection.", promptSnippet: "Use subagent to delegate a bounded task to an isolated child context. Named YAML-defined subagents can specialize tools, skills, prompts, model, and effort; pass model or thinkingLevel only when this call needs an opt-in override beyond those defaults. Set background=true for async work.", promptGuidelines: [ "Use subagent for scoped delegated work where an isolated child context is preferable.", "Prefer a named YAML-defined subagent when its description matches the task.", "Use model or thinkingLevel only as explicit per-call overrides when the YAML default is not enough.", "Set background=true when the work should continue asynchronously while the main agent proceeds.", "Use subagent_status to inspect running and recently completed background jobs.", "Use subagent_inspect with a runId to inspect persisted state before deciding whether to resume an interrupted run.", "Use subagent_cancel when a queued or running background job is no longer wanted.", ], parameters: SubagentParams, renderShell: "self" as const, renderCall(args, theme, ctx) { const renderState = ctx.state as { startedAt?: number }; if (renderState.startedAt === undefined) renderState.startedAt = Date.now(); return kylinFrameForCall(ctx, { name: "subagent", theme, status: "pending", paramSummary: buildSubagentParamSummary(args, theme), startedAt: renderState.startedAt, }); }, renderResult(result, { expanded, isPartial }, theme, context) { const renderState = context.state as { startedAt?: number }; const startedAt = renderState.startedAt; const paramSummary = buildSubagentParamSummary(context.args, theme); const expandedParams = expanded ? buildExpandedParams([ { label: "agent", value: context.args?.agent }, { label: "task", value: context.args?.task, multiline: true }, { label: "model", value: context.args?.model }, { label: "thinking_level", value: context.args?.thinkingLevel }, { label: "cwd", value: context.args?.cwd }, { label: "background", value: context.args?.background }, { label: "system_prompt", value: context.args?.systemPrompt, multiline: true }, { label: "resume_run_id", value: context.args?.resumeRunId }, { label: "resume_artifacts_dir", value: context.args?.resumeArtifactsDir }, ], theme) : undefined; const details = result.details as SubagentRunDetails | undefined; if (isPartial) { // Add the "running" indicator to the title suffix (top border) rather // than the content row. Title-only changes mean Pi's differential // renderer rewrites just line 0 once when partial starts, then sees // an identical spec on every subsequent partial update — no cascade // of content-row repaints, no ghost rows. The "running" label stays // visible until the final result transitions the status chip to // `✓ duration`. return kylinFrameForResult(context, { name: "subagent", theme, status: "pending", titleSuffix: theme.fg("warning", "running"), paramSummary, startedAt, }); } if (!details) { const fallback = result.content?.find?.((item: any) => item?.type === "text")?.text ?? "(no output)"; return kylinFrameForResult(context, { name: "subagent", theme, status: result.isError ? "error" : "success", paramSummary, resultSummary: result.isError ? undefined : theme.fg("dim", clip(String(fallback), 80)), errorMessage: result.isError ? clip(String(fallback), 160) : undefined, expanded, expandedParams, startedAt, }); } const body = buildSubagentExpandedBody(details, theme, isPartial); const status = details.phase === "error" || result.isError ? "error" : "success"; return kylinFrameForResult(context, { name: "subagent", theme, status, paramSummary, resultSummary: status === "error" ? undefined : subagentResultSummary(details, theme), errorMessage: status === "error" ? details.error ?? "subagent failed" : undefined, expanded, expandedParams, expandedBody: expanded ? body : undefined, expandedTotalLines: body.length, durationMs: details.durationMs, startedAt, }); }, async execute(_toolCallId, params, signal, onUpdate, ctx) { if (params.resumeRunId) { const forbidden = ["task", "agent", "model", "thinkingLevel", "systemPrompt", "cwd"]; const violations = forbidden.filter((key) => params[key] !== undefined); if (violations.length > 0) { return { content: [{ type: "text" as const, text: `subagent resume: cannot combine resumeRunId with ${violations.join(", ")}` }], isError: true, }; } try { const result = await resumeSubagentTask({ ctx, runId: String(params.resumeRunId), artifactsDirOverride: params.resumeArtifactsDir, signal, onUpdate, }); return { content: [{ type: "text" as const, text: result.content }], details: result.details, isError: result.details.phase === "error" ? true : undefined, }; } catch (error: any) { return formatResumeErrorResult(error) ?? { content: [{ type: "text" as const, text: `subagent resume failed: ${error?.message ?? String(error)}` }], isError: true, }; } } if (!params.task || !String(params.task).trim()) { return { content: [{ type: "text" as const, text: "subagent: task is required when not resuming" }], isError: true, }; } // Refresh definitions on every dispatch to guarantee dispatch reliability // even when a Pi extension reload left the closure-captured registry empty. state.refresh?.(ctx.cwd); const agentName = String(params.agent ?? "").trim(); const definition = agentName ? state.registry.definitions.find((item) => item.name === agentName) : undefined; if (agentName && !definition) { const available = state.registry.definitions.map((item) => item.name).join(", ") || "(none)"; return { content: [{ type: "text" as const, text: `Unknown subagent '${agentName}'. Available subagents: ${available}.` }], details: { version: 1, phase: "error", task: String(params.task ?? ""), cwd: ctx.cwd, startedAt: Date.now(), endedAt: Date.now(), durationMs: 0, finalText: "", error: `Unknown subagent '${agentName}'.`, toolErrors: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 }, timeline: [{ kind: "error", text: `Unknown subagent '${agentName}'. Available subagents: ${available}.`, isError: true }], } satisfies SubagentRunDetails, isError: true, }; } if (params.background) { const cwd = resolveSubagentCwd(ctx.cwd, params.cwd); const jobId = createJobId(state.background); const task = String(params.task ?? ""); const job = createQueuedJob({ state: state.background, jobId, task, cwd, definition, modelOverride: params.model, effortOverride: params.thinkingLevel, }); startBackgroundJob({ pi, state: state.background, job, ctx, task, cwd: params.cwd, systemPrompt: params.systemPrompt, thinkingLevel: pi.getThinkingLevel(), definition, modelOverride: params.model, effortOverride: params.thinkingLevel, }); return { content: [{ type: "text" as const, text: `Queued background subagent job ${jobId}${definition?.name ? ` (${definition.name})` : ""}.` }], details: job.details, }; } // Track foreground run so the cards widget can show it alongside // background jobs. Lifecycle: register on entry, update on each // emitUpdate, mark completed on exit (a janitor in cards.ts drops // entries after FOREGROUND_RETAIN_MS). const foregroundKey = _toolCallId; const startedAt = Date.now(); const foregroundTask = String(params.task ?? ""); state.foregroundRuns.set(foregroundKey, { toolCallId: foregroundKey, agentName: definition?.name ?? "worker", task: foregroundTask, startedAt, }); state.foregroundOnChange?.(); const wrappedOnUpdate = onUpdate ? (partial: any) => { const entry = state.foregroundRuns.get(foregroundKey); if (entry) { entry.details = partial?.details as SubagentRunDetails | undefined; state.foregroundOnChange?.(); } onUpdate(partial); } : (partial: any) => { const entry = state.foregroundRuns.get(foregroundKey); if (entry) { entry.details = partial?.details as SubagentRunDetails | undefined; state.foregroundOnChange?.(); } }; try { const result = await runSubagentTask({ ctx, task: foregroundTask, cwd: params.cwd, systemPrompt: params.systemPrompt, thinkingLevel: pi.getThinkingLevel(), definition, modelOverride: params.model, effortOverride: params.thinkingLevel, signal, onUpdate: wrappedOnUpdate, }); const entry = state.foregroundRuns.get(foregroundKey); if (entry) { entry.details = result.details; entry.completedAt = Date.now(); state.foregroundOnChange?.(); } return { content: [{ type: "text" as const, text: result.content }], details: result.details, isError: result.details.phase === "error" ? true : undefined, }; } catch (err) { const entry = state.foregroundRuns.get(foregroundKey); if (entry) { entry.completedAt = Date.now(); state.foregroundOnChange?.(); } throw err; } }, }); }