import { profilePeerGate } from '../policy/profile-runtime.ts'; import type { ExtensionContext } from "@mariozechner/pi-coding-agent"; import { consumeReservedTaskId, reserveActualTaskId } from "../budget-recovery.ts"; import { DEFAULT_TASK_TIER, applyTierChange } from "../run-budget.js"; import { validateAssertionBatch } from "../assertion-ledger.js"; import { validateEvidence } from "../evidence-rules.js"; import { applyProcessClassification, createProcessState, processOpenObligations, processPreEffectGate, type ProcessObligationState } from "../process-obligations.ts"; import { safePathWithin } from "../helpers.ts"; import { TIMEOUT_MS } from "../../lib/coms-core.ts"; import type { Assertion, AssertionStatus, AssertionsArtifactsContext } from "../context/assertions-artifacts.ts"; import type { BudgetContext, TurnReport } from "../context/budgets.ts"; import type { HubStateContext } from "../context/hub-state.ts"; import type { ComsAwaitParams, ComsGetParams, ComsListParams, ComsSendParams, SetAssertionsParams, SetTaskTierParams, TeamAdjustParams, ToolExecutionResult, ToolExecutor, UpdateAssertionParams, } from "./context.ts"; const TEAM_ADJUST_ROSTER_CAP = 8; type Refusal = ToolExecutionResult | null; export interface ActionExecutorDeps { budget: BudgetContext; artifacts: AssertionsArtifactsContext; hubState: HubStateContext; provisionalCapabilityRefusal(pack: "fleet" | "peer"): Refusal; getTaskTier(): string | null; setTaskTier(value: string): void; getTaskTierAssumed(): boolean; setTaskTierAssumed(value: boolean): void; getProcessState?(): ProcessObligationState | null; processBlock?(): { reason: string; message: string } | null; adoptTaskTriage?(taskId: string, newTask: boolean, expandedScope?: string): Promise; setProcessState?(value: ProcessObligationState): void; persistProcessState?(value: ProcessObligationState): void; getTaskDispatchCount(): number; getTaskResearchCount(): number; getTurnReport(): TurnReport; getAssertions(): Assertion[]; setAssertions(value: Assertion[]): void; currentTaskId(): string; currentRevision(ctx: ExtensionContext): string; confirmTaskSupersession?(input: { oldTaskId: string; newTaskId: string; reason: string }, ctx: ExtensionContext, signal?: AbortSignal): Promise; adoptReservedTaskId?(id: string, resetTaskWindow: () => void): void; getAgentStates(): Map; rosterAdd(agent: string): { ok: boolean; message: string }; rosterDrop(agent: string): { ok: boolean; message: string }; getIdentity(): unknown | null; getComs(): any; resolveTarget(target: string): { name: string; model?: string } | null; appendMachineHandoffSections(brief: string): string; markPeerAddressed(name: string): void; } export interface ActionExecutors { executeSetTaskTier: ToolExecutor; executeTeamAdjust: ToolExecutor; executeSetAssertions: ToolExecutor; executeUpdateAssertion: ToolExecutor; executeGetAssertions: ToolExecutor>; executeComsList: ToolExecutor; executeComsSend: ToolExecutor; executeComsGet: ToolExecutor; executeComsAwait: ToolExecutor; } export function createActionExecutors(d: ActionExecutorDeps): ActionExecutors { let fallbackProcessState = createProcessState(); const getProcessState = () => d.getProcessState?.() ?? fallbackProcessState; const setProcessState = (value: ProcessObligationState) => { fallbackProcessState = value; d.setProcessState?.(value); d.persistProcessState?.(value); }; const executeSetTaskTier: ToolExecutor = async (_callId, params, signal, _onUpdate, ctx) => { const { tier, risk, scope, reason, new_task } = params; // T7: single authoritative snapshot for setter/result/gate. Prompt prose never // changes state — only this explicit tool call does, against one snapshot. const snapshot = { taskId: d.currentTaskId(), tier: d.getTaskTier(), tierAssumed: d.getTaskTierAssumed(), revision: (() => { try { return d.currentRevision(ctx); } catch { return "unavailable"; } })(), process: getProcessState(), }; if (new_task && !(typeof reason === "string" && reason.trim())) return { content: [{ type: "text", text: "A genuine new task requires a non-empty reason." }], details: { status: "error", reason: "new_task_reason_required" } }; if (!new_task) { const refusal = d.provisionalCapabilityRefusal("fleet"); if (refusal) return refusal; } const currentTier = new_task ? null : (snapshot.tierAssumed ? null : snapshot.tier); const change = applyTierChange(currentTier, tier, reason); if (!change.ok) return { content: [{ type: "text", text: change.message }], details: { status: "error", reason: change.reason, tier: change.tier } }; if (d.processBlock?.()?.reason === "process_state_corrupt") return { content: [{ type: "text", text: d.processBlock()!.message }], details: { status: "refused", reason: "process_state_corrupt" } }; const processChange = applyProcessClassification(snapshot.process, { risk, scope, reason, newTask: !!new_task }); if (!processChange.ok) return { content: [{ type: "text", text: processChange.message }], details: { status: "error", reason: "process_classification", tier: change.tier, risk: processChange.state.risk, scope: processChange.state.scope } }; let reservedNewTaskId: string | undefined; if (new_task) reservedNewTaskId = reserveActualTaskId(); if (new_task && processOpenObligations(snapshot.process).length) { let consumed = false; try { const confirmed = !!d.confirmTaskSupersession && await d.confirmTaskSupersession({ oldTaskId: d.currentTaskId(), newTaskId: reservedNewTaskId!, reason: String(reason).trim() }, ctx, signal); consumed = confirmed && consumeReservedTaskId(reservedNewTaskId!); if (!consumed) return { content: [{ type: "text", text: "Open obligations were not superseded: operator confirmation was absent, denied, stale, duplicate, or for another task." }], details: { status: "refused", reason: "task_supersession_not_authorized" } }; } finally { if (!consumed) consumeReservedTaskId(reservedNewTaskId!); } } let adopted = processChange.state; try { if (d.adoptTaskTriage) { const bound = await d.adoptTaskTriage(new_task ? reservedNewTaskId! : snapshot.taskId, !!new_task, !new_task && processChange.state.scope !== snapshot.process.scope ? processChange.state.scope : undefined); adopted = applyProcessClassification(bound, { risk, scope, reason, newTask: false }).state; } } catch { if (reservedNewTaskId) consumeReservedTaskId(reservedNewTaskId); return { content: [{ type: "text", text: "Task-triage binding could not be persisted; task transition refused." }], details: { status: "refused", reason: "task_triage_persistence" } }; } if (new_task) { const resetAt = Date.now(); const prior = d.budget.taskResetSnapshot(resetAt); const resetTaskWindow = () => d.budget.resetTaskWindow(null, resetAt); if (reservedNewTaskId && d.adoptReservedTaskId) d.adoptReservedTaskId(reservedNewTaskId, resetTaskWindow); else resetTaskWindow(); if (reservedNewTaskId) consumeReservedTaskId(reservedNewTaskId); d.setAssertions([]); d.artifacts.persistAssertions(); d.budget.appendTaskResetEntry("tool:set_task_tier", null, prior, ctx); } setProcessState(adopted); d.setTaskTier(change.tier); d.setTaskTierAssumed(false); d.getTurnReport().tier = change.tier; d.budget.updateModeStatus(); const b = d.budget.currentBudget(); const tb = d.budget.currentTaskBudget(); const cap = (n: number | null) => n == null ? "unlimited" : String(n); const spent = `${d.getTaskDispatchCount()}/${cap(tb.maxDispatches)} dispatches, ${d.getTaskResearchCount()}/${cap(tb.maxResearch)} research`; return { content: [{ type: "text", text: `${change.message}${new_task ? " (new task window opened; prior assertion ledger cleared)" : ""}\n${processChange.message} Budget tier differs intentionally: it limits spend and cannot erase correctness obligations.\nPer turn: ${cap(b.maxDispatches)} dispatches, ${cap(b.maxResearch)} research. Whole task: ${spent} spent. Size the apparatus accordingly — do not spend a cap just because it exists.` }], details: { status: "ok", tier: change.tier, risk: adopted.risk, scope: adopted.scope, process: adopted, escalated: change.escalated, newTask: !!new_task, snapshot: { taskId: snapshot.taskId, tier: snapshot.tier, revision: snapshot.revision }, ...(reservedNewTaskId ? { newTaskId: reservedNewTaskId } : {}) } }; }; const executeTeamAdjust: ToolExecutor = async (_id, params, _signal, _update, ctx) => { const refusal = d.provisionalCapabilityRefusal("fleet"); if (refusal) return refusal; const act = String(params.action || "").trim().toLowerCase(); if (!d.budget.currentBudget().delegation) { const tier = d.getTaskTier() ?? DEFAULT_TASK_TIER; return { content: [{ type: "text", text: `team_adjust is disabled at tier "${tier}" — a single-specialist path never needs roster changes. Raise the task tier with set_task_tier if the work outgrew "${tier}".` }], details: { status: "refused" } }; } if (act !== "add" && act !== "drop") return { content: [{ type: "text", text: `Unknown action "${params.action}" — expected add or drop.` }], details: { status: "error" } }; if (act === "add" && d.getAgentStates().size >= TEAM_ADJUST_ROSTER_CAP) return { content: [{ type: "text", text: `Roster cap reached (${TEAM_ADJUST_ROSTER_CAP}) — drop an unused member first, or ask the user to /af-agents-add manually.` }], details: { status: "refused" } }; const result = act === "add" ? d.rosterAdd(params.agent) : d.rosterDrop(params.agent); if (result.ok) ctx.ui.notify(`team_adjust (${act}): ${result.message} — dispatcher's reason: ${params.reason || "(none given)"}`, "info"); const roster = Array.from(d.getAgentStates().values()).map(s => s.def.name).join(", "); return { content: [{ type: "text", text: `${result.message}. Active team: ${roster}.` }], details: { status: result.ok ? "ok" : "refused", roster } }; }; const executeSetAssertions: ToolExecutor = async (_id, params, _signal, _update, ctx) => { const verdict = validateAssertionBatch(params.assertions); if (!verdict.ok) return { content: [{ type: "text", text: verdict.refusal! }], details: { status: "rejected", reason: "missing-source" } }; const assertions = verdict.assertions.map(a => ({ ...a, status: "open" as AssertionStatus })); d.setAssertions(assertions); d.artifacts.persistAssertions(); d.artifacts.updateAssertionStatus(); if (verdict.warning) ctx.ui.notify(verdict.warning, "warning"); const head = `Ledger set: ${assertions.length} assertion(s) open — ${assertions.map(a => a.id).join(", ") || "(none)"}. Pass the relevant ones verbatim into each dispatch and advance only on proven.`; return { content: [{ type: "text", text: verdict.warning ? `${head}\n\n${verdict.warning}` : head }], details: { count: assertions.length, capWarning: Boolean(verdict.warning) } }; }; const executeUpdateAssertion: ToolExecutor = async (_id, params, _signal, _update, ctx) => { const assertions = d.getAssertions(); const wanted = String(params.status).trim().toLowerCase(); if (!["proven", "unproven", "failed"].includes(wanted)) return { content: [{ type: "text", text: `status must be one of proven | unproven | failed (got "${params.status}").` }], details: { status: "error" } }; const a = assertions.find(x => x.id.toLowerCase() === String(params.id).trim().toLowerCase()); if (!a) return { content: [{ type: "text", text: `No assertion "${params.id}" in the ledger. Call set_assertions first, or check the id. Current: ${assertions.map(x => x.id).join(", ") || "(empty)"}.` }], details: { status: "error" } }; if (wanted === "proven") { const gate = d.processBlock?.() ?? processPreEffectGate(getProcessState(), "prove", "", d.currentRevision(ctx)); if (gate) return { content: [{ type: "text", text: gate.message }], details: { status: "refused", reason: gate.reason } }; const validation = validateEvidence(a.tag, params.evidence || "", { fileExists: d.artifacts.evidencePathExists, evidenceRoot: safePathWithin(d.artifacts.artifactsRoot(), "evidence") }); if (!validation.ok) return { content: [{ type: "text", text: `${a.id} stays ${a.status}: ${validation.reason}` }], details: { status: "rejected", reason: validation.reason } }; } a.status = wanted as AssertionStatus; a.evidence = wanted === "unproven" ? undefined : (params.evidence?.trim() || undefined); if (wanted === "unproven") { a.evidenceTaskId = undefined; a.evidenceRevision = undefined; } else { a.evidenceTaskId = d.currentTaskId(); a.evidenceRevision = d.currentRevision(ctx); } d.artifacts.persistAssertions(); d.artifacts.updateAssertionStatus(); const open = assertions.filter(x => x.status === "open" || x.status === "unproven").map(x => x.id); const failed = assertions.filter(x => x.status === "failed").map(x => x.id); const tail = failed.length ? `Failed: ${failed.join(", ")}. Still open: ${open.join(", ") || "none"}.` : open.length ? `Still open: ${open.join(", ")}.` : "All assertions proven."; return { content: [{ type: "text", text: `${a.id} → ${a.status}${a.evidence ? ` (${a.evidence})` : ""}. ${tail}` }], details: { id: a.id, status: a.status } }; }; const executeGetAssertions: ToolExecutor> = async () => d.getAssertions().length === 0 ? { content: [{ type: "text", text: "Ledger is empty. Call set_assertions to build the acceptance assertions before dispatching." }], details: { count: 0 } } : { content: [{ type: "text", text: d.artifacts.renderAssertionLedgerText() }], details: { count: d.getAssertions().length } }; const executeComsList: ToolExecutor = async (_id, params) => { if (!d.getIdentity()) return { content: [{ type: "text", text: "coms not initialised." }], details: { agents: [], project: null } }; const coms = d.getComs(); const result = await coms.list(params); const notice = result.widenRequested ? `\n\n(Discovery is scoped to "${result.project}"${coms.scope.includeExplicit ? "" : ", explicit peers hidden"}. Widening to other projects or revealing --explicit peers is a human action via /af-coms --project or /af-coms --all.)` : ""; const lines = result.agents.length === 0 ? "No peer agents in your pool." : result.agents.map((agent: any) => `${agent.alive ? "●" : "✗"} ${agent.name} (${agent.model})${agent.context_used_pct != null ? ` ${agent.context_used_pct}%` : " ?%"} [${agent.alive ? agent.status ?? "unknown" : "unreachable"}${agent.pane_id ? ` pane ${agent.pane_id}` : ""}]${agent.purpose ? ` — ${agent.purpose}` : ""}`).join("\n"); return { content: [{ type: "text", text: `${result.agents.length} peer(s) in pool (project ${result.project}):\n${lines}${notice}` }], details: result }; }; const executeComsSend: ToolExecutor = async (_id, params) => { const gate = d.processBlock?.() ?? processPreEffectGate(getProcessState(), "child", ""); if (gate) { const recovery = gate.reason === "process_plan_open" ? " Direct coms_send has no planner persona dispatch contract; peer names and prompt claims cannot supply it. Use dispatch_agent with agent: planner, declared scope and deliverables, and the applicable backend policy. If planner is missing, ask the human to use /af-agents-add planner or /af-agents-team. Inspect an existing message with coms_list/coms_get/coms_await without resending." : ""; return { content: [{ type: "text", text: gate.message + recovery }], details: { status: "refused", reason: gate.reason } }; } const target = d.resolveTarget(params.target); const profileRefusal = profilePeerGate({ peerModel: target?.model, targetResolved: !!target }); if (profileRefusal) return profileRefusal; const refusal = d.provisionalCapabilityRefusal("peer"); if (refusal) return refusal; const pending = d.hubState.getPendingHandoff(); const authorized = !!(target && pending && pending.target === target.name && params.handoff_token === pending.token); const prompt = authorized ? d.appendMachineHandoffSections(String(params.prompt || "")) : String(params.prompt || ""); const sent = await d.getComs().send({ target: params.target, prompt, conversation_id: params.conversation_id ?? null, response_schema: (params.response_schema as object | undefined) ?? null, reply_timeout_ms: params.reply_timeout_ms ?? null }); d.markPeerAddressed(sent.target); if (authorized) d.hubState.setPendingHandoff(null); return { content: [{ type: "text", text: `coms_send → ${sent.target}\nmsg_id ${sent.msg_id}\nhops ${sent.hops}` }], details: { msg_id: sent.msg_id, target: sent.target, target_session: sent.target_session, hops: sent.hops } }; }; const executeComsGet: ToolExecutor = async (_id, params) => { const result = d.getComs().get(params.msg_id); const text = result.status === "error" ? `coms_get: unknown msg_id ${params.msg_id}` : result.status === "pending" ? "coms_get: pending" : result.error ? `coms_get: error — ${result.error}` : `coms_get: complete\n${typeof result.response === "string" ? result.response : JSON.stringify(result.response, null, 2)}`; return { content: [{ type: "text", text }], details: result }; }; const executeComsAwait: ToolExecutor = async (_id, params) => { const result = await d.getComs().await(params.msg_id, typeof params.timeout_ms === "number" && params.timeout_ms > 0 ? params.timeout_ms : TIMEOUT_MS); if (result.status === "pending") return { content: [{ type: "text", text: "coms_await: pending — wait budget exhausted; the peer may still complete" }], details: { status: "pending" } }; if (result.status === "error") { const unknown = result.error === "unknown msg_id"; return { content: [{ type: "text", text: unknown ? `coms_await: unknown msg_id ${params.msg_id}` : `coms_await: error — ${result.error}` }], details: unknown ? { error: "unknown msg_id" } : { status: "error", error: result.error } }; } return { content: [{ type: "text", text: typeof result.response === "string" ? result.response : JSON.stringify(result.response, null, 2) }], details: { response: result.response } }; }; return { executeSetTaskTier, executeTeamAdjust, executeSetAssertions, executeUpdateAssertion, executeGetAssertions, executeComsList, executeComsSend, executeComsGet, executeComsAwait }; }