Spaces:
Sleeping
Sleeping
| /** | |
| * 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<string, unknown> = {}; | |
| try { args = JSON.parse(tc.function.arguments) as Record<string, unknown>; } 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<string, unknown> }>, | |
| 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<ActionPlan> { | |
| 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<StepStatus, number> { | |
| const counts: Record<StepStatus, number> = { | |
| 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"); | |
| }, | |
| }; | |