import { join } from "node:path";
import { Agent, } from "@earendil-works/pi-agent-core";
import { clampThinkingLevel, streamSimple, } from "@earendil-works/pi-ai/compat";
import { getAgentDir } from "../config.js";
import { resolvePath } from "../utils/paths.js";
import { AgentSession } from "./agent-session.js";
import { formatNoModelsAvailableMessage } from "./auth-guidance.js";
import { AuthStorage } from "./auth-storage.js";
import { shouldApplyCodexFastMode, streamWithCodexFastMode, withCodexFastModePayload, withCodexFastModeStreamOptions, } from "./codex-fast-mode.js";
import { restoreAnthropicReplayThinkingBlocks } from "./anthropic-thinking-guard.js";
import { sanitizeCopilotGeminiPayload } from "./copilot-gemini-payload-sanitizer.js";
import { restoreCopilotGeminiReasoningOpaque } from "./copilot-gemini-reasoning.js";
import { normalizeCopilotGeminiReplayToolArguments } from "./copilot-gemini-tool-arguments.js";
import { getModelDefaultContextWindow, getSupportedContextWindows, selectContextWindow } from "./context-window.js";
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
import { convertToLlm } from "./messages.js";
import { ModelRegistry } from "./model-registry.js";
import { findInitialModel, resolveRestoredModelReference } from "./model-resolver.js";
import { DefaultResourceLoader } from "./resource-loader.js";
import { getDefaultSessionDir, SessionManager } from "./session-manager.js";
import { SettingsManager } from "./settings-manager.js";
import { mergeProviderAttributionHeaders } from "./provider-attribution.js";
import { sanitizeOpenAIResponsesPayload } from "./openai-responses-payload-sanitizer.js";
import { scrubPreCompactionAssistantUsage } from "./provider-context-usage.js";
import { time } from "./timings.js";
import { defaultToolNames } from "./tools/index.js";
export * from "./sdk-exports.js";
// Helper Functions
function getDefaultAgentDir() {
    return getAgentDir();
}
const COPILOT_CONTEXT_WINDOW_SELECTION_OPTIONS = { allowCopilotLongContextFallback: true };
function getAlreadyAppliedContextWindow(model) {
    const defaultContextWindow = getModelDefaultContextWindow(model);
    if (model.contextWindow === defaultContextWindow) {
        return undefined;
    }
    return getSupportedContextWindows(model).includes(model.contextWindow)
        ? model.contextWindow
        : undefined;
}
/**
 * Create an AgentSession with the specified options.
 *
 * @example
 * ```typescript
 * // Minimal - uses defaults
 * const { session } = await createAgentSession();
 *
 * // With explicit model
 * import { getModel } from '@earendil-works/pi-ai/compat';
 * const { session } = await createAgentSession({
 *   model: getModel('anthropic', 'claude-opus-4-5'),
 *   thinkingLevel: 'high',
 * });
 *
 * // Continue previous session
 * const { session, modelFallbackMessage } = await createAgentSession({
 *   continueSession: true,
 * });
 *
 * // Full control
 * const loader = new DefaultResourceLoader({
 *   cwd: process.cwd(),
 *   agentDir: getAgentDir(),
 *   settingsManager: SettingsManager.create(),
 * });
 * await loader.reload();
 * const { session } = await createAgentSession({
 *   model: myModel,
 *   tools: ["read", "bash"],
 *   resourceLoader: loader,
 *   sessionManager: SessionManager.inMemory(),
 * });
 * ```
 */
export async function createAgentSession(options = {}) {
    const cwd = resolvePath(options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd());
    const agentDir = options.agentDir ? resolvePath(options.agentDir) : getDefaultAgentDir();
    let resourceLoader = options.resourceLoader;
    // Use provided or create AuthStorage and ModelRegistry. When a modelRegistry
    // is supplied (e.g. a workflow stage reusing one registry across model
    // fallback candidates), do NOT also build a fresh AuthStorage: its
    // constructor eagerly calls reload(), which acquires the auth.json file lock
    // and, under contention, can fail and leave an empty credential set. Reusing
    // the supplied registry's already-loaded auth avoids that race (issue #1431).
    const authPath = options.agentDir ? join(agentDir, "auth.json") : undefined;
    const modelsPath = options.agentDir
        ? join(agentDir, "models.json")
        : undefined;
    const modelRegistry = options.modelRegistry ??
        ModelRegistry.create(options.authStorage ?? AuthStorage.create(authPath), modelsPath);
    const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
    const sessionManager = options.sessionManager ??
        SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));
    // Mark workflow-created sessions as internal so they are excluded from the
    // standard `/resume` history while remaining resumable via `/workflow resume`.
    // Only stamped when the orchestration context identifies a workflow stage;
    // reattaching to an already-marked session preserves its existing marker.
    if (options.orchestrationContext?.kind === "workflow-stage") {
        const ctx = options.orchestrationContext;
        sessionManager.markSessionInternal({
            runId: ctx.workflowRunId,
            stageId: ctx.workflowStageId,
            stageName: ctx.workflowStageName,
        });
    }
    if (!resourceLoader) {
        resourceLoader = new DefaultResourceLoader({
            cwd,
            agentDir,
            settingsManager,
        });
        await resourceLoader.reload();
        time("resourceLoader.reload");
    }
    // Check if session has existing data to restore
    const existingSession = sessionManager.buildSessionContext();
    const hasExistingSession = existingSession.messages.length > 0;
    const hasThinkingEntry = sessionManager
        .getBranch()
        .some((entry) => entry.type === "thinking_level_change");
    let model = options.model;
    let modelFallbackMessage;
    // If session has data, try to restore model from it
    if (!model && hasExistingSession && existingSession.model) {
        const restoredModel = await resolveRestoredModelReference(existingSession.model.provider, existingSession.model.modelId, modelRegistry);
        if (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {
            model = restoredModel;
        }
        if (!model) {
            modelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;
        }
    }
    // If still no model, use findInitialModel (checks settings default, then provider defaults)
    if (!model) {
        const result = await findInitialModel({
            scopedModels: [],
            isContinuing: hasExistingSession,
            defaultProvider: settingsManager.getDefaultProvider(),
            defaultModelId: settingsManager.getDefaultModel(),
            defaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),
            modelRegistry,
        });
        model = result.model;
        if (!model) {
            modelFallbackMessage = formatNoModelsAvailableMessage();
        }
        else if (modelFallbackMessage) {
            modelFallbackMessage += `. Using ${model.provider}/${model.id}`;
        }
    }
    let thinkingLevel = options.thinkingLevel;
    // If session has data, restore thinking level from it
    if (thinkingLevel === undefined && hasExistingSession) {
        thinkingLevel = hasThinkingEntry
            ? existingSession.thinkingLevel
            : (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);
    }
    // Fall back to settings default
    if (thinkingLevel === undefined) {
        thinkingLevel =
            settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
    }
    // Clamp to model capabilities
    if (!model) {
        thinkingLevel = "off";
    }
    else {
        thinkingLevel = clampThinkingLevel(model, thinkingLevel);
    }
    let selectedContextWindow;
    let contextWindowWarning;
    let contextWindowError;
    const explicitContextWindowSelection = options.contextWindow !== undefined;
    const incomingModelContextWindow = model && options.model ? getAlreadyAppliedContextWindow(model) : undefined;
    const sessionContextWindow = hasExistingSession ? existingSession.contextWindow : undefined;
    const modelSettingsContextWindow = model ? settingsManager.getDefaultContextWindowForModel(model.provider, model.id) : undefined;
    const globalSettingsContextWindow = settingsManager.getDefaultContextWindow();
    const contextWindowRequest = options.contextWindow !== undefined
        ? { contextWindow: options.contextWindow, source: "explicit" }
        : incomingModelContextWindow !== undefined
            ? { contextWindow: incomingModelContextWindow, source: "incoming-model" }
            : sessionContextWindow !== undefined
                ? { contextWindow: sessionContextWindow, source: "session" }
                : modelSettingsContextWindow !== undefined
                    ? { contextWindow: modelSettingsContextWindow, source: "model-settings" }
                    : globalSettingsContextWindow !== undefined
                        ? { contextWindow: globalSettingsContextWindow, source: "global-settings" }
                        : undefined;
    if (model && contextWindowRequest !== undefined) {
        const selected = selectContextWindow(model, contextWindowRequest.contextWindow, COPILOT_CONTEXT_WINDOW_SELECTION_OPTIONS);
        if ("error" in selected) {
            if (options.contextWindowStrict) {
                contextWindowError = selected.error;
            }
            else if (contextWindowRequest.source !== "global-settings") {
                contextWindowWarning = selected.error;
            }
        }
        else {
            model = selected.model;
            selectedContextWindow = selected.contextWindow;
        }
    }
    const allowedToolNames = options.tools ?? (options.noTools === "all" ? [] : undefined);
    const initialActiveToolNames = options.tools
        ? [...options.tools]
        : options.noTools
            ? []
            : [...defaultToolNames];
    let agent;
    let lastConvertedLlmMessages;
    // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
    const convertToLlmWithBlockImages = (messages) => {
        const converted = convertToLlm(messages);
        // Check setting dynamically so mid-session changes take effect
        if (!settingsManager.getBlockImages()) {
            lastConvertedLlmMessages = converted;
            return converted;
        }
        // Filter out ImageContent from all messages, replacing with text placeholder
        const filtered = converted.map((msg) => {
            if (msg.role === "user" || msg.role === "toolResult") {
                const content = msg.content;
                if (Array.isArray(content)) {
                    const hasImages = content.some((c) => c.type === "image");
                    if (hasImages) {
                        const filteredContent = content
                            .map((c) => c.type === "image"
                            ? {
                                type: "text",
                                text: "Image reading is disabled.",
                            }
                            : c)
                            .filter((c, i, arr) => 
                        // Dedupe consecutive "Image reading is disabled." texts
                        !(c.type === "text" &&
                            c.text === "Image reading is disabled." &&
                            i > 0 &&
                            arr[i - 1].type === "text" &&
                            arr[i - 1].text ===
                                "Image reading is disabled."));
                        return { ...msg, content: filteredContent };
                    }
                }
            }
            return msg;
        });
        lastConvertedLlmMessages = filtered;
        return filtered;
    };
    const extensionRunnerRef = {};
    const isCodexFastModeEnabled = (requestModel) => shouldApplyCodexFastMode(requestModel, settingsManager.getCodexFastModeSettings(), options.orchestrationContext);
    agent = new Agent({
        initialState: {
            systemPrompt: "",
            model,
            thinkingLevel,
            tools: [],
        },
        convertToLlm: convertToLlmWithBlockImages,
        streamFn: async (model, context, streamOptions) => {
            const auth = await modelRegistry.getApiKeyAndHeaders(model);
            if (!auth.ok) {
                throw new Error(auth.error);
            }
            const providerRetrySettings = settingsManager.getProviderRetrySettings();
            const httpIdleTimeoutMs = settingsManager.getHttpIdleTimeoutMs();
            // SDKs treat timeout=0 as 0ms (immediate timeout), not "no timeout".
            // Use max int32 to effectively disable the timeout.
            const effectiveTimeoutMs = httpIdleTimeoutMs === 0 ? 2147483647 : httpIdleTimeoutMs;
            const timeoutMs = streamOptions?.timeoutMs ?? providerRetrySettings.timeoutMs ?? effectiveTimeoutMs;
            const websocketConnectTimeoutMs = streamOptions?.websocketConnectTimeoutMs ?? settingsManager.getWebSocketConnectTimeoutMs();
            const attributionHeaders = mergeProviderAttributionHeaders(model, settingsManager, streamOptions?.sessionId, auth.headers, streamOptions?.headers);
            const fastModeEnabled = isCodexFastModeEnabled(model);
            const codexFastModeStreamOptions = withCodexFastModeStreamOptions({
                ...streamOptions,
                apiKey: auth.apiKey,
                timeoutMs,
                websocketConnectTimeoutMs,
                maxRetries: streamOptions?.maxRetries ?? providerRetrySettings.maxRetries,
                maxRetryDelayMs: streamOptions?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,
                headers: attributionHeaders,
            }, fastModeEnabled);
            if (modelRegistry.hasRegisteredStreamSimpleForApi(model.api)) {
                return streamSimple(model, context, codexFastModeStreamOptions);
            }
            return streamWithCodexFastMode(model, context, codexFastModeStreamOptions);
        },
        onPayload: async (payload, model) => {
            const fastModeEnabled = isCodexFastModeEnabled(model);
            const guardedPayload = withCodexFastModePayload(payload, fastModeEnabled);
            const sourceMessages = lastConvertedLlmMessages;
            const replayGuardedPayload = sourceMessages
                ? restoreAnthropicReplayThinkingBlocks(guardedPayload, sourceMessages, model)
                : guardedPayload;
            const runner = extensionRunnerRef.current;
            let finalPayload;
            if (!runner?.hasHandlers("before_provider_request")) {
                finalPayload = replayGuardedPayload;
            }
            else {
                const extensionPayload = await runner.emitBeforeProviderRequest(replayGuardedPayload);
                finalPayload = sourceMessages
                    ? restoreAnthropicReplayThinkingBlocks(extensionPayload, sourceMessages, model)
                    : extensionPayload;
            }
            // GitHub Copilot Gemini models are served through CAPI, which translates
            // the OpenAI request into Google GenAI and rejects tool schemas whose
            // `anyOf`/`oneOf` wraps a complex object (HTTP 400 invalid request body).
            // Sanitize tool JSON Schemas into Gemini's supported subset. No-op for
            // every other provider/model, and runs last so it also covers tools
            // injected by `before_provider_request` extensions.
            const schemaSanitized = sanitizeCopilotGeminiPayload(finalPayload, model);
            // Reconstruct flattened tool-call arguments on replayed assistant
            // messages (for example `edits[0].newText` -> `edits: [{ newText }]`).
            // CAPI parses replayed arguments straight into Gemini's FunctionCall,
            // and a flattened/malformed prior call ends the next turn with
            // `finish_reason: "error"`. No-op for well-formed args / other models.
            const replayArgsNormalized = normalizeCopilotGeminiReplayToolArguments(schemaSanitized, model);
            // CAPI carries Gemini thought signatures in a `reasoning_opaque` field it
            // reads back off the assistant message on replay. Convert the
            // `reasoning_details` the client re-emits (captured inbound by the SSE
            // interceptor) into that field so multi-turn tool use keeps its thought
            // signature instead of dying on an empty completion. No-op otherwise.
            const reasoningRestored = restoreCopilotGeminiReasoningOpaque(replayArgsNormalized, model);
            return sanitizeOpenAIResponsesPayload(reasoningRestored, model);
        },
        onResponse: async (response, _model) => {
            const runner = extensionRunnerRef.current;
            if (!runner?.hasHandlers("after_provider_response")) {
                return;
            }
            await runner.emit({
                type: "after_provider_response",
                status: response.status,
                headers: response.headers,
            });
        },
        sessionId: sessionManager.getSessionId(),
        transformContext: async (messages) => {
            const runner = extensionRunnerRef.current;
            const transformed = runner ? await runner.emitContext(messages) : messages;
            return scrubPreCompactionAssistantUsage(transformed, sessionManager.getBranch());
        },
        steeringMode: settingsManager.getSteeringMode(),
        followUpMode: settingsManager.getFollowUpMode(),
        transport: settingsManager.getTransport(),
        thinkingBudgets: settingsManager.getThinkingBudgets(),
        maxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,
    });
    // Restore messages if session has existing data
    if (hasExistingSession) {
        agent.state.messages = existingSession.messages;
        const transcriptContextWindow = model
            ? (existingSession.contextWindow ?? getModelDefaultContextWindow(model))
            : undefined;
        if (selectedContextWindow !== undefined &&
            (explicitContextWindowSelection || selectedContextWindow !== transcriptContextWindow)) {
            sessionManager.appendContextWindowChange(selectedContextWindow);
        }
        if (!hasThinkingEntry) {
            sessionManager.appendThinkingLevelChange(thinkingLevel);
        }
    }
    else {
        // Save initial model and thinking level for new sessions so they can be restored on resume
        if (model) {
            sessionManager.appendModelChange(model.provider, model.id);
            if (selectedContextWindow !== undefined &&
                (explicitContextWindowSelection || selectedContextWindow !== getModelDefaultContextWindow(model))) {
                sessionManager.appendContextWindowChange(selectedContextWindow);
            }
        }
        sessionManager.appendThinkingLevelChange(thinkingLevel);
    }
    const session = new AgentSession({
        agent,
        sessionManager,
        settingsManager,
        cwd,
        scopedModels: options.scopedModels,
        fallbackModels: options.fallbackModels ?? settingsManager.getFallbackModels(),
        resourceLoader,
        customTools: options.customTools,
        modelRegistry,
        initialActiveToolNames,
        allowedToolNames,
        excludedToolNames: options.excludedTools,
        extensionRunnerRef,
        sessionStartEvent: options.sessionStartEvent,
        orchestrationContext: options.orchestrationContext,
    });
    const extensionsResult = resourceLoader.getExtensions();
    return {
        session,
        extensionsResult,
        modelFallbackMessage,
        contextWindowWarning,
        contextWindowError,
    };
}
//# sourceMappingURL=sdk.js.map