import { createNodeRuntime } from './node.js';
import { AGENT_CONTRACT_VERSION } from '../contracts/v1/agent.js';
import { logDebug } from '../utils/debugLogger.js';
import { isFallbackEligibleError, getFallbackReason } from '../providers/resilientProvider.js';
import { getConfiguredProviders, getLatestModelForProvider } from '../core/modelDiscovery.js';
class EventStream {
    queue = [];
    pending = null;
    closed = false;
    failure = null;
    push(value) {
        if (this.closed || this.failure) {
            return;
        }
        if (this.pending) {
            this.pending.resolve({ value, done: false });
            this.pending = null;
            return;
        }
        this.queue.push(value);
    }
    close() {
        if (this.closed || this.failure) {
            return;
        }
        this.closed = true;
        if (this.pending) {
            this.pending.resolve({ value: undefined, done: true });
            this.pending = null;
        }
    }
    fail(error) {
        if (this.closed || this.failure) {
            return;
        }
        this.failure = error;
        this.closed = true; // Mark as closed to prevent new pending promises
        if (this.pending) {
            this.pending.reject(error);
            this.pending = null;
        }
    }
    next() {
        if (this.queue.length) {
            const value = this.queue.shift();
            return Promise.resolve({ value, done: false });
        }
        if (this.failure) {
            const error = this.failure;
            this.failure = null;
            return Promise.reject(error);
        }
        if (this.closed) {
            return Promise.resolve({ value: undefined, done: true });
        }
        return new Promise((resolve, reject) => {
            this.pending = { resolve, reject };
        });
    }
    return() {
        this.close();
        return Promise.resolve({ value: undefined, done: true });
    }
    throw(error) {
        const err = error instanceof Error ? error : new Error(String(error));
        this.fail(err);
        return Promise.reject(err);
    }
    [Symbol.asyncIterator]() {
        return this;
    }
}
function mergeToolObservers(primary, secondary) {
    if (!secondary) {
        return primary;
    }
    return {
        onToolStart(call) {
            primary.onToolStart?.(call);
            secondary.onToolStart?.(call);
        },
        onToolResult(call, output) {
            primary.onToolResult?.(call, output);
            secondary.onToolResult?.(call, output);
        },
        onToolProgress(call, progress) {
            primary.onToolProgress?.(call, progress);
            secondary.onToolProgress?.(call, progress);
        },
        onToolError(call, error) {
            primary.onToolError?.(call, error);
            secondary.onToolError?.(call, error);
        },
        onCacheHit(call) {
            primary.onCacheHit?.(call);
            secondary.onCacheHit?.(call);
        },
        onToolWarning(call, warning) {
            primary.onToolWarning?.(call, warning);
            secondary.onToolWarning?.(call, warning);
        },
    };
}
function createControllerToolObserver(ref) {
    const emit = (event) => {
        ref.current?.push(event);
    };
    const timestamp = () => Date.now();
    return {
        onToolStart(call) {
            emit({
                type: 'tool.start',
                timestamp: timestamp(),
                toolName: call.name,
                toolCallId: call.id,
                parameters: { ...call.arguments },
            });
        },
        onToolResult(call, output) {
            emit({
                type: 'tool.complete',
                timestamp: timestamp(),
                toolName: call.name,
                toolCallId: call.id,
                result: output,
            });
        },
        onToolError(call, error) {
            emit({
                type: 'tool.error',
                timestamp: timestamp(),
                toolName: call.name,
                toolCallId: call.id,
                error,
            });
        },
    };
}
export async function createAgentController(options, additionalObserver) {
    const sinkRef = { current: null };
    const observer = createControllerToolObserver(sinkRef);
    const runtime = await createNodeRuntime({
        profile: options.profile,
        workspaceContext: options.workspaceContext,
        workingDir: options.workingDir,
        env: options.env,
        toolObserver: mergeToolObservers(observer, additionalObserver),
        additionalModules: options.modules,
    });
    return new AgentController({ runtime, sinkRef, externalCallbacks: options.callbacks });
}
export class AgentController {
    session;
    sinkRef;
    externalCallbacks;
    activeSink = null;
    agent = null;
    cachedHistory = [];
    selection;
    /** Set of providers that have failed with non-retryable errors in this session */
    failedProviders = new Set();
    /** Maximum fallback attempts per send() call */
    static MAX_FALLBACK_ATTEMPTS = 3;
    activeTimeout = null;
    inflightReject = null;
    constructor(dependencies) {
        this.session = dependencies.runtime.session;
        this.sinkRef = dependencies.sinkRef;
        this.externalCallbacks = dependencies.externalCallbacks;
        this.selection = this.buildInitialSelection();
    }
    buildInitialSelection() {
        const config = this.session.profileConfig;
        return {
            provider: config.provider,
            model: config.model,
            temperature: config.temperature,
            maxTokens: config.maxTokens,
            systemPrompt: config.systemPrompt,
        };
    }
    ensureAgent() {
        if (this.agent) {
            return this.agent;
        }
        const agent = this.session.createAgent(this.selection, this.createAgentCallbacks(), undefined, {
            explainEdits: true,
        });
        if (this.cachedHistory.length) {
            agent.loadHistory(this.cachedHistory);
        }
        this.agent = agent;
        return agent;
    }
    createAgentCallbacks() {
        return {
            onRequestReceived: (requestPreview) => {
                // Signal to UI that request was received - let model handle natural acknowledgment
                // Don't emit verbatim echo - it's redundant and the model's response should acknowledge contextually
                this.externalCallbacks?.onRequestReceived?.(requestPreview);
            },
            onAssistantMessage: (content, metadata) => {
                this.handleAssistantMessage(content, metadata);
                this.externalCallbacks?.onAssistantMessage?.(content, metadata);
            },
            onStreamChunk: (chunk, type) => {
                if (type === 'content') {
                    // Content chunks go to message.delta for streaming display
                    this.emitDelta(chunk, false);
                }
                else if (type === 'reasoning') {
                    // Reasoning chunks go to reasoning event for thought display
                    this.emitReasoning(chunk);
                }
                // Pass all chunks to external callbacks
                this.externalCallbacks?.onStreamChunk?.(chunk, type);
            },
            onUsage: (usage) => {
                this.emitUsage(usage);
                this.externalCallbacks?.onUsage?.(usage);
            },
            onContextPruned: (removedCount, stats) => {
                this.externalCallbacks?.onContextPruned?.(removedCount, stats);
            },
            onContextSquishing: (message) => {
                this.externalCallbacks?.onContextSquishing?.(message);
            },
            onContextRecovery: (attempt, maxAttempts, message) => {
                this.externalCallbacks?.onContextRecovery?.(attempt, maxAttempts, message);
            },
            onContinueAfterRecovery: () => {
                this.externalCallbacks?.onContinueAfterRecovery?.();
            },
            onMultilinePaste: (summary, metadata) => {
                this.externalCallbacks?.onMultilinePaste?.(summary, metadata);
            },
            onEditExplanation: (payload) => {
                this.handleEditExplanation(payload);
                this.externalCallbacks?.onEditExplanation?.(payload);
            },
            onRetrying: (attempt, maxAttempts, error) => {
                // Emit delta event to show retry status
                this.emitDelta(`[Retrying ${attempt}/${maxAttempts}: ${error.message}]`, false);
                this.externalCallbacks?.onRetrying?.(attempt, maxAttempts, error);
            },
            // onBeforeFirstToolCall not needed - model's reasoning is now emitted as thought events
        };
    }
    /**
     * Check if content looks like garbage/leaked reasoning fragments.
     * Returns true if the content should be filtered out.
     * NOTE: Keep this minimal to avoid suppressing legitimate short responses.
     */
    isGarbageContent(content) {
        const trimmed = content.trim();
        if (!trimmed)
            return true;
        // Only filter pure punctuation/markdown artifacts
        if (/^[)\]}>*`'".:,!?|│┃─━═\s]+$/.test(trimmed))
            return true;
        // Just newlines or whitespace
        if (/^[\s\n\r]+$/.test(trimmed))
            return true;
        // Removed aggressive short fragment filtering - was suppressing legitimate content
        return false;
    }
    emitDelta(content, isFinal) {
        if (!content?.trim()) {
            return;
        }
        // Filter out garbage/leaked reasoning fragments
        if (this.isGarbageContent(content)) {
            return;
        }
        this.activeSink?.push({
            type: 'message.delta',
            timestamp: Date.now(),
            content,
            isFinal,
        });
    }
    emitError(message) {
        this.activeSink?.push({
            type: 'error',
            timestamp: Date.now(),
            error: message,
        });
    }
    emitReasoning(content) {
        if (!content?.trim()) {
            return;
        }
        // Filter out garbage/leaked formatting fragments in reasoning too
        if (this.isGarbageContent(content)) {
            return;
        }
        this.activeSink?.push({
            type: 'reasoning',
            timestamp: Date.now(),
            content,
        });
    }
    emitUsage(usage) {
        if (!usage) {
            return;
        }
        this.activeSink?.push({
            type: 'usage',
            timestamp: Date.now(),
            inputTokens: usage.inputTokens,
            outputTokens: usage.outputTokens,
            totalTokens: usage.totalTokens,
        });
    }
    handleEditExplanation(payload) {
        if (!this.activeSink) {
            return;
        }
        if (!payload.explanation?.trim()) {
            return;
        }
        this.activeSink.push({
            type: 'edit.explanation',
            timestamp: Date.now(),
            content: payload.explanation,
            files: payload.files,
            toolName: payload.toolName,
            toolCallId: payload.toolCallId,
        });
    }
    handleAssistantMessage(content, metadata) {
        if (!this.activeSink) {
            return;
        }
        if (!content.trim()) {
            return;
        }
        if (metadata.suppressDisplay) {
            return;
        }
        if (!metadata.isFinal) {
            this.emitDelta(content, false);
            return;
        }
        const elapsedMs = metadata.elapsedMs ?? 0;
        this.activeSink.push({
            type: 'message.complete',
            timestamp: Date.now(),
            content,
            elapsedMs,
        });
        this.emitUsage(metadata.usage ?? null);
    }
    updateCachedHistory() {
        if (this.agent) {
            this.cachedHistory = this.agent.getHistory();
        }
    }
    cancel(reason) {
        if (!this.activeSink) {
            return;
        }
        const error = new Error(reason ?? 'Run cancelled');
        try {
            this.agent?.cancel?.(reason);
        }
        catch {
            // ignore cancellation errors
        }
        this.rejectInflight(error);
        this.activeSink.fail(error);
        this.activeSink = null;
        this.sinkRef.current = null;
        this.clearActiveTimeout();
    }
    clearActiveTimeout() {
        if (this.activeTimeout) {
            clearTimeout(this.activeTimeout);
            this.activeTimeout = null;
        }
    }
    rejectInflight(error) {
        if (this.inflightReject) {
            this.inflightReject(error);
            this.inflightReject = null;
        }
    }
    async *send(message) {
        if (this.activeSink) {
            throw new Error('Agent runtime is already processing a message. Please wait for the current run to finish.');
        }
        // Reset failed providers at the start of each new message
        // (providers might have recovered, quotas might have reset, etc.)
        this.failedProviders.clear();
        let fallbackAttempts = 0;
        // Retry loop for fallback handling
        while (fallbackAttempts < AgentController.MAX_FALLBACK_ATTEMPTS) {
            const agent = this.ensureAgent();
            const sink = new EventStream();
            this.activeSink = sink;
            this.sinkRef.current = sink;
            sink.push({ type: 'message.start', timestamp: Date.now() });
            const timeoutMsRaw = process.env['AGI_AGENT_RUN_TIMEOUT_MS'];
            const timeoutMs = timeoutMsRaw ? Number(timeoutMsRaw) : NaN;
            if (!Number.isNaN(timeoutMs) && timeoutMs > 0 && timeoutMs < 24 * 60 * 60 * 1000) {
                // Only set timeout if less than 24 hours (practically infinite)
                this.activeTimeout = setTimeout(() => {
                    const err = new Error(`Run timed out after ${timeoutMs}ms`);
                    this.rejectInflight(err);
                    sink.fail(err);
                    try {
                        this.agent?.cancel?.('timeout');
                    }
                    catch {
                        // ignore
                    }
                }, timeoutMs);
            }
            else {
                // Set a very large timeout (24 hours) as fallback
                this.activeTimeout = setTimeout(() => {
                    const err = new Error(`Run timed out after 24h`);
                    this.rejectInflight(err);
                    sink.fail(err);
                    try {
                        this.agent?.cancel?.('timeout');
                    }
                    catch {
                        // ignore
                    }
                }, 24 * 60 * 60 * 1000);
            }
            let caughtError = null;
            let fallbackSucceeded = false;
            let cancelRun = null;
            const cancelPromise = new Promise((_, reject) => {
                cancelRun = reject;
            });
            this.inflightReject = (error) => {
                cancelRun?.(error);
            };
            const run = Promise.race([
                agent.send(message, true),
                cancelPromise,
            ])
                .then(() => {
                this.updateCachedHistory();
                this.clearActiveTimeout();
                sink.close();
            })
                .catch(async (error) => {
                const errorObj = error instanceof Error ? error : new Error(String(error));
                caughtError = errorObj;
                this.clearActiveTimeout();
                const cancelled = /cancel/i.test(errorObj.message);
                const timedOut = /timed out/i.test(errorObj.message);
                if (cancelled || timedOut) {
                    this.emitError(errorObj.message);
                    sink.fail(errorObj);
                    return;
                }
                // Check if this error is eligible for fallback
                if (isFallbackEligibleError(error) && fallbackAttempts < AgentController.MAX_FALLBACK_ATTEMPTS - 1) {
                    logDebug(`[AgentController] Fallback-eligible error detected: ${errorObj.message}`);
                    fallbackSucceeded = await this.attemptFallback(errorObj);
                    if (fallbackSucceeded) {
                        // Close this sink without error - we'll retry with new provider
                        sink.close();
                        return;
                    }
                }
                // Not fallback-eligible or no fallback available - emit error and fail
                this.emitError(errorObj.message);
                sink.fail(errorObj);
            })
                .finally(() => {
                this.clearActiveTimeout();
                this.inflightReject = null;
                if (this.activeSink === sink) {
                    this.activeSink = null;
                    this.sinkRef.current = null;
                }
            });
            try {
                for await (const event of sink) {
                    yield event;
                }
            }
            finally {
                await run;
            }
            // If we successfully fell back, increment counter and continue loop
            if (fallbackSucceeded && caughtError) {
                fallbackAttempts++;
                logDebug(`[AgentController] Retrying with fallback provider (attempt ${fallbackAttempts}/${AgentController.MAX_FALLBACK_ATTEMPTS})`);
                continue;
            }
            // No fallback happened or it failed - exit loop
            break;
        }
    }
    async switchModel(config) {
        this.updateCachedHistory();
        this.agent = null;
        this.selection = {
            provider: config.provider,
            model: config.model,
            temperature: config.temperature,
            maxTokens: config.maxTokens,
            systemPrompt: this.selection.systemPrompt,
        };
        this.session.updateToolContext(this.selection);
    }
    getCapabilities() {
        const tools = this.session.toolRuntime.listProviderTools();
        const manifestTools = tools.map((tool) => ({
            name: tool.name,
            description: tool.description,
            category: 'general',
        }));
        return {
            contractVersion: AGENT_CONTRACT_VERSION,
            profile: this.session.profile,
            model: this.toModelConfig(this.selection),
            tools: manifestTools,
            features: ['streaming', 'tool-calls'],
        };
    }
    registerToolSuite(suiteId, suite) {
        this.session.toolRuntime.registerSuite({ ...suite, id: suiteId });
    }
    unregisterToolSuite(suiteId) {
        this.session.toolRuntime.unregisterSuite(suiteId);
    }
    getHistory() {
        if (this.agent) {
            return this.agent.getHistory();
        }
        return [...this.cachedHistory];
    }
    clearHistory() {
        this.cachedHistory = [];
        this.agent?.clearHistory();
    }
    /**
     * Check if the controller is currently processing a message.
     */
    isProcessing() {
        return this.activeSink !== null;
    }
    /**
     * Force-clear any lingering active state. Use this before starting a new
     * operation (like attack tournament) to ensure clean state.
     * This will close any active sink without waiting for completion.
     */
    forceReset() {
        if (this.activeSink) {
            try {
                this.activeSink.close();
            }
            catch {
                // Ignore errors - sink may already be closed
            }
            this.activeSink = null;
            this.sinkRef.current = null;
        }
    }
    /**
     * Sanitize history by fixing orphaned tool calls (tool_calls without corresponding tool results).
     * This can happen when a run is interrupted mid-tool-execution.
     * We add placeholder error results for any orphaned tool calls to keep history valid.
     */
    sanitizeHistory() {
        const history = this.getHistory();
        if (history.length === 0)
            return;
        const sanitized = [];
        for (let i = 0; i < history.length; i++) {
            const msg = history[i];
            sanitized.push(msg);
            // Check if this is an assistant message with tool_calls
            if (msg.role === 'assistant' && msg.toolCalls?.length) {
                // Look ahead for tool results
                const toolCallIds = new Set(msg.toolCalls.map(tc => tc.id));
                let nextIdx = i + 1;
                // Consume any following tool messages
                while (nextIdx < history.length && history[nextIdx].role === 'tool') {
                    const toolMsg = history[nextIdx];
                    if (toolMsg.toolCallId) {
                        toolCallIds.delete(toolMsg.toolCallId);
                    }
                    sanitized.push(history[nextIdx]);
                    nextIdx++;
                }
                // Add placeholder results for any orphaned tool calls
                for (const orphanedId of toolCallIds) {
                    const orphanedCall = msg.toolCalls.find(tc => tc.id === orphanedId);
                    const toolName = orphanedCall?.name ?? 'unknown';
                    sanitized.push({
                        role: 'tool',
                        name: toolName,
                        toolCallId: orphanedId,
                        content: `[Interrupted: ${toolName} execution was cancelled]`,
                    });
                }
                // Skip the tool messages we already processed
                i = nextIdx - 1;
            }
        }
        // Update both cached history and agent history
        this.cachedHistory = sanitized;
        if (this.agent) {
            this.agent.loadHistory(sanitized);
        }
    }
    toModelConfig(selection) {
        return {
            provider: selection.provider,
            model: selection.model,
            temperature: selection.temperature,
            maxTokens: selection.maxTokens,
        };
    }
    /**
     * Find the next available provider for fallback.
     * Excludes providers that have already failed in this session.
     */
    findFallbackProvider() {
        const configured = getConfiguredProviders();
        const currentProvider = this.selection.provider;
        // Provider preference order (excluding current and failed)
        // deepseek-reasoner is default, grok (xai) is backup
        const preferenceOrder = ['deepseek', 'xai'];
        for (const providerId of preferenceOrder) {
            // Skip current provider and already failed providers
            if (providerId === currentProvider || this.failedProviders.has(providerId)) {
                continue;
            }
            // Check if this provider is configured
            const provider = configured.find(p => p.id === providerId);
            if (provider) {
                const model = getLatestModelForProvider(providerId);
                if (model) {
                    return { provider: providerId, model };
                }
            }
        }
        return null;
    }
    /**
     * Emit a provider fallback event
     */
    emitFallbackEvent(fromProvider, fromModel, toProvider, toModel, reason, error) {
        this.activeSink?.push({
            type: 'provider.fallback',
            timestamp: Date.now(),
            fromProvider,
            fromModel,
            toProvider,
            toModel,
            reason,
            error,
        });
    }
    /**
     * Attempt to switch to a fallback provider
     */
    async attemptFallback(error) {
        const fallback = this.findFallbackProvider();
        if (!fallback) {
            logDebug('[AgentController] No fallback provider available');
            return false;
        }
        const reason = getFallbackReason(error);
        const fromProvider = this.selection.provider;
        const fromModel = this.selection.model;
        // Mark current provider as failed
        this.failedProviders.add(fromProvider);
        // Emit fallback event
        this.emitFallbackEvent(fromProvider, fromModel, fallback.provider, fallback.model, reason, error.message);
        logDebug(`[AgentController] Falling back from ${fromProvider}/${fromModel} to ${fallback.provider}/${fallback.model}: ${reason}`);
        // Switch to fallback provider
        await this.switchModel({
            provider: fallback.provider,
            model: fallback.model,
        });
        return true;
    }
    getToolSuites() {
        return this.session.toolSuites;
    }
}
//# sourceMappingURL=agentController.js.map