/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
// DISCLAIMER: This is a copied version of https://github.com/googleapis/js-genai/blob/main/src/chats.ts with the intention of working around a key bug
// where function responses are not treated as "valid" responses: https://b.corp.google.com/issues/420354090
import { createUserContent, FinishReason, } from '@google/genai';
import { toParts } from '../code_assist/converter.js';
import { retryWithBackoff, isRetryableError, getRetryErrorType, } from '../utils/retry.js';
import { resolveModel, isGemini2Model, supportsModernFeatures, } from '../config/models.js';
import { hasCycleInSchema } from '../tools/tools.js';
import { logContentRetry, logContentRetryFailure, logNetworkRetryAttempt, } from '../telemetry/loggers.js';
import { ChatRecordingService, } from '../services/chatRecordingService.js';
import { ContentRetryEvent, ContentRetryFailureEvent, NetworkRetryAttemptEvent, } from '../telemetry/types.js';
import { handleFallback } from '../fallback/handler.js';
import { isFunctionResponse } from '../utils/messageInspectors.js';
import { partListUnionToString } from './geminiRequest.js';
import { estimateTokenCountSync } from '../utils/tokenCalculation.js';
import { applyModelSelection, createAvailabilityContextProvider, } from '../availability/policyHelpers.js';
import { coreEvents } from '../utils/events.js';
export var StreamEventType;
(function (StreamEventType) {
    /** A regular content chunk from the API. */
    StreamEventType["CHUNK"] = "chunk";
    /** A signal that a retry is about to happen. The UI should discard any partial
     * content from the attempt that just failed. */
    StreamEventType["RETRY"] = "retry";
    /** A signal that the agent execution has been stopped by a hook. */
    StreamEventType["AGENT_EXECUTION_STOPPED"] = "agent_execution_stopped";
    /** A signal that the agent execution has been blocked by a hook. */
    StreamEventType["AGENT_EXECUTION_BLOCKED"] = "agent_execution_blocked";
})(StreamEventType || (StreamEventType = {}));
const MID_STREAM_RETRY_OPTIONS = {
    maxAttempts: 4, // 1 initial call + 3 retries mid-stream
    initialDelayMs: 1000,
    useExponentialBackoff: true,
};
export const SYNTHETIC_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';
/**
 * Returns true if the response is valid, false otherwise.
 */
function isValidResponse(response) {
    if (response.candidates === undefined || response.candidates.length === 0) {
        return false;
    }
    const content = response.candidates[0]?.content;
    if (content === undefined) {
        return false;
    }
    return isValidContent(content);
}
export function isValidNonThoughtTextPart(part) {
    return (typeof part.text === 'string' &&
        !part.thought &&
        // Technically, the model should never generate parts that have text and
        //  any of these but we don't trust them so check anyways.
        !part.functionCall &&
        !part.functionResponse &&
        !part.inlineData &&
        !part.fileData);
}
function isValidContent(content) {
    if (content.parts === undefined || content.parts.length === 0) {
        return false;
    }
    for (const part of content.parts) {
        if (part === undefined || Object.keys(part).length === 0) {
            return false;
        }
        if (!part.thought && part.text !== undefined && part.text === '') {
            return false;
        }
    }
    return true;
}
/**
 * Validates the history contains the correct roles.
 *
 * @throws Error if the history does not start with a user turn.
 * @throws Error if the history contains an invalid role.
 */
function validateHistory(history) {
    for (const content of history) {
        if (content.role !== 'user' && content.role !== 'model') {
            throw new Error(`Role must be user or model, but got ${content.role}.`);
        }
    }
}
/**
 * Extracts the curated (valid) history from a comprehensive history.
 *
 * @remarks
 * The model may sometimes generate invalid or empty contents(e.g., due to safety
 * filters or recitation). Extracting valid turns from the history
 * ensures that subsequent requests could be accepted by the model.
 */
function extractCuratedHistory(comprehensiveHistory) {
    if (comprehensiveHistory === undefined || comprehensiveHistory.length === 0) {
        return [];
    }
    const curatedHistory = [];
    const length = comprehensiveHistory.length;
    let i = 0;
    while (i < length) {
        if (comprehensiveHistory[i].role === 'user') {
            curatedHistory.push(comprehensiveHistory[i]);
            i++;
        }
        else {
            const modelOutput = [];
            let isValid = true;
            while (i < length && comprehensiveHistory[i].role === 'model') {
                modelOutput.push(comprehensiveHistory[i]);
                if (isValid && !isValidContent(comprehensiveHistory[i])) {
                    isValid = false;
                }
                i++;
            }
            if (isValid) {
                curatedHistory.push(...modelOutput);
            }
        }
    }
    return curatedHistory;
}
/**
 * Custom error to signal that a stream completed with invalid content,
 * which should trigger a retry.
 */
export class InvalidStreamError extends Error {
    type;
    constructor(message, type) {
        super(message);
        this.name = 'InvalidStreamError';
        this.type = type;
    }
}
/**
 * Custom error to signal that agent execution has been stopped.
 */
export class AgentExecutionStoppedError extends Error {
    reason;
    constructor(reason) {
        super(reason);
        this.reason = reason;
        this.name = 'AgentExecutionStoppedError';
    }
}
/**
 * Custom error to signal that agent execution has been blocked.
 */
export class AgentExecutionBlockedError extends Error {
    reason;
    syntheticResponse;
    constructor(reason, syntheticResponse) {
        super(reason);
        this.reason = reason;
        this.syntheticResponse = syntheticResponse;
        this.name = 'AgentExecutionBlockedError';
    }
}
/**
 * Chat session that enables sending messages to the model with previous
 * conversation context.
 *
 * @remarks
 * The session maintains all the turns between user and model.
 */
export class GeminiChat {
    context;
    systemInstruction;
    tools;
    history;
    onModelChanged;
    // A promise to represent the current state of the message being sent to the
    // model.
    sendPromise = Promise.resolve();
    chatRecordingService;
    lastPromptTokenCount;
    constructor(context, systemInstruction = '', tools = [], history = [], resumedSessionData, onModelChanged, kind = 'main') {
        this.context = context;
        this.systemInstruction = systemInstruction;
        this.tools = tools;
        this.history = history;
        this.onModelChanged = onModelChanged;
        validateHistory(history);
        this.chatRecordingService = new ChatRecordingService(context);
        this.chatRecordingService.initialize(resumedSessionData, kind);
        this.lastPromptTokenCount = estimateTokenCountSync(this.history.flatMap((c) => c.parts || []));
    }
    setSystemInstruction(sysInstr) {
        this.systemInstruction = sysInstr;
    }
    /**
     * Sends a message to the model and returns the response in chunks.
     *
     * @remarks
     * This method will wait for the previous message to be processed before
     * sending the next message.
     *
     * @see {@link Chat#sendMessage} for non-streaming method.
     * @param modelConfigKey - The key for the model config.
     * @param message - The list of messages to send.
     * @param prompt_id - The ID of the prompt.
     * @param signal - An abort signal for this message.
     * @param displayContent - An optional user-friendly version of the message to record.
     * @return The model's response.
     *
     * @example
     * ```ts
     * const chat = ai.chats.create({model: 'gemini-2.0-flash'});
     * const response = await chat.sendMessageStream({
     * message: 'Why is the sky blue?'
     * });
     * for await (const chunk of response) {
     * console.log(chunk.text);
     * }
     * ```
     */
    async sendMessageStream(modelConfigKey, message, prompt_id, signal, role, displayContent) {
        await this.sendPromise;
        let streamDoneResolver;
        const streamDonePromise = new Promise((resolve) => {
            streamDoneResolver = resolve;
        });
        this.sendPromise = streamDonePromise;
        const userContent = createUserContent(message);
        const { model } = this.context.config.modelConfigService.getResolvedConfig(modelConfigKey);
        // Record user input - capture complete message with all parts (text, files, images, etc.)
        // but skip recording function responses (tool call results) as they should be stored in tool call records
        if (!isFunctionResponse(userContent)) {
            const userMessageParts = userContent.parts || [];
            const userMessageContent = partListUnionToString(userMessageParts);
            let finalDisplayContent = undefined;
            if (displayContent !== undefined) {
                const displayParts = toParts(Array.isArray(displayContent) ? displayContent : [displayContent]);
                const displayContentString = partListUnionToString(displayParts);
                if (displayContentString !== userMessageContent) {
                    finalDisplayContent = displayParts;
                }
            }
            this.chatRecordingService.recordMessage({
                model,
                type: 'user',
                content: userMessageParts,
                displayContent: finalDisplayContent,
            });
        }
        // Add user content to history ONCE before any attempts.
        this.history.push(userContent);
        const requestContents = this.getHistory(true);
        const streamWithRetries = async function* () {
            try {
                const maxAttempts = this.context.config.getMaxAttempts();
                for (let attempt = 0; attempt < maxAttempts; attempt++) {
                    let isConnectionPhase = true;
                    try {
                        if (attempt > 0) {
                            yield { type: StreamEventType.RETRY };
                        }
                        // If this is a retry, update the key with the new context.
                        const currentConfigKey = attempt > 0
                            ? { ...modelConfigKey, isRetry: true }
                            : modelConfigKey;
                        isConnectionPhase = true;
                        const stream = await this.makeApiCallAndProcessStream(currentConfigKey, requestContents, prompt_id, signal, role);
                        isConnectionPhase = false;
                        for await (const chunk of stream) {
                            yield { type: StreamEventType.CHUNK, value: chunk };
                        }
                        return;
                    }
                    catch (error) {
                        if (error instanceof AgentExecutionStoppedError) {
                            yield {
                                type: StreamEventType.AGENT_EXECUTION_STOPPED,
                                reason: error.reason,
                            };
                            return; // Stop the generator
                        }
                        if (error instanceof AgentExecutionBlockedError) {
                            yield {
                                type: StreamEventType.AGENT_EXECUTION_BLOCKED,
                                reason: error.reason,
                            };
                            if (error.syntheticResponse) {
                                yield {
                                    type: StreamEventType.CHUNK,
                                    value: error.syntheticResponse,
                                };
                            }
                            return; // Stop the generator
                        }
                        if (isConnectionPhase) {
                            // Connection phase errors have already been retried by retryWithBackoff.
                            // If they bubble up here, they are exhausted or fatal.
                            throw error;
                        }
                        // Check if the error is retryable (e.g., transient SSL errors
                        // like ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC or ApiError)
                        const isRetryable = isRetryableError(error, this.context.config.getRetryFetchErrors());
                        const isContentError = error instanceof InvalidStreamError;
                        const errorType = isContentError
                            ? error.type
                            : getRetryErrorType(error);
                        if ((isContentError && isGemini2Model(model)) ||
                            (isRetryable && !signal.aborted)) {
                            // The issue requests exactly 3 retries (4 attempts) for API errors during stream iteration.
                            // Regardless of the global maxAttempts (e.g. 10), we only want to retry these mid-stream API errors
                            // up to 3 times before finally throwing the error to the user.
                            const maxMidStreamAttempts = MID_STREAM_RETRY_OPTIONS.maxAttempts;
                            if (attempt < maxAttempts - 1 &&
                                attempt < maxMidStreamAttempts - 1) {
                                const delayMs = MID_STREAM_RETRY_OPTIONS.useExponentialBackoff
                                    ? MID_STREAM_RETRY_OPTIONS.initialDelayMs *
                                        Math.pow(2, attempt)
                                    : MID_STREAM_RETRY_OPTIONS.initialDelayMs * (attempt + 1);
                                if (isContentError) {
                                    logContentRetry(this.context.config, new ContentRetryEvent(attempt, errorType, delayMs, model));
                                }
                                else {
                                    logNetworkRetryAttempt(this.context.config, new NetworkRetryAttemptEvent(attempt + 1, maxAttempts, errorType, delayMs, model));
                                }
                                coreEvents.emitRetryAttempt({
                                    attempt: attempt + 1,
                                    maxAttempts: Math.min(maxAttempts, maxMidStreamAttempts),
                                    delayMs,
                                    error: errorType,
                                    model,
                                });
                                await new Promise((res) => setTimeout(res, delayMs));
                                continue;
                            }
                        }
                        // If we've aborted, we throw without logging a failure.
                        if (signal.aborted) {
                            throw error;
                        }
                        logContentRetryFailure(this.context.config, new ContentRetryFailureEvent(attempt + 1, errorType, model));
                        throw error;
                    }
                }
            }
            finally {
                streamDoneResolver();
            }
        };
        return streamWithRetries.call(this);
    }
    async makeApiCallAndProcessStream(modelConfigKey, requestContents, prompt_id, abortSignal, role) {
        const contentsForPreviewModel = this.ensureActiveLoopHasThoughtSignatures(requestContents);
        // Track final request parameters for AfterModel hooks
        const { model: availabilityFinalModel, config: newAvailabilityConfig, maxAttempts: availabilityMaxAttempts, } = applyModelSelection(this.context.config, modelConfigKey);
        let lastModelToUse = availabilityFinalModel;
        let currentGenerateContentConfig = newAvailabilityConfig;
        let lastConfig = currentGenerateContentConfig;
        let lastContentsToUse = [...requestContents];
        const getAvailabilityContext = createAvailabilityContextProvider(this.context.config, () => lastModelToUse);
        // Track initial active model to detect fallback changes
        const initialActiveModel = this.context.config.getActiveModel();
        const apiCall = async () => {
            const useGemini3_1 = (await this.context.config.getGemini31Launched?.()) ?? false;
            // Default to the last used model (which respects arguments/availability selection)
            let modelToUse = resolveModel(lastModelToUse, useGemini3_1);
            // If the active model has changed (e.g. due to a fallback updating the config),
            // we switch to the new active model.
            if (this.context.config.getActiveModel() !== initialActiveModel) {
                modelToUse = resolveModel(this.context.config.getActiveModel(), useGemini3_1);
            }
            if (modelToUse !== lastModelToUse) {
                const { generateContentConfig: newConfig } = this.context.config.modelConfigService.getResolvedConfig({
                    ...modelConfigKey,
                    model: modelToUse,
                });
                currentGenerateContentConfig = newConfig;
            }
            lastModelToUse = modelToUse;
            const config = {
                ...currentGenerateContentConfig,
                // TODO(12622): Ensure we don't overrwrite these when they are
                // passed via config.
                systemInstruction: this.systemInstruction,
                tools: this.tools,
                abortSignal,
            };
            let contentsToUse = supportsModernFeatures(modelToUse)
                ? [...contentsForPreviewModel]
                : [...requestContents];
            const hookSystem = this.context.config.getHookSystem();
            if (hookSystem) {
                const beforeModelResult = await hookSystem.fireBeforeModelEvent({
                    model: modelToUse,
                    config,
                    contents: contentsToUse,
                });
                if (beforeModelResult.stopped) {
                    throw new AgentExecutionStoppedError(beforeModelResult.reason || 'Agent execution stopped by hook');
                }
                if (beforeModelResult.blocked) {
                    const syntheticResponse = beforeModelResult.syntheticResponse;
                    for (const candidate of syntheticResponse?.candidates ?? []) {
                        if (!candidate.finishReason) {
                            candidate.finishReason = FinishReason.STOP;
                        }
                    }
                    throw new AgentExecutionBlockedError(beforeModelResult.reason || 'Model call blocked by hook', syntheticResponse);
                }
                if (beforeModelResult.modifiedConfig) {
                    Object.assign(config, beforeModelResult.modifiedConfig);
                }
                if (beforeModelResult.modifiedContents &&
                    Array.isArray(beforeModelResult.modifiedContents)) {
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
                    contentsToUse = beforeModelResult.modifiedContents;
                }
                const toolSelectionResult = await hookSystem.fireBeforeToolSelectionEvent({
                    model: modelToUse,
                    config,
                    contents: contentsToUse,
                });
                if (toolSelectionResult.toolConfig) {
                    config.toolConfig = toolSelectionResult.toolConfig;
                }
                if (toolSelectionResult.tools &&
                    Array.isArray(toolSelectionResult.tools)) {
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
                    config.tools = toolSelectionResult.tools;
                }
            }
            if (this.onModelChanged) {
                this.tools = await this.onModelChanged(modelToUse);
            }
            // Track final request parameters for AfterModel hooks
            lastModelToUse = modelToUse;
            lastConfig = config;
            lastContentsToUse = contentsToUse;
            return this.context.config.getContentGenerator().generateContentStream({
                model: modelToUse,
                contents: contentsToUse,
                config,
            }, prompt_id, role);
        };
        const onPersistent429Callback = async (authType, error) => handleFallback(this.context.config, lastModelToUse, authType, error);
        const onValidationRequiredCallback = async (validationError) => {
            const handler = this.context.config.getValidationHandler();
            if (typeof handler !== 'function') {
                // No handler registered, re-throw to show default error message
                throw validationError;
            }
            return handler(validationError.validationLink, validationError.validationDescription, validationError.learnMoreUrl);
        };
        const streamResponse = await retryWithBackoff(apiCall, {
            onPersistent429: onPersistent429Callback,
            onValidationRequired: onValidationRequiredCallback,
            authType: this.context.config.getContentGeneratorConfig()?.authType,
            retryFetchErrors: this.context.config.getRetryFetchErrors(),
            signal: abortSignal,
            maxAttempts: availabilityMaxAttempts ?? this.context.config.getMaxAttempts(),
            getAvailabilityContext,
            onRetry: (attempt, error, delayMs) => {
                coreEvents.emitRetryAttempt({
                    attempt,
                    maxAttempts: availabilityMaxAttempts ?? this.context.config.getMaxAttempts(),
                    delayMs,
                    error: error instanceof Error ? error.message : String(error),
                    model: lastModelToUse,
                });
            },
        });
        // Store the original request for AfterModel hooks
        const originalRequest = {
            model: lastModelToUse,
            config: lastConfig,
            contents: lastContentsToUse,
        };
        return this.processStreamResponse(lastModelToUse, streamResponse, originalRequest);
    }
    /**
     * Returns the chat history.
     *
     * @remarks
     * The history is a list of contents alternating between user and model.
     *
     * There are two types of history:
     * - The `curated history` contains only the valid turns between user and
     * model, which will be included in the subsequent requests sent to the model.
     * - The `comprehensive history` contains all turns, including invalid or
     * empty model outputs, providing a complete record of the history.
     *
     * The history is updated after receiving the response from the model,
     * for streaming response, it means receiving the last chunk of the response.
     *
     * The `comprehensive history` is returned by default. To get the `curated
     * history`, set the `curated` parameter to `true`.
     *
     * @param curated - whether to return the curated history or the comprehensive
     * history.
     * @return History contents alternating between user and model for the entire
     * chat session.
     */
    getHistory(curated = false) {
        const history = curated
            ? extractCuratedHistory(this.history)
            : this.history;
        return [...history];
    }
    /**
     * Clears the chat history.
     */
    clearHistory() {
        this.history = [];
    }
    /**
     * Adds a new entry to the chat history.
     */
    addHistory(content) {
        this.history.push(content);
    }
    setHistory(history) {
        this.history = [...history];
        this.lastPromptTokenCount = estimateTokenCountSync(this.history.flatMap((c) => c.parts || []));
        this.chatRecordingService.updateMessagesFromHistory(history);
    }
    stripThoughtsFromHistory() {
        this.history = this.history.map((content) => {
            const newContent = { ...content };
            if (newContent.parts) {
                newContent.parts = newContent.parts.map((part) => {
                    if (part && typeof part === 'object' && 'thoughtSignature' in part) {
                        const newPart = { ...part };
                        delete newPart.thoughtSignature;
                        return newPart;
                    }
                    return part;
                });
            }
            return newContent;
        });
    }
    // To ensure our requests validate, the first function call in every model
    // turn within the active loop must have a `thoughtSignature` property.
    // If we do not do this, we will get back 400 errors from the API.
    ensureActiveLoopHasThoughtSignatures(requestContents) {
        // First, find the start of the active loop by finding the last user turn
        // with a text message, i.e. that is not a function response.
        let activeLoopStartIndex = -1;
        for (let i = requestContents.length - 1; i >= 0; i--) {
            const content = requestContents[i];
            if (content.role === 'user' && content.parts?.some((part) => part.text)) {
                activeLoopStartIndex = i;
                break;
            }
        }
        if (activeLoopStartIndex === -1) {
            return requestContents;
        }
        // Iterate through every message in the active loop, ensuring that the first
        // function call in each message's list of parts has a valid
        // thoughtSignature property. If it does not we replace the function call
        // with a copy that uses the synthetic thought signature.
        const newContents = requestContents.slice(); // Shallow copy the array
        for (let i = activeLoopStartIndex; i < newContents.length; i++) {
            const content = newContents[i];
            if (content.role === 'model' && content.parts) {
                const newParts = content.parts.slice();
                for (let j = 0; j < newParts.length; j++) {
                    const part = newParts[j];
                    if (part.functionCall) {
                        if (!part.thoughtSignature) {
                            newParts[j] = {
                                ...part,
                                thoughtSignature: SYNTHETIC_THOUGHT_SIGNATURE,
                            };
                            newContents[i] = {
                                ...content,
                                parts: newParts,
                            };
                        }
                        break; // Only consider the first function call
                    }
                }
            }
        }
        return newContents;
    }
    setTools(tools) {
        this.tools = tools;
    }
    async maybeIncludeSchemaDepthContext(error) {
        // Check for potentially problematic cyclic tools with cyclic schemas
        // and include a recommendation to remove potentially problematic tools.
        if (isSchemaDepthError(error.message) ||
            isInvalidArgumentError(error.message)) {
            const tools = this.context.toolRegistry.getAllTools();
            const cyclicSchemaTools = [];
            for (const tool of tools) {
                if ((tool.schema.parametersJsonSchema &&
                    hasCycleInSchema(tool.schema.parametersJsonSchema)) ||
                    (tool.schema.parameters && hasCycleInSchema(tool.schema.parameters))) {
                    cyclicSchemaTools.push(tool.displayName);
                }
            }
            if (cyclicSchemaTools.length > 0) {
                const extraDetails = `\n\nThis error was probably caused by cyclic schema references in one of the following tools, try disabling them with excludeTools:\n\n - ` +
                    cyclicSchemaTools.join(`\n - `) +
                    `\n`;
                error.message += extraDetails;
            }
        }
    }
    async *processStreamResponse(model, streamResponse, originalRequest) {
        const modelResponseParts = [];
        let hasToolCall = false;
        let hasThoughts = false;
        let finishReason;
        for await (const chunk of streamResponse) {
            const candidateWithReason = chunk?.candidates?.find((candidate) => candidate.finishReason);
            if (candidateWithReason) {
                // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
                finishReason = candidateWithReason.finishReason;
            }
            if (isValidResponse(chunk)) {
                const content = chunk.candidates?.[0]?.content;
                if (content?.parts) {
                    if (content.parts.some((part) => part.thought)) {
                        // Record thoughts
                        hasThoughts = true;
                        this.recordThoughtFromContent(content);
                    }
                    if (content.parts.some((part) => part.functionCall)) {
                        hasToolCall = true;
                    }
                    modelResponseParts.push(...content.parts.filter((part) => !part.thought));
                }
            }
            // Record token usage if this chunk has usageMetadata
            if (chunk.usageMetadata) {
                this.chatRecordingService.recordMessageTokens(chunk.usageMetadata);
                if (chunk.usageMetadata.promptTokenCount !== undefined) {
                    this.lastPromptTokenCount = chunk.usageMetadata.promptTokenCount;
                }
            }
            const hookSystem = this.context.config.getHookSystem();
            if (originalRequest && chunk && hookSystem) {
                const hookResult = await hookSystem.fireAfterModelEvent(originalRequest, chunk);
                if (hookResult.stopped) {
                    throw new AgentExecutionStoppedError(hookResult.reason || 'Agent execution stopped by hook');
                }
                if (hookResult.blocked) {
                    throw new AgentExecutionBlockedError(hookResult.reason || 'Agent execution blocked by hook', hookResult.response);
                }
                yield hookResult.response;
            }
            else {
                yield chunk;
            }
        }
        // String thoughts and consolidate text parts.
        const consolidatedParts = [];
        for (const part of modelResponseParts) {
            const lastPart = consolidatedParts[consolidatedParts.length - 1];
            if (lastPart?.text &&
                isValidNonThoughtTextPart(lastPart) &&
                isValidNonThoughtTextPart(part)) {
                lastPart.text += part.text;
            }
            else {
                consolidatedParts.push(part);
            }
        }
        const responseText = consolidatedParts
            .filter((part) => part.text)
            .map((part) => part.text)
            .join('')
            .trim();
        // Record model response text from the collected parts.
        // Also flush when there are thoughts or a tool call (even with no text)
        // so that BeforeTool hooks always see the latest transcript state.
        if (responseText || hasThoughts || hasToolCall) {
            this.chatRecordingService.recordMessage({
                model,
                type: 'gemini',
                content: responseText,
            });
        }
        // Stream validation logic: A stream is considered successful if:
        // 1. There's a tool call OR
        // 2. A not MALFORMED_FUNCTION_CALL finish reason and a non-mepty resp
        //
        // We throw an error only when there's no tool call AND:
        // - No finish reason, OR
        // - MALFORMED_FUNCTION_CALL finish reason OR
        // - Empty response text (e.g., only thoughts with no actual content)
        if (!hasToolCall) {
            if (!finishReason) {
                throw new InvalidStreamError('Model stream ended without a finish reason.', 'NO_FINISH_REASON');
            }
            if (finishReason === FinishReason.MALFORMED_FUNCTION_CALL) {
                throw new InvalidStreamError('Model stream ended with malformed function call.', 'MALFORMED_FUNCTION_CALL');
            }
            if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) {
                throw new InvalidStreamError('Model stream ended with unexpected tool call.', 'UNEXPECTED_TOOL_CALL');
            }
            if (!responseText) {
                throw new InvalidStreamError('Model stream ended with empty response text.', 'NO_RESPONSE_TEXT');
            }
        }
        this.history.push({ role: 'model', parts: consolidatedParts });
    }
    getLastPromptTokenCount() {
        return this.lastPromptTokenCount;
    }
    /**
     * Gets the chat recording service instance.
     */
    getChatRecordingService() {
        return this.chatRecordingService;
    }
    /**
     * Records completed tool calls with full metadata.
     * This is called by external components when tool calls complete, before sending responses to Gemini.
     */
    recordCompletedToolCalls(model, toolCalls) {
        const toolCallRecords = toolCalls.map((call) => {
            const resultDisplayRaw = call.response?.resultDisplay;
            const resultDisplay = typeof resultDisplayRaw === 'string' ||
                (typeof resultDisplayRaw === 'object' && resultDisplayRaw !== null)
                ? resultDisplayRaw
                : undefined;
            return {
                id: call.request.callId,
                name: call.request.name,
                args: call.request.args,
                result: call.response?.responseParts || null,
                status: call.status,
                timestamp: new Date().toISOString(),
                resultDisplay,
                description: 'invocation' in call ? call.invocation?.getDescription() : undefined,
            };
        });
        this.chatRecordingService.recordToolCalls(model, toolCallRecords);
    }
    /**
     * Extracts and records thought from thought content.
     */
    recordThoughtFromContent(content) {
        if (!content.parts || content.parts.length === 0) {
            return;
        }
        const thoughtPart = content.parts[0];
        if (thoughtPart.text) {
            // Extract subject and description using the same logic as turn.ts
            const rawText = thoughtPart.text;
            const subjectStringMatches = rawText.match(/\*\*(.*?)\*\*/s);
            const subject = subjectStringMatches
                ? subjectStringMatches[1].trim()
                : '';
            const description = rawText.replace(/\*\*(.*?)\*\*/s, '').trim();
            this.chatRecordingService.recordThought({
                subject,
                description,
            });
        }
    }
}
/** Visible for Testing */
export function isSchemaDepthError(errorMessage) {
    return errorMessage.includes('maximum schema depth exceeded');
}
export function isInvalidArgumentError(errorMessage) {
    return errorMessage.includes('Request contains an invalid argument');
}
//# sourceMappingURL=geminiChat.js.map