/** * Shared utilities for Gemini 3 native SDK support. * * Both GoogleAIStudioProvider and GoogleVertexProvider route Gemini 3 models * with tools to the native @google/genai SDK (bypassing the Vercel AI SDK) * in order to properly handle thought_signature in multi-turn tool calling. * * This module extracts the functions that are duplicated between the two * providers so they can share a single implementation. */ import type { GenerateStopReason, ThinkingConfig, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, TextChannel, VertexNativePart, GeminiMultimodalInput } from "../types/index.js"; import type { Tool } from "../types/index.js"; /** * A per-turn tool execute map that deduplicates identical tool calls. * * Gemini occasionally re-emits a tool call with identical arguments across * agentic steps even though the prior result is already in the conversation * history (BZ-3327). Re-executing produces duplicate side effects and * duplicate reports for the user, plus wasted tokens. This map caches the * result of each {tool name + args} executed within the turn and returns it * for any identical re-request instead of running the tool again. * * Providers build a fresh executeMap per request, so the cache scope is * exactly one turn. Only `.get()` is overridden (the sole access path used by * the native agentic loops), so iteration still yields the raw executors. */ export declare class DedupExecuteMap extends Map { private readonly resultCache; get(name: string): Tool["execute"] | undefined; } export declare function sanitizeForGoogleFunctionName(name: string): string; /** * Resolve a sanitized Gemini tool name to one that is both unique within * the current request and at most 128 characters. When the candidate * collides with an already-used name we append `_2`, `_3`, … — but * reserve room for the suffix by truncating the base first so the * resolved name never exceeds Google's `function_declarations[].name` * limit. * * @param base The already-sanitized candidate name. * @param isTaken Predicate that returns true if `name` is already used. */ export declare function resolveUniqueGoogleFunctionName(base: string, isTaken: (name: string) => boolean): string; /** * Sanitize a JSON Schema for Gemini's proto-based API. * * Gemini cannot handle `anyOf`/`oneOf` union types in function declarations * because its proto format expects a single `type` field, not a list of types. * This function recursively converts unions to `string` type (the most * permissive primitive that can represent any value as text). * * Also removes `$schema`, `additionalProperties`, and `default` keys that * Gemini's proto format doesn't support. */ export declare function sanitizeSchemaForGemini(schema: Record): Record; /** * Sanitize Vercel AI SDK tools for Gemini compatibility. * * For the Vercel AI SDK path (non-native), tool parameters are Zod schemas that * get converted to JSON Schema internally by @ai-sdk/google. This conversion * doesn't sanitize union types (anyOf/oneOf), causing Gemini proto errors. * * This function pre-converts each tool's Zod parameters to sanitized JSON Schema * and re-wraps with the Vercel AI SDK's jsonSchema() helper. */ export declare function sanitizeToolsForGemini(tools: Record): { tools: Record; dropped: string[]; /** * Reverse map: Google-safe sanitized name → original consumer-supplied * name. Lets the calling layer translate tool-call results back so the * sanitization stays transport-only (see CodeRabbit thread, PR #1006). */ originalNameMap: Map; }; export declare function normalizeToolsForJsonSchemaProvider(tools: Record): { tools: Record; normalized: string[]; }; /** * Convert Vercel AI SDK tools to @google/genai FunctionDeclarations and an execute map. * * This handles both Zod schemas and plain JSON Schema objects for tool parameters. */ export declare function buildNativeToolDeclarations(tools: Record): NativeToolDeclarationsResult; /** * Build the native @google/genai config object shared by stream and generate. * * Caller is responsible for the tools-vs-JSON conflict resolution: Gemini's * function calling cannot be combined with `responseMimeType: * "application/json"`, and `responseSchema` requires that mime type. So * when tools are active, callers must NOT pass `wantsJsonOutput`/ * `responseSchema` here; when JSON/schema output is requested, callers * must omit `toolsConfig`. The AI Studio path enforces this by forcing * `disableTools: true` whenever JSON/schema output is requested. */ export declare function buildNativeConfig(options: { temperature?: number; maxTokens?: number; systemPrompt?: string; thinkingConfig?: ThinkingConfig; /** * When true (and `toolsConfig` is undefined), set * `responseMimeType: "application/json"` to enforce native JSON output. */ wantsJsonOutput?: boolean; /** * Pre-converted JSON Schema for native `responseSchema`. Implies * `wantsJsonOutput`. Ignored if `toolsConfig` is present. */ responseSchema?: Record; }, toolsConfig?: NativeToolsConfig): Record; /** * Compute a safe, clamped maxSteps value. */ export declare function computeMaxSteps(rawMaxSteps?: number): number; /** * Map a `@google/genai` `Candidate.finishReason` enum value onto NeuroLink's * unified finish reason, mirroring anthropic.ts `mapAnthropicStopReason`. * * Enum values per `@google/genai` `FinishReason`: STOP, MAX_TOKENS, SAFETY, * RECITATION, LANGUAGE, OTHER, BLOCKLIST, PROHIBITED_CONTENT, SPII, * MALFORMED_FUNCTION_CALL, IMAGE_SAFETY, UNEXPECTED_TOOL_CALL, * FINISH_REASON_UNSPECIFIED. Unknown / unset / non-terminal values default to * "stop" (a clean completion is the safe assumption). * * Returns a plain string (not a `{ unified, raw }` object): the Vertex result * builders and the consuming layer (neurolink.ts `finishReason || "unknown"` * and `finishReason === "length"`) compare against plain strings. * * MALFORMED_FUNCTION_CALL / UNEXPECTED_TOOL_CALL map to "error", NOT * "tool-calls": they are provider/model failures, while "tool-calls" is the * exclusive contract for "step budget exhausted while the model still wanted * tools" — consumers (e.g. curator's step-cap intercept) branch on it and * were rendering fake step-limit messages for 2-4-step malformed-call turns. */ export declare function mapGeminiFinishReason(raw: string | null | undefined): "stop" | "length" | "tool-calls" | "content-filter" | "error"; /** * Append a step's text to a running cross-step accumulator, ignoring empty * steps and inserting a single newline between non-empty contributions. * * The native Gemini loops overwrite per-step text into `lastStepText`, so when * the loop is force-terminated by the step cap the intermediate tool-step prose * is lost and a canned placeholder becomes the answer. Accumulating here mirrors * the Vertex-Claude loop's `aggregatedTurnText += block.text` so the gathered * text can be surfaced at the maxSteps-exhaustion exit instead of the placeholder. */ export declare function appendStepText(accumulated: string, stepText: string): string; /** * Process stream chunks to extract raw response parts, function calls, and usage metadata. * * Consumes the full async iterable and returns all collected data. */ export declare function collectStreamChunks(stream: AsyncIterable<{ functionCalls?: NativeFunctionCall[]; [key: string]: unknown; }>): Promise; /** * Create a push-based text channel that bridges a background producer * (the agentic tool-calling loop) with an async-iterable consumer. * * This enables truly incremental streaming: text parts are yielded to the * caller as they arrive from the network, rather than being buffered until * the model finishes generating. */ export declare function createTextChannel(): TextChannel; /** * Iterate a single stream step incrementally, pushing text parts to `channel` * as they arrive from the network while simultaneously accumulating the full * `CollectedChunkResult` needed for history and token accounting. * * Used for all steps (both intermediate tool-calling steps and the final * text-only step). Text parts are pushed to the channel as they arrive, * enabling truly incremental streaming. The complete `rawResponseParts` * (including thoughtSignature) are still returned at the end for use by * `pushModelResponseToHistory`. */ export declare function collectStreamChunksIncremental(stream: AsyncIterable<{ functionCalls?: NativeFunctionCall[]; [key: string]: unknown; }>, channel: TextChannel): Promise; /** * Extract the thoughtSignature token from raw response parts. * Returns the last thoughtSignature found (each step may produce one). */ export declare function extractThoughtSignature(rawResponseParts: unknown[]): string | undefined; /** * Extract text from raw response parts, filtering out non-text parts * (thoughtSignature, functionCall) to avoid SDK warnings. */ export declare function extractTextFromParts(rawResponseParts: unknown[]): string; /** * Execute a batch of native function calls with retry tracking and permanent failure detection. * * @param logLabel - Label for log messages (e.g. "[GoogleAIStudio]" or "[GoogleVertex]") * @param stepFunctionCalls - The function calls from the model * @param executeMap - Map of tool name to execute function * @param failedTools - Mutable map tracking per-tool failure counts * @param allToolCalls - Mutable array accumulating all tool call records * @param options - Optional settings for execution tracking and cancellation, * plus an `originalNameMap` (Google-safe → consumer-supplied * identifier) so the sanitization stays transport-only and * consumers see the names they registered. * @returns Array of function responses for conversation history */ export declare function executeNativeToolCalls(logLabel: string, stepFunctionCalls: NativeFunctionCall[], executeMap: Map, failedTools: Map, allToolCalls: Array<{ toolName: string; args: Record; }>, options?: { toolExecutions?: Array<{ name: string; input: Record; output: unknown; }>; abortSignal?: AbortSignal; originalNameMap?: Map; }): Promise; /** * Handle maxSteps termination by producing a final answer when the model was * still calling tools as the step limit was reached. * * Resolution order (always returns a non-empty string): the caller's * `finalText` if present; else the `lastStepText` gathered from the final step; * else a graceful, user-facing cap message from {@link buildToolLoopCapMessage} * — which replaced the legacy bracketed step-limit placeholder string. * * @param logLabel - Label for log messages (e.g. "[GoogleAIStudio]" or "[GoogleVertex]") * @returns The resolved final answer text (never the legacy placeholder). */ export declare function handleMaxStepsTermination(logLabel: string, step: number, maxSteps: number, finalText: string, lastStepText: string): string; /** * Detect whether an error represents an abort/cancellation (from an * AbortSignal firing during a fetch/stream drain). Used by the native * Gemini-3 agentic loops to break gracefully rather than re-throw. */ export declare function isAbortError(error: unknown): boolean; /** * Build a graceful, user-facing message for when a single agentic turn hits * the step cap without producing a final answer. Replaces the legacy * bracketed "Tool execution limit reached" placeholder. * * Emit this ONLY when `step >= maxSteps` genuinely terminated the loop — * aborted/timed-out/stalled turns must use {@link buildAbortedTurnMessage}, * {@link buildTurnTimeoutMessage}, or {@link buildTurnStalledMessage}, never * this text (a killed 23-step turn once claimed "reached the 200-step limit"). */ export declare function buildToolLoopCapMessage(maxSteps: number, toolCallCount: number): string; /** * Honest message for a turn stopped by the in-loop context guard * (stopReason "context-cap") without a synthesized answer. Sibling of * {@link buildToolLoopCapMessage} — context exits must never claim a step * limit was reached. */ export declare function buildContextCapMessage(toolCallCount: number): string; /** * Honest message for a turn ended by the `turnTimeoutMs` wall-clock deadline * (stopReason "time-limit"). Sibling of {@link buildToolLoopCapMessage} — * time exits must never claim a step limit was reached. */ export declare function buildTurnTimeoutMessage(elapsedMs: number, toolCallCount: number): string; /** * Honest message for a turn ended by the `stallTimeoutMs` no-progress * watchdog (stopReason "stalled") — typically a wedged tool or a hung * model call. */ export declare function buildTurnStalledMessage(stallTimeoutMs: number, toolCallCount: number): string; /** * Honest message for a caller-aborted turn (admin kill, coding-task * short-circuit — stopReason "aborted"). */ export declare function buildAbortedTurnMessage(toolCallCount: number): string; /** * Wrap-up nudge injected when the remaining turn time drops inside the * `wrapupTimeLeadMs` window — the time-budget twin of the soft step-budget * nudge. Rides as a trailing text block on the tool-result user turn. */ export declare function buildWrapupNudgeText(useFinalResultTool: boolean): string; /** * Resolve the turn's `stopReason` discriminator from the loop's exit * bookkeeping. Precedence: time conditions beat the generic abort flag * (the deadline/stall watchdogs abort through the same internal controller), * abort beats step-cap, and a provider-error finishReason (e.g. persistent * MALFORMED_FUNCTION_CALL) beats "completed". */ export declare function resolveTurnStopReason(params: { timedOut: boolean; stalled: boolean; wasAborted: boolean; /** Step budget ran out AND no clean/forced answer was produced. */ cappedWithoutAnswer: boolean; /** Context guard stopped the loop AND no clean/forced answer was produced. */ contextCappedWithoutAnswer?: boolean; /** The turn's resolved unified finishReason. */ finishReason?: string; }): GenerateStopReason; /** * Wall-clock + progress watchdogs for a native agentic turn. * * - Deadline: `turnTimeoutMs` (caller knob) or `defaultTurnTimeoutMs` (the * loop's pre-existing defensive bound) arms a whole-turn timer. * - Stall: when `stallTimeoutMs` is set, a low-frequency interval checks the * time since the last recorded progress (chunk received, tool started or * finished, step started). * * Both fire `onDeadline(kind)` exactly once each and latch a flag the loop's * terminal handling reads to pick the honest exit message and `stopReason`. * Timers are unref'd so they never hold the process open; call `dispose()` * in the loop's finally. */ export declare function createTurnClock(params: { /** Explicit whole-turn deadline (ms); wins over defaultTurnTimeoutMs. */ turnTimeoutMs?: number; /** Loop-specific defensive default when turnTimeoutMs is unset (undefined = no deadline). */ defaultTurnTimeoutMs?: number; /** No-progress watchdog (ms); undefined = disabled. */ stallTimeoutMs?: number; /** Wrap-up lead (ms); only honored when turnTimeoutMs is explicitly set. */ wrapupTimeLeadMs?: number; onDeadline: (kind: "timeout" | "stall") => void; }): { readonly timedOut: boolean; readonly stalled: boolean; /** True when the turn ended on a time condition (deadline or stall). */ readonly expired: boolean; /** The effective whole-turn deadline in ms (explicit or defensive default). */ readonly turnTimeoutMs: number | undefined; elapsedMs(): number; /** Record progress: chunk received, tool started/finished, step started. */ noteProgress(): void; /** True when an explicit deadline exists and remaining time is inside the wrap-up lead. */ shouldNudgeWrapup(): boolean; dispose(): void; }; /** * In-loop context guard for native agentic turns. * * The provider reports the ACTUAL prompt size of every model call * (usage.input_tokens + cache reads/writes on Anthropic; * usageMetadata.promptTokenCount on Gemini). The guard tracks that number * plus an estimate of what the current step appends (assistant output, tool * results), and tells the loop to stop calling tools once the projected next * prompt crosses `thresholdRatio` of the model's context window — the turn * then synthesizes a final answer from what it has instead of stepping into * a provider 400 ("prompt is too long") that destroys all completed work. * * Fail-open: until the first usage report arrives, `shouldStop()` is false. */ export declare function createContextGuard(contextWindowTokens: number, thresholdRatio?: number): { /** Tokens at which the guard trips (ratio × window). */ readonly thresholdTokens: number; /** Last observed prompt size plus estimated growth since. */ readonly projectedNextPromptTokens: number; /** * Record a model call's reported usage. `promptTokens` must be the FULL * prompt size (uncached input + cache read + cache creation for * Anthropic). The response's own output is counted as growth — it is * appended to the conversation for the next call. */ noteUsage(promptTokens: number, outputTokens: number): void; /** * Add growth for content appended since the last model call (tool * results, nudge text) using the ~4 chars/token heuristic. */ noteAppendedChars(chars: number): void; /** True when issuing another model call risks crossing the threshold. */ shouldStop(): boolean; }; /** * Push model response parts to conversation history, preserving thoughtSignature * for Gemini 3 multi-turn tool calling. */ export declare function pushModelResponseToHistory(currentContents: Array<{ role: string; parts: unknown[]; }>, rawResponseParts: unknown[], stepFunctionCalls: NativeFunctionCall[]): void; /** * Convert a Zod schema (or AI SDK `jsonSchema()` wrapper) into the shape * `@google/genai` accepts as `responseSchema`. Mirrors the inline pipeline * the Vertex Gemini paths already use: * * convertZodToJsonSchema → inlineJsonSchema → strip `$schema` → ensure * every nested schema has a `type` (Vertex/Gemini reject schemas missing * that field, even on nested objects). * * Lives here so the AI Studio and Vertex paths can share the same * sanitization without duplicating the schema-conversion churn. */ export declare function buildGeminiResponseSchema(schema: unknown): Record; /** * Map NeuroLink ChatMessage[] history into the @google/genai content format * and push the entries onto a contents array. * * Used by the native Vertex Gemini and Google AI Studio paths to honor * `options.conversationMessages` so multi-turn conversations (memory, loop * REPL, agent flows) actually carry prior turns into the request. * * Behavior notes: * - Only `user` and `assistant` roles are forwarded; system messages are * expected to be wired via `systemInstruction`, and tool-call / * tool-result roles only appear inside intra-call tool loops which build * their own model/function entries. * - String content is wrapped as a single `{ text }` part. Empty strings * are skipped to avoid sending empty parts that some Gemini regions * reject. * - The current user input should be appended AFTER calling this helper * so the prior turns appear first in chronological order. */ export declare function prependConversationMessages(contents: Array<{ role: string; parts: unknown[]; }>, conversationMessages?: Array): void; /** * Build the `parts` array for the current user turn of a Gemini native * `generateContent` request, including inline image + PDF blobs. * * Both providers that hit the native `@google/genai` SDK — `GoogleVertex` * and `GoogleAIStudio` — need this. The previous AI Studio code only * pushed a single `{ text }` part, which silently dropped `input.images` * and `input.pdfFiles` on the floor: the model received text only and * legitimately reported "no image attached". Extracting this from the * Vertex copy keeps both providers on one definition. * * Accepted shapes per element (mirroring the runtime behaviour the Vertex * code already supported): * - `Buffer` → used as-is * - local file path → read via `readFileSync`, MIME guessed from extension * - `data:;base64,...` URL → mime parsed, data base64-decoded * - `http(s)://...` URL → fetched, mime from `content-type` * - any other string → assumed to be a base64-encoded payload * * Image MIME guessing is conservative — only known extensions override the * default `image/jpeg`. Fetch failures are logged and the offending entry * is skipped rather than aborting the entire request, matching prior * Vertex behaviour. */ export declare function buildUserPartsWithMultimodal(input: GeminiMultimodalInput | undefined, textOverride?: string, logPrefix?: string): Promise;