/** * actionPlanner.ts — Action Planner (Sessione 25) * * Converte un goal in una sequenza di azioni eseguibili, * con policy check integrato prima di ogni step. * * Flusso: * 1. plan(goal, context) → ActionPlan (array di ActionStep) * 2. Ogni step passa per checkPolicy → risk level * 3. execute(plan, callbacks) esegue step in sequenza * 4. Step risky/dangerous → onNeedConfirm prima di procedere * 5. Ogni step emette RuntimeEvent via eventRuntime * * Invarianti: * - Nessuna azione dangerous senza conferma esplicita * - Ogni step è idempotente: può essere ripreso dopo interruzione * - Safari-safe: nessun Worker, nessun SharedArrayBuffer * - Piano salvato su Dexie via taskStore (sopravvive a tab kill) */ import { eventRuntime } from "@/core/events"; import { checkPolicy, requiresConfirm } from "@/lib/capabilityPolicy"; import { runtimeLogger } from "@/lib/runtimeLogger"; import type { PolicyCheckResult } from "@/lib/capabilityPolicy"; // ─── Tipi pubblici (estratti in actionPlannerTypes.ts — S553 — re-esportati per backward compat) ── import type { StepStatus, ActionStep, ActionPlan, PlannerCallbacks, ToolExecutor } from "./actionPlannerTypes"; export type { StepStatus, ActionStep, ActionPlan, PlannerCallbacks, ToolExecutor } from "./actionPlannerTypes"; // ─── ID generator (no nanoid dep) ───────────────────────────────────────────── function uid(): string { return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`; } // ─── Action Planner ─────────────────────────────────────────────────────────── export const actionPlanner = { /** * Crea un piano da una lista di tool calls (formato OpenAI tool_calls). * Non esegue nulla — solo struttura e applica policy. */ fromToolCalls( toolCalls: Array<{ id?: string; function: { name: string; arguments: string } }>, taskId: string, goal: string, ): ActionPlan { const steps: ActionStep[] = toolCalls.map((tc) => { let args: Record = {}; try { args = JSON.parse(tc.function.arguments) as Record; } catch { /* ignore */ } const policy: PolicyCheckResult = checkPolicy(tc.function.name, args); return { id: tc.id ?? uid(), tool: tc.function.name, args, label: policy.label, risk: policy.risk, preview: policy.preview, status: "pending", }; }); return { id: uid(), goal, steps, taskId }; }, /** * Crea un piano manuale da una lista di step descrittivi. * Utile per piani generati dal reasoning loop. */ fromSteps( steps: Array<{ tool: string; args: Record }>, taskId: string, goal: string, ): ActionPlan { return this.fromToolCalls( steps.map(s => ({ function: { name: s.tool, arguments: JSON.stringify(s.args) } })), taskId, goal, ); }, /** * Esegue un piano step per step. * Rispetta la policy: risky/dangerous → onNeedConfirm. * Emette RuntimeEvent per ogni step. */ async execute( plan: ActionPlan, executor: ToolExecutor, callbacks: PlannerCallbacks = {}, ): Promise { const { onStepStart, onStepDone, onStepError, onNeedConfirm, onPlanDone } = callbacks; let allOk = true; for (const step of plan.steps) { if (step.status === "done" || step.status === "skipped") continue; // Policy check if (requiresConfirm(step.tool)) { step.status = "waiting_confirm"; onStepStart?.(step); if (onNeedConfirm) { const confirmed = await onNeedConfirm(step).catch(() => false); if (!confirmed) { step.status = "skipped"; runtimeLogger.log("info", "task", `Step skipped (no confirm): ${step.tool}`, { stepId: step.id }); eventRuntime.emit({ kind: "agent_step_complete", taskId: plan.taskId, step: plan.steps.indexOf(step) + 1, action: `${step.tool} (skipped — no confirm)`, success: false, }); continue; } } else { // Nessun handler confirm → salta step dangerous, procedi su risky if (step.risk === "dangerous") { step.status = "skipped"; runtimeLogger.log("warn", "task", `Step dangerous skipped (no confirm handler): ${step.tool}`); continue; } } } // Esecuzione step.status = "running"; step.startedAt = Date.now(); onStepStart?.(step); eventRuntime.emit({ kind: "agent_step_start", taskId: plan.taskId, step: plan.steps.indexOf(step) + 1, action: `${step.tool}: ${step.preview.slice(0, 150)}`, // S609: 80→150 }); try { const result = await executor(step.tool, step.args); step.result = result; step.status = "done"; step.doneAt = Date.now(); runtimeLogger.log("info", "task", `Step done: ${step.tool} in ${step.doneAt - (step.startedAt ?? step.doneAt)}ms`, { stepId: step.id }); onStepDone?.(step, result); eventRuntime.emit({ kind: "agent_step_complete", taskId: plan.taskId, step: plan.steps.indexOf(step) + 1, action: step.tool, success: true, }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); step.error = msg; step.status = "error"; step.doneAt = Date.now(); allOk = false; runtimeLogger.log("error", "task", `Step error: ${step.tool} — ${msg}`, { stepId: step.id }); onStepError?.(step, msg); eventRuntime.emit({ kind: "agent_step_error", taskId: plan.taskId, step: plan.steps.indexOf(step) + 1, action: step.tool, message: msg, }); // Non interrompere il piano — continua con il prossimo step } } onPlanDone?.(plan, allOk); return plan; }, /** Conta step per stato */ stats(plan: ActionPlan): Record { const counts: Record = { pending: 0, running: 0, done: 0, error: 0, skipped: 0, waiting_confirm: 0, }; for (const s of plan.steps) counts[s.status]++; return counts; }, /** Ritorna true se tutti gli step non-skipped sono completati */ isDone(plan: ActionPlan): boolean { return plan.steps.every(s => s.status === "done" || s.status === "skipped"); }, };