Initial commit: Agent Governance System Phase 8
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>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
@@ -0,0 +1,106 @@
|
||||
|
||||
Default to using Bun instead of Node.js.
|
||||
|
||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||
- Use `bun test` instead of `jest` or `vitest`
|
||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
||||
- Bun automatically loads .env, so don't use dotenv.
|
||||
|
||||
## APIs
|
||||
|
||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||
- `WebSocket` is built-in. Don't use `ws`.
|
||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||
- Bun.$`ls` instead of execa.
|
||||
|
||||
## Testing
|
||||
|
||||
Use `bun test` to run tests.
|
||||
|
||||
```ts#index.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("hello world", () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||
|
||||
Server:
|
||||
|
||||
```ts#index.ts
|
||||
import index from "./index.html"
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/users/:id": {
|
||||
GET: (req) => {
|
||||
return new Response(JSON.stringify({ id: req.params.id }));
|
||||
},
|
||||
},
|
||||
},
|
||||
// optional websocket support
|
||||
websocket: {
|
||||
open: (ws) => {
|
||||
ws.send("Hello, world!");
|
||||
},
|
||||
message: (ws, message) => {
|
||||
ws.send(message);
|
||||
},
|
||||
close: (ws) => {
|
||||
// handle close
|
||||
}
|
||||
},
|
||||
development: {
|
||||
hmr: true,
|
||||
console: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||
|
||||
```html#index.html
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
<script type="module" src="./frontend.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
With the following `frontend.tsx`:
|
||||
|
||||
```tsx#frontend.tsx
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
// import .css files directly and it works
|
||||
import './index.css';
|
||||
|
||||
const root = createRoot(document.body);
|
||||
|
||||
export default function Frontend() {
|
||||
return <h1>Hello, world!</h1>;
|
||||
}
|
||||
|
||||
root.render(<Frontend />);
|
||||
```
|
||||
|
||||
Then, run index.ts
|
||||
|
||||
```sh
|
||||
bun --hot ./index.ts
|
||||
```
|
||||
|
||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
||||
@@ -0,0 +1,15 @@
|
||||
# llm-planner-ts
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Status: Llm Planner Ts
|
||||
|
||||
## Current Phase
|
||||
|
||||
** NOT STARTED**
|
||||
|
||||
## Tasks
|
||||
|
||||
| Status | Task | Updated |
|
||||
|--------|------|---------|
|
||||
| ☐ | *No tasks defined* | - |
|
||||
|
||||
## Dependencies
|
||||
|
||||
*No external dependencies.*
|
||||
|
||||
## Issues / Blockers
|
||||
|
||||
*No current issues or blockers.*
|
||||
|
||||
## Activity Log
|
||||
|
||||
### 2026-01-23 23:25:09 UTC
|
||||
- **Phase**: NOT STARTED
|
||||
- **Action**: Initialized
|
||||
- **Details**: Status tracking initialized for this directory.
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "llm-planner-ts",
|
||||
"dependencies": {
|
||||
"openai": "^6.16.0",
|
||||
"redis": "^5.10.0",
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@redis/bloom": ["@redis/bloom@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-doIF37ob+l47n0rkpRNgU8n4iacBlKM9xLiP1LtTZTvz8TloJB8qx/MgvhMhKdYG+CvCY2aPBnN2706izFn/4A=="],
|
||||
|
||||
"@redis/client": ["@redis/client@5.10.0", "", { "dependencies": { "cluster-key-slot": "1.1.2" } }, "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA=="],
|
||||
|
||||
"@redis/json": ["@redis/json@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-B2G8XlOmTPUuZtD44EMGbtoepQG34RCDXLZbjrtON1Djet0t5Ri7/YPXvL9aomXqP8lLTreaprtyLKF4tmXEEA=="],
|
||||
|
||||
"@redis/search": ["@redis/search@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-3SVcPswoSfp2HnmWbAGUzlbUPn7fOohVu2weUQ0S+EMiQi8jwjL+aN2p6V3TI65eNfVsJ8vyPvqWklm6H6esmg=="],
|
||||
|
||||
"@redis/time-series": ["@redis/time-series@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-cPkpddXH5kc/SdRhF0YG0qtjL+noqFT0AcHbQ6axhsPsO7iqPi1cjxgdkE9TNeKiBUUdCaU1DbqkR/LzbzPBhg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"openai": ["openai@6.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg=="],
|
||||
|
||||
"redis": ["redis@5.10.0", "", { "dependencies": { "@redis/bloom": "5.10.0", "@redis/client": "5.10.0", "@redis/json": "5.10.0", "@redis/search": "5.10.0", "@redis/time-series": "5.10.0" } }, "sha512-0/Y+7IEiTgVGPrLFKy8oAEArSyEJkU0zvgV5xyi9NzNQ+SLZmyFbUsWIbgPcd4UdUh00opXGKlXJwMmsis5Byw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,919 @@
|
||||
/**
|
||||
* Governed LLM Agent - Full Pipeline (TypeScript/Bun)
|
||||
* ====================================================
|
||||
* Complete governance integration with DragonflyDB runtime control.
|
||||
*/
|
||||
|
||||
import OpenAI from "openai";
|
||||
import { createClient, RedisClientType } from "redis";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { $ } from "bun";
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
type AgentPhase = "BOOTSTRAP" | "PREFLIGHT" | "PLAN" | "EXECUTE" | "VERIFY" | "PACKAGE" | "REPORT" | "EXIT" | "REVOKED";
|
||||
type AgentStatus = "PENDING" | "RUNNING" | "PAUSED" | "COMPLETED" | "REVOKED" | "FAILED";
|
||||
type RevocationType = "ERROR_BUDGET_EXCEEDED" | "PROCEDURE_VIOLATION" | "FORBIDDEN_ACTION" | "HEARTBEAT_TIMEOUT" | "MANUAL";
|
||||
|
||||
interface ErrorBudget {
|
||||
max_total_errors: number;
|
||||
max_same_error_repeats: number;
|
||||
max_procedure_violations: number;
|
||||
}
|
||||
|
||||
interface InstructionPacket {
|
||||
agent_id: string;
|
||||
task_id: string;
|
||||
created_for: string;
|
||||
objective: string;
|
||||
deliverables: string[];
|
||||
constraints: {
|
||||
scope: string[];
|
||||
forbidden: string[];
|
||||
required_steps: string[];
|
||||
};
|
||||
success_criteria: string[];
|
||||
error_budget: ErrorBudget;
|
||||
escalation_rules: string[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface AgentState {
|
||||
agent_id: string;
|
||||
status: AgentStatus;
|
||||
phase: AgentPhase;
|
||||
step: string;
|
||||
started_at: string;
|
||||
last_progress_at: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
interface HandoffObject {
|
||||
task_id: string;
|
||||
previous_agent_id: string;
|
||||
revoked: boolean;
|
||||
revocation_reason: { type: string; details: string };
|
||||
last_known_state: { phase: string; step: string };
|
||||
what_was_tried: string[];
|
||||
blocking_issue: string;
|
||||
required_next_actions: string[];
|
||||
constraints_reminder: string[];
|
||||
artifacts: string[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Utilities
|
||||
// =============================================================================
|
||||
|
||||
function now(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function errorSignature(errorType: string, message: string): string {
|
||||
const normalized = (errorType + ":" + message.slice(0, 100)).toLowerCase();
|
||||
let hash = 0;
|
||||
for (let i = 0; i < normalized.length; i++) {
|
||||
const char = normalized.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return Math.abs(hash).toString(16).slice(0, 12);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Governance Manager
|
||||
// =============================================================================
|
||||
|
||||
class GovernanceManager {
|
||||
private redis!: RedisClientType;
|
||||
private lockTtl = 300;
|
||||
private heartbeatTtl = 60;
|
||||
|
||||
async connect() {
|
||||
const creds = await getVaultSecret("services/dragonfly");
|
||||
this.redis = createClient({
|
||||
url: "redis://" + creds.host + ":" + creds.port,
|
||||
password: creds.password,
|
||||
});
|
||||
await this.redis.connect();
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
await this.redis.quit();
|
||||
}
|
||||
|
||||
// Instruction Packets
|
||||
async createPacket(packet: InstructionPacket): Promise<void> {
|
||||
await this.redis.set("agent:" + packet.agent_id + ":packet", JSON.stringify(packet));
|
||||
}
|
||||
|
||||
async getPacket(agentId: string): Promise<InstructionPacket | null> {
|
||||
const data = await this.redis.get("agent:" + agentId + ":packet");
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
// State
|
||||
async setState(state: AgentState): Promise<void> {
|
||||
state.last_progress_at = now();
|
||||
await this.redis.set("agent:" + state.agent_id + ":state", JSON.stringify(state));
|
||||
}
|
||||
|
||||
async getState(agentId: string): Promise<AgentState | null> {
|
||||
const data = await this.redis.get("agent:" + agentId + ":state");
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
// Locking
|
||||
async acquireLock(agentId: string): Promise<boolean> {
|
||||
const result = await this.redis.set("agent:" + agentId + ":lock", now(), { NX: true, EX: this.lockTtl });
|
||||
return result === "OK";
|
||||
}
|
||||
|
||||
async refreshLock(agentId: string): Promise<boolean> {
|
||||
return await this.redis.expire("agent:" + agentId + ":lock", this.lockTtl);
|
||||
}
|
||||
|
||||
async releaseLock(agentId: string): Promise<void> {
|
||||
await this.redis.del("agent:" + agentId + ":lock");
|
||||
}
|
||||
|
||||
async hasLock(agentId: string): Promise<boolean> {
|
||||
return await this.redis.exists("agent:" + agentId + ":lock") === 1;
|
||||
}
|
||||
|
||||
// Heartbeat
|
||||
async heartbeat(agentId: string): Promise<void> {
|
||||
await this.redis.set("agent:" + agentId + ":heartbeat", now(), { EX: this.heartbeatTtl });
|
||||
}
|
||||
|
||||
// Errors
|
||||
async recordError(agentId: string, errorType: string, message: string): Promise<Record<string, any>> {
|
||||
const key = "agent:" + agentId + ":errors";
|
||||
const sig = errorSignature(errorType, message);
|
||||
|
||||
await this.redis.hIncrBy(key, "total_errors", 1);
|
||||
await this.redis.hIncrBy(key, "same_error:" + sig, 1);
|
||||
await this.redis.hSet(key, "last_error_signature", sig);
|
||||
await this.redis.hSet(key, "last_error_at", now());
|
||||
await this.redis.hSet(key, "last_error_type", errorType);
|
||||
await this.redis.hSet(key, "last_error_message", message.slice(0, 500));
|
||||
|
||||
return this.getErrorCounts(agentId);
|
||||
}
|
||||
|
||||
async recordViolation(agentId: string, violation: string): Promise<number> {
|
||||
const key = "agent:" + agentId + ":errors";
|
||||
await this.redis.hSet(key, "last_violation", violation);
|
||||
await this.redis.hSet(key, "last_violation_at", now());
|
||||
return this.redis.hIncrBy(key, "procedure_violations", 1);
|
||||
}
|
||||
|
||||
async getErrorCounts(agentId: string): Promise<Record<string, any>> {
|
||||
const key = "agent:" + agentId + ":errors";
|
||||
const data = await this.redis.hGetAll(key);
|
||||
|
||||
const sameErrorCounts: Record<string, number> = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (k.startsWith("same_error:")) {
|
||||
sameErrorCounts[k.replace("same_error:", "")] = parseInt(v);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total_errors: parseInt(data.total_errors || "0"),
|
||||
procedure_violations: parseInt(data.procedure_violations || "0"),
|
||||
last_error_signature: data.last_error_signature || "",
|
||||
last_error_at: data.last_error_at || "",
|
||||
same_error_counts: sameErrorCounts,
|
||||
};
|
||||
}
|
||||
|
||||
async checkErrorBudget(agentId: string): Promise<[boolean, string | null]> {
|
||||
const packet = await this.getPacket(agentId);
|
||||
if (!packet) return [false, "NO_INSTRUCTION_PACKET"];
|
||||
|
||||
const counts = await this.getErrorCounts(agentId);
|
||||
const budget = packet.error_budget;
|
||||
|
||||
if (counts.procedure_violations >= budget.max_procedure_violations) {
|
||||
return [false, "PROCEDURE_VIOLATIONS (" + counts.procedure_violations + " >= " + budget.max_procedure_violations + ")"];
|
||||
}
|
||||
|
||||
if (counts.total_errors >= budget.max_total_errors) {
|
||||
return [false, "TOTAL_ERRORS (" + counts.total_errors + " >= " + budget.max_total_errors + ")"];
|
||||
}
|
||||
|
||||
for (const [sig, count] of Object.entries(counts.same_error_counts)) {
|
||||
if (count >= budget.max_same_error_repeats) {
|
||||
return [false, "SAME_ERROR_REPEATED (" + sig + ": " + count + " >= " + budget.max_same_error_repeats + ")"];
|
||||
}
|
||||
}
|
||||
|
||||
return [true, null];
|
||||
}
|
||||
|
||||
// Task Management
|
||||
async assignAgentToTask(taskId: string, agentId: string): Promise<void> {
|
||||
await this.redis.set("task:" + taskId + ":active_agent", agentId);
|
||||
await this.redis.rPush("task:" + taskId + ":history", JSON.stringify({
|
||||
agent_id: agentId,
|
||||
assigned_at: now(),
|
||||
event: "ASSIGNED",
|
||||
}));
|
||||
}
|
||||
|
||||
// Revocation
|
||||
async revokeAgent(agentId: string, reasonType: RevocationType, details: string): Promise<void> {
|
||||
const state = await this.getState(agentId);
|
||||
if (state) {
|
||||
state.status = "REVOKED";
|
||||
state.phase = "REVOKED";
|
||||
state.notes = "Revoked: " + reasonType + " - " + details;
|
||||
await this.setState(state);
|
||||
}
|
||||
|
||||
await this.releaseLock(agentId);
|
||||
|
||||
await this.redis.rPush("revocations:ledger", JSON.stringify({
|
||||
agent_id: agentId,
|
||||
reason_type: reasonType,
|
||||
details: details,
|
||||
revoked_at: now(),
|
||||
}));
|
||||
|
||||
const packet = await this.getPacket(agentId);
|
||||
if (packet) {
|
||||
await this.redis.rPush("task:" + packet.task_id + ":history", JSON.stringify({
|
||||
agent_id: agentId,
|
||||
event: "REVOKED",
|
||||
reason: reasonType,
|
||||
revoked_at: now(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async getRecentRevocations(count: number = 50): Promise<any[]> {
|
||||
const data = await this.redis.lRange("revocations:ledger", -count, -1);
|
||||
return data.map(d => JSON.parse(d));
|
||||
}
|
||||
|
||||
// Artifacts
|
||||
async registerArtifact(taskId: string, artifactType: string, reference: string): Promise<void> {
|
||||
await this.redis.rPush("task:" + taskId + ":artifacts", JSON.stringify({
|
||||
type: artifactType,
|
||||
reference: reference,
|
||||
created_at: now(),
|
||||
}));
|
||||
}
|
||||
|
||||
async getArtifacts(taskId: string): Promise<any[]> {
|
||||
const data = await this.redis.lRange("task:" + taskId + ":artifacts", 0, -1);
|
||||
return data.map(d => JSON.parse(d));
|
||||
}
|
||||
|
||||
async hasRequiredArtifact(taskId: string, artifactType: string): Promise<boolean> {
|
||||
const artifacts = await this.getArtifacts(taskId);
|
||||
return artifacts.some(a => a.type === artifactType);
|
||||
}
|
||||
|
||||
// Handoff
|
||||
async createHandoff(handoff: HandoffObject): Promise<void> {
|
||||
await this.redis.set("handoff:" + handoff.task_id + ":latest", JSON.stringify(handoff));
|
||||
}
|
||||
|
||||
async getHandoff(taskId: string): Promise<HandoffObject | null> {
|
||||
const data = await this.redis.get("handoff:" + taskId + ":latest");
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SQLite Ledger
|
||||
// =============================================================================
|
||||
|
||||
function logToSqliteLedger(agentId: string, version: string, tier: number, action: string,
|
||||
decision: string, confidence: number, success: boolean,
|
||||
errorType?: string, errorMessage?: string) {
|
||||
const db = new Database("/opt/agent-governance/ledger/governance.db");
|
||||
const timestamp = now();
|
||||
|
||||
db.run(`
|
||||
INSERT INTO agent_actions (timestamp, agent_id, agent_version, tier, action, decision, confidence, success, error_type, error_message)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, [timestamp, agentId, version, tier, action, decision, confidence, success ? 1 : 0, errorType || null, errorMessage || null]);
|
||||
|
||||
db.close();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Governed LLM Agent
|
||||
// =============================================================================
|
||||
|
||||
class GovernedLLMAgent {
|
||||
private agentId: string;
|
||||
private model: string;
|
||||
private gov!: GovernanceManager;
|
||||
private llm!: OpenAI;
|
||||
private packet!: InstructionPacket;
|
||||
private state!: AgentState;
|
||||
private startTime!: number;
|
||||
|
||||
constructor(agentId: string, model: string = "anthropic/claude-sonnet-4") {
|
||||
this.agentId = agentId;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
private log(phase: string, message: string) {
|
||||
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
||||
console.log("[" + elapsed + "s] [" + phase + "] " + message);
|
||||
}
|
||||
|
||||
async createTask(taskId: string, objective: string, constraints?: any): Promise<void> {
|
||||
const packet: InstructionPacket = {
|
||||
agent_id: this.agentId,
|
||||
task_id: taskId,
|
||||
created_for: "Governed LLM Task",
|
||||
objective: objective,
|
||||
deliverables: ["implementation plan", "execution logs", "artifacts"],
|
||||
constraints: constraints || {
|
||||
scope: ["sandbox only"],
|
||||
forbidden: ["no prod access", "no unrecorded changes", "no direct database modifications"],
|
||||
required_steps: ["plan before execute", "verify after execute", "document assumptions"],
|
||||
},
|
||||
success_criteria: ["plan generated", "all steps documented", "artifacts registered"],
|
||||
error_budget: {
|
||||
max_total_errors: 8,
|
||||
max_same_error_repeats: 2,
|
||||
max_procedure_violations: 1,
|
||||
},
|
||||
escalation_rules: [
|
||||
"If confidence < 0.7 -> escalate",
|
||||
"If blocked > 10m -> escalate",
|
||||
"If dependencies unclear -> escalate",
|
||||
],
|
||||
created_at: now(),
|
||||
};
|
||||
await this.gov.createPacket(packet);
|
||||
}
|
||||
|
||||
async bootstrap(): Promise<[boolean, string]> {
|
||||
this.startTime = Date.now();
|
||||
|
||||
console.log("\n" + "=".repeat(70));
|
||||
console.log("GOVERNED LLM AGENT: " + this.agentId);
|
||||
console.log("Model: " + this.model);
|
||||
console.log("=".repeat(70) + "\n");
|
||||
|
||||
// Connect to governance
|
||||
this.gov = new GovernanceManager();
|
||||
await this.gov.connect();
|
||||
this.log("BOOTSTRAP", "Connected to DragonflyDB");
|
||||
|
||||
// Read revocation ledger
|
||||
const revocations = await this.gov.getRecentRevocations(50);
|
||||
this.log("BOOTSTRAP", "Read " + revocations.length + " recent revocations");
|
||||
|
||||
for (const rev of revocations) {
|
||||
if (rev.agent_id === this.agentId) {
|
||||
return [false, "AGENT_PREVIOUSLY_REVOKED: " + rev.reason_type];
|
||||
}
|
||||
}
|
||||
|
||||
// Load instruction packet
|
||||
const packet = await this.gov.getPacket(this.agentId);
|
||||
if (!packet) {
|
||||
return [false, "NO_INSTRUCTION_PACKET"];
|
||||
}
|
||||
this.packet = packet;
|
||||
this.log("BOOTSTRAP", "Loaded instruction packet for task: " + packet.task_id);
|
||||
|
||||
// Check for handoff from previous agent
|
||||
const handoff = await this.gov.getHandoff(packet.task_id);
|
||||
if (handoff && handoff.previous_agent_id !== this.agentId) {
|
||||
this.log("BOOTSTRAP", "Found handoff from: " + handoff.previous_agent_id);
|
||||
this.log("BOOTSTRAP", "Revocation reason: " + JSON.stringify(handoff.revocation_reason));
|
||||
this.log("BOOTSTRAP", "Required next actions: " + handoff.required_next_actions.join(", "));
|
||||
}
|
||||
|
||||
// Acquire lock
|
||||
if (!await this.gov.acquireLock(this.agentId)) {
|
||||
return [false, "CANNOT_ACQUIRE_LOCK"];
|
||||
}
|
||||
this.log("BOOTSTRAP", "Acquired execution lock");
|
||||
|
||||
// Initialize state
|
||||
this.state = {
|
||||
agent_id: this.agentId,
|
||||
status: "RUNNING",
|
||||
phase: "BOOTSTRAP",
|
||||
step: "initialized",
|
||||
started_at: now(),
|
||||
last_progress_at: now(),
|
||||
notes: "",
|
||||
};
|
||||
await this.gov.setState(this.state);
|
||||
|
||||
// Start heartbeat
|
||||
await this.gov.heartbeat(this.agentId);
|
||||
|
||||
// Assign to task
|
||||
await this.gov.assignAgentToTask(packet.task_id, this.agentId);
|
||||
|
||||
// Initialize LLM
|
||||
const secrets = await getVaultSecret("api-keys/openrouter");
|
||||
this.llm = new OpenAI({
|
||||
baseURL: "https://openrouter.ai/api/v1",
|
||||
apiKey: secrets.api_key,
|
||||
});
|
||||
this.log("BOOTSTRAP", "LLM client initialized");
|
||||
|
||||
return [true, "BOOTSTRAP_COMPLETE"];
|
||||
}
|
||||
|
||||
async transition(phase: AgentPhase, step: string, notes: string = ""): Promise<boolean> {
|
||||
await this.gov.heartbeat(this.agentId);
|
||||
await this.gov.refreshLock(this.agentId);
|
||||
|
||||
const [ok, reason] = await this.gov.checkErrorBudget(this.agentId);
|
||||
if (!ok) {
|
||||
this.log("REVOKE", "Error budget exceeded: " + reason);
|
||||
await this.gov.revokeAgent(this.agentId, "ERROR_BUDGET_EXCEEDED", reason!);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.state.phase = phase;
|
||||
this.state.step = step;
|
||||
this.state.notes = notes;
|
||||
await this.gov.setState(this.state);
|
||||
|
||||
this.log(phase, step + (notes ? " - " + notes : ""));
|
||||
return true;
|
||||
}
|
||||
|
||||
async reportError(errorType: string, message: string): Promise<boolean> {
|
||||
const counts = await this.gov.recordError(this.agentId, errorType, message);
|
||||
this.log("ERROR", errorType + ": " + message);
|
||||
this.log("ERROR", "Counts: total=" + counts.total_errors + ", violations=" + counts.procedure_violations);
|
||||
|
||||
const [ok, reason] = await this.gov.checkErrorBudget(this.agentId);
|
||||
if (!ok) {
|
||||
this.log("REVOKE", "Error budget exceeded: " + reason);
|
||||
await this.gov.revokeAgent(this.agentId, "ERROR_BUDGET_EXCEEDED", reason!);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async runPreflight(): Promise<boolean> {
|
||||
if (!await this.transition("PREFLIGHT", "scope_check")) return false;
|
||||
|
||||
this.log("PREFLIGHT", "Scope: " + this.packet.constraints.scope.join(", "));
|
||||
this.log("PREFLIGHT", "Forbidden: " + this.packet.constraints.forbidden.join(", "));
|
||||
this.log("PREFLIGHT", "Required steps: " + this.packet.constraints.required_steps.join(", "));
|
||||
|
||||
return await this.transition("PREFLIGHT", "complete", "All preflight checks passed");
|
||||
}
|
||||
|
||||
async runPlan(): Promise<any | null> {
|
||||
if (!await this.transition("PLAN", "generating")) return null;
|
||||
|
||||
const systemPrompt = `You are a governed infrastructure automation agent operating under strict compliance rules.
|
||||
|
||||
TASK: ${this.packet.objective}
|
||||
|
||||
CONSTRAINTS (MUST FOLLOW):
|
||||
- Scope: ${this.packet.constraints.scope.join(", ")}
|
||||
- Forbidden: ${this.packet.constraints.forbidden.join(", ")}
|
||||
- Required steps: ${this.packet.constraints.required_steps.join(", ")}
|
||||
|
||||
ESCALATION RULES:
|
||||
${this.packet.escalation_rules.join("\n")}
|
||||
|
||||
You are in the PLAN phase. Generate a comprehensive, detailed implementation plan.
|
||||
Be thorough - identify ALL steps, dependencies, risks, and assumptions.
|
||||
|
||||
Output JSON:
|
||||
{
|
||||
"title": "Plan title",
|
||||
"summary": "Brief summary",
|
||||
"confidence": 0.0-1.0,
|
||||
"complexity": "low|medium|high|very_high",
|
||||
"estimated_steps": number,
|
||||
"phases": [
|
||||
{
|
||||
"phase": "Phase name",
|
||||
"steps": [
|
||||
{
|
||||
"step": number,
|
||||
"action": "Detailed action description",
|
||||
"reasoning": "Why this step is needed",
|
||||
"dependencies": ["what must be done first"],
|
||||
"outputs": ["what this produces"],
|
||||
"reversible": boolean,
|
||||
"rollback": "How to undo if needed",
|
||||
"risks": ["potential issues"],
|
||||
"verification": "How to verify success"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"assumptions": ["explicit assumptions"],
|
||||
"uncertainties": ["things that are unclear"],
|
||||
"risks": ["overall risks"],
|
||||
"success_criteria": ["how to know we succeeded"],
|
||||
"estimated_tier_required": 0-4,
|
||||
"requires_human_review": boolean,
|
||||
"blockers": ["anything that would prevent execution"]
|
||||
}`;
|
||||
|
||||
try {
|
||||
const response = await this.llm.chat.completions.create({
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: "Create a comprehensive implementation plan for:\n\n" + this.packet.objective + "\n\nBe thorough and identify all steps, risks, and dependencies." },
|
||||
],
|
||||
max_tokens: 8000,
|
||||
temperature: 0.3,
|
||||
});
|
||||
|
||||
const llmResponse = response.choices[0].message.content || "";
|
||||
|
||||
let plan: any;
|
||||
try {
|
||||
// Try to extract JSON from markdown code blocks or raw JSON
|
||||
let jsonStr = llmResponse;
|
||||
|
||||
// Remove markdown code block wrappers if present
|
||||
const jsonBlockMatch = llmResponse.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||
if (jsonBlockMatch) {
|
||||
jsonStr = jsonBlockMatch[1].trim();
|
||||
} else if (llmResponse.includes("```json")) {
|
||||
// Handle truncated code block (no closing ```)
|
||||
const start = llmResponse.indexOf("```json") + 7;
|
||||
jsonStr = llmResponse.slice(start).trim();
|
||||
}
|
||||
|
||||
// Find the JSON object
|
||||
const jsonStart = jsonStr.indexOf("{");
|
||||
if (jsonStart < 0) {
|
||||
throw new Error("No JSON object found");
|
||||
}
|
||||
|
||||
// Try to find complete JSON, or extract what we can
|
||||
let jsonContent = jsonStr.slice(jsonStart);
|
||||
|
||||
// Attempt to repair truncated JSON by closing open structures
|
||||
let braceCount = 0;
|
||||
let bracketCount = 0;
|
||||
let inString = false;
|
||||
let lastValidPos = 0;
|
||||
|
||||
for (let i = 0; i < jsonContent.length; i++) {
|
||||
const char = jsonContent[i];
|
||||
const prev = i > 0 ? jsonContent[i - 1] : "";
|
||||
|
||||
if (char === '"' && prev !== '\\') {
|
||||
inString = !inString;
|
||||
} else if (!inString) {
|
||||
if (char === '{') braceCount++;
|
||||
else if (char === '}') braceCount--;
|
||||
else if (char === '[') bracketCount++;
|
||||
else if (char === ']') bracketCount--;
|
||||
}
|
||||
|
||||
if (braceCount === 0 && bracketCount === 0 && !inString) {
|
||||
lastValidPos = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastValidPos > 0) {
|
||||
// Found complete JSON
|
||||
plan = JSON.parse(jsonContent.slice(0, lastValidPos));
|
||||
} else {
|
||||
// JSON is truncated, try to extract key fields
|
||||
this.log("PLAN", "JSON appears truncated, extracting available fields");
|
||||
|
||||
// Extract what we can
|
||||
const titleMatch = jsonContent.match(/"title"\s*:\s*"([^"]*)"/);
|
||||
const summaryMatch = jsonContent.match(/"summary"\s*:\s*"([^"]*)"/);
|
||||
const confidenceMatch = jsonContent.match(/"confidence"\s*:\s*([\d.]+)/);
|
||||
const complexityMatch = jsonContent.match(/"complexity"\s*:\s*"([^"]*)"/);
|
||||
const stepsMatch = jsonContent.match(/"estimated_steps"\s*:\s*(\d+)/);
|
||||
|
||||
// Count phases we can find
|
||||
const phaseMatches = jsonContent.match(/"phase"\s*:\s*"[^"]*"/g) || [];
|
||||
|
||||
plan = {
|
||||
title: titleMatch ? titleMatch[1] : "Extracted Plan",
|
||||
summary: summaryMatch ? summaryMatch[1] : "Plan details extracted from truncated response",
|
||||
confidence: confidenceMatch ? parseFloat(confidenceMatch[1]) : 0.6,
|
||||
complexity: complexityMatch ? complexityMatch[1] : "high",
|
||||
estimated_steps: stepsMatch ? parseInt(stepsMatch[1]) : phaseMatches.length * 5,
|
||||
phases: phaseMatches.map((m, i) => ({
|
||||
phase: m.match(/"phase"\s*:\s*"([^"]*)"/)?.[1] || "Phase " + (i + 1),
|
||||
steps: [{ step: i + 1, action: "Step " + (i + 1), reversible: true, rollback: "Undo" }]
|
||||
})),
|
||||
_truncated: true,
|
||||
_raw_length: jsonContent.length,
|
||||
};
|
||||
}
|
||||
} catch (parseError: any) {
|
||||
this.log("PLAN", "JSON parse error: " + parseError.message);
|
||||
plan = { raw_response: llmResponse.slice(0, 500) + "...", confidence: 0.4 };
|
||||
this.log("PLAN", "Warning: Could not parse plan JSON, using raw response");
|
||||
}
|
||||
|
||||
// Register plan artifact
|
||||
await this.gov.registerArtifact(this.packet.task_id, "plan", "plan_" + this.agentId + "_" + now());
|
||||
|
||||
const confidence = plan.confidence || 0.5;
|
||||
if (confidence < 0.7) {
|
||||
this.log("PLAN", "Low confidence (" + confidence + ") - would escalate in production");
|
||||
}
|
||||
|
||||
await this.transition("PLAN", "complete", "Confidence: " + confidence);
|
||||
|
||||
logToSqliteLedger(this.agentId, "0.1.0", 0, "generate_plan", "EXECUTE", confidence, true);
|
||||
|
||||
return plan;
|
||||
|
||||
} catch (e: any) {
|
||||
await this.reportError("LLM_ERROR", e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async runExecute(plan: any): Promise<boolean> {
|
||||
// Compliance check: must have plan artifact
|
||||
if (!await this.gov.hasRequiredArtifact(this.packet.task_id, "plan")) {
|
||||
await this.gov.recordViolation(this.agentId, "EXECUTE_WITHOUT_PLAN");
|
||||
await this.gov.revokeAgent(this.agentId, "PROCEDURE_VIOLATION", "Attempted EXECUTE without plan artifact");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await this.transition("EXECUTE", "starting")) return false;
|
||||
|
||||
const phases = plan.phases || [{ steps: plan.steps || [] }];
|
||||
let totalSteps = 0;
|
||||
|
||||
for (const phase of phases) {
|
||||
const phaseName = phase.phase || "Main";
|
||||
this.log("EXECUTE", "Phase: " + phaseName);
|
||||
|
||||
const steps = phase.steps || [];
|
||||
for (const step of steps) {
|
||||
totalSteps++;
|
||||
const stepNum = step.step || totalSteps;
|
||||
const action = step.action || "Unknown action";
|
||||
|
||||
this.log("EXECUTE", " Step " + stepNum + ": " + action.slice(0, 70) + "...");
|
||||
|
||||
// Check for forbidden actions
|
||||
for (const forbidden of this.packet.constraints.forbidden) {
|
||||
if (action.toLowerCase().includes(forbidden.toLowerCase().replace("no ", ""))) {
|
||||
this.log("EXECUTE", " WARNING: Step may violate constraint: " + forbidden);
|
||||
}
|
||||
}
|
||||
|
||||
// Register step completion
|
||||
await this.gov.registerArtifact(
|
||||
this.packet.task_id,
|
||||
"step_" + stepNum,
|
||||
"executed_" + stepNum + "_" + now()
|
||||
);
|
||||
|
||||
// Simulate step execution time for realism
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
}
|
||||
|
||||
this.log("EXECUTE", "Completed " + totalSteps + " steps");
|
||||
return await this.transition("EXECUTE", "complete", totalSteps + " steps executed");
|
||||
}
|
||||
|
||||
async runVerify(): Promise<boolean> {
|
||||
if (!await this.transition("VERIFY", "checking_artifacts")) return false;
|
||||
|
||||
const artifacts = await this.gov.getArtifacts(this.packet.task_id);
|
||||
this.log("VERIFY", "Found " + artifacts.length + " artifacts");
|
||||
|
||||
// Verify we have required artifacts
|
||||
const hasPlan = artifacts.some(a => a.type === "plan");
|
||||
const hasSteps = artifacts.some(a => a.type.startsWith("step_"));
|
||||
|
||||
if (!hasPlan) {
|
||||
await this.reportError("MISSING_ARTIFACT", "No plan artifact found");
|
||||
}
|
||||
if (!hasSteps) {
|
||||
await this.reportError("MISSING_ARTIFACT", "No step artifacts found");
|
||||
}
|
||||
|
||||
return await this.transition("VERIFY", "complete", "Verified " + artifacts.length + " artifacts");
|
||||
}
|
||||
|
||||
async runPackage(): Promise<any> {
|
||||
if (!await this.transition("PACKAGE", "collecting")) return {};
|
||||
|
||||
const artifacts = await this.gov.getArtifacts(this.packet.task_id);
|
||||
const errors = await this.gov.getErrorCounts(this.agentId);
|
||||
|
||||
const pkg = {
|
||||
agent_id: this.agentId,
|
||||
task_id: this.packet.task_id,
|
||||
objective: this.packet.objective,
|
||||
artifacts_count: artifacts.length,
|
||||
artifacts: artifacts.slice(0, 10), // First 10
|
||||
error_counts: errors,
|
||||
completed_at: now(),
|
||||
};
|
||||
|
||||
await this.gov.registerArtifact(this.packet.task_id, "package", "package_" + now());
|
||||
await this.transition("PACKAGE", "complete");
|
||||
|
||||
return pkg;
|
||||
}
|
||||
|
||||
async runReport(pkg: any, plan: any): Promise<any> {
|
||||
if (!await this.transition("REPORT", "generating")) return {};
|
||||
|
||||
const report = {
|
||||
agent_id: this.agentId,
|
||||
task_id: pkg.task_id,
|
||||
model: this.model,
|
||||
status: "COMPLETED",
|
||||
objective: pkg.objective,
|
||||
plan_summary: plan.summary || "Plan generated",
|
||||
plan_confidence: plan.confidence || 0,
|
||||
plan_complexity: plan.complexity || "unknown",
|
||||
total_phases: (plan.phases || []).length,
|
||||
total_steps: pkg.artifacts_count - 2, // Subtract plan and package
|
||||
artifacts_generated: pkg.artifacts_count,
|
||||
errors_encountered: pkg.error_counts.total_errors,
|
||||
procedure_violations: pkg.error_counts.procedure_violations,
|
||||
assumptions: plan.assumptions || [],
|
||||
risks_identified: (plan.risks || []).length,
|
||||
requires_human_review: plan.requires_human_review || false,
|
||||
estimated_tier_required: plan.estimated_tier_required || "unknown",
|
||||
elapsed_seconds: ((Date.now() - this.startTime) / 1000).toFixed(1),
|
||||
timestamp: now(),
|
||||
};
|
||||
|
||||
await this.transition("REPORT", "complete");
|
||||
return report;
|
||||
}
|
||||
|
||||
async finish(report: any): Promise<void> {
|
||||
this.state.status = "COMPLETED";
|
||||
this.state.phase = "EXIT";
|
||||
this.state.notes = "Task completed successfully";
|
||||
await this.gov.setState(this.state);
|
||||
await this.gov.releaseLock(this.agentId);
|
||||
|
||||
this.log("EXIT", "Agent completed successfully");
|
||||
|
||||
console.log("\n" + "=".repeat(70));
|
||||
console.log("FINAL REPORT");
|
||||
console.log("=".repeat(70));
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
console.log("=".repeat(70) + "\n");
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.gov) {
|
||||
await this.gov.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
async run(): Promise<any> {
|
||||
try {
|
||||
// Bootstrap
|
||||
const [ok, msg] = await this.bootstrap();
|
||||
if (!ok) {
|
||||
console.error("Bootstrap failed: " + msg);
|
||||
return { status: "FAILED", reason: msg };
|
||||
}
|
||||
|
||||
// Preflight
|
||||
if (!await this.runPreflight()) {
|
||||
return { status: "FAILED", reason: "PREFLIGHT_FAILED" };
|
||||
}
|
||||
|
||||
// Plan
|
||||
const plan = await this.runPlan();
|
||||
if (!plan) {
|
||||
return { status: "FAILED", reason: "PLAN_FAILED" };
|
||||
}
|
||||
|
||||
console.log("\n" + "-".repeat(70));
|
||||
console.log("GENERATED PLAN");
|
||||
console.log("-".repeat(70));
|
||||
console.log(JSON.stringify(plan, null, 2));
|
||||
console.log("-".repeat(70) + "\n");
|
||||
|
||||
// Execute
|
||||
if (!await this.runExecute(plan)) {
|
||||
return { status: "FAILED", reason: "EXECUTE_FAILED" };
|
||||
}
|
||||
|
||||
// Verify
|
||||
if (!await this.runVerify()) {
|
||||
return { status: "FAILED", reason: "VERIFY_FAILED" };
|
||||
}
|
||||
|
||||
// Package
|
||||
const pkg = await this.runPackage();
|
||||
|
||||
// Report
|
||||
const report = await this.runReport(pkg, plan);
|
||||
|
||||
// Finish
|
||||
await this.finish(report);
|
||||
|
||||
return report;
|
||||
|
||||
} finally {
|
||||
await this.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CLI
|
||||
// =============================================================================
|
||||
|
||||
async function createInstructionPacket(gov: GovernanceManager, agentId: string, taskId: string, objective: string): Promise<void> {
|
||||
const packet: InstructionPacket = {
|
||||
agent_id: agentId,
|
||||
task_id: taskId,
|
||||
created_for: "Governed LLM Task",
|
||||
objective: objective,
|
||||
deliverables: ["implementation plan", "execution logs", "artifacts"],
|
||||
constraints: {
|
||||
scope: ["sandbox only"],
|
||||
forbidden: ["no prod access", "no unrecorded changes", "no direct database modifications"],
|
||||
required_steps: ["plan before execute", "verify after execute", "document assumptions"],
|
||||
},
|
||||
success_criteria: ["plan generated", "all steps documented", "artifacts registered"],
|
||||
error_budget: {
|
||||
max_total_errors: 8,
|
||||
max_same_error_repeats: 2,
|
||||
max_procedure_violations: 1,
|
||||
},
|
||||
escalation_rules: [
|
||||
"If confidence < 0.7 -> escalate",
|
||||
"If blocked > 10m -> escalate",
|
||||
"If dependencies unclear -> escalate",
|
||||
],
|
||||
created_at: now(),
|
||||
};
|
||||
await gov.createPacket(packet);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 3) {
|
||||
console.log("Usage: bun run governed-agent.ts <agent_id> <task_id> \"<objective>\"");
|
||||
console.log(" bun run governed-agent.ts <agent_id> <task_id> \"<objective>\" --model <model>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const agentId = args[0];
|
||||
const taskId = args[1];
|
||||
const objective = args[2];
|
||||
|
||||
let model = "anthropic/claude-sonnet-4";
|
||||
const modelIdx = args.indexOf("--model");
|
||||
if (modelIdx !== -1 && args[modelIdx + 1]) {
|
||||
model = args[modelIdx + 1];
|
||||
}
|
||||
|
||||
// Connect to governance and create instruction packet
|
||||
const gov = new GovernanceManager();
|
||||
await gov.connect();
|
||||
await createInstructionPacket(gov, agentId, taskId, objective);
|
||||
await gov.disconnect();
|
||||
|
||||
// Create and run agent
|
||||
const agent = new GovernedLLMAgent(agentId, model);
|
||||
const result = await agent.run();
|
||||
|
||||
process.exit(result.status === "COMPLETED" ? 0 : 1);
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error("Fatal error:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* 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);
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "llm-planner-ts",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"openai": "^6.16.0",
|
||||
"redis": "^5.10.0",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user