Phase 8 Production Hardening with complete governance infrastructure: - Vault integration with tiered policies (T0-T4) - DragonflyDB state management - SQLite audit ledger - Pipeline DSL and templates - Promotion/revocation engine - Checkpoint system for session persistence - Health manager and circuit breaker for fault tolerance - GitHub/Slack integrations - Architectural test pipeline with bug watcher, suggestion engine, council review - Multi-agent chaos testing framework Test Results: - Governance tests: 68/68 passing - E2E workflow: 16/16 passing - Phase 2 Vault: 14/14 passing - Integration tests: 27/27 passing Coverage: 57.6% average across 12 phases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
321 lines
9.7 KiB
TypeScript
321 lines
9.7 KiB
TypeScript
/**
|
|
* 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<string, any>;
|
|
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<string, any>;
|
|
error?: {
|
|
type: string;
|
|
message: string;
|
|
triggering_input: string;
|
|
recommended_action: string;
|
|
};
|
|
}
|
|
|
|
// =============================================================================
|
|
// Vault Client
|
|
// =============================================================================
|
|
|
|
async function getVaultSecret(path: string): Promise<Record<string, any>> {
|
|
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<AgentOutput> {
|
|
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<string, any> = {};
|
|
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<AgentOutput> {
|
|
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 <plan|analyze> \"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);
|