/** * BrainRuntime — the live, rebuildable owner of `config.brain`. * * The Brain chain used to be assembled once at boot; every knob except the * risk ceiling and escalation mode was frozen until restart. This module * makes the whole `BrainConfig` surface live-editable AND persistable from * any host surface (/brain subcommands, TUI Brain panel, WebUI BrainSection): * * - `arbiter` is a STABLE delegating handle (`decide` reads a mutable * `current`), so hosts wrap it once in their EscalationRouting/Observable * layers and no consumer ever needs a rebind after a settings change. * - `apply(patch)` normalizes + validates the patch, commits it, rebuilds * the tier chain when a structural knob changed (pool, council, timeout, * ledger guard), and persists through an injected callback — core never * touches config files itself. * - `getSnapshot()` is the JSON-safe truth both UIs render: configured * values plus derived facts (resolved pool/council labels, EFFECTIVE * council enablement, session-model fallback). * * Persistence contract: `config.brain` is on the in-project deny list, so * the injected `persist` MUST write the GLOBAL config. A persist failure * never rolls back the live change; it is reported via the returned promise. * * @module brain-runtime */ import { type BrainArbiter, type BrainDecisionRequest, type BrainEscalationMode, type BrainTerminalPolicy } from '../coordination/brain.js'; import type { BrainHeuristicsConfig } from '../coordination/brain-heuristics.js'; import type { EventBus } from '../kernel/events.js'; import { type BrainRule } from '../coordination/brain-rules.js'; import type { BrainConfig, BrainCouncilVoterConfig, BrainModelEntry } from '../types/config.js'; import type { Provider } from '../types/provider.js'; import { type BrainAutoRisk } from './autonomy-brain.js'; export type BrainCouncilMinRisk = 'medium' | 'high' | 'critical'; export type BrainPoolStrategy = 'fallback' | 'round-robin'; /** JSON-safe view of the live Brain configuration + derived facts. */ export interface BrainConfigSnapshot { mode: BrainEscalationMode; maxAutoRisk: BrainAutoRisk; /** Configured pool entries (normalized). Empty = session model. */ models: BrainModelEntry[]; strategy: BrainPoolStrategy; decisionTimeoutMs: number | undefined; humanTimeoutMs: number | undefined; council: { /** EFFECTIVE enablement (default rule `voters >= 2` applied). */ enabled: boolean; /** Raw configured value (undefined = default rule decides). */ configured: boolean | undefined; minRisk: BrainCouncilMinRisk; /** Explicitly configured voters; seats derived from the pool are visible via `councilLabels`. */ voters: BrainCouncilVoterConfig[]; quorum: number | undefined; approval: number | undefined; judge: BrainModelEntry | undefined; perCallTimeoutMs: number | undefined; maxConcurrency: number | undefined; distinctness: 'none' | 'model' | 'provider'; judgeMaxTokens: number | undefined; seats: Array<{ persona: string; veto?: boolean | undefined; }>; }; ledger: { enabled: boolean; autoDenyAfterFailures: number | undefined; path: string | undefined; maxMemoryEntries: number | undefined; interventionRetryWindowMs: number | undefined; }; /** Configured deterministic rules, in evaluation order. */ rules: BrainRule[]; /** Effective single-LLM quality gate. */ llm: { maxTokens: number; rejectUncertain: boolean; minConfidence: number; denyIsTerminal: 'never' | 'when-decided' | 'always'; }; /** Effective replay-trace settings. */ trace: { enabled: boolean; content: 'none' | 'redacted' | 'full'; path: string | undefined; }; /** Configured monitor overrides (defaults are applied by BrainMonitor itself). */ monitor: NonNullable; /** Effective headless escalation variant. */ terminalPolicy: BrainTerminalPolicy; /** Effective rolling decision-log size. */ decisionLogMaxEntries: number; /** Live LLM circuit-breaker state, when a breaker is wired. */ circuit: { state: string; consecutiveFailures: number; } | undefined; /** Effective decision-cache settings plus live hit/miss counters. */ cache: { enabled: boolean; ttlMs: number; maxEntries: number; hits: number; misses: number; size: number; }; /** Effective heuristic toggles (defaults filled in). */ heuristics: { lowRiskAutoAnswer: boolean; blockedResolved: boolean; deadlockSkip: boolean; retryExhausted: boolean; continuePing: boolean; blockedResolvedMarkers: string[] | undefined; }; /** * Compile diagnostics from the LAST assembly, one per dropped rule. Empty * when every configured rule compiled. Surfaced by the settings UIs so a * typo'd pattern is visible instead of silently inert. */ ruleErrors: string[]; /** Resolved pool labels from the LAST assembly (may be fewer than `models` — unresolvable refs are skipped). */ poolLabels: string[]; /** Resolved council seat labels; empty = council effectively disabled. */ councilLabels: string[]; /** * EFFECTIVE council judge, undefined when no council is wired. * * Distinct from `council.judge`, which is the CONFIGURED one and is usually * absent — the judge is then derived from the pool. Since the judge only * runs to break a tie or synthesize a split panel, whether it is one of the * seats that produced that tie is the difference between an independent * tie-breaker and voter #1 winning twice. Surfacing it is what makes that * checkable instead of implicit. */ judgeLabel: string | undefined; usingSessionModel: boolean; } /** Council sub-patch. Arrays REPLACE; `null` clears back to the default. */ export interface BrainCouncilPatch { enabled?: boolean | null | undefined; minRisk?: BrainCouncilMinRisk | null | undefined; voters?: Array | null | undefined; quorum?: number | null | undefined; approval?: number | null | undefined; judge?: string | BrainModelEntry | null | undefined; perCallTimeoutMs?: number | null | undefined; maxConcurrency?: number | null | undefined; distinctness?: 'none' | 'model' | 'provider' | null | undefined; judgeMaxTokens?: number | null | undefined; seats?: Array<{ persona: string; veto?: boolean | undefined; }> | null | undefined; } /** * Partial update for the live Brain config. Omitted fields are untouched, * `null` clears a field back to its default, arrays replace wholesale. */ export interface BrainConfigPatch { mode?: BrainEscalationMode | undefined; maxAutoRisk?: BrainAutoRisk | undefined; models?: Array | null | undefined; strategy?: BrainPoolStrategy | null | undefined; decisionTimeoutMs?: number | null | undefined; humanTimeoutMs?: number | null | undefined; /** Replaces the whole table; `null` clears it. Rejected wholesale if any rule is invalid. */ rules?: BrainRule[] | null | undefined; /** Merged field-by-field; `null` clears the whole block back to all-defaults. */ heuristics?: BrainHeuristicsConfig | null | undefined; /** Single-LLM tier quality gate. Merged field-by-field; `null` clears it. */ llm?: BrainConfig['llm'] | null | undefined; /** Replay trace. Merged field-by-field; `null` clears it. */ trace?: BrainConfig['trace'] | null | undefined; /** Headless escalation variant. */ terminalPolicy?: BrainTerminalPolicy | null | undefined; /** Rolling decision-log size for `/brain status`. */ decisionLogMaxEntries?: number | null | undefined; /** Decision cache. Merged field-by-field; `null` clears it. */ cache?: BrainConfig['cache'] | null | undefined; /** * Monitor thresholds. Merged field-by-field; `null` clears it. NOTE the * BrainMonitor is constructed once at boot, so these take effect on the * next session rather than live — unlike every other patch field. */ monitor?: BrainConfig['monitor'] | null | undefined; council?: BrainCouncilPatch | null | undefined; ledger?: { enabled?: boolean | undefined; autoDenyAfterFailures?: number | null | undefined; maxMemoryEntries?: number | null | undefined; interventionRetryWindowMs?: number | null | undefined; } | null | undefined; } /** Host-owned ledger controller — the runtime never touches ledger files. */ export interface BrainRuntimeLedgerHost { getPath: () => string | undefined; isEnabled: () => boolean; /** Host starts/stops its BrainDecisionLedger instance. */ setEnabled: (enabled: boolean) => void; failureStreakFor?: ((request: Pick) => number) | undefined; getDecisionDigest?: ((request: BrainDecisionRequest) => string | undefined) | undefined; } export interface BrainRuntimeOptions { /** Boot-time `config.brain`. Invalid entries are dropped leniently here (apply() is strict). */ initialConfig: BrainConfig | undefined; /** The session's provider id (`config.provider`). */ defaultProviderId: string; /** Live session provider — read per decision so `/setmodel` switches apply. */ sessionProvider: () => Provider; /** Live session model id — read per decision. */ sessionModel: () => string; /** Resolve a NON-default provider id. May throw / return null → entry skipped. */ resolveProvider: (providerId: string, model: string) => Provider | null; ledger?: BrainRuntimeLedgerHost | undefined; /** * Injected writer for the canonical `BrainConfig`. MUST target the global * config (`config.brain` is denied in project scope). Absent = live-only. */ persist?: ((config: BrainConfig) => Promise) | undefined; /** Fired after every successful apply, with the fresh snapshot. */ onApplied?: ((snapshot: BrainConfigSnapshot) => void) | undefined; /** Bus for Brain trace events. Absent = no LLM/council tracing. */ events?: EventBus | undefined; } export interface BrainApplyResult { snapshot: BrainConfigSnapshot; /** Resolves `{ok: true}` immediately when persistence was skipped or absent. */ persisted: Promise<{ ok: boolean; error?: string | undefined; }>; } export interface BrainRuntime { /** STABLE arbiter handle — the inner tier chain swaps on `apply`. */ arbiter: BrainArbiter; getMode(): BrainEscalationMode; getMaxAutoRisk(): BrainAutoRisk; getHumanTimeoutMs(): number | undefined; getSnapshot(): BrainConfigSnapshot; /** Canonical shape written to `config.brain` (compact string refs where lossless). */ getConfig(): BrainConfig; /** Live-apply synchronously; persistence (default on) is async best-effort. */ apply(patch: BrainConfigPatch, opts?: { persist?: boolean | undefined; }): BrainApplyResult; } export interface BrainDefaultsContext { /** The session's fallback chain (`config.fallbackModels`) — seeds the default pool. */ fallbackModels?: readonly string[] | undefined; } /** * Product defaults for hosts that want a minimum-human Brain out of the box * (CLI/TUI wiring and the standalone WebUI server both apply this before * `createBrainRuntime`). Only FILLS GAPS — every explicitly configured field * wins, so existing `config.brain` blocks are untouched by updates. * * - `models` → the user's own `fallbackModels` chain (never hardcoded * model ids; every install has different providers). With ≥2 entries the * council auto-derives from the pool, so multi-model users get a council * by default. * - `mode` → 'headless': decisions never block on a human; the terminal * policy (safe default / deny) is the escalation of last resort. * - `maxAutoRisk` → adaptive: 'all' when a council can convene (critical * questions get a multi-model panel), otherwise 'high' (critical * questions resolve via the conservative terminal policy instead of a * single unchecked model). * - `humanTimeoutMs` → 120s: if the user explicitly switches back to * 'interactive', an unanswered prompt still auto-resolves instead of * hanging an unattended run forever. Set `humanTimeoutMs: 0` to restore * the legacy wait-indefinitely behavior. * * NOTE deliberately NOT persisted anywhere — resolved at boot, so existing * users pick these up on update without any config migration/write. */ export declare function resolveBrainConfigDefaults(brain: BrainConfig | undefined, ctx?: BrainDefaultsContext): BrainConfig; export declare function createBrainRuntime(opts: BrainRuntimeOptions): BrainRuntime; //# sourceMappingURL=brain-runtime.d.ts.map