import { JsonObject } from '@ggui-ai/protocol'; import { LLMToolDef } from './llm.js'; interface AgentConfig { provider: 'anthropic' | 'openai' | 'google' | 'openrouter'; model: string; /** * Sampling temperature. When defined it is threaded into the provider * request; when undefined the provider default is used (unchanged * behavior for all existing callers). */ temperature?: number; /** * Explicit per-call credentials/routing, bypassing `process.env` * entirely when supplied. Absent (default) preserves today's * behavior — each agent falls back to reading its provider's env * var(s) directly, so existing callers (including self-hosters who * set env vars once at process start) are unaffected. Use this to * run concurrent generations safely in one process: two simultaneous * calls with different `routeOverride`s never race on shared * `process.env` state. */ routeOverride?: { readonly apiKey?: string; /** Anthropic-only: force the Bedrock-IAM client instead of the direct API client. */ readonly useBedrock?: boolean; }; /** * Observer invoked once per retried attempt when `apiCall()` (#489) * retries a rate-limited (HTTP 429) provider call. Fires AFTER the * retry decision is made and BEFORE the delay is awaited — never on * the terminal failure/success of the whole call. Purely an * observability hook: it never influences whether or how long * `apiCall()` retries. If the observer itself throws, `apiCall()` * catches it, logs a `console.warn`, and proceeds to the retry * delay unaffected — a broken observer cannot turn a recoverable * 429 into an unrelated hard failure. Absent (default) is a no-op; * `apiCall()` still logs every retry via `console.warn` regardless. */ onRetry?: (info: ProviderRetryInfo) => void; } /** One retried attempt's context — passed to `AgentConfig.onRetry`. */ interface ProviderRetryInfo { readonly provider: AgentConfig['provider']; /** 1-based index of this retry (1 = first retry, 2 = second, …). */ readonly attempt: number; /** Total retries `apiCall()` will attempt before giving up. */ readonly maxAttempts: number; /** HTTP status of the failed call, when the caught error carries one. */ readonly status: number | undefined; /** Provider-supplied `Retry-After` value in seconds, when present and within the cap. */ readonly retryAfterSec: number | undefined; /** Delay actually awaited before the next attempt. */ readonly delayMs: number; /** One-line error summary (same shape as the existing `console.error` line). */ readonly message: string; } interface LLMResponse { text: string; inputTokens: number; outputTokens: number; } interface LLMTool { name: string; description: string; parameters: JsonObject; handler: (args: JsonObject) => Promise<{ content: Array<{ text: string; }>; isError?: boolean; }>; } interface LLMWithToolsResponse { text: string; inputTokens: number; outputTokens: number; turnsUsed: number; } interface LLMToolCall { /** Provider-specific call ID (for sendToolResult) */ id?: string; name: string; input: JsonObject; } interface LLMToolCallResponse { toolCalls: LLMToolCall[]; inputTokens: number; outputTokens: number; /** * Tokens read from / written to the prompt cache, when the provider * reports them. Optional because not every provider exposes * prompt-cache accounting — absent means "unreported", never zero. */ cacheReadTokens?: number; cacheCreationTokens?: number; } /** Result of executing a tool — passed to sendToolResult to close the API contract. */ interface LLMToolResult { /** Tool call ID from the response (for providers that need it) */ callId?: string; /** Tool name */ name: string; /** Text result of executing the tool */ result: string; /** Whether the tool execution failed */ isError?: boolean; } declare abstract class LLMAgent { abstract readonly provider: AgentConfig['provider']; private client; protected lastSessionId: string | undefined; /** * Explicit per-call routing override — see `AgentConfig.routeOverride`. * Absent when constructed via the bare `new XAgent()` form (every * existing call site), so `createClient()`/`resolveModel()` fall * back to `process.env` exactly as before. */ protected readonly routeOverride: AgentConfig['routeOverride']; /** See `AgentConfig.onRetry` (#489). Absent is a no-op. */ protected readonly onRetry: AgentConfig['onRetry']; constructor(routeOverride?: AgentConfig['routeOverride'], onRetry?: AgentConfig['onRetry']); protected abstract resolveModel(model: string): string; protected abstract createClient(): Promise; protected getClient(): Promise; /** Text-only call — no tools */ abstract callText(model: string, systemPrompt: string, userPrompt: string, maxTokens?: number, temperature?: number): Promise; /** * Single-turn function calling — returns tool calls without executing them. * Each provider uses its native function/tool calling: * - Anthropic: tool_use blocks * - OpenAI: function_call output items * - Google: functionCall parts * SDK handles JSON escaping — safe for code, diffs, and other content. */ abstract callTools(model: string, systemPrompt: string, userPrompt: string, tools: LLMToolDef[], toolChoice?: 'required' | 'auto', /** * Optional scoped fallback tools. If the primary tools fail with a * transport-class error (e.g. `malformed_tool_call` on Gemini after * retry exhaustion), the provider may retry once with these narrower * tools before throwing. Universal signal — not provider-gated. */ scopedTools?: LLMToolDef[]): Promise; /** Multi-turn agentic loop — executes tools internally */ abstract callWithTools(model: string, systemPrompt: string, userPrompt: string, tools: LLMTool[], maxTurns: number): Promise; /** * Pre-warm cache for repeated callTools() calls with the same system prompt + tools. * Override in providers that support server-side context caching (e.g., Google). * No-op by default (Anthropic/OpenAI handle caching automatically per-request). */ warmCache(_model: string, _systemPrompt: string, _tools: LLMToolDef[], _toolChoice?: 'required' | 'auto'): Promise; /** Cleanup any cached resources. Call after generation completes. No-op by default. */ cleanup(): Promise; /** Reset session state between independent generation runs. */ resetSession(): void; /** * Send tool execution results back to the provider to close the API contract. * Call this after executing tools from callTools() and before the next callTools(). * * For providers with server-side state (Google, OpenAI), this sends the * function results so the next callTools() can chain properly. * For stateless providers (Anthropic), this is a no-op. * * Override in providers that need it. */ sendToolResult(_results: LLMToolResult[]): Promise; /** * Execute an API call, retrying once or twice on HTTP 429 * (rate-limited) before giving up (#489). * * Policy: on a 429, honor the provider's `Retry-After` header when * present (capped at `RETRY_AFTER_CAP_SEC` — a provider asking for a * longer wait is treated as "don't retry", not "wait longer": making * the caller sit through 15+ more seconds on a single failed call is * a bad experience regardless of what's driving the request); * otherwise fall back to exponential backoff with jitter, capped at * `DEFAULT_BACKOFF_MAX_MS` per attempt. Stops after * `MAX_RETRY_ATTEMPTS` retries OR once the cumulative delay would * exceed `MAX_TOTAL_RETRY_DELAY_MS`, whichever comes first. Any * non-429 error, or a 429 with no attempts left, is logged and * re-thrown immediately — unchanged from the pre-#489 behavior. * * **Budget scope (final-review correction):** `MAX_TOTAL_RETRY_DELAY_MS` * bounds a single `apiCall()` invocation, not a whole generation. A * generation issues many sequential `apiCall()` calls (coding turns, * tool-result round-trips, eval rounds); under sustained throttling * each one can independently add up to `MAX_TOTAL_RETRY_DELAY_MS`, so * a generation with k rate-limited calls can add up to roughly `k * * MAX_TOTAL_RETRY_DELAY_MS` to how long it holds its caller's * resources (e.g. the caller's concurrency slot) — not a flat 20s * ceiling. No shared per-generation budget is threaded here by * design (accepted ruling, not an oversight): the caller's own * queue/admission backstop is what bounds how long other * queued work waits, independent of any one generation's retry * total. */ protected apiCall(fn: () => Promise): Promise; } /** * Create a fresh agent instance for the given provider. * Each call returns a new instance — no shared state between callers. * * Accepts either a bare provider string (every pre-existing call * site — the agent falls back to reading its provider's env var(s) * directly) or a full {@link AgentConfig}, whose `routeOverride` * (when present) is threaded into the constructed agent so it never * touches `process.env`. */ declare function createAgent(provider: AgentConfig['provider']): LLMAgent; declare function createAgent(config: AgentConfig): LLMAgent; declare function callLLM(config: AgentConfig, systemPrompt: string, userPrompt: string, maxTokens?: number): Promise; export { type AgentConfig as A, type LLMResponse as L, type ProviderRetryInfo as P, createAgent as a, callLLM as c };