/** * Agent loop - autonomous task execution. * * Private chat/stream logic lives in agentChat.ts and agentStream.ts. */ import { ProjectContext } from './project'; import { loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent } from './agentChat'; import type { AgentChatResponse } from './agentChat'; import { type Personality } from './personalities'; export { loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent }; export type { AgentChatResponse }; import { ToolCall, ToolResult, ActionLog } from './tools'; import { type TrustBearingWrite } from './toolExecution'; import { undoLastAction, undoAllActions, getCurrentSession, getRecentSessions, formatSession, ActionSession } from './history'; import { VerifyResult } from './verify'; import { TaskPlan, SubTask } from './taskPlanner'; export type PermissionOutcome = 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always'; export type PermissionDecision = 'allow-once' | 'allow-always' | 'deny-once' | 'deny-always'; /** * Map a permission outcome to a decision, FAILING CLOSED: a dangerous tool is * allowed only on an explicit allow outcome. `reject_*` deny, and — critically — * any unknown/malformed outcome from a buggy or hostile client also denies * (deny-once) rather than slipping through to execution. Pure + exported so the * invariant is unit-tested independently of the agent loop. */ export declare function classifyPermissionOutcome(outcome: string | undefined | null): PermissionDecision; /** * Build the set of tools that require a permission prompt this run. Derived from * the global agentConfirm* settings, plus any `extra` tools forced in for this * run only (ACP manual mode passes ['write_file','edit_file'] this way instead * of mutating the global `agentConfirmWriteFile` config — which would leak the * session's mode into the TUI and race on restore). Exported for unit testing. */ export declare function buildDangerousTools(extra?: string[]): Set; /** * Whether a tool call must go through the permission prompt this run. * * MCP tools (`__`) can do anything their server can — write * files, run SQL, drive a browser — and their names never appear in the * built-in set, so a mode that confirms dangerous operations confirms these * too. The resource/prompt wrappers only read, and stay unprompted. */ export declare function requiresPermission(tool: string, dangerousTools: ReadonlySet): boolean; /** Provider, model and protocol a run talks to. */ export interface AgentModelRuntime { providerId: string; model: string; protocol: 'openai' | 'anthropic'; } /** * Read a sub-agent's `model:` setting ("provider/model" or a bare model) as * the runtime for its nested run. A known provider prefix switches provider; * anything else is a model on the current provider. The protocol is kept when * the provider stays the same and supports it, and is otherwise that * provider's default. */ export declare function resolveDelegateModel(spec: string, current: AgentModelRuntime): Promise; export interface AgentOptions { maxIterations: number; maxDuration: number; onChunk?: (text: string) => void; onToolCall?: (tool: ToolCall) => void; onToolResult?: (result: ToolResult, toolCall: ToolCall) => void; onIteration?: (iteration: number, message: string) => void; onThinking?: (text: string) => void; onVerification?: (results: VerifyResult[]) => void; onTaskPlan?: (plan: TaskPlan) => void; onTaskUpdate?: (task: SubTask) => void; /** * Ask the user about one tool call. * * `trustBearing` is what this run already worked out about the call — the * file it would write that decides what runs later, or null when it writes * no such file. It is passed so the side putting up the dialog can word it * without calling trustBearingWrite() a second time: that call stats the * path, follows a symlink and may ask git where the repo's hooks live. * Optional, so a caller that would rather work it out itself (or was not * called from the gate below) still type-checks. */ onRequestPermission?: (toolCall: ToolCall, trustBearing?: TrustBearingWrite | null) => Promise; /** Tool names to force into the per-run dangerous set, on top of the global * agentConfirm* settings. ACP manual mode passes ['write_file','edit_file'] * here to gate them for THIS run only, instead of mutating global config. */ extraDangerousTools?: string[]; onExecuteCommand?: (command: string, args: string[], cwd: string) => Promise<{ stdout: string; stderr: string; exitCode: number; }>; /** * Optional filesystem callbacks. When the ACP client advertises `fs` * capability, the server populates these so read_file/write_file/edit_file * tools route through the client (preserving dirty buffers and undo * history) instead of touching disk directly. Falls back to disk if not * provided or if a delegated call throws. */ fs?: import('./toolExecution').FsCallbacks; /** * Optional ACP session id used to route MCP-prefixed tool calls * (`__`) to the per-session `mcpRegistry`. Not set in TUI * mode (no MCP support there yet); set by `runAgentSession` in ACP mode. * When set, the agent loop also fetches the session's MCP tool list and * passes it into the provider's tool catalog so the model can invoke * those tools natively. */ mcpSessionId?: string; abortSignal?: AbortSignal; dryRun?: boolean; autoVerify?: 'off' | 'build' | 'typecheck' | 'test' | 'all' | boolean; maxFixAttempts?: number; usePlanning?: boolean; chatHistory?: Array<{ role: 'user' | 'assistant'; content: string; }>; /** Delegated sub-agent run. Skips the undo/history session + progress log so * it doesn't clobber the parent's (history.ts uses a module-level * `currentSession` singleton — a nested startSession would reset it). The * sub-agent's tool actions still record into the parent's session, so undo * spans delegation. */ nested?: boolean; /** Run under this capability boundary instead of whatever the user has * selected. Used by non-interactive callers that must pin the boundary * themselves — a CI fix, for example, runs files+tests regardless of the * machine's active bot. Enforced by the same gate as any other bot; this * chooses which one applies, never whether one does. */ personalityOverride?: Personality; /** Delegation depth. 0 = top-level orchestrator (gets the `delegate` tool); * sub-agents run at depth 1 and cannot delegate further (v1). */ depth?: number; /** Tool allowlist for a scoped sub-agent. Undefined = all tools. Enforced at * dispatch — a disallowed tool call returns an error result. */ allowedTools?: string[]; /** Role system-prompt addendum injected for a delegated sub-agent. */ roleAddendum?: string; /** "Always allow" / "always deny" answers shared with a delegating parent, * so a sub-agent neither asks again about a tool the user already decided * on nor runs one the user refused. `alwaysRejectedPaths` holds the same * for a single file that decides what runs later, which is refused by name * rather than by tool. */ permissionMemory?: { alwaysAllowed: Set; alwaysRejected: Set; alwaysRejectedPaths?: Set; }; /** Provider/model for this run only, used in place of the global selection. * A sub-agent with its own `model:` runs on it this way; the global config * is saved to disk and read by every session in the process, so it is * never changed for a single run. */ modelOverride?: AgentModelRuntime; } /** Why a run stopped early at a safety limit — both are resumable, not errors. */ export type InterruptKind = 'iteration_limit' | 'time_limit'; export interface AgentResult { success: boolean; iterations: number; actions: ActionLog[]; finalResponse: string; error?: string; aborted?: boolean; /** Set when the run paused at a step/time safety limit. The caller can offer * a "continue" affordance instead of treating it as a failure. */ interrupted?: InterruptKind; /** Commands of the verification checks that still failed when the run * ended. Set only when that is why the run did not succeed. */ failedChecks?: string[]; /** * The end of `finalResponse` that runAgent wrote itself instead of taking * it from the model's last reply: the verification passed / failed / could * not run blocks and the auto-review section, or the whole notice when one * replaced the reply (stopped, paused, API errors). None of it went through * `onChunk`, so a client that streamed the reply sends exactly this after * it, separated by a blank line. Trimmed; '' when `finalResponse` is only * the model's reply. Set on every result runAgent returns. */ unstreamedText?: string; } /** * Build the result for a run that paused at a safety limit. Pausing is a normal, * resumable state — not an error — so the summary tells the user how to resume. * Shared by both limit checks in the loop so the wording + `interrupted` signal * stay in sync. */ export declare function buildPausedResult(kind: InterruptKind, ctx: { iterations: number; actions: ActionLog[]; maxIterations?: number; durationMin?: number; }): AgentResult; /** * Run the agent loop */ export declare function runAgent(prompt: string, projectContext: ProjectContext, options?: Partial): Promise; /** * Format agent result for display */ export declare function formatAgentResult(result: AgentResult): string; export { undoLastAction, undoAllActions, getCurrentSession, getRecentSessions, formatSession, type ActionSession }; /** * Get agent history for display */ export declare function getAgentHistory(): Array<{ timestamp: number; task: string; actions: Array<{ type: string; target: string; result: string; }>; success: boolean; }>; /** * Actions of the run undo acts on (see getCurrentSession). Pass the workspace * to leave out a run in another one. `result` is 'undone' for an action that * has since been undone, 'success' otherwise. */ export declare function getCurrentSessionActions(projectRoot?: string): Array<{ type: string; target: string; result: 'success' | 'undone'; }>;