/** * Mid-session lifecycle reconciliation for `ask_user_question`. * * WHY THIS EXISTS — read before changing it. * * Subagent children run headless (`ctx.hasUI === false`). They must never be * able to block waiting on a human, and they must never route a question up to * the supervisor. Stripping the tool from the active set is the mechanism that * enforces that: the LLM in a headless run never sees the tool, so it cannot * call it, so it cannot stall a background run forever. * * Do NOT "improve" this into an escalation bridge. See * docs/specs/pi-ask-user-question.md §2 and §7 — no-subagent-escalation is an * explicit non-goal, not an oversight. * * Unlike @juicesharp/rpiv-ask-user-question there is no carve-out for * `ctx.mode === "rpc"`, because this package ships no RPC dialog fallback: * `hasUI` is the only signal and it is honest here. * * The in-handler `!ctx.hasUI` guard in ask-user-question.ts stays as a * one-turn backstop in case a future pi release snapshots the tool list before * `before_agent_start` runs. */ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { TOOL_NAME } from "./tool/schema.ts"; /** * Strip or restore the tool to match `ctx.hasUI`. Idempotent: when the tool is * already in the right state, the active set (and every sibling tool in it) is * left untouched. */ export function reconcileTool(pi: ExtensionAPI, ctx: ExtensionContext): void { const active = pi.getActiveTools(); const present = active.includes(TOOL_NAME); if (!ctx.hasUI && present) { pi.setActiveTools(active.filter((name) => name !== TOOL_NAME)); } else if (ctx.hasUI && !present) { pi.setActiveTools([...active, TOOL_NAME]); } } export function registerReconciler(pi: ExtensionAPI): void { pi.on("before_agent_start", (_event, ctx) => { reconcileTool(pi, ctx); }); }