/** * BrainRules — the configurable deterministic tier. * * Every deterministic decision the Brain could make used to be a hardcoded * regex or `if` buried in `DefaultBrainArbiter` / `quickDecide`. That made the * cheapest tier the least controllable one: operators could change WHEN the * expensive tiers engage (risk ceilings, council floors) but never WHAT the * free tier is able to settle on its own. The practical consequence was that * high-frequency, low-information questions — above all the BrainMonitor's * steer/continue engagements — always fell through to a provider call. * * This module adds a rule table in front of the policy tier: * * rules → policy → cache → council → LLM → escalation * * Rules are pure predicates over the request. They perform no I/O, cannot * call a provider, and evaluate first-match-wins. A rule that matches but * whose action is `defer` explicitly hands the request to the next tier, * which makes "everything EXCEPT X is deterministic" expressible. * * ## Trust * `config.brain` is on the in-project config deny list, so rules can only * come from the user's own active-profile config — never from a repo. Regex * sources are still length-capped and compiled defensively (a bad pattern * disables its own rule rather than taking the Brain down), and matching runs * against truncated inputs so a pathological pattern cannot stall a decision * indefinitely. * * @module brain-rules */ import { type BrainArbiter, type BrainDecision, type BrainDecisionRequest, type BrainDecisionSource, type BrainFallback, type BrainRisk } from './brain.js'; /** Longest regex source accepted from config. */ export declare const BRAIN_RULE_PATTERN_MAX = 1000; /** * Longest question/context prefix a rule pattern is matched against. * * Bounding the SUBJECT is the only backtracking mitigation available to a * JS regex engine without a timeout: a catastrophic pattern's blow-up is * driven by input length, so a hard cap keeps the worst case bounded. Brain * questions are short by construction and context is already truncated * upstream, so this never changes a legitimate match. */ export declare const BRAIN_RULE_SUBJECT_MAX = 8000; /** Predicate over a decision request. All present fields must match (AND). */ export interface BrainRuleMatch { /** Request source(s). Omitted = any. */ source?: BrainDecisionSource | BrainDecisionSource[] | undefined; /** Exact request risk(s). Omitted = any. */ risk?: BrainRisk | BrainRisk[] | undefined; /** Inclusive lower bound on request risk. */ minRisk?: BrainRisk | undefined; /** Inclusive upper bound on request risk. */ maxRisk?: BrainRisk | undefined; /** Caller-declared fallback(s). Omitted = any. */ fallback?: BrainFallback | BrainFallback[] | undefined; /** true = request must carry options; false = must carry none. */ hasOptions?: boolean | undefined; /** Request must offer an option with this exact id. */ offersOption?: string | undefined; /** Case-insensitive regex the question must match. */ question?: string | undefined; /** Case-insensitive regex the context must match (no context = no match). */ context?: string | undefined; /** Case-insensitive regex the question must NOT match. */ notQuestion?: string | undefined; /** Case-insensitive regex the context must NOT match (no context = passes). */ notContext?: string | undefined; } /** What a matching rule does. */ export type BrainRuleAction = { action: 'answer'; /** Must name an option the request actually offers, else the rule defers. */ optionId?: string | undefined; text?: string | undefined; rationale?: string | undefined; } | { action: 'deny'; reason?: string | undefined; } | { action: 'escalate'; prompt?: string | undefined; } /** Match, but hand the request to the next tier anyway. */ | { action: 'defer'; }; /** One deterministic rule from `config.brain.rules`. */ export interface BrainRule { /** Stable identifier, surfaced in rationales and telemetry. */ id: string; /** Default true. */ enabled?: boolean | undefined; /** Free-text note for humans; never interpreted. */ description?: string | undefined; when: BrainRuleMatch; then: BrainRuleAction; } interface CompiledMatch { sources?: ReadonlySet | undefined; risks?: ReadonlySet | undefined; minRiskLevel?: number | undefined; maxRiskLevel?: number | undefined; fallbacks?: ReadonlySet | undefined; hasOptions?: boolean | undefined; offersOption?: string | undefined; question?: RegExp | undefined; context?: RegExp | undefined; notQuestion?: RegExp | undefined; notContext?: RegExp | undefined; } export interface CompiledBrainRule { id: string; match: CompiledMatch; then: BrainRuleAction; } export interface BrainRuleCompileResult { rules: CompiledBrainRule[]; /** One entry per rejected rule: `: `. Hosts surface these; nothing throws. */ errors: string[]; } /** * Compile a rule table. Invalid rules are DROPPED with an explanatory error * rather than throwing: one bad pattern in a long-lived config must not take * the whole Brain offline, and the surviving rules are still useful. */ export declare function compileBrainRules(rules: readonly BrainRule[] | undefined): BrainRuleCompileResult; /** Does this compiled rule apply to the request? */ export declare function ruleMatches(rule: CompiledBrainRule, request: BrainDecisionRequest): boolean; /** * Turn a matching rule into a decision. Returns null when the rule defers, or * when its action cannot be honoured (an `answer` naming an option the * request does not offer) — failing open to the next tier rather than * fabricating an option id the caller would not recognise. */ export declare function applyRule(rule: CompiledBrainRule, request: BrainDecisionRequest): BrainDecision | null; /** Evaluate a compiled table; first rule that yields a decision wins. */ export declare function evaluateBrainRules(rules: readonly CompiledBrainRule[], request: BrainDecisionRequest): { decision: BrainDecision; ruleId: string; } | null; export interface RuleBrainArbiterOptions { /** Consulted when no rule decides. */ inner: BrainArbiter; /** Live rule table — read per decision so config edits apply immediately. */ getRules: () => readonly CompiledBrainRule[]; /** Observability hook fired only when a rule actually decided. */ onRuleDecision?: ((ruleId: string, request: BrainDecisionRequest, decision: BrainDecision) => void) | undefined; } /** * Deterministic rule tier. Sits in front of the policy tier so a configured * rule can settle a question before any of the tiers that cost tokens. */ export declare function createRuleBrainArbiter(opts: RuleBrainArbiterOptions): BrainArbiter; export {}; //# sourceMappingURL=brain-rules.d.ts.map