/** * Agentic loop — the core execution cycle that replaces Claude CLI. * * Flow: API call -> stream tokens -> collect tool_use blocks -> execute tools * concurrently -> send tool_results -> repeat until stop_reason !== 'tool_use' * * Tool deferral: Only eager tools (10) have their schemas sent each turn. * Deferred tools (MCP tools) are registered with ToolSearch — the LLM calls * ToolSearch to get the schema, then calls the tool. Schemas are re-deferred * each turn (not accumulated), keeping per-turn token count flat. * * AskUserQuestion: When the LLM calls AskUserQuestion, the loop pauses and * returns a NeedsInputResult. The caller provides the user's answer and * resumes the loop by calling runAgenticLoop again with resumeState. */ import type Anthropic from '@anthropic-ai/sdk'; import type { LLMProvider, SystemBlock, ToolDefinition, ReasoningEffort } from './provider.js'; import type { Tool } from './tools/index.js'; export interface AgenticLoopOptions { provider: LLMProvider; model: string; system: SystemBlock[]; userMessage: string; imageBlocks?: Array<{ type: string; source: { type: string; media_type: string; data: string; }; }>; /** Eager tools — schemas sent every turn. */ tools: Tool[]; /** MCP tools — all have executors registered, but schemas are deferred by default. */ mcpTools?: McpToolEntry[]; /** If true, send ALL tool schemas eagerly (disable deferral). Default: false. */ eagerAll?: boolean; /** Exact tool definition names omitted from the model and executor registry. */ disallowedTools?: string[]; /** * Hard ceiling on model turns. Omit for unbounded (the default — task 160). */ maxTurns?: number; maxTokens?: number; /** * Re-run retrieval at every step boundary (task 160), so memory tracks what the * model is actually doing instead of what the user asked at round 1. * * Called after each turn's tool results are appended and before the next provider * call. Returns the replacement text for the block tagged * RETRIEVED_CONTEXT_BLOCK_ID, or null to leave the current one in place. Only * consulted when that slot exists and is cache-safe (see `planContextRefresh`). * Purely informational — a throw is logged and the turn continues. */ refreshContext?: (req: ContextRefreshRequest) => Promise; /** * Folded reasoning effort, forwarded verbatim to the provider (task 155). * The loop does not interpret it; `resolveReasoningEffort` owns the mapping. */ reasoningEffort?: ReasoningEffort; onToken?: (text: string) => void; onToolUse?: (name: string, input: Record) => void; onToolResult?: (name: string, output: string, isError: boolean) => void; signal?: AbortSignal; cwd: string; /** Thread name for per-thread state. */ threadName?: string; /** Resume state from a previous NeedsInputResult. When provided, userMessage and imageBlocks are ignored. */ resumeState?: ResumeState; } /** What the loop tells a `refreshContext` callback about the step just completed. */ export interface ContextRefreshRequest { /** 1-based turn that just finished. */ turn: number; /** Query built by `buildRefreshQuery` from the user's goal + recent activity. */ query: string; /** Tool calls issued on the turn that just finished. */ toolCalls: Array<{ name: string; input: Record; }>; } export interface McpToolEntry { definition: ToolDefinition; execute: (input: Record) => Promise; } export interface AgenticLoopResult { /** 'done' = loop completed normally. 'needs_input' = paused waiting for user. */ status: 'done' | 'needs_input'; response: string; toolCalls: Array<{ name: string; input: Record; output: string; isError: boolean; }>; turns: number; usage: { totalInputTokens: number; totalOutputTokens: number; cacheReadTokens: number; cacheCreationTokens: number; }; /** Present when status === 'needs_input'. Contains state needed to resume the loop. */ needsInput?: NeedsInputPayload; /** * Task 166: true when the turn ended because the livelock guard fired — the model * re-issued an identical tool call `MAX_IDENTICAL_TOOL_REPEATS` times in a row. * That is a definite stall (the thread was told to keep working and demonstrably * could not make progress), so the gateway persists it as message metadata and the * stall detector nudges immediately, without consulting the classifier. */ livelocked?: boolean; } /** Single question in a multi-question carousel. */ export interface QuestionItem { question: string; options: string[]; multiSelect?: boolean; writeIn?: boolean; } /** Payload describing the question the LLM wants to ask. */ export interface NeedsInputPayload { /** The question text (single-question mode). */ question: string; /** Optional choices for the user (single-question mode). */ options?: string[]; /** Whether the user can type a free-text answer. */ allowFreeform: boolean; /** Multi-question carousel (overrides question/options when present). */ questions?: QuestionItem[]; /** The tool_use_id that needs a tool_result. */ toolUseId: string; /** Serialized conversation state for resumption. */ resumeState: ResumeState; } /** Opaque state blob that allows the loop to resume after a pause. */ export interface ResumeState { /** Accumulated messages up to the pause point (includes the assistant's tool_use). */ messages: Anthropic.MessageParam[]; /** The tool_use_id that needs a tool_result. */ toolUseId: string; /** Tool calls collected so far. */ toolCalls: Array<{ name: string; input: Record; output: string; isError: boolean; }>; /** Any other tool_result blocks from the same turn that were already resolved. */ resolvedToolResults: Anthropic.ToolResultBlockParam[]; /** Text response accumulated so far. */ partialResponse: string; /** Usage accumulated so far. */ usage: { totalInputTokens: number; totalOutputTokens: number; cacheReadTokens: number; cacheCreationTokens: number; }; /** Turns completed so far. */ turns: number; } /** Slot name the gateway puts on the retrieved-context system block. */ export declare const RETRIEVED_CONTEXT_BLOCK_ID = "retrieved-context"; /** * Text carried by a response's content blocks. * * The loop otherwise builds its answer from streamed `text` events, which never * include reasoning — providers accumulate that channel separately and only fold it * into `content` as a last-resort text block (`huggingface-provider.ts`: * `fullText || reasoningText`). Reading the blocks is what makes that fallback * reachable at all; before task 150 it was dead code for every agentic turn. */ export declare function textFromContent(content: Anthropic.ContentBlock[] | undefined): string; /** * Where a mid-loop context refresh may write, and why it may not (task 160). * * Refreshing must never invalidate the cached prompt prefix. The cacheable blocks * (instructions, always-include) carry `cache_control` and sit BEFORE the retrieval * slot, so replacing that slot leaves the prefix byte-identical. If a future * reordering puts a cached block at or after the slot, refreshing would re-pay for * the whole prompt on every round — so this refuses, and the loop keeps today's * frozen-context behaviour rather than silently costing money. */ export declare function planContextRefresh(system: SystemBlock[], id: string): { index: number; reason?: 'no-slot' | 'cache-unsafe'; }; /** * Build the query a mid-loop retrieval runs against (task 160). * * The user's message is the anchor — it is what the turn is FOR — and the recent * tool activity is what the model has actually drifted onto. String inputs (file * paths, shell commands, search queries) carry nearly all of the topical signal; * numbers, booleans and nested objects carry almost none, so they are dropped. */ export declare function buildRefreshQuery(userMessage: string, toolCalls: Array<{ name: string; input: Record; }>, recentText: string, opts?: { maxCalls?: number; maxChars?: number; }): string; export declare function runAgenticLoop(options: AgenticLoopOptions): Promise; //# sourceMappingURL=agentic-loop.d.ts.map