Direction shift 2026-04-22: docs/CONTROL_PLANE_PRD.md becomes the long-horizon architecture target. Existing Lakehouse (docs/PRD.md, Phases 0-37) is preserved as the reference implementation and first consumer. New 6-layer architecture: L1 Universal API /v1/chat /v1/usage /v1/sessions /v1/tools /v1/context L2 Routing & Policy Engine (rules, fallback chains, cost gating) L3 Provider Adapter Layer (Ollama + OpenRouter + Gemini + Claude) L4 Knowledge + Memory + Playbooks (already built) L5 Execution Loop (scenarios + bot/cycle.ts instances) L6 Observability + token accounting Phases 38-44 sequenced with detailed per-phase specs in the PRD. Current scope: staffing domain (synthetic workers_500k, contracts, emails, SMS, playbooks). DevOps (Terraform/Ansible) is long-horizon target — architecture-compatible but not current. Files added: - docs/CONTROL_PLANE_PRD.md — 6-layer architecture, Phase 38-44 sequencing with staffing-first Truth Layer + Validation pipeline - bot/ — manual-only PR bot scaffold. First consumer test-bed for /v1/chat (Phase 38). Mem0-aligned ADD/UPDATE/NOOP apply semantics; KB feedback loop reads prior cycles on same gap and injects into cloud prompt so bot cycles compound like scenario.ts runs do. - tests/multi-agent/run_stress.ts — the 6-task diverse stress test referenced in the previous commit but missing from its staging Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
// Bot-local knowledge base. Every finished cycle already persists a
|
|
// CycleResult to data/_bot/cycles/{id}.json — that IS the outcome log.
|
|
// KB here just reads that dir, filters to prior cycles on the same gap,
|
|
// and produces a short summary the cloud model can condition on.
|
|
//
|
|
// No separate jsonl, no new write path, no embedding calls. The bot's
|
|
// "memory" is the same primary artifact that the observer consumes.
|
|
//
|
|
// Future: embedding-based neighbor matching across gaps (cheap once
|
|
// sidecar is local), cross-pollination with scenario KB's
|
|
// pathway_recommendations. Not required for the feedback loop to work
|
|
// on a single gap — that's the floor we're building first.
|
|
|
|
import { readdir, readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import type { CycleResult } from "./types.ts";
|
|
|
|
const CYCLES_DIR = "/home/profit/lakehouse/data/_bot/cycles";
|
|
|
|
export interface HistoryEntry {
|
|
cycle_id: string;
|
|
ended_at: string;
|
|
outcome: string;
|
|
reason: string;
|
|
pr_url: string | null;
|
|
tests_green: boolean | null;
|
|
files_added: string[];
|
|
files_updated: string[];
|
|
tokens_used: number;
|
|
}
|
|
|
|
export async function loadHistory(gap_id: string, max: number = 5): Promise<HistoryEntry[]> {
|
|
let entries: string[] = [];
|
|
try {
|
|
entries = await readdir(CYCLES_DIR);
|
|
} catch {
|
|
return [];
|
|
}
|
|
const matches: HistoryEntry[] = [];
|
|
for (const e of entries) {
|
|
if (!e.endsWith(".json")) continue;
|
|
try {
|
|
const raw = await readFile(join(CYCLES_DIR, e), "utf8");
|
|
const r = JSON.parse(raw) as CycleResult;
|
|
if (r.gap?.id !== gap_id) continue;
|
|
matches.push({
|
|
cycle_id: r.cycle_id,
|
|
ended_at: r.ended_at,
|
|
outcome: r.outcome,
|
|
reason: r.reason,
|
|
pr_url: r.prUrl,
|
|
tests_green: r.testsGreen,
|
|
files_added: r.filesAdded ?? [],
|
|
files_updated: r.filesUpdated ?? [],
|
|
tokens_used: r.tokens_used,
|
|
});
|
|
} catch {
|
|
// Skip unreadable / malformed cycle files. Don't fail the current
|
|
// cycle because an old one is corrupt.
|
|
}
|
|
}
|
|
matches.sort((a, b) => b.ended_at.localeCompare(a.ended_at));
|
|
return matches.slice(0, max);
|
|
}
|
|
|
|
// Compact prompt-ready summary. Empty string when there's no history —
|
|
// caller can skip the "prior attempts" block entirely.
|
|
export function summarizeHistory(h: HistoryEntry[]): string {
|
|
if (h.length === 0) return "";
|
|
const lines = h.map(e => {
|
|
const when = e.ended_at.slice(0, 16).replace("T", " ");
|
|
const files = [...e.files_added, ...e.files_updated];
|
|
const filesStr = files.length > 0 ? ` touched: ${files.join(", ")}` : "";
|
|
const prStr = e.pr_url ? ` PR: ${e.pr_url}` : "";
|
|
return `- ${when} UTC — ${e.outcome}${prStr}${filesStr}\n reason: ${e.reason}`;
|
|
});
|
|
return [
|
|
`Prior attempts on this gap (${h.length} most recent):`,
|
|
...lines,
|
|
"",
|
|
"Learn from these: build on what worked, avoid paths that failed.",
|
|
].join("\n");
|
|
}
|
|
|
|
// Aggregate stats for telemetry — lets the bot expose "% of cycles on
|
|
// this gap that landed a PR" without re-parsing the raw history.
|
|
export function statsFor(h: HistoryEntry[]): {
|
|
attempts: number;
|
|
pr_opened: number;
|
|
tests_failed: number;
|
|
proposal_rejected: number;
|
|
noop: number;
|
|
} {
|
|
return {
|
|
attempts: h.length,
|
|
pr_opened: h.filter(e => e.outcome === "ok").length,
|
|
tests_failed: h.filter(e => e.outcome === "tests_failed").length,
|
|
proposal_rejected: h.filter(e => e.outcome === "proposal_rejected").length,
|
|
noop: h.filter(e => e.outcome === "cycle_noop").length,
|
|
};
|
|
}
|