/**
 * OwlCoda Native Conversation Loop
 *
 * The core agentic loop: send request → parse response → execute tools → repeat.
 * Continues until the model returns end_turn or max iterations reached.
 */
import type { Conversation, AssistantResponse } from './protocol/types.js';
import type { AskUserQuestionOpts, ToolProgressEvent } from './tools/types.js';
import { ToolDispatcher } from './dispatch.js';
import { type ProviderRequestDiagnostic } from '../provider-error.js';
export interface ConversationCallbacks {
    /** Called when text is streamed from the model */
    onText?: (text: string) => void;
    /** Called when a tool starts executing */
    onToolStart?: (toolName: string, input: Record<string, unknown>) => void;
    /** Called when a tool finishes */
    onToolEnd?: (toolName: string, result: string, isError: boolean, durationMs: number, metadata?: Record<string, unknown>) => void;
    /** Called with live progress updates during tool execution (e.g. bash output). */
    onToolProgress?: (toolName: string, event: ToolProgressEvent) => void;
    /** Called when the model responds (text + any tool calls) */
    onResponse?: (response: AssistantResponse) => void;
    /** Called on error */
    onError?: (error: string) => void;
    /** Called for non-error notices (e.g. successful compaction) */
    onNotice?: (message: string) => void;
    /** Called with token usage updates during streaming */
    onUsage?: (tokens: {
        input: number;
        output: number;
    }) => void;
    /**
     * Called when an API error occurs and the system will retry.
     * Receives the error message, retry delay in ms, attempt number, and max retries.
     * The REPL uses this to display a visible countdown.
     */
    onRetry?: (info: {
        error: string;
        delayMs: number;
        attempt: number;
        maxRetries: number;
    }) => void;
    /**
     * Called before a tool executes to request user approval.
     * Return true to proceed, false to skip.
     * If not provided, all tools auto-execute.
     */
    onToolApproval?: (toolName: string, input: Record<string, unknown>) => Promise<boolean>;
    /**
     * Called when a tool write is inside the workspace but outside the current
     * task-contract write scope. This is separate from generic tool approval:
     * auto-approving Write should not silently broaden the task contract.
     */
    onTaskScopeApproval?: (request: {
        toolName: string;
        input: Record<string, unknown>;
        attemptedPath: string;
        allowedPaths: string[];
        message: string;
    }) => Promise<boolean>;
    /**
     * Called when a tool needs to ask the user a free-text question.
     * Used by the AskUserQuestion tool path. When the host provides this
     * callback, the tool routes through the host UI (no direct
     * stdout.write, which would race Ink's frame paint). When omitted,
     * tools fall back to a readline prompt (headless/non-interactive).
     * Resolves with the user's answer; empty string = cancelled.
     */
    onUserQuestion?: (toolName: string, question: string, opts?: AskUserQuestionOpts) => Promise<string>;
    /**
     * Called during extended thinking blocks.
     * event='start' when thinking begins, 'delta' for text chunks, 'end' when complete.
     */
    onThinking?: (event: 'start' | 'delta' | 'end', text?: string) => void;
    /** Called when auto-compact triggers due to context window limit. */
    onAutoCompact?: (info: {
        before: number;
        after: number;
        threshold: number;
        beforeTokens?: number;
        afterTokens?: number;
        thresholdTokens?: number;
        usageRatio?: number;
        reason?: 'threshold' | 'hard_limit' | 'provider_limit' | 'heap_pressure' | 'context_exceeded';
        /** 0.13.98 Batch B: how the compaction was actually performed.
         *  'llm_summary' = bounded LLM-summary path succeeded.
         *  'truncation' = fallback (LLM unavailable / failed validation /
         *  3-strike disabled / heap-pressure path that never tries LLM).
         *  'no_op' = nothing to compact (turn count below recent-keep threshold). */
        method?: 'llm_summary' | 'truncation' | 'no_op';
        /** 0.13.98 Batch B: surfaced when method='truncation' to explain why
         *  LLM compact wasn't used or didn't succeed. */
        fallbackReason?: 'llm_compact_failed' | 'llm_compact_disabled_3strikes' | 'heap_pressure_skip_llm' | 'no_llm_opts';
        /** 0.13.98 Batch B: ms taken by the LLM attempt (when one was made). */
        llmMs?: number;
    }) => void;
}
export interface ConversationLoopOptions {
    /** Base URL for the Anthropic-compatible API */
    apiBaseUrl: string;
    /** API key (sent as Bearer token) */
    apiKey: string;
    /** Max agentic loop iterations before stopping */
    maxIterations?: number;
    /** Callbacks for real-time display */
    callbacks?: ConversationCallbacks;
    /** AbortSignal for request cancellation */
    signal?: AbortSignal;
    /** Context window size in tokens (for auto-compact) */
    contextWindow?: number;
    /** Per-request timeout in ms (default 180_000 = 3 minutes).
     *  For streaming turns this only bounds waiting for the local
     *  `/v1/messages` response to start; active stream body lifetime is
     *  governed by the proxy's first-token/idle watchdogs. */
    requestTimeoutMs?: number;
    /** Fallback models to try when the primary model is exhausted/overloaded. Requires allowCrossModelFallback. */
    fallbackModels?: string[];
    /** Explicit opt-in for cross-model fallback; interactive REPL keeps this off. */
    allowCrossModelFallback?: boolean;
    /** 0.13.98 Batch B: model used by auto-compact's LLM-summary path.
     *  Defaults to conversation.model when undefined. Resolved by REPL
     *  from `middleware.compactionModel`. */
    compactionModel?: string;
    /** 0.13.98 Batch B: bounded compactor input cap in tokens (default
     *  8000). Resolved by REPL from `middleware.compactionInputMaxTokens`. */
    compactionInputMaxTokens?: number;
    /**
     * Working directory for the task execution state. When provided, this is
     * passed to `ensureTaskExecutionState` as the `cwd` so that write-scope
     * path resolution uses the intended workspace root rather than
     * `process.cwd()`. Required when the workspace lives outside the process
     * working directory (e.g. benchmark smoke runs under /tmp).
     */
    cwd?: string;
}
export type ConversationRuntimeFailureKind = 'pre_first_token_stream_close' | 'post_token_stream_close' | 'timeout' | 'abort' | 'http_error' | 'provider_error' | 'empty_provider_response'
/** 0.13.97: per-chunk idle watchdog fired AFTER first-token already
 *  arrived. Distinct from `post_token_stream_close` — there the upstream
 *  socket actually closed; here it's still nominally open but went silent
 *  past `streamIdleTimeoutMs` / `routerTimeoutMs`. Diagnostic carries
 *  partialOutputSeen=true; recovery guidance suggests sending a follow-up
 *  to continue from where partial output left off rather than full retry. */
 | 'stream_idle_timeout';
export type ConversationRuntimeFailurePhase = 'request' | 'continuation' | 'tool_continuation';
export interface ConversationRuntimeFailure {
    kind: ConversationRuntimeFailureKind;
    phase: ConversationRuntimeFailurePhase;
    message: string;
    retryable: boolean;
    diagnostic?: ProviderRequestDiagnostic;
}
/**
 * Run one turn of the agentic loop:
 * 1. Send conversation to API
 * 2. Parse response
 * 3. If tool_use → execute tools → add results → recurse
 * 4. If end_turn → return final text
 */
export declare function runConversationLoop(conversation: Conversation, dispatcher: ToolDispatcher, opts: ConversationLoopOptions): Promise<{
    conversation: Conversation;
    finalText: string;
    iterations: number;
    stopReason: string | null;
    usage: {
        inputTokens: number;
        outputTokens: number;
        requestCount: number;
    };
    runtimeFailure: ConversationRuntimeFailure | null;
}>;
export declare function shouldShowNoResponseFallback(options: {
    finalText: string;
    stopReason: string | null;
    runtimeFailure: ConversationRuntimeFailure | null;
    aborted: boolean;
}): boolean;
/** Detect context window limit errors (HTTP 400, error code 2013, token limit messages). */
export declare function isContextLimitError(msg: string): boolean;
export declare function isRetryableError(msg: string): boolean;
/**
 * Turn a raw transport error into something the user can act on.
 * Classifies common failure modes (network, auth, timeout, upstream 5xx,
 * provider-level rate limit) and appends a concrete next step (/doctor,
 * /model, /retry, check backend process). The original cause is preserved
 * in parentheses at the end so the user can still paste it into an issue.
 */
export declare function explainRequestFailure(rawMessage: string, currentModel: string): string;
export declare function classifyConversationRuntimeFailure(err: unknown, conversation: Pick<Conversation, 'model' | 'turns'>, iterations: number): ConversationRuntimeFailure | null;
/**
 * Auto-compact conversation if context usage exceeds threshold.
 * Keeps the most recent turns to stay within budget.
 */
export interface AutoCompactDecision {
    contextWindow: number;
    totalTokens: number;
    thresholdRatio: number;
    thresholdTokens: number;
    usageRatio: number;
    turnCount: number;
    keepCount: number;
    shouldCompact: boolean;
    reason: 'no_context_window' | 'below_threshold' | 'too_few_turns' | 'threshold';
}
export declare function getAutoCompactDecision(conversation: Conversation, contextWindow?: number): AutoCompactDecision;
export declare function autoCompact(conversation: Conversation, contextWindow?: number): boolean;
/**
 * 0.13.62 heap-pressure emergency compact (A).
 *
 * Reads `process.memoryUsage().heapUsed` against
 * `v8.getHeapStatistics().heap_size_limit` (the real V8 heap ceiling,
 * usually ~4GB on 64-bit Node, configurable via `--max-old-space-size`).
 * Returns the current ratio as a number in [0, 1]. When ratio crosses
 * `HEAP_PRESSURE_THRESHOLD`, the conversation loop runs an aggressive
 * compact that's separate from the token-window compact path.
 *
 * Lifted out as a standalone helper (a) so tests can assert behavior
 * by stubbing the limit and (b) so the loop can call it cheaply once
 * per iteration without re-importing v8 each time.
 */
export declare function getHeapPressureRatio(memoryUsage?: () => NodeJS.MemoryUsage, heapLimit?: () => number): number;
/**
 * 0.13.62: emergency heap compaction. Trims the conversation to the
 * last 20% of turns when heap pressure is high. Returns the new turn
 * count if compaction happened, otherwise null (no-op when below
 * threshold or already small enough).
 *
 * The 20% keep ratio is intentionally aggressive — when the host
 * process is genuinely at OOM risk the cost of dropping context is
 * trivial vs the cost of a hard crash that ends the session.
 */
export declare function emergencyHeapCompact(conversation: Conversation, ratio: number): {
    before: number;
    after: number;
} | null;
/** Create a new conversation. */
export declare function createConversation(opts: {
    system: string;
    model: string;
    maxTokens?: number;
    tools?: Conversation['tools'];
}): Conversation;
export declare function resolveDefaultMaxOutputTokens(): number;
/** Add a user message to the conversation. */
export declare function addUserMessage(conversation: Conversation, text: string): void;
