import { join } from "node:path";
import { Agent } from "@pie-lab/agent-core";
import { clampThinkingLevel, createAssistantMessageEventStream, streamSimple, } from "@pie-lab/ai";
import { resolvePiModelRoutePlan } from "@pie-lab/router";
import { createUsageRecordId } from "@pie-lab/storage";
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 { DEFAULT_THINKING_LEVEL } from "./defaults.js";
import { BackgroundLearningReview, createLearningToolDefinitions, LearningReviewStore, MemoryStore, SkillCurator, SkillManager, } from "./learning/index.js";
import { convertToLlm } from "./messages.js";
import { ModelRegistry } from "./model-registry.js";
import { findInitialModel } from "./model-resolver.js";
import { mergeProviderAttributionHeaders } from "./provider-attribution.js";
import { DefaultResourceLoader } from "./resource-loader.js";
import { CronJobStore, createSchedulerToolDefinitions } from "./scheduler/index.js";
import { getDefaultSessionDir, SessionManager } from "./session-manager.js";
import { createSessionSearchToolDefinition } from "./session-search-tool.js";
import { SettingsManager } from "./settings-manager.js";
import { time } from "./timings.js";
import { createBashTool, createCodingTools, createEditTool, createFindTool, createGrepTool, createLsTool, createReadOnlyTools, createReadTool, createWriteTool, withFileMutationQueue, } from "./tools/index.js";
// Re-exports
export * from "./agent-session-runtime.js";
export { withFileMutationQueue, 
// Tool factories (for custom cwd)
createCodingTools, createReadOnlyTools, createReadTool, createBashTool, createEditTool, createWriteTool, createGrepTool, createFindTool, createLsTool, };
// Helper Functions
function getDefaultAgentDir() {
    return getAgentDir();
}
function formatUnknownError(error) {
    if (error instanceof Error)
        return error.message;
    if (typeof error === "string")
        return error;
    try {
        return JSON.stringify(error);
    }
    catch {
        return String(error);
    }
}
function createStreamErrorMessage(model, error) {
    return {
        role: "assistant",
        content: [],
        api: model.api,
        provider: model.provider,
        model: model.id,
        usage: {
            input: 0,
            output: 0,
            cacheRead: 0,
            cacheWrite: 0,
            totalTokens: 0,
            cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
        },
        stopReason: "error",
        errorMessage: formatUnknownError(error),
        timestamp: Date.now(),
    };
}
function getMessageErrorText(message) {
    if (message.errorMessage)
        return message.errorMessage;
    const firstText = message.content.find((item) => item.type === "text");
    return firstText?.text;
}
function getUsageClientOrigin() {
    return process.env.PIE_LAB_USAGE_ORIGIN?.trim() || process.env.PIE_USAGE_ORIGIN?.trim() || undefined;
}
function getUsageEndpoint() {
    return process.env.PIE_LAB_USAGE_ENDPOINT?.trim() || process.env.PIE_USAGE_ENDPOINT?.trim() || undefined;
}
/**
 * Create an AgentSession with the specified options.
 *
 * @example
 * ```typescript
 * // Minimal - uses defaults
 * const { session } = await createAgentSession();
 *
 * // With explicit model
 * import { getModel } from '@pie-lab/ai';
 * 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
    const authPath = options.agentDir ? join(agentDir, "auth.json") : undefined;
    const modelsPath = options.agentDir ? join(agentDir, "models.json") : undefined;
    const authStorage = options.authStorage ?? AuthStorage.create(authPath);
    const modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);
    const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
    const sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));
    const usageStore = options.usageStore;
    if (!resourceLoader) {
        resourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });
        await resourceLoader.reload();
        time("resourceLoader.reload");
    }
    const learningSettings = settingsManager.getLearningSettings();
    const schedulerSettings = settingsManager.getSchedulerSettings();
    const memoryStore = new MemoryStore({ agentDir });
    const reviewStore = new LearningReviewStore({ agentDir });
    const skillManager = new SkillManager({ agentDir, cwd });
    const cronJobStore = new CronJobStore({ agentDir, cwd });
    const learningMemorySnapshot = learningSettings.enabled && learningSettings.memory.enabled
        ? memoryStore.formatForSystemPrompt(memoryStore.readSnapshot())
        : undefined;
    let reloadSessionResources;
    const onLearningSkillsChanged = async () => {
        if (reloadSessionResources) {
            await reloadSessionResources();
            return;
        }
        await resourceLoader.reload();
    };
    const learningTools = !options.noTools &&
        learningSettings.enabled &&
        (learningSettings.memory.enabled || learningSettings.skills.enabled)
        ? createLearningToolDefinitions({
            memoryStore,
            skillManager,
            onSkillsChanged: onLearningSkillsChanged,
        })
        : [];
    const schedulerTools = !options.noTools && schedulerSettings.enabled
        ? createSchedulerToolDefinitions({
            store: cronJobStore,
            getOrigin: () => options.chatOrigin,
        })
        : [];
    const sessionSearchTool = !options.noTools ? [createSessionSearchToolDefinition(agentDir)] : [];
    const customTools = [...(options.customTools ?? []), ...learningTools, ...schedulerTools, ...sessionSearchTool];
    // 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 = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);
        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);
    }
    const defaultActiveToolNames = ["read", "bash", "edit", "write"];
    const allowedToolNames = options.tools ?? (options.noTools === "all" ? [] : undefined);
    // session_search and scheduler tools are custom tools — they are registered in
    // _toolRegistry but NOT added to active tools automatically unless we list them here.
    const extraDefaultTools = [...sessionSearchTool.map((t) => t.name), ...schedulerTools.map((t) => t.name)];
    const excludedToolNames = options.excludeTools;
    const excludedToolNameSet = excludedToolNames ? new Set(excludedToolNames) : undefined;
    const initialActiveToolNames = (options.tools ? [...options.tools] : options.noTools ? [] : [...defaultActiveToolNames, ...extraDefaultTools]).filter((name) => !excludedToolNameSet?.has(name));
    let agent;
    // 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()) {
            return converted;
        }
        // Filter out ImageContent from all messages, replacing with text placeholder
        return 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;
        });
    };
    const extensionRunnerRef = {};
    agent = new Agent({
        initialState: {
            systemPrompt: "",
            model,
            thinkingLevel,
            tools: [],
        },
        convertToLlm: convertToLlmWithBlockImages,
        streamFn: async (model, context, options) => {
            const outer = createAssistantMessageEventStream();
            const requestId = createUsageRecordId("request");
            void (async () => {
                try {
                    const routerPolicy = await modelRegistry.getRouterPolicy();
                    const plan = await resolvePiModelRoutePlan({
                        requestedModel: model,
                        catalog: modelRegistry,
                        policy: routerPolicy,
                    });
                    const attemptCount = plan.routes.length;
                    for (let attemptIndex = 0; attemptIndex < plan.routes.length; attemptIndex++) {
                        const resolved = plan.routes[attemptIndex];
                        const route = resolved.route;
                        const resolvedModel = resolved.model;
                        let connectionId = route.connectionId;
                        const recordUsage = async (message, status, errorMessage) => {
                            const usage = message.usage;
                            await usageStore?.recordUsage({
                                id: createUsageRecordId(),
                                requestId,
                                timestamp: new Date().toISOString(),
                                requestedModel: plan.requestedModel,
                                routingMode: plan.routingMode,
                                routeSource: route.source,
                                resolvedProvider: route.resolvedProvider,
                                resolvedModel: route.resolvedModel,
                                connectionId,
                                attemptIndex,
                                attemptCount,
                                endpoint: getUsageEndpoint(),
                                clientOrigin: getUsageClientOrigin(),
                                usage: usage
                                    ? {
                                        input: usage.input,
                                        output: usage.output,
                                        cacheRead: usage.cacheRead,
                                        cacheWrite: usage.cacheWrite,
                                        totalTokens: usage.totalTokens,
                                    }
                                    : undefined,
                                cost: usage?.cost
                                    ? {
                                        input: usage.cost.input,
                                        output: usage.cost.output,
                                        cacheRead: usage.cost.cacheRead,
                                        cacheWrite: usage.cost.cacheWrite,
                                        total: usage.cost.total,
                                        currency: "USD",
                                        pricingSource: "pie-metadata",
                                    }
                                    : undefined,
                                costUsd: usage?.cost.total,
                                status,
                                errorMessage,
                            });
                        };
                        try {
                            const auth = await modelRegistry.getApiKeyAndHeaders(resolvedModel);
                            if (!auth.ok) {
                                throw new Error(auth.error);
                            }
                            connectionId = auth.connectionId ?? connectionId;
                            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 = options?.timeoutMs ?? providerRetrySettings.timeoutMs ?? effectiveTimeoutMs;
                            const websocketConnectTimeoutMs = options?.websocketConnectTimeoutMs ?? settingsManager.getWebSocketConnectTimeoutMs();
                            const inner = await streamSimple(resolvedModel, context, {
                                ...options,
                                apiKey: auth.apiKey,
                                timeoutMs,
                                websocketConnectTimeoutMs,
                                maxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,
                                maxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,
                                headers: mergeProviderAttributionHeaders(resolvedModel, settingsManager, options?.sessionId, auth.headers, options?.headers),
                            });
                            // Single-route fast path: stream events directly to outer without buffering.
                            // Buffering is only needed for multi-route fallback (to detect whether
                            // any content was emitted before deciding to retry the next route).
                            const isFinalAttempt = attemptIndex >= attemptCount - 1;
                            if (isFinalAttempt) {
                                for await (const event of inner) {
                                    if (event.type === "done") {
                                        await recordUsage(event.message, "success");
                                        outer.push(event);
                                        return;
                                    }
                                    if (event.type === "error") {
                                        const errorText = getMessageErrorText(event.error);
                                        await recordUsage(event.error, event.error.stopReason === "aborted" ? "aborted" : "error", errorText);
                                        await modelRegistry.markProviderConnectionUnavailable(connectionId, resolvedModel, event.error);
                                        outer.push(event);
                                        return;
                                    }
                                    outer.push(event);
                                }
                                // Stream ended without an explicit done/error event. Providers may
                                // finish via end(message) instead (e.g. extension streamSimple overrides),
                                // which resolves result() without emitting a done event. Mirror the
                                // non-routed behavior by completing the outer stream from result().
                                const finalMessage = await inner.result();
                                await recordUsage(finalMessage, "success");
                                const doneReason = finalMessage.stopReason === "length" || finalMessage.stopReason === "toolUse"
                                    ? finalMessage.stopReason
                                    : "stop";
                                outer.push({ type: "done", reason: doneReason, message: finalMessage });
                                return;
                            }
                            // Multi-route fallback path: buffer until done/error to decide retry.
                            const bufferedEvents = [];
                            let shouldTryNextRoute = false;
                            for await (const event of inner) {
                                bufferedEvents.push(event);
                                if (event.type === "done") {
                                    await recordUsage(event.message, "success");
                                    for (const bufferedEvent of bufferedEvents)
                                        outer.push(bufferedEvent);
                                    return;
                                }
                                if (event.type === "error") {
                                    const errorText = getMessageErrorText(event.error);
                                    await recordUsage(event.error, event.error.stopReason === "aborted" ? "aborted" : "error", errorText);
                                    await modelRegistry.markProviderConnectionUnavailable(connectionId, resolvedModel, event.error);
                                    const emittedContent = bufferedEvents.some((bufferedEvent) => bufferedEvent.type !== "error");
                                    if (!emittedContent) {
                                        shouldTryNextRoute = true;
                                        break;
                                    }
                                    for (const bufferedEvent of bufferedEvents)
                                        outer.push(bufferedEvent);
                                    return;
                                }
                            }
                            if (shouldTryNextRoute) {
                                continue;
                            }
                            const errorMessage = createStreamErrorMessage(resolvedModel, "Provider stream ended without a final event.");
                            await recordUsage(errorMessage, "error", errorMessage.errorMessage);
                            await modelRegistry.markProviderConnectionUnavailable(connectionId, resolvedModel, errorMessage);
                            if (attemptIndex < attemptCount - 1) {
                                continue;
                            }
                            outer.push({ type: "error", reason: "error", error: errorMessage });
                            return;
                        }
                        catch (error) {
                            const errorMessage = createStreamErrorMessage(resolvedModel, error);
                            await recordUsage(errorMessage, "error", errorMessage.errorMessage);
                            await modelRegistry.markProviderConnectionUnavailable(connectionId, resolvedModel, error);
                            if (attemptIndex < attemptCount - 1) {
                                continue;
                            }
                            outer.push({ type: "error", reason: "error", error: errorMessage });
                            return;
                        }
                    }
                }
                catch (error) {
                    const errorMessage = createStreamErrorMessage(model, error);
                    outer.push({ type: "error", reason: "error", error: errorMessage });
                }
            })();
            return outer;
        },
        onPayload: async (payload, _model) => {
            const runner = extensionRunnerRef.current;
            if (!runner?.hasHandlers("before_provider_request")) {
                return payload;
            }
            return runner.emitBeforeProviderRequest(payload);
        },
        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;
            return runner ? runner.emitContext(messages) : messages;
        },
        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;
        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);
        }
        sessionManager.appendThinkingLevelChange(thinkingLevel);
    }
    const backgroundLearningReview = new BackgroundLearningReview({
        settings: learningSettings,
        memoryStore,
        skillManager,
        reviewStore,
        streamFn: agent.streamFn?.bind(agent),
        onSkillsChanged: onLearningSkillsChanged,
    });
    const skillCurator = learningSettings.enabled && learningSettings.skills.enabled && learningSettings.skills.curatorEnabled
        ? new SkillCurator({ skillManager, policy: learningSettings.skills.curator })
        : undefined;
    const session = new AgentSession({
        agent,
        sessionManager,
        settingsManager,
        cwd,
        scopedModels: options.scopedModels,
        resourceLoader,
        customTools,
        modelRegistry,
        initialActiveToolNames,
        allowedToolNames,
        excludedToolNames,
        extensionRunnerRef,
        sessionStartEvent: options.sessionStartEvent,
        learningMemorySnapshot,
        backgroundLearningReview,
        learningSkillManager: skillManager,
        skillCurator,
    });
    reloadSessionResources = () => session.reload();
    const extensionsResult = resourceLoader.getExtensions();
    return {
        session,
        extensionsResult,
        modelFallbackMessage,
    };
}
//# sourceMappingURL=sdk.js.map