import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { StringEnum } from "@earendil-works/pi-ai"; import { type ExtensionAPI, defineTool } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import type { ModeController } from "../mode.ts"; import { validateAgentName } from "../state.ts"; import { TERMINAL_STATES, type RunState } from "../worker/status.ts"; import type { Supervisor } from "../worker/supervisor.ts"; const ControlAction = StringEnum(["interrupt", "stop", "resume", "stop_all"]); type ModelControlSupervisor = Pick; export interface ControlDeps { supervisor: () => ModelControlSupervisor | undefined; readOnlySession: () => boolean; } export interface ControlDetails { action: string; name?: string; state?: string; names?: string[]; } export interface SteerDetails { agiSteer: { name: string; reqId: string; state: string; interrupt: boolean; detail?: string }; } function duration(ms: number): string { const seconds = Math.max(0, Math.floor(ms / 1000)); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return seconds % 60 === 0 ? `${minutes}m` : `${minutes}m${seconds % 60}s`; const hours = Math.floor(minutes / 60); return minutes % 60 === 0 ? `${hours}h` : `${hours}h${minutes % 60}m`; } function steeringActivity(result: Awaited>, now = Date.now()): string[] { const active = [...(result.status.activity.activeTools ?? [])] .sort((left, right) => left.startedAt.localeCompare(right.startedAt)) .at(-1); const lines: string[] = []; if (active !== undefined) { const startedAt = Date.parse(active.startedAt); const elapsed = Number.isNaN(startedAt) ? "unknown" : duration(now - startedAt); const label = active.tool ?? "tool"; const target = active.target === null ? "" : ` ${active.target}`; const log = active.logPath === undefined ? "" : ` · ${active.logPath}`; lines.push(`running · ${label}${target} · ${elapsed}${log}`); } if (result.status.attention !== null) { lines.push(`Attention: ${result.status.attention.reason} — ${result.status.attention.detail}`); } return lines; } function requireText(value: string | undefined, label: string): string { const text = value?.trim() ?? ""; if (text.length === 0) throw new Error(`agi_control: '${label}' is required.`); return text; } /** * R-CTRL-18 is scoped to this orchestrator's own runs, so runs belonging to another * live session are reported as skipped rather than silently omitted — otherwise the * orchestrator would believe it had stopped a fleet it does not control. */ function describeStopAll(results: Array<{ runId: string; state: string; detail?: string }>): string { const refused = results.filter((result) => result.state === "refused"); const alreadyStopped = (result: { state: string; detail?: string }): boolean => result.state === "stopped" && result.detail === "already stopped"; const stopped = results.filter((result) => (result.state === "stopped" && !alreadyStopped(result)) || result.state === "stopping"); const terminal = results.filter((result) => (TERMINAL_STATES.has(result.state as RunState) && result.state !== "stopped") || alreadyStopped(result)); const skipped = results.filter((result) => result.state === "skipped"); const unclassified = results.filter((result) => !refused.includes(result) && !stopped.includes(result) && !terminal.includes(result) && !skipped.includes(result)); const sections: string[] = []; const mixedMutationOutcome = refused.length > 0 && stopped.length > 0; if (mixedMutationOutcome) sections.push("Inconsistent mixed stop-all outcomes returned; inspect each run."); if (refused.length > 0) sections.push(`Refused: ${refused.length} agent${refused.length === 1 ? "" : "s"}.`); if (stopped.length > 0) sections.push(`Stopped or stopping: ${stopped.length} agent${stopped.length === 1 ? "" : "s"}.`); if (refused.length === 0 && stopped.length === 0 && terminal.length === 0 && skipped.length === 0 && unclassified.length === 0) { sections.push("No active runs owned by this session to stop."); } if (terminal.length > 0) sections.push(`Preserved terminal: ${terminal.length} agent${terminal.length === 1 ? "" : "s"}.`); if (skipped.length > 0) sections.push(`Left alone because another session owns them: ${skipped.length} agent${skipped.length === 1 ? "" : "s"}.`); if (unclassified.length > 0) sections.push(`Left alone with unrecognized outcomes: ${unclassified.length} agent${unclassified.length === 1 ? "" : "s"}.`); return sections.join("\n\n"); } export function registerSteerTool(pi: ExtensionAPI, deps: ControlDeps): void { pi.registerTool( defineTool({ name: "agi_steer", label: "AGI Steer", description: "Queue a durable ordinary user message for one worker. The default lets active reasoning or tools finish and delivers at the next model boundary. Set interrupt:true to abort the active turn or command before delivery. Delivered means the worker input hook matched the exact text and wrote an ack; other outcomes report queued or failed.", promptSnippet: "agi_steer: queue a durable user message; use interrupt:true for a deliberate active abort", parameters: Type.Object({ name: Type.String({ minLength: 1, description: "Agent name returned by agi_delegate." }), message: Type.String({ minLength: 1 }), waitForAck: Type.Optional(Type.Boolean({ default: true })), interrupt: Type.Optional(Type.Boolean({ default: false, description: "Abort the worker's active reasoning or command before delivery. Defaults to false." })), }), execute: async (_id, params, _signal, _onUpdate, _ctx): Promise> => { if (deps.readOnlySession()) throw new Error("agi_steer: this AGI session is read-only."); const supervisor = deps.supervisor(); if (supervisor === undefined) throw new Error("agi_steer: AGI mode is not enabled."); const name = validateAgentName(params.name); const result = await supervisor.steerAgent(name, params.message, params.waitForAck !== false, "orchestrator", params.interrupt === true); const activity = steeringActivity(result); const text = result.state === "delivered" ? result.interrupt ? `Steering accepted by ${name} after interrupting its active work; exact input was durably acknowledged.${activity.length === 0 ? "" : `\n${activity.join("\n")}`}` : `Steering accepted by ${name} without interruption; exact input was durably acknowledged.${activity.length === 0 ? "" : `\n${activity.join("\n")}`}\nThe agent will read it at the next model boundary after active work returns.` : result.state === "queued" ? `Steering queued for ${name}${result.interrupt ? " with interruption requested" : " without interruption"}. It is durable, but the agent has not acknowledged accepting it yet.${activity.length === 0 ? "" : `\n${activity.join("\n")}`}` : result.indeterminate === true // R-CTRL-14/E40: the request could not be withdrawn, so it may still be // delivered. Reporting a clean failure here would be a lie. ? `Steering failed for ${name}: ${result.detail ?? "unknown reason"}. The durable request could NOT be withdrawn, so it may still reach the agent — verify before re-sending.` : `Steering failed for ${name}: ${result.detail ?? "unknown reason"}.`; return { content: [{ type: "text", text }], details: { agiSteer: { name, reqId: result.reqId, state: result.state, interrupt: result.interrupt, ...(result.detail === undefined ? {} : { detail: result.detail }) } }, }; }, }), ); } export function registerControlTool(pi: ExtensionAPI, mode: ModeController, deps: ControlDeps): void { pi.registerTool( defineTool({ name: "agi_control", label: "AGI Control", description: "Interrupt live work for resumable inspection, resume the same session, or terminally stop queued/paused work. Live workers must be interrupted before model-issued stop or stop_all. Destructive actions require a reason.", promptSnippet: "agi_control: interrupt live work; resume or stop queued/paused work", parameters: Type.Object({ action: ControlAction, name: Type.Optional(Type.String()), message: Type.Optional(Type.String()), reason: Type.Optional(Type.String()), }), execute: async (_id, params, _signal, _onUpdate, _ctx): Promise> => { if (!mode.isEnabled()) throw new Error("agi_control: AGI mode is not enabled."); if (deps.readOnlySession()) throw new Error("agi_control: this AGI session is read-only."); const supervisor = deps.supervisor(); if (supervisor === undefined) throw new Error("agi_control: AGI mode is not enabled."); if (params.action === "stop_all") { const reason = requireText(params.reason, "reason"); const results = supervisor.stopAll(reason); return { content: [{ type: "text", text: describeStopAll(results) }], details: { action: params.action }, }; } const name = validateAgentName(requireText(params.name, "name")); if (params.action === "interrupt") { const result = supervisor.interruptAgent(name, "orchestrator"); // R-CTRL-2: a skipped or degraded signal fast path is reported, because it // changes how soon the worker will see the durable request. const note = result.detail === undefined ? "" : ` (${result.detail})`; return { content: [{ type: "text", text: `${name} is interrupting and will remain paused after settlement.${note}\n` + "This action creates a checkpoint; it does not deliver a continuation message. " + `Continue the same session with agi_control({action:"resume", name:"${name}", message:"..."}), or finish it with action:"stop". ` + "A paused worker has no trajectory wake.", }], details: { action: params.action, name, state: result.state }, }; } if (params.action === "stop") { const reason = requireText(params.reason, "reason"); const result = supervisor.stopAgent(name, reason); if (result.state === "refused") { return { content: [{ type: "text", text: `Stop refused for ${name}: ${result.detail ?? "the agent is still live"}.` }], details: { action: params.action, name, state: result.state }, }; } const note = result.detail === undefined ? "" : ` (${result.detail})`; const terminalAlready = !["stopped", "stopping"].includes(result.state); const text = terminalAlready ? `${name}: ${result.state}.${result.detail === undefined ? "" : ` ${result.detail}.`}` : `${name}: ${result.state}. Use a different name for fresh work.${note}`; return { content: [{ type: "text", text }], details: { action: params.action, name, state: result.state } }; } const message = requireText(params.message, "message"); const resumed = supervisor.resumeAgent(name, message); const acceptance = await resumed.acceptance; if (!acceptance.ok) throw new Error(`Agent ${name} could not be resumed (${acceptance.reason}).`); return { content: [{ type: "text", text: `Resumed ${name}. The same saved conversation continues.` }], details: { action: params.action, name, state: resumed.status.state }, }; }, }), ); }