/** * AutonomyBrain — a self-driving decision layer for autonomous workflows. * * Unlike the standard BrainArbiter which asks the human when uncertain, * AutonomyBrain makes decisions autonomously within configured risk * boundaries, keeping the system running unattended. It uses the session * LLM to evaluate situations and produce decisions. * * ## Identity * The AutonomyBrain is NOT the main agent. It is a dedicated decision * engine with a single purpose: evaluate blocked/stuck situations in * autonomous workflows and decide whether to continue, pivot, or stop. * * ## Decision Flow * 1. RISK GATE — if request risk > maxAutoRisk, auto-deny * 2. HEURISTIC — fast pattern-match for common situations (deadlock, retry-exhausted) * 3. LLM EVALUATION — complex decisions (goal completion, conflict resolution) * * ## Decision Logging * Every decision is emitted via `onDecision` callback with a human-readable * summary suitable for chat history and journal entries. * * Usage: * const brain = createAutonomyBrain({ * provider, model, maxAutoRisk: 'high', * onDecision: (summary) => journal.push(summary), * }); */ import type { Provider, Usage } from '../types/provider.js'; import { type BrainArbiter, type BrainDecision, type BrainDecisionRequest } from '../coordination/brain.js'; import { type BrainHeuristicsConfig } from '../coordination/brain-heuristics.js'; import type { BrainCircuitBreaker } from './brain-circuit.js'; import type { EventBus } from '../kernel/events.js'; /** One (provider, model) pair the Brain may call for a decision. */ export interface BrainLlmTarget { provider: Provider; model: string; /** Display label for logs/status (e.g. "anthropic/claude-haiku"). */ label?: string | undefined; } export interface AutonomyBrainOptions { /** LLM provider for decision-making. Ignored when `targets` is non-empty. */ provider?: Provider | undefined; /** Model to use for decisions (should be fast + cheap). Ignored when `targets` is non-empty. */ model?: string | undefined; /** * Ordered LLM pool. With `strategy: 'fallback'` (default) the first target * is primary and later ones are tried in order when a call fails/times * out; with 'round-robin' successive decisions rotate the starting target. * At least one of `targets` / (`provider` + `model`) must be provided. */ targets?: BrainLlmTarget[] | undefined; /** Pool selection strategy. Default 'fallback'. */ strategy?: 'fallback' | 'round-robin' | undefined; /** Maximum risk level the brain will auto-decide. Default: 'high'. * 'low' — only auto-decide low-risk questions * 'medium' — auto-decide low/medium * 'high' — auto-decide low/medium/high * 'all' — auto-decide everything (including critical) */ maxAutoRisk?: 'low' | 'medium' | 'high' | 'all' | undefined; /** Timeout for each decision call (ms). Default: 15_000. */ decisionTimeoutMs?: number | undefined; /** Per-heuristic toggles for `quickDecide`. Omitted fields default to enabled. */ heuristics?: BrainHeuristicsConfig | undefined; /** * Bus for `brain.llm_call` trace events — one per pool target per decision, * including the failures the fallback loop otherwise swallows. */ events?: EventBus | undefined; /** Include raw response text in trace events. Off by default (production content). */ traceContent?: boolean | undefined; /** Output budget per decision call. Default `DEFAULT_BRAIN_MAX_TOKENS` (200). */ maxTokens?: number | undefined; /** * Reject responses in which the model declined to decide ("I don't know", * "insufficient evidence", empty text) instead of presenting them as an * answer. Default true — an empty or hedging response is not a decision. */ rejectUncertain?: boolean | undefined; /** * Reject answers whose self-reported `confidence` is below this (0..1). * Default 0 = off. Responses that report no confidence are never rejected * by this gate. */ minConfidence?: number | undefined; /** * Failure memory for the pool. When the breaker is open the tier is skipped * outright instead of paying `pool.length × decisionTimeoutMs` again for a * conclusion the previous decision already reached. */ circuit?: BrainCircuitBreaker | undefined; /** * Decision-history digest for the LLM prompt (typically * `BrainDecisionLedger.digestFor`): how similar past decisions went and * how they turned out. Appended to the user message when non-empty. */ getDecisionDigest?: ((request: BrainDecisionRequest) => string | undefined) | undefined; /** * Called after every decision with a human-readable summary. * Use this to log decisions into chat history, journal, or status line. * Example: "🧠 Brain: skipped deadlocked tasks → continuing with phase 3/5" */ onDecision?: ((summary: string, decision: BrainDecision, request: BrainDecisionRequest) => void) | undefined; } /** Runtime-adjustable autonomy ceiling for the tiered brain. */ export type BrainAutoRisk = 'off' | 'low' | 'medium' | 'high' | 'all'; /** * Resolve an autonomy ceiling to a level comparable against `RISK_LEVELS`. * * `RISK_LEVELS` is keyed by REQUEST risk (`low|medium|high|critical`), but a * ceiling is keyed by `BrainAutoRisk`, whose `'off'` and `'all'` values have * no entry there. Looking a ceiling up directly therefore silently produced * the fallback level — `'all'` resolved to `2` (= `high`), so * `createAutonomyBrain({ maxAutoRisk: 'all' })` auto-denied every `critical` * request despite being explicitly configured as permissive (see * `assembleBrainTiers`, which passes `'all'` precisely to keep the inner tier * ungated and let the tiered ceiling do the gating). * * Both the tiered arbiter and the single-LLM brain now share this one * resolver so the two ladders cannot drift apart again. * * @returns `-1` for `'off'` (nothing is auto-decidable), `3` for `'all'` * (including `critical`), otherwise the matching `RISK_LEVELS` entry. */ export declare function resolveRiskCeiling(ceiling: BrainAutoRisk | undefined): number; export interface TieredBrainArbiterOptions { /** Fast deterministic policy layer (DefaultBrainArbiter). Consulted first. */ policy: BrainArbiter; /** LLM-backed autonomous layer (createAutonomyBrain). Consulted when the * policy layer would escalate to the human and the request's risk is * within the live ceiling. */ autonomous?: BrainArbiter | undefined; /** * Live autonomy ceiling — read on EVERY decision so `/brain risk ` * changes take effect immediately. 'off' disables the autonomous layer * entirely (everything the policy can't answer goes to the human). */ getMaxAutoRisk?: (() => BrainAutoRisk) | undefined; /** * Multi-LLM council (createCouncilBrainArbiter). When present, requests at * or above the council risk floor are decided by the council INSTEAD of * the single-LLM layer. Council answers AND denies are terminal — a panel * that considered the question and refused is a real decision, not a * failure. Only `ask_human` (quorum not met / judge unavailable) falls * through to the escalation tier. */ council?: BrainArbiter | undefined; /** Live council risk floor. Default 'high'. Read on every decision. */ getCouncilMinRisk?: (() => 'medium' | 'high' | 'critical') | undefined; /** * Whether an LLM-tier `deny` ends the decision. Read per decision. * Default 'never' — see `BrainConfig.llm.denyIsTerminal`. */ getDenyIsTerminal?: (() => 'never' | 'when-decided' | 'always') | undefined; /** * Bus for `brain.tier_transition` events — one per tier the ladder ran, * recording what it returned and why the chain did or did not stop there. * Without it a decision's path through the ladder is unrecoverable. */ events?: EventBus | undefined; } /** * The standard Brain positioning: policy first, LLM/council second, * escalation last. * * 1. POLICY — deterministic DefaultBrainArbiter (low-risk fast path, * fallback semantics). Denies and option-backed answers pass through * untouched. A fallback-produced `continue` answer (no optionId, the * request declared `fallback: 'continue'`) is only PROVISIONAL: it * means "nobody could decide", not "this is the right call", so the * LLM tier still gets consulted within the ceiling. Historically it * short-circuited here, which meant e.g. goal-completion checks never * reached the LLM at all. * 2. COUNCIL — requests at/above the council floor (default 'high') are * decided by the multi-LLM council when one is wired. * 3. LLM — everything else within the live ceiling goes to the * single-LLM autonomous brain. Only a real `answer` short-circuits; * denials/failures fall through. * 4. ESCALATION — anything left escalates. Callers wrap this in * `EscalationRoutingBrainArbiter` (interactive prompt or headless * terminal policy) or the legacy `HumanEscalatingBrainArbiter`. */ export declare function createTieredBrainArbiter(opts: TieredBrainArbiterOptions): BrainArbiter; /** * Create a self-driving brain that makes autonomous decisions. * Never asks the human — within its risk boundary it answers, above it denies. */ export declare function createAutonomyBrain(opts: AutonomyBrainOptions): BrainArbiter; /** * Format a decision as a human-readable one-liner for chat history. */ export declare function formatDecisionSummary(decision: BrainDecision, request: BrainDecisionRequest): string; /** * Fast heuristic decisions that don't need an LLM call. * * Deliberately narrow: heuristics never fire on option-bearing requests * (options are control-plane input demanding a structured choice, not a * keyword guess) and the continue fast-path only fires when the caller * itself declared continue the safe fallback AND the question offers no * alternative — "Should we continue or stop?" must reach the LLM. */ export declare function quickDecide(request: BrainDecisionRequest, heuristics?: BrainHeuristicsConfig | undefined): BrainDecision | null; /** * Render a decision request as the user message for a Brain LLM call. * Shared by the single-LLM tier and every council voter so all of them see * the same question/context/options shape. */ export declare function buildBrainUserMessage(request: BrainDecisionRequest): string; /** Append a decision-history digest to a Brain user message (shared shape). */ export declare function withDecisionDigest(user: string, digest: string | undefined): string; /** * One Brain LLM call against a single target. Throws on transport failure, * timeout, or abort — callers own the pool/fallback semantics. */ export declare function completeBrainLlm(target: BrainLlmTarget, input: { system: string; user: string; timeoutMs: number; maxTokens?: number | undefined; }): Promise; /** Text plus the observable call metadata a trace/quality gate needs. */ interface BrainLlmCallResult { text: string; usage?: Usage | undefined; /** * `'max_tokens'` means the response was CUT OFF. With the default 200-token * budget a rationale can be truncated mid-sentence, and the free-text path * would happily turn that fragment into a decision — so callers must be * able to see it. */ stopReason?: string | undefined; } /** * One Brain LLM call, returning the metadata `completeBrainLlm` discards. * * Usage was previously thrown away at this boundary, which is why the council * adapter reported hardcoded zero tokens for every seat and why "what do * Brain decisions cost" had no answer. */ export declare function completeBrainLlmDetailed(target: BrainLlmTarget, input: { system: string; user: string; timeoutMs: number; maxTokens?: number | undefined; }): Promise; /** * Output budget for a Brain decision call. * * Deliberately small — a Brain response is one decision plus a one-sentence * rationale, not prose. Raise it via `brain.llm.maxTokens` if truncation * shows up in the trace as `stopReason: 'max_tokens'`. */ export declare const DEFAULT_BRAIN_MAX_TOKENS = 200; /** * Ask the LLM pool for a decision on complex questions. Targets are tried * in the given order; the first one that answers wins. Uses a carefully * crafted system prompt that establishes the brain's identity, purpose, * and decision-making framework. */ type BrainLlmDenyKind = /** No target in the pool produced a response (transport/timeout). */ 'unavailable' /** A response came back but carried no exact valid option id. */ | 'unparseable' /** The model considered the question and refused. */ | 'refused'; /** * Read the reason a Brain LLM tier denied, when known. Returns undefined for * decisions that did not come from `llmDecide` (policy denials, council * denials, ledger-guard denials), which callers should treat as decided. */ export declare function readLlmDenyKind(decision: BrainDecision): BrainLlmDenyKind | undefined; /** Read a self-reported confidence out of a raw response, if it carries one. */ export declare function extractConfidence(rawText: string): number | undefined; /** Does this free-text response decline to decide? */ export declare function isNonAnswer(text: string): boolean; /** Structured envelope a Brain LLM may return for an optionless question. */ interface BrainFreeTextEnvelope { decision: string; rationale?: string | undefined; confidence?: number | undefined; } /** * Parse the optionless response. * * Accepts the structured envelope the prompt now asks for, and falls back to * bare prose so older/simpler models keep working. Returns null when the * model declined to decide — the caller must then fall through rather than * present the refusal as an answer. */ export declare function parseFreeTextDecision(rawText: string): BrainFreeTextEnvelope | null; export declare function parseOptionDecision(rawText: string, options: NonNullable): BrainDecision | null; export {}; //# sourceMappingURL=autonomy-brain.d.ts.map