import { createHash } from "node:crypto"; import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import { type Api, clampThinkingLevel, type Model, type ToolResultMessage, } from "@earendil-works/pi-ai"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, DynamicBorder, type ExtensionAPI, type ExtensionContext, getMarkdownTheme, keyHint, type Theme, truncateHead, } from "@earendil-works/pi-coding-agent"; import type { AutocompleteItem } from "@earendil-works/pi-tui"; import { Box, Container, Loader, Markdown, matchesKey, Text, } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import type { ConsultationMetadata, ConsultationProgress, ConsultationRequest, ConsultationResult, RunConsultationOptions, } from "./advisor.ts"; import { closeDebug, dbg, span } from "./src/debug.ts"; import { resolveSetting, type SettingDeclaration, } from "./src/pi-ext-settings.ts"; interface AngelSettings { pairs?: Array<{ executor: string; advisor: string }>; enabled?: boolean; thinkingLevel?: ThinkingLevel; subagentExtensions?: "none" | "all" | string[]; } interface Binding { executor: Model; advisor: Model; } interface SubagentExtensionSelection { all: boolean; paths: string[]; } interface AdviceDisplay { version: 1; advice: string; metadata: ConsultationMetadata; } interface ToolDetails { kind: "progress" | "complete"; progress?: ConsultationProgress; result?: ConsultationResult; } interface StatusDisplay { version: 1; kind: "cancelled" | "failed"; message: string; } export interface AngelDependencies { runConsultation?: ( ctx: ExtensionContext, request: ConsultationRequest, options: RunConsultationOptions, ) => Promise; loadSettings?: (ctx: ExtensionContext) => AngelSettings; } const ADVICE_TYPE = "angel-advice"; const STATUS_TYPE = "angel-status"; const CHILD_MARKER = "angel-child"; const STATUS_ID = "angel"; const ANGEL_ICON = "󰧑"; const PREVIEW_MAX_CHARS = 1_200; const DEFAULT_THINKING_LEVEL: ThinkingLevel = "max"; const LOW_SIGNAL_BUILTINS = new Set([ "read", "edit", "write", "grep", "find", "ls", ]); const TOOL_DESCRIPTION = `Consult Angel for deep reasoning and independent investigation: difficult diagnosis, competing explanations, architectural trade-offs, contradictory evidence, or a second opinion before committing to an approach. Angel receives the focused assignment, loads project instructions, and can inspect the pinned parent session on demand alongside project and web evidence. Provide the question or decision you need resolved, including any competing hypotheses. Returns evidence-backed advice, not implementation or permission.`; const TOOL_GUIDELINES = [ "Call angel when independent investigation can materially improve the next decision, including difficult diagnosis, competing explanations, architectural trade-offs, contradictory evidence, or consequential uncertainty.", "Give angel a concrete question, uncertainty, or competing hypothesis; do not call it merely for generic reassurance.", "Do not copy the parent transcript into angel arguments; Angel can retrieve relevant entries from the pinned parent session on demand.", "Evaluate angel advice against primary evidence, user instructions, and observed results; Angel advises but does not override them.", ]; function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } function canonicalValue(value: unknown): unknown { if (Array.isArray(value)) return value.map(canonicalValue); if (!isPlainObject(value)) return value; return Object.fromEntries( Object.keys(value) .sort() .map((key) => [key, canonicalValue(value[key])]), ); } function operationKey( toolName: string, arguments_: Record, ): string | undefined { try { const encoded = JSON.stringify(canonicalValue(arguments_)); if (encoded === undefined) return undefined; return createHash("sha256") .update(toolName) .update("\0") .update(encoded) .digest("hex"); } catch { return undefined; } } function toolCallArguments( message: unknown, ): Map> { const calls = new Map>(); if (!isPlainObject(message) || message.role !== "assistant") return calls; if (!Array.isArray(message.content)) return calls; for (const part of message.content) { if (!isPlainObject(part) || part.type !== "toolCall") continue; if (typeof part.id !== "string" || !isPlainObject(part.arguments)) continue; calls.set(part.id, part.arguments); } return calls; } const THINKING_LEVELS = new Set([ "off", "minimal", "low", "medium", "high", "xhigh", "max", ]); function parseAngelSettings(raw: unknown): AngelSettings { if (!isPlainObject(raw)) throw new Error("expected an object"); const { pairs, enabled, thinkingLevel, subagentExtensions } = raw; if ( pairs !== undefined && !( Array.isArray(pairs) && pairs.every( (pair) => isPlainObject(pair) && typeof pair.executor === "string" && typeof pair.advisor === "string", ) ) ) throw new Error("pairs must be an array of { executor, advisor } strings"); if (enabled !== undefined && typeof enabled !== "boolean") throw new Error("enabled must be a boolean"); if (thinkingLevel !== undefined && !THINKING_LEVELS.has(thinkingLevel)) throw new Error( `thinkingLevel must be one of ${[...THINKING_LEVELS].join(", ")}`, ); if ( subagentExtensions !== undefined && subagentExtensions !== "all" && subagentExtensions !== "none" && !( Array.isArray(subagentExtensions) && subagentExtensions.every((name) => typeof name === "string") ) ) throw new Error( 'subagentExtensions must be "all", "none", or an extension-name array', ); return raw as AngelSettings; } // Precedence: trusted project deep-merged over user, then {}. const ANGEL_SETTING: SettingDeclaration = { key: "angel", parse: parseAngelSettings, default: {}, }; function resolveSubagentExtensions( setting: AngelSettings["subagentExtensions"], ): SubagentExtensionSelection { if (setting === undefined || setting === "none") return { all: false, paths: [] }; if (setting === "all") return { all: true, paths: [] }; const names = [ ...new Set(setting.map((name) => name.trim()).filter(Boolean)), ]; const paths = names.map((name) => { if (!/^[a-z0-9][a-z0-9-]*$/u.test(name)) throw new Error(`angel: invalid sub-agent extension name: ${name}`); const path = fileURLToPath(new URL(`../${name}/index.ts`, import.meta.url)); if (!existsSync(path)) throw new Error(`angel: unknown sub-agent extension: ${name}`); return path; }); return { all: false, paths }; } function modelKey(model: Model): string { return `${model.provider}/${model.id}`; } function modelMatches(spec: string, model: Model): boolean { return spec === model.id || spec === modelKey(model); } function resolveBinding( ctx: ExtensionContext, settings: AngelSettings, executor = ctx.model, ): Binding | undefined { if (!executor) return undefined; for (const pair of settings.pairs ?? []) { if (!modelMatches(pair.executor, executor)) continue; const separator = pair.advisor.indexOf("/"); const provider = separator < 0 ? executor.provider : pair.advisor.slice(0, separator); const id = separator < 0 ? pair.advisor : pair.advisor.slice(separator + 1); if (!provider || !id) return undefined; const advisor = ctx.modelRegistry.find(provider, id); return advisor ? { executor, advisor } : undefined; } return undefined; } function metadataValues(metadata: ConsultationMetadata) { return { seconds: (metadata.runtimeMs / 1_000).toFixed(1), cost: metadata.cost === undefined ? "cost unavailable" : `$${metadata.cost.toFixed(4)}`, }; } function compactTokenCount(total: number): string { if (total < 1_000) return total.toLocaleString("en-US"); return `${(total / 1_000).toFixed(1).replace(/\.0$/u, "")}k`; } function metadataFooterLines(metadata: ConsultationMetadata): string[] { const { seconds, cost } = metadataValues(metadata); const session = metadata.childSessionId.length > 14 ? `${metadata.childSessionId.slice(0, 13)}…` : metadata.childSessionId; return [ `${metadata.model}/${metadata.thinkingLevel} · ${compactTokenCount(metadata.tokens.total)} tokens · ${cost} · ${seconds}s`, `${metadata.origin} · session ${session}`, ]; } function metadataDetailLines(metadata: ConsultationMetadata): string[] { const { seconds, cost } = metadataValues(metadata); return [ `${metadata.origin} · ${metadata.model} · ${metadata.thinkingLevel}`, `${metadata.tokens.total.toLocaleString("en-US")} tokens · ${cost} · ${seconds}s`, `child ${metadata.childSessionId}`, ]; } function adviceComponent( data: AdviceDisplay, theme: Theme, padding: number, ): Container { const box = new Box(padding, 1, (text) => theme.bg("customMessageBg", text)); box.addChild(new Text(theme.fg("accent", theme.bold("Angel advice")), 0, 0)); box.addChild(new Markdown(data.advice, 0, 0, getMarkdownTheme())); box.addChild(new Text("", 0, 0)); box.addChild(new DynamicBorder((text) => theme.fg("dim", text))); box.addChild( new Text( theme.fg("dim", metadataFooterLines(data.metadata).join("\n")), 0, 0, ), ); const container = new Container(); container.addChild(box); return container; } function statusComponent( data: StatusDisplay, theme: Theme, padding: number, ): Container { const box = new Box(padding, 1, (text) => theme.bg("customMessageBg", text)); const label = data.kind === "failed" ? "Angel failed" : "Angel cancelled"; const color = data.kind === "failed" ? "error" : "muted"; box.addChild(new Text(theme.fg(color, theme.bold(label)), 0, 0)); if (data.message !== label) box.addChild(new Text(theme.fg("dim", `\n${data.message}`), 0, 0)); const container = new Container(); container.addChild(box); return container; } function boundedAdvice(result: ConsultationResult): string { const truncated = truncateHead(result.advice, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES, }); if (!truncated.truncated) return truncated.content; return `${truncated.content}\n\n[Angel advice truncated to ${truncated.outputLines}/${truncated.totalLines} lines for tool context. Full advice is preserved in child session ${result.metadata.childSessionId}.]`; } function textFromToolResult(result: ToolResultMessage): string { return result.content .filter( (part): part is { type: "text"; text: string } => part.type === "text" && typeof part.text === "string", ) .map((part) => part.text) .join("\n"); } function isExplicitCancellation(result: ToolResultMessage): boolean { if (!result.isError) return false; const details = result.details; if (isPlainObject(details)) { if (details.cancelled === true || details.aborted === true) return true; } const status = textFromToolResult(result) .trim() .toLowerCase() .split("\n") .at(-1) ?.trim(); return /^(?:(?:tool execution|command|operation) )?(?:cancelled|canceled|aborted)[.!]?$/.test( status ?? "", ); } function abortError(): DOMException { return new DOMException("Angel consultation cancelled", "AbortError"); } function isAbortError(error: unknown): boolean { return error instanceof Error && error.name === "AbortError"; } async function withAbort( operation: Promise, signal: AbortSignal, ): Promise { signal.throwIfAborted(); return new Promise((resolve, reject) => { let settled = false; const finish = (callback: () => void) => { if (settled) return; settled = true; signal.removeEventListener("abort", onAbort); callback(); }; const onAbort = () => finish(() => reject(abortError())); signal.addEventListener("abort", onAbort, { once: true }); operation.then( (value) => finish(() => resolve(value)), (error) => finish(() => reject(error)), ); }); } function completedBatchContext( results: ToolResultMessage[], repeatedFailures: ToolResultMessage[], ): string { const repeatedIds = new Set( repeatedFailures.map((result) => result.toolCallId), ); const references = results.map((result) => { const status = result.isError ? "error" : "success"; const repeated = repeatedIds.has(result.toolCallId) ? " · repeated" : ""; return `- ${result.toolName} · ${result.toolCallId} · ${status}${repeated}`; }); return [ "The completed executor tool batch is available in the pinned parent session.", "Use these references with parent_session to locate every sibling result without duplicating result content:", ...references, ].join("\n"); } function isAdviceDisplay(value: unknown): value is AdviceDisplay { if (!isPlainObject(value)) return false; return ( value.version === 1 && typeof value.advice === "string" && isPlainObject(value.metadata) && typeof value.metadata.childSessionId === "string" ); } function isStatusDisplay(value: unknown): value is StatusDisplay { return ( isPlainObject(value) && value.version === 1 && (value.kind === "cancelled" || value.kind === "failed") && typeof value.message === "string" ); } function isCompleteToolDetails(value: unknown): value is ToolDetails & { kind: "complete"; result: ConsultationResult; } { if (!isPlainObject(value) || value.kind !== "complete") return false; if (!isPlainObject(value.result)) return false; return ( typeof value.result.advice === "string" && isPlainObject(value.result.metadata) ); } function questionSummary(question: unknown): string { if (typeof question !== "string") return ""; const normalized = question.replaceAll(/\s+/g, " ").trim(); return normalized.length <= 72 ? normalized : `${normalized.slice(0, 69)}...`; } // Shared so concurrent consultations evaluate ./advisor.ts once. // Pi's jiti loader (module cache disabled) can otherwise hand one caller a // partially initialized module whose imports are still undefined. let advisorModule: Promise | undefined; const loadAdvisor = (): Promise => { advisorModule ??= import("./advisor.ts"); return advisorModule; }; async function realRunConsultation( ctx: ExtensionContext, request: ConsultationRequest, options: RunConsultationOptions, ): Promise { const implementation = await loadAdvisor(); return implementation.runConsultation(ctx, request, options); } function isChildSession(ctx: ExtensionContext): boolean { return ctx.sessionManager .getBranch() .some( (entry) => entry.type === "custom" && entry.customType === CHILD_MARKER, ); } export function createAngelExtension(dependencies: AngelDependencies = {}) { const consult = dependencies.runConsultation ?? realRunConsultation; return function angelExtension(pi: ExtensionAPI) { const readSettings = dependencies.loadSettings ?? ((ctx: ExtensionContext): AngelSettings => { const setting = resolveSetting(pi, ctx, ANGEL_SETTING); if (setting.ok) return setting.value; if (ctx.hasUI) ctx.ui.notify(`${setting.error}; Angel unconfigured`, "warning"); return {}; }); let settings: AngelSettings = {}; let sessionEnabled = true; let childSession = false; let active = true; let sessionGeneration = 0; let consultationSequence = 0; let completedTurnSequence = 0; let taskGeneration = 0; let cancellationGeneration = 0; let activeTurnIndex: number | undefined; const consultations = new Set(); const operationKeysByCall = new Map(); const pendingFailures = new Map(); const consultedFailures = new Set(); const taskGenerationByTurn = new Map(); const enabled = () => active && !childSession && sessionEnabled; const clearFailureTracking = () => { operationKeysByCall.clear(); pendingFailures.clear(); consultedFailures.clear(); }; const invalidateTask = () => { taskGeneration += 1; clearFailureTracking(); }; const cancelConsultations = () => { cancellationGeneration += 1; for (const controller of consultations) controller.abort(); }; const tracksFailure = (toolName: string) => { if (toolName === "angel") return false; const tool = pi .getAllTools() .find((candidate) => candidate.name === toolName); return !( tool?.sourceInfo.source === "builtin" && LOW_SIGNAL_BUILTINS.has(toolName) ); }; function isAvailable(ctx: ExtensionContext, executor = ctx.model): boolean { const binding = resolveBinding(ctx, settings, executor); return ( enabled() && !!binding && ctx.modelRegistry.hasConfiguredAuth(binding.advisor) ); } function clearAvailabilityStatus(ctx?: ExtensionContext): void { pi.events.emit("model-route:angel", { advisor: undefined }); if (ctx?.hasUI) ctx.ui.setStatus(STATUS_ID, undefined); } function syncAvailability( ctx: ExtensionContext, executor = ctx.model, ): void { const binding = resolveBinding(ctx, settings, executor); const available = enabled() && !!binding && ctx.modelRegistry.hasConfiguredAuth(binding.advisor); const tools = pi.getActiveTools(); const present = tools.includes("angel"); if (available && !present) pi.setActiveTools([...tools, "angel"]); if (!available && present) pi.setActiveTools(tools.filter((tool) => tool !== "angel")); if (!available || !binding) { clearAvailabilityStatus(ctx); return; } const thinkingLevel = clampThinkingLevel( binding.advisor, settings.thinkingLevel ?? DEFAULT_THINKING_LEVEL, ); if (ctx.hasUI) ctx.ui.setStatus( STATUS_ID, `${ANGEL_ICON} angel: ${binding.advisor.id}:${thinkingLevel}`, ); pi.events.emit("model-route:angel", { advisor: { modelId: binding.advisor.id, thinkingLevel }, }); } async function run( ctx: ExtensionContext, request: ConsultationRequest, options: { signal?: AbortSignal; onProgress?: (progress: ConsultationProgress) => void; } = {}, ): Promise { const generation = sessionGeneration; if (!enabled()) { dbg?.("consultation.skip", { reason: "disabled" }); throw new Error("Angel is disabled in this session"); } const binding = resolveBinding(ctx, settings); if (!binding) { dbg?.("consultation.skip", { reason: "unconfigured" }); throw new Error( `Angel has no advisor configured for ${ctx.model ? modelKey(ctx.model) : "the current model"}`, ); } const controller = new AbortController(); const finish = span?.("consultation", { kind: request.origin }); consultations.add(controller); const abort = () => controller.abort(options.signal?.reason); if (options.signal?.aborted) abort(); else options.signal?.addEventListener("abort", abort, { once: true }); try { const auth = await withAbort( ctx.modelRegistry.getApiKeyAndHeaders(binding.advisor), controller.signal, ); if (!auth.ok) throw new Error(`Angel advisor authentication failed: ${auth.error}`); if ( !active || generation !== sessionGeneration || controller.signal.aborted ) throw abortError(); const extensions = resolveSubagentExtensions( settings.subagentExtensions, ); const allTools = pi.getAllTools(); const additionalExtensionPaths = extensions.all ? [ ...new Set([ ...allTools .filter( (tool) => tool.sourceInfo.scope === "temporary" && !tool.sourceInfo.path.startsWith("<"), ) .map((tool) => tool.sourceInfo.path), ...pi .getCommands() .filter( (command) => command.source === "extension" && command.sourceInfo.scope === "temporary" && !command.sourceInfo.path.startsWith("<"), ) .map((command) => command.sourceInfo.path), ]), ] : extensions.paths; const result = await consult(ctx, request, { advisor: binding.advisor, thinkingLevel: settings.thinkingLevel ?? DEFAULT_THINKING_LEVEL, signal: controller.signal, loadExtensions: extensions.all, additionalExtensionPaths, runtimeApiKey: ctx.modelRegistry.isUsingOAuth(binding.advisor) ? undefined : auth.apiKey, onProgress: (progress) => { if ( active && generation === sessionGeneration && !controller.signal.aborted ) options.onProgress?.(progress); }, }); if ( !active || generation !== sessionGeneration || controller.signal.aborted ) throw abortError(); finish?.("finish", { status: "success" }); return result; } catch (error) { finish?.("error", { status: isAbortError(error) ? "cancelled" : "failed", }); throw error; } finally { options.signal?.removeEventListener("abort", abort); consultations.delete(controller); } } pi.registerMessageRenderer( ADVICE_TYPE, (message, { outputPad }, theme) => { if (!isAdviceDisplay(message.details)) return undefined; return adviceComponent(message.details, theme, outputPad); }, ); pi.registerEntryRenderer( ADVICE_TYPE, (entry, _options, theme) => { if (!isAdviceDisplay(entry.data)) return undefined; return adviceComponent(entry.data, theme, 1); }, ); pi.registerMessageRenderer( STATUS_TYPE, (message, { outputPad }, theme) => { if (!isStatusDisplay(message.details)) return undefined; return statusComponent(message.details, theme, outputPad); }, ); pi.on("session_start", (_event, ctx) => { sessionGeneration += 1; taskGeneration += 1; active = true; clearFailureTracking(); taskGenerationByTurn.clear(); activeTurnIndex = undefined; settings = readSettings(ctx); sessionEnabled = settings.enabled !== false; childSession = isChildSession(ctx); dbg?.("session.start"); syncAvailability(ctx); }); pi.on("session_shutdown", (_event, ctx) => { clearAvailabilityStatus(ctx); sessionGeneration += 1; taskGeneration += 1; active = false; clearFailureTracking(); taskGenerationByTurn.clear(); activeTurnIndex = undefined; for (const controller of consultations) controller.abort(); consultations.clear(); dbg?.("session.shutdown"); closeDebug(); }); pi.on("session_before_tree", () => { sessionGeneration += 1; taskGeneration += 1; clearFailureTracking(); taskGenerationByTurn.clear(); activeTurnIndex = undefined; for (const controller of consultations) controller.abort(); consultations.clear(); }); pi.on("input", (event) => { if (event.source !== "extension") invalidateTask(); }); pi.on("message_start", (event) => { if (event.message.role !== "user") return; invalidateTask(); if (activeTurnIndex !== undefined) taskGenerationByTurn.set(activeTurnIndex, taskGeneration); }); pi.on("turn_start", (event) => { activeTurnIndex = event.turnIndex; taskGenerationByTurn.set(event.turnIndex, taskGeneration); }); pi.on("model_select", (event, ctx) => syncAvailability(ctx, event.model)); pi.on("tool_result", (event, ctx) => { if (!isAvailable(ctx) || !tracksFailure(event.toolName)) return; const key = operationKey(event.toolName, event.input); if (key) operationKeysByCall.set(event.toolCallId, key); }); pi.on("turn_end", async (event, ctx) => { const generation = sessionGeneration; const task = taskGenerationByTurn.get(event.turnIndex) ?? taskGeneration; taskGenerationByTurn.delete(event.turnIndex); if (activeTurnIndex === event.turnIndex) activeTurnIndex = undefined; if (childSession) return; for (const result of event.toolResults) { if (result.toolName !== "angel") continue; if (!isCompleteToolDetails(result.details)) continue; pi.appendEntry(ADVICE_TYPE, { version: 1, advice: result.details.result.advice, metadata: result.details.result.metadata, }); } if (task !== taskGeneration) { for (const result of event.toolResults) operationKeysByCall.delete(result.toolCallId); return; } const turnSequence = completedTurnSequence++; if (!isAvailable(ctx)) { for (const result of event.toolResults) operationKeysByCall.delete(result.toolCallId); return; } const argumentsByCall = toolCallArguments(event.message); const keyedResults = event.toolResults.flatMap((result) => { if (!tracksFailure(result.toolName)) return []; const observedKey = operationKeysByCall.get(result.toolCallId); const arguments_ = argumentsByCall.get(result.toolCallId); const key = observedKey ?? (arguments_ ? operationKey(result.toolName, arguments_) : undefined); return key ? [{ key, result }] : []; }); for (const result of event.toolResults) operationKeysByCall.delete(result.toolCallId); const successfulKeys = new Set(); for (const { key, result } of keyedResults) { if (result.isError) continue; successfulKeys.add(key); pendingFailures.delete(key); consultedFailures.delete(key); } const repeatedFailures: Array<{ key: string; result: ToolResultMessage; }> = []; for (const { key, result } of keyedResults) { if ( !result.isError || successfulKeys.has(key) || isExplicitCancellation(result) ) continue; if (consultedFailures.has(key)) continue; const previousTurn = pendingFailures.get(key); if (previousTurn !== undefined && previousTurn !== turnSequence) { repeatedFailures.push({ key, result }); continue; } pendingFailures.set(key, turnSequence); } if (repeatedFailures.length === 0) return; for (const { key } of repeatedFailures) { pendingFailures.delete(key); consultedFailures.add(key); } const widgetKey = `angel-consultation-${generation}-${consultationSequence++}`; const progress = (update: ConsultationProgress) => { if ( !ctx.hasUI || !active || generation !== sessionGeneration || task !== taskGeneration ) return; const lines = [ ctx.ui.theme.fg("accent", `${ANGEL_ICON} Angel`), ctx.ui.theme.fg("dim", update.message), ]; if (update.preview) lines.push( ctx.ui.theme.fg("dim", update.preview.slice(-PREVIEW_MAX_CHARS)), ); ctx.ui.setWidget(widgetKey, lines); }; try { const result = await run( ctx, { origin: "error", question: `Diagnose this completed tool batch containing ${repeatedFailures.length} operation${repeatedFailures.length === 1 ? "" : "s"} that failed again after an earlier attempt, then recommend the executor's next step.`, extraContext: completedBatchContext( event.toolResults, repeatedFailures.map(({ result }) => result), ), }, { signal: ctx.signal, onProgress: progress }, ); if ( !active || generation !== sessionGeneration || task !== taskGeneration ) return; pi.sendMessage( { customType: ADVICE_TYPE, content: result.advice, display: true, details: { version: 1, advice: result.advice, metadata: result.metadata, }, }, { deliverAs: "steer" }, ); } catch (error) { if ( !active || generation !== sessionGeneration || task !== taskGeneration ) return; if (isAbortError(error)) { pi.sendMessage( { customType: STATUS_TYPE, content: "Angel consultation cancelled.", display: true, details: { version: 1, kind: "cancelled", message: "Angel cancelled", }, }, { triggerTurn: false }, ); return; } const message = error instanceof Error ? error.message : String(error); const failure = `Angel consultation failed: ${message}`; pi.sendMessage( { customType: STATUS_TYPE, content: failure, display: true, details: { version: 1, kind: "failed", message }, }, { deliverAs: "steer" }, ); } finally { if (ctx.hasUI) ctx.ui.setWidget(widgetKey, undefined); } }); pi.registerTool({ name: "angel", label: "Angel", description: TOOL_DESCRIPTION, promptSnippet: "Investigate difficult questions independently and return evidence-backed advice", promptGuidelines: TOOL_GUIDELINES, parameters: Type.Object({ question: Type.String({ minLength: 1, description: "Concrete question, uncertainty, decision, or competing hypotheses for Angel to investigate", }), context: Type.Optional( Type.String({ description: "Concise context that makes the assignment more precise; do not copy the parent transcript", }), ), }), renderCall(args, theme, context) { const question = typeof args.question === "string" ? args.question : ""; const extraContext = typeof args.context === "string" ? args.context : ""; let output = theme.fg("toolTitle", theme.bold(`${ANGEL_ICON} angel`)); const summary = questionSummary(question); if (summary) output += ` ${theme.fg("dim", summary)}`; if (context.expanded) { if (question) output += `\n${theme.fg("dim", "question: ")}${theme.fg("toolOutput", question)}`; if (extraContext) output += `\n${theme.fg("dim", "context: ")}${theme.fg("toolOutput", extraContext)}`; } else { output += theme.fg( "dim", ` (${keyHint("app.tools.expand", "to expand")})`, ); } return new Text(output, 0, 0); }, renderResult(result, { expanded, isPartial }, theme) { const details = result.details as ToolDetails | undefined; if (isPartial || details?.kind === "progress") { const progress = details?.progress; let output = theme.fg( "accent", `${ANGEL_ICON} ${progress?.message ?? "Consulting Angel"}`, ); if (expanded && progress?.preview) { const preview = progress.preview.slice(-PREVIEW_MAX_CHARS); output += `\n\n${theme.fg("dim", preview)}`; } return new Text(output, 0, 0); } if (!isCompleteToolDetails(details)) { let output = theme.fg("error", "Angel failed"); const content = result.content[0]; if (expanded && content?.type === "text" && content.text.trim()) output += `\n${theme.fg("dim", content.text)}`; return new Text(output, 0, 0); } const metadata = details.result.metadata; let output = theme.fg("success", `${ANGEL_ICON} Advice ready`); output += theme.fg( "dim", ` · ${metadata.tokens.total.toLocaleString("en-US")} tokens`, ); if (expanded) { output += `\n${theme.fg("dim", metadataDetailLines(metadata).join("\n"))}`; output += `\n${theme.fg("dim", metadata.childSessionFile)}`; } return new Text(output, 0, 0); }, async execute(_id, params, signal, onUpdate, ctx) { const result = await run( ctx, { origin: "executor", question: params.question, extraContext: params.context, }, { signal, onProgress: (progress) => onUpdate?.({ content: [{ type: "text", text: progress.message }], details: { kind: "progress", progress }, }), }, ); return { content: [{ type: "text", text: boundedAdvice(result) }], details: { kind: "complete", result } satisfies ToolDetails, }; }, }); pi.registerCommand("angel", { description: "Ask Angel a question directly, or enable/disable it for this session", getArgumentCompletions: (prefix): AutocompleteItem[] | null => { const items: AutocompleteItem[] = [ { value: "on", label: "on" }, { value: "off", label: "off" }, { value: "cancel", label: "cancel" }, ]; const filtered = items.filter((item) => item.value.startsWith(prefix.toLowerCase()), ); return filtered.length > 0 ? filtered : null; }, handler: async (args, ctx) => { const question = args.trim(); const control = question.toLowerCase(); if (control === "cancel") { invalidateTask(); cancelConsultations(); if (ctx.hasUI) ctx.ui.notify("Angel consultation cancelled", "info"); return; } if (control === "on" || control === "off") { sessionEnabled = control === "on"; invalidateTask(); cancelConsultations(); syncAvailability(ctx); if (ctx.hasUI) { const available = pi.getActiveTools().includes("angel"); const message = !sessionEnabled ? "Angel disabled" : available ? "Angel enabled" : "Angel remains unavailable for the current session and model"; ctx.ui.notify( message, available || !sessionEnabled ? "info" : "warning", ); } return; } if (!question) { if (ctx.hasUI) ctx.ui.notify("Usage: /angel |on|off|cancel", "error"); return; } const generation = sessionGeneration; const cancellation = cancellationGeneration; await ctx.waitForIdle(); if ( !active || generation !== sessionGeneration || cancellation !== cancellationGeneration ) return; const parentLeafId = ctx.sessionManager.getLeafId(); if (!enabled()) { if (ctx.hasUI) ctx.ui.notify("Angel is disabled", "error"); return; } const execute = async ( signal: AbortSignal | undefined, onProgress?: (progress: ConsultationProgress) => void, ) => run(ctx, { origin: "human", question }, { signal, onProgress }); let outcome: | { result: ConsultationResult } | { error: unknown } | undefined; if (ctx.mode === "tui") { outcome = await ctx.ui.custom((tui, theme, _keybindings, done) => { const controller = new AbortController(); const container = new Container(); const borderColor = (text: string) => theme.fg("accent", text); const loader = new Loader( tui, (text) => theme.fg("accent", text), (text) => theme.fg("muted", text), "Starting Angel", ); const preview = new Text("", 1, 0); container.addChild(new DynamicBorder(borderColor)); container.addChild(loader); container.addChild(preview); container.addChild(new Text(theme.fg("dim", "esc cancel"), 1, 0)); container.addChild(new DynamicBorder(borderColor)); loader.start(); execute(controller.signal, (progress) => { loader.setMessage(progress.message); preview.setText( progress.preview ? theme.fg("dim", progress.preview.slice(-PREVIEW_MAX_CHARS)) : "", ); tui.requestRender(); }) .then((result) => done({ result })) .catch((error: unknown) => done({ error })); return { render: (width) => container.render(width), invalidate: () => container.invalidate(), handleInput: (data) => { if (matchesKey(data, "escape")) controller.abort(); }, dispose: () => { loader.stop(); controller.abort(); }, }; }); } else { try { outcome = { result: await execute(undefined) }; } catch (error) { outcome = { error }; } } if ( !active || generation !== sessionGeneration || cancellation !== cancellationGeneration || parentLeafId !== ctx.sessionManager.getLeafId() ) return; if (!outcome || ("error" in outcome && isAbortError(outcome.error))) { if (ctx.mode === "print") process.stdout.write("Angel consultation cancelled.\n"); pi.sendMessage( { customType: STATUS_TYPE, content: "Angel consultation cancelled.", display: true, details: { version: 1, kind: "cancelled", message: "Angel cancelled", }, }, { triggerTurn: false }, ); return; } if ("error" in outcome) { const message = outcome.error instanceof Error ? outcome.error.message : String(outcome.error); const failure = `Angel consultation failed: ${message}`; if (ctx.mode === "print") process.stdout.write(`${failure}\n`); pi.sendMessage( { customType: STATUS_TYPE, content: failure, display: true, details: { version: 1, kind: "failed", message }, }, { triggerTurn: false }, ); return; } const result = outcome.result; if (ctx.mode === "print") process.stdout.write(`${result.advice}\n`); pi.sendMessage( { customType: ADVICE_TYPE, content: result.advice, display: true, details: { version: 1, advice: result.advice, metadata: result.metadata, }, }, { triggerTurn: false }, ); }, }); }; } export default createAngelExtension(); export const __test = { ADVICE_TYPE, STATUS_TYPE, TOOL_DESCRIPTION, TOOL_GUIDELINES, boundedAdvice, resolveBinding, resolveSubagentExtensions, };