/** * BrainMonitor — the Brain's SELF-ACTIVATION layer. * * The BrainArbiter alone is reactive: subsystems (director, goal, * eternal engine) ask it questions. The monitor closes the loop the other * way — it WATCHES the live EventBus for distress signals, consults the * Brain proactively, and when the decision calls for it, INTERVENES in the * running work by delivering a corrective steer to the working agent * (steers are folded into the agent's conversation before its next step * via the mailbox loop, so no new plumbing is needed). * * Watched signals: * - tool-failure streak — the same tool failing N times consecutively * (default 3). Classic stuck-loop: the agent keeps retrying an * approach that does not work. * - error storm — N `error` events within a sliding window (default * 4 in 60s). Something is systematically wrong. * - agent stall — a run is active (`agent.run.started` without its * matching completion) but no tool call or iteration progress has been * observed for `stallMs` (default 5 min). A wedged provider call or a * dead-loop that produces no work. * - file churn — the same file edited ≥ N times (default 5) within a * sliding window (default 10 min). Classic edit/revert oscillation: * the agent keeps rewriting the same file without converging. * * Decision contract: every consultation offers [steer | continue] with * fallback `continue`, at `medium` risk. Degradation is safe by design: * - tiered brain with an LLM layer → a real judgement call, with the * LLM's rationale becoming the steer text; * - policy-only brain → fallback `continue` → observe, never interfere. * * Every engagement (whether or not it intervened) emits * `brain.intervention` for the TUI/WebUI surfaces, and is rate-limited by * a per-signal cooldown so the Brain never spams the agent. * * @module brain-monitor */ import type { BrainInterventionKind, EventBus } from '../kernel/events.js'; import type { BrainArbiter } from './brain.js'; export type { BrainInterventionKind }; export interface BrainInterventionInput { subject: string; body: string; } /** * How a detected distress signal is resolved. * * - `llm` (default) — consult the Brain. Historically the ONLY behaviour, and * an expensive one: the monitor's request carries options, `medium` risk and * `fallback: 'ask_human'`, which defeats `quickDecide` (it declines * option-bearing requests) and the low-risk policy fast path alike, so every * engagement reached a provider. Deterministic handling is available without * leaving this mode by adding a `brain.rules` entry matching * `source: 'system'` with `offersOption: 'steer'` — the rule tier runs in * front of everything that costs tokens. * - `steer` — always intervene, no Brain call at all. * - `observe` — never intervene; only emit `brain.intervention` for the * surfaces. Useful to measure how often signals fire before acting on them. */ export type BrainMonitorPolicy = 'llm' | 'steer' | 'observe'; /** Per-signal kill switches. Omitted = enabled. */ export interface BrainMonitorSignalToggles { toolFailureStreak?: boolean | undefined; errorStorm?: boolean | undefined; agentStall?: boolean | undefined; fileChurn?: boolean | undefined; } export interface BrainMonitorOptions { events: EventBus; brain: BrainArbiter; /** Active host session id, read lazily so resume/new-session switches are reflected. */ sessionId?: (() => string | undefined) | undefined; /** * Leader session id used to filter out subagent events. The BrainMonitor * subscribes to global events (tool.executed, agent.run.*, error, etc.) * which fire for BOTH the leader agent and subagents. Without this filter, * a subagent's tool failures or stalls incorrectly trigger a steer to the * leader — disrupting the leader's work for problems it didn't cause. * * When set, any event whose `sessionId` differs from this value is skipped. * Events without a `sessionId` field are always passed through (backward * compatibility). Pass a **lazy getter** when the session id may change * at runtime (session resume), or a string for static sessions. */ leaderSessionId?: string | (() => string | undefined) | undefined; /** * Deliver a corrective steer to the working agent(s). Hosts typically * send a `steer` mail to this session's leader via the project * GlobalMailbox — the agent loop injects it before the next LLM call. */ intervene: (input: BrainInterventionInput) => Promise; /** Consecutive failures of the SAME tool before engaging. Default 3. */ toolFailureStreak?: number | undefined; /** Number of `error` events within the window before engaging. Default 4. */ errorStormCount?: number | undefined; /** Sliding window for the error storm signal (ms). Default 60_000. */ errorStormWindowMs?: number | undefined; /** * Active run with no observable progress (tool call / iteration) for this * long → agent-stall signal. Default 300_000 (5 min). 0 disables. */ stallMs?: number | undefined; /** How often the stall watchdog checks (ms). Default 30_000. */ stallCheckIntervalMs?: number | undefined; /** Edits to the SAME file within the churn window before engaging. Default 5. */ fileChurnThreshold?: number | undefined; /** Sliding window for the file-churn signal (ms). Default 600_000 (10 min). */ fileChurnWindowMs?: number | undefined; /** Minimum gap between engagements of the same signal kind (ms). Default 120_000. */ cooldownMs?: number | undefined; /** Master kill switch. Default true; false makes `start()` a no-op. */ enabled?: boolean | undefined; /** How an engagement is resolved. Default 'llm'. */ policy?: BrainMonitorPolicy | undefined; /** Per-signal kill switches. Omitted signals stay enabled. */ signals?: BrainMonitorSignalToggles | undefined; /** * Tool names whose successful execution counts as a file edit for the * churn signal. Replaces the built-in set; matched case-insensitively. * Needed by hosts whose edit tools are named differently — otherwise the * churn signal silently never fires for them. */ fileEditTools?: readonly string[] | undefined; } /** Tools whose successful execution mutates a file we can churn-track. */ export declare const DEFAULT_FILE_EDIT_TOOLS: readonly string[]; export declare class BrainMonitor { private readonly opts; private readonly failStreaks; private errorTimestamps; private readonly editTimestamps; private readonly lastEngagedAt; private readonly unsubscribers; private engaging; private activeRuns; private lastProgressAt; private stallTimer; private readonly toolFailureStreak; private readonly errorStormCount; private readonly errorStormWindowMs; private readonly stallMs; private readonly stallCheckIntervalMs; private readonly fileChurnThreshold; private readonly fileChurnWindowMs; private readonly cooldownMs; private readonly enabled; private readonly policy; private readonly signals; private readonly fileEditTools; /** Resolve the leader's own session id for event filtering. */ private resolveLeaderSessionId; constructor(opts: BrainMonitorOptions); start(): void; stop(): void; /** Sliding-window count of successful edits per file → churn signal. */ private trackFileChurn; private engage; private maybeIntervene; } //# sourceMappingURL=brain-monitor.d.ts.map