/** * BrainDecisionLedger — the Brain's persistent memory + learning loop. * * Every Brain decision already flows over the EventBus (`brain.decision_*`, * `brain.intervention` — emitted by `ObservableBrainArbiter` and * `BrainMonitor`). The ledger subscribes to those events, appends each * decision to a JSONL file, and — the part that makes it a LEARNING loop — * correlates decisions with their real-world outcomes using events the host * already emits, with ZERO changes to the deciders: * * - budget extensions — `subagent.budget_extended` carries subagentId + * kind, which deterministically reconstructs the director's decision id * (`director-budget--`). When that subagent's task * later lands (`subagent.task_completed`), the extension decision gets a * success/failure outcome: "we granted more budget — did it help?" * - steers — a `brain.intervention` of the same kind re-triggering within * the retry window means the previous steer did NOT fix the problem → * failure outcome for the earlier intervention. * * Outcomes are appended as their own JSONL rows, re-emitted as * `brain.outcome` events for the UI surfaces, and folded into * `digestFor(request)` — a short text block the LLM/council tiers inject * into their decision prompts so the Brain sees how its past similar calls * actually turned out ("the last 3 extends all failed — stop extending"). * * The file survives sessions: `start()` seeds the in-memory ring from the * JSONL tail, so learning carries across runs of the same project. * * @module brain-ledger */ import type { EventBus } from '../kernel/events.js'; import type { BrainArbiter, BrainDecisionRequest } from './brain.js'; export interface BrainLedgerEntry { at: number; sessionId?: string | undefined; /** `BrainDecisionRequest.id` this row belongs to. */ requestId: string; kind: 'answered' | 'denied' | 'ask_human' | 'intervention' | 'outcome'; source?: string | undefined; risk?: string | undefined; question?: string | undefined; optionId?: string | undefined; /** Rationale / deny reason / outcome detail (truncated). */ detail?: string | undefined; outcome?: 'success' | 'failure' | undefined; } export interface BrainDecisionLedgerOptions { events: EventBus; /** JSONL file the ledger appends to (e.g. `/brain-ledger.jsonl`). */ filePath: string; /** In-memory ring size (also the seed size read from disk). Default 500. */ maxMemoryEntries?: number | undefined; /** * A same-kind BrainMonitor intervention re-triggering within this window * marks the PREVIOUS steer as a failure. Default 10 minutes. */ interventionRetryWindowMs?: number | undefined; /** Override the clock for deterministic tests. */ now?: (() => number) | undefined; } /** * Group similar decisions: identical questions about different subjects * (subagent ids, task ids, counts) should share one history. Tokens that * contain a digit are id/number-shaped — collapse them to `#`. */ export declare function brainDecisionKey(source: string | undefined, question: string): string; export interface LedgerGuardBrainArbiterOptions { inner: BrainArbiter; /** Typically `BrainDecisionLedger.failureStreakFor`. */ failureStreakFor: (request: Pick) => number; /** * Deny deterministically once this many consecutive failure outcomes are * on record for the request's decision group. Default 3. 0 disables. */ denyAfter?: number | undefined; } /** * Deterministic guard derived from the ledger's outcome history: when the * last `denyAfter` approvals of a decision group ALL ended in observed * failures, the request is denied outright — no LLM call, no council, no * escalation. Deny is safe by contract at every Brain call site ("do not * take the proposed action"), so this converts repeated observed waste * (budget extensions that never help, steers that never stick) into a hard * policy rule. A later observed success resets the streak and lifts the * guard automatically. * * Position OUTSIDE the tiered arbiter (between it and the escalation * router): a guard denial must be terminal, not a fall-through to the * escalation tier where a `recommended: true` option could resurrect the * very action the history condemned. */ export declare function createLedgerGuardBrainArbiter(opts: LedgerGuardBrainArbiterOptions): BrainArbiter; export declare class BrainDecisionLedger { private readonly opts; private readonly entries; private readonly outcomeByRequest; /** subagentId → decision request ids awaiting that subagent's task result. */ private readonly pendingBudgetOutcomes; private readonly lastInterventionByKind; private readonly unsubscribers; /** Serialized fire-and-forget writes — awaited by `stop()` (drain). */ private writeChain; private pendingWriteCount; private pendingWriteBytes; private static readonly MAX_PENDING_WRITES; private static readonly MAX_PENDING_WRITE_BYTES; /** Rotate the JSONL to `.1` past this size so it can't grow unbounded. */ private static readonly MAX_FILE_BYTES; /** Stat the file for rotation only once per this many writes. */ private static readonly ROTATE_CHECK_EVERY; private writesSinceRotateCheck; private dirReady; private readonly maxMemoryEntries; private readonly retryWindowMs; private readonly now; constructor(opts: BrainDecisionLedgerOptions); /** * Subscribe to the EventBus and seed the in-memory ring from the JSONL * tail. The returned promise resolves once the seed load finished * (best-effort — a missing/corrupt file is an empty history, not an error). */ start(): Promise; /** Unsubscribe and drain pending writes. Safe to call twice. */ stop(): Promise; /** * Attach a real-world outcome to an earlier decision. Appended to the * JSONL, folded into future digests, and re-emitted as `brain.outcome`. */ recordOutcome(requestId: string, outcome: 'success' | 'failure', detail?: string, sessionId?: string): void; /** Last `n` ledger rows, oldest first. */ tail(n: number): BrainLedgerEntry[]; /** * Consecutive most-recent FAILURE outcomes among answered decisions of * this request's group. Decisions whose outcome is still unknown are * skipped (they neither extend nor break the streak); the first known * SUCCESS ends it. Powers the deterministic ledger guard: "the last K * times we approved this, it went wrong — stop approving". */ failureStreakFor(request: Pick): number; /** * Short decision-history block for the LLM/council prompt: how similar * past decisions went and — when known — how they turned OUT. Returns * undefined when there is no relevant history (no prompt noise). */ digestFor(request: Pick): string | undefined; private record; private append; /** * Size-bounded rotation: past {@link MAX_FILE_BYTES} the file moves to a * single `.1` sibling (previous `.1` is overwritten), bounding disk to * ~2× the cap. Checked once per {@link ROTATE_CHECK_EVERY} writes; runs on * the write chain, so it never races an append. The ring `load()` reads * only the tail, so rotation never costs queryable history. */ private maybeRotate; /** Seed the in-memory ring from the JSONL tail (cross-session learning). */ private load; } //# sourceMappingURL=brain-ledger.d.ts.map