import type { Agent } from '../core/agent.js'; import type { EventBus } from '../kernel/events.js'; import type { Logger } from '../types/logger.js'; import type { Compactor } from '../types/compactor.js'; import type { BrainArbiter } from '../coordination/brain.js'; import { type JournalEntry } from '../storage/goal-store.js'; /** * Sense-decide-execute-reflect loop on top of a long-running Goal. * * Each iteration: * 1. Sense — read goal, pending todos, `git status --porcelain`. * 2. Decide — pick a source (todo / git / brainstorm) and a task. * 3. Execute — single agent.run with a directive prompt. * 4. Reflect — append a journal entry, persist state to disk. * * The loop runs forever until `stop()` is called externally (REPL SIGINT * handler, /autonomy stop). No internal time/cost cap by design — the * user wants "sittin sene". Failures are logged and the loop continues * with a different source on the next tick. */ export interface EternalAutonomyOptions { agent: Agent; projectRoot: string; /** * Per-iteration agent timeout. Defaults to 5 minutes. A single hung * provider call should not freeze the whole eternal loop. */ iterationTimeoutMs?: number | undefined; /** * Maximum number of internal agent.run iterations the engine grants per * eternal-loop tick. The engine sets `autonomousContinue: true` so the * agent can run multi-step tasks end-to-end within one tick instead of * bouncing back to the engine after every single tool call. Default 500. * Previous 50 was far too low — agents hit the limit and restart constantly. */ iterationMaxAgentSteps?: number | undefined; /** * Minimum sleep between iterations. Defaults to 1 s — enough for * SIGINT handlers to fire mid-loop without pegging a core when the * provider is being rate-limited. */ cycleGapMs?: number | undefined; /** * Maximum consecutive failures before the source rotation forces a * brainstorm cycle. Default 3. Acts as a soft-recovery, not a stop. */ failureBudget?: number | undefined; /** * Per-todo failed-attempt ceiling. When the engine picks the same todo * and the iteration fails this many times in total, the todo is taken * out of rotation (engine prefers other sources) until it changes * status. Default 3. Prevents the loop from spinning forever on one * stuck task. */ todoMaxAttempts?: number | undefined; /** * Consecutive brainstorm-DONE responses required to consider the goal * complete and stop the engine. When the LLM's brainstorm step keeps * answering `DONE`, the engine treats that as "no more work" and, after * this many in a row, marks the goal as completed instead of sleeping * forever. Default 3. */ brainstormDoneStopThreshold?: number | undefined; /** Side-channel notifications (logging, UI updates). */ onIteration?: (((entry: JournalEntry) => void)) | undefined; onError?: (err: Error | undefined, iteration: number) => void; /** * Per-iteration phase notifications for live UI updates (TUI status bar, * etc.). Fires at each major stage transition: idle → decide → execute → * reflect → (sleep | paused | stopped). Fire-and-forget — the engine * does not await the callback. */ onStage?: (((stage: IterationStage) => void)) | undefined; /** * Optional injected git status reader — production code uses git, tests * stub this out so they don't shell out. */ gitStatusReader?: ((() => Promise)) | undefined; /** * Optional clock — tests stub for deterministic timestamps. */ now?: ((() => Date)) | undefined; /** Logger for structured warnings/errors. Falls back to console.error when omitted. */ logger?: Logger | undefined; /** * Optional compactor. When provided, the engine runs compaction every * `compactEveryNIterations` iterations to keep the agent's message * history under control during multi-day eternal loops. Without * compaction, an infinite loop will eventually overflow the provider's * context window and start failing. */ compactor?: Compactor | undefined; /** How many iterations between compaction calls. Default 25. */ compactEveryNIterations?: number | undefined; /** * Aggressive compaction threshold. When ctx token usage exceeds this * fraction of `maxContextTokens`, compaction runs in aggressive mode * regardless of the iteration cadence. 0.85 by default. */ aggressiveCompactRatio?: number | undefined; /** * Model's max context window in tokens. When set, the engine watches * `currentRequestTokens()` against this and triggers aggressive compact * before the next iteration would overflow. Omit to disable threshold * checks (iteration cadence still applies). */ maxContextTokens?: number | undefined; /** * Base delay (ms) for the first transient-error backoff. Subsequent * transient failures double this, capped at `transientBackoffMaxMs`. * Default 2_000. * * "Transient" means the underlying error is recoverable per * `WrongStackError.recoverable` (ProviderError sets this for 429/529/5xx * /network errors). Permanent errors (auth/invalid request) skip the * backoff and count toward `failureBudget` like before — backing off * on a permanent failure is wasted time. */ transientBackoffBaseMs?: number | undefined; /** Ceiling for the exponential backoff. Default 60_000 (60 s). */ transientBackoffMaxMs?: number | undefined; /** Called when the eternal loop stops for any reason (manual stop, goal complete, etc.). */ onEternalStop?: ((() => void)) | undefined; /** * Optional brain arbiter for autonomous decision-making. When wired, * the engine consults the brain instead of auto-stopping on: * - Brainstorm DONE threshold reached * - Consecutive failures exceeding failure budget * - Goal completion verification * Without a brain, the engine uses its built-in heuristics. */ brain?: BrainArbiter | undefined; /** Optional EventBus for emitting storage.* observability events from goal I/O. */ events?: EventBus | undefined; } export type EternalEngineState = 'idle' | 'running' | 'stopped'; /** * Per-iteration phase emitted via `onStage` so UIs can render the * engine's live location in the sense-decide-execute-reflect loop. */ export type IterationStage = { phase: 'idle'; } | { phase: 'decide'; reason: string; } | { phase: 'execute'; task: string; } | { phase: 'reflect'; status: 'success' | 'failure' | 'aborted' | 'skipped'; note?: string | undefined; } | { phase: 'sleep'; ms: number; } | { phase: 'paused'; } | { phase: 'stopped'; } | { phase: 'error'; message: string; }; export declare class EternalAutonomyEngine { private readonly opts; private state; private stopRequested; private consecutiveFailures; private consecutiveBrainstormDone; /** * Count of consecutive transient (recoverable) provider failures. Drives * the exponential backoff between iterations. Reset on the first * successful iteration so a single bad afternoon doesn't permanently * slow the loop down. */ private consecutiveTransientRetries; private currentCtrl; private iterationsSinceCompact; private readonly goalPath; constructor(opts: EternalAutonomyOptions); /** * Emit a structured error. Uses the configured Logger when available; * falls back to console.error(JSON) so events are never silently dropped. */ private logError; /** Current engine state — readable for UIs. */ get currentState(): EternalEngineState; /** * Load the goal file with structured logging for parse/schema warnings. */ private loadGoal; /** Synchronously request stop. Resolves once the running iteration aborts. */ stop(): void; /** * Mark the engine as 'running' on disk + reset stop state so a new * batch of `runOneIteration()` calls can proceed. Called by the REPL * when the user invokes `/autonomy eternal`. Idempotent. */ prime(): Promise; /** * Main loop. Returns when stop() is called or the goal file is removed. * Does NOT throw — every iteration is wrapped to keep the loop alive. */ run(): Promise; /** * Execute a single sense-decide-execute-reflect cycle. * Returns true on success, false on handled failure / no-op. * * Exposed publicly so the REPL can pace iterations from its main loop * — running the engine and the REPL as a single sequential consumer of * `agent.run()` avoids race conditions on the shared Context. */ runOneIteration(): Promise; /** * Run compaction when either trigger fires: * - We've done >= compactEveryNIterations since the last compact. * - Current request tokens exceed aggressiveCompactRatio * maxContext. * * The second check uses *aggressive* mode to free more headroom; the * cadence check uses non-aggressive (cheaper). */ private maybeCompact; /** * Hybrid idea source. * 1. Pending todos on the agent's context. * 2. Dirty git working tree → propose a "review and finish this" task. * 3. Otherwise: brainstorm via the LLM against the goal. * * After failureBudget consecutive failures, force brainstorm so the * engine doesn't loop on the same broken todo or stuck git state. */ private decide; private pickPendingTodo; private pickGitTask; private readGitStatus; private brainstormTask; private buildDirective; /** * Exponential backoff for transient provider errors. `2^N * base` * capped at `transientBackoffMaxMs`. Zero base disables backoff. * Public-private to keep `runOneIteration` readable; the value is * recomputed each call from the current retry count, so callers * don't have to track state. */ private computeTransientBackoffMs; /** * Sleep that wakes early if `stopRequested` flips. Polls every 250 ms * so SIGINT / `/autonomy stop` can land in the middle of a long * backoff instead of waiting up to a minute for the timer. */ private sleepInterruptible; private appendIterationEntry; /** * Persistent per-todo failure counter. Skipped silently when the goal * file has been removed (graceful clear). Each non-success iteration * against a todo source bumps the counter by 1; `pickPendingTodo` reads * the counter to rotate past stuck todos once they cross `todoMaxAttempts`. */ private bumpTodoAttempt; /** * Flip the mission to `completed` and journal it. Called from two * paths: (a) `[GOAL_COMPLETE]` marker in a successful iteration's * finalText, (b) `brainstorm` returning DONE consecutively past the * configured threshold. Idempotent — re-entry is a no-op once the * goal is already `completed`. */ private markGoalCompleted; /** * Manually clear the goal — equivalent to `/goal clear` typed by the user. * Sets goalState to `abandoned`, removes the goal file, and fires * `onEternalStop` so the REPL returns to normal mode. */ private clearGoalManually; private appendFailure; /** * Consult the brain on whether the goal is truly complete. * Without a brain, always returns true (use heuristic: DONE threshold met = done). */ private consultBrainForDone; /** * Persist a progress update from the agent's [PROGRESS: N%] output. */ private updateProgress; private persistEngineState; } //# sourceMappingURL=eternal-autonomy.d.ts.map