AUDIT / src /lib /actionPlanner.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
6.93 kB
/**
* 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");
},
};