/** * LLM Planner Agent - TypeScript/Bun Version * Tier 0 Observer with OpenRouter LLM capabilities */ import OpenAI from "openai"; import { z } from "zod"; import { $ } from "bun"; import { Database } from "bun:sqlite"; // ============================================================================= // Agent Metadata // ============================================================================= const AGENT_METADATA = { agent_id: "llm-planner-ts-001", agent_role: "observer", owner: "system", version: "0.1.0", tier: 0, allowed_side_effects: ["read_docs", "read_inventory", "read_logs", "generate_plan", "llm_inference"], forbidden_actions: ["ssh", "create_vm", "modify_vm", "delete_vm", "run_ansible", "run_terraform"], confidence_threshold: 0.7, }; // ============================================================================= // Types // ============================================================================= interface TaskRequest { task_type: "plan" | "analyze"; description: string; context?: Record; constraints?: string[]; } interface AgentOutput { agent_id: string; version: string; timestamp: string; action: string; decision: "EXECUTE" | "SKIP" | "ESCALATE" | "ERROR"; confidence: number; assumptions: string[]; side_effects: { type: string; target: string; reversible: boolean }[]; notes_for_humans: string; llm_model?: string; llm_response?: string; plan?: Record; error?: { type: string; message: string; triggering_input: string; recommended_action: string; }; } // ============================================================================= // Vault Client // ============================================================================= async function getVaultSecret(path: string): Promise> { const initKeys = await Bun.file("/opt/vault/init-keys.json").json(); const token = initKeys.root_token; const result = await $`curl -sk -H "X-Vault-Token: ${token}" https://127.0.0.1:8200/v1/secret/data/${path}`.json(); return result.data.data; } // ============================================================================= // Ledger // ============================================================================= function logToLedger(output: AgentOutput, success: boolean) { const db = new Database("/opt/agent-governance/ledger/governance.db"); db.run(` INSERT INTO agent_actions (timestamp, agent_id, agent_version, tier, action, decision, confidence, success, error_type, error_message) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ output.timestamp, output.agent_id, output.version, AGENT_METADATA.tier, output.action, output.decision, output.confidence, success ? 1 : 0, output.error?.type ?? null, output.error?.message ?? null, ]); db.run(` INSERT INTO agent_metrics (agent_id, current_tier, total_runs, last_active_at, compliant_runs, consecutive_compliant) VALUES (?, ?, 1, ?, ?, ?) ON CONFLICT(agent_id) DO UPDATE SET total_runs = total_runs + 1, compliant_runs = CASE WHEN ? = 1 THEN compliant_runs + 1 ELSE compliant_runs END, consecutive_compliant = CASE WHEN ? = 1 THEN consecutive_compliant + 1 ELSE 0 END, last_active_at = ?, updated_at = ? `, [ output.agent_id, AGENT_METADATA.tier, output.timestamp, success ? 1 : 0, success ? 1 : 0, success ? 1 : 0, success ? 1 : 0, output.timestamp, output.timestamp, ]); db.close(); } // ============================================================================= // Agent // ============================================================================= class LLMPlannerAgent { private llm!: OpenAI; private model: string; constructor(model: string = "anthropic/claude-sonnet-4") { this.model = model; } async init() { const secrets = await getVaultSecret("api-keys/openrouter"); this.llm = new OpenAI({ baseURL: "https://openrouter.ai/api/v1", apiKey: secrets.api_key, }); console.log("[INIT] Agent " + AGENT_METADATA.agent_id + " v" + AGENT_METADATA.version); console.log("[INIT] Model: " + this.model); } private now(): string { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); } private validateAction(action: string): boolean { if (AGENT_METADATA.forbidden_actions.includes(action)) return false; if (AGENT_METADATA.allowed_side_effects.includes(action)) return true; return false; } async generatePlan(request: TaskRequest): Promise { const action = "generate_plan"; if (!this.validateAction(action)) { const output: AgentOutput = { agent_id: AGENT_METADATA.agent_id, version: AGENT_METADATA.version, timestamp: this.now(), action, decision: "ERROR", confidence: 0, assumptions: [], side_effects: [], notes_for_humans: "", error: { type: "FORBIDDEN_ACTION", message: "Action '" + action + "' not permitted", triggering_input: request.description, recommended_action: "Escalate to higher tier", }, }; logToLedger(output, false); return output; } // Get context let contextInfo = "Context unavailable"; try { const inventory = await getVaultSecret("inventory/proxmox"); contextInfo = "Cluster: " + inventory.cluster + ", Pools: " + inventory.pools; } catch {} const systemPrompt = `You are a Tier 0 Observer agent. Generate implementation plans only - you CANNOT execute. Output JSON: {"title":"","summary":"","confidence":0.0-1.0,"assumptions":[],"steps":[{"step":1,"action":"","reversible":true,"rollback":""}],"estimated_tier_required":0-4,"risks":[]} Context: ${contextInfo}`; try { const response = await this.llm.chat.completions.create({ model: this.model, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: "Task: " + request.description + "\nGenerate a plan." }, ], max_tokens: 2000, temperature: 0.3, }); const llmResponse = response.choices[0].message.content || ""; let plan: Record = {}; let confidence = 0.5; try { const jsonMatch = llmResponse.match(/\{[\s\S]*\}/); if (jsonMatch) { plan = JSON.parse(jsonMatch[0]); confidence = plan.confidence || 0.5; } } catch { plan = { raw_response: llmResponse }; } const tierRequired = plan.estimated_tier_required || "unknown"; const output: AgentOutput = { agent_id: AGENT_METADATA.agent_id, version: AGENT_METADATA.version, timestamp: this.now(), action, decision: confidence >= AGENT_METADATA.confidence_threshold ? "EXECUTE" : "ESCALATE", confidence, assumptions: plan.assumptions || [], side_effects: [{ type: "llm_inference", target: this.model, reversible: true }], notes_for_humans: "Tier required: " + tierRequired, llm_model: this.model, plan, }; logToLedger(output, true); return output; } catch (e: any) { const output: AgentOutput = { agent_id: AGENT_METADATA.agent_id, version: AGENT_METADATA.version, timestamp: this.now(), action, decision: "ERROR", confidence: 0, assumptions: [], side_effects: [], notes_for_humans: "", error: { type: "LLM_ERROR", message: e.message, triggering_input: request.description, recommended_action: "Check API connectivity", }, }; logToLedger(output, false); return output; } } async run(request: TaskRequest): Promise { console.log("[TASK] " + request.task_type + ": " + request.description.slice(0, 80) + "..."); if (request.task_type === "plan") { return this.generatePlan(request); } const output: AgentOutput = { agent_id: AGENT_METADATA.agent_id, version: AGENT_METADATA.version, timestamp: this.now(), action: request.task_type, decision: "ERROR", confidence: 0, assumptions: [], side_effects: [], notes_for_humans: "", error: { type: "UNSUPPORTED", message: "Task type '" + request.task_type + "' not implemented in TS version", triggering_input: request.description, recommended_action: "Use 'plan' task type or Python agent", }, }; logToLedger(output, false); return output; } } // ============================================================================= // CLI // ============================================================================= async function main() { const args = process.argv.slice(2); if (args.length < 2) { console.log("Usage: bun run index.ts \"description\""); process.exit(1); } const taskType = args[0] as "plan" | "analyze"; const description = args[1]; const agent = new LLMPlannerAgent(); await agent.init(); const output = await agent.run({ task_type: taskType, description }); console.log("\n" + "=".repeat(60)); console.log("Decision: " + output.decision); console.log("Confidence: " + output.confidence); console.log("=".repeat(60)); if (output.plan) { console.log("\nPLAN:"); console.log(JSON.stringify(output.plan, null, 2)); } if (output.error) { console.log("\nERROR:"); console.log(" Type: " + output.error.type); console.log(" Message: " + output.error.message); } if (output.assumptions && output.assumptions.length > 0) { console.log("\nASSUMPTIONS:"); output.assumptions.forEach(a => console.log(" - " + a)); } console.log("\n" + "=".repeat(60)); } main().catch(console.error);