/**
 * Agent class that uses the agent-loop directly.
 * No transport abstraction - calls streamSimple via the loop.
 */
import { getModel, streamSimple, } from "@nerf/pi-ai";
import { agentLoop, agentLoopContinue, ZERO_USAGE } from "./agent-loop.js";
/**
 * Default convertToLlm: Keep only LLM-compatible messages, convert attachments.
 */
function defaultConvertToLlm(messages) {
    return messages.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult");
}
export class Agent {
    constructor(opts = {}) {
        this._state = {
            systemPrompt: "",
            model: getModel("google", "gemini-2.5-flash-lite-preview-06-17"),
            thinkingLevel: "off",
            tools: [],
            messages: [],
            isStreaming: false,
            streamMessage: null,
            pendingToolCalls: new Set(),
            error: undefined,
        };
        this.listeners = new Set();
        this.steeringQueue = [];
        this.followUpQueue = [];
        this._state = { ...this._state, ...opts.initialState };
        this.convertToLlm = opts.convertToLlm || defaultConvertToLlm;
        this.transformContext = opts.transformContext;
        this.steeringMode = opts.steeringMode || "one-at-a-time";
        this.followUpMode = opts.followUpMode || "one-at-a-time";
        this.streamFn = opts.streamFn || streamSimple;
        this._sessionId = opts.sessionId;
        this.getApiKey = opts.getApiKey;
        this._onPayload = opts.onPayload;
        this._thinkingBudgets = opts.thinkingBudgets;
        this._transport = opts.transport ?? "sse";
        this._maxRetryDelayMs = opts.maxRetryDelayMs;
        this._externalToolExecution = opts.externalToolExecution;
    }
    /**
     * Get the current session ID used for provider caching.
     */
    get sessionId() {
        return this._sessionId;
    }
    /**
     * Set the session ID for provider caching.
     * Call this when switching sessions (new session, branch, resume).
     */
    set sessionId(value) {
        this._sessionId = value;
    }
    /**
     * Get the current thinking budgets.
     */
    get thinkingBudgets() {
        return this._thinkingBudgets;
    }
    /**
     * Set custom thinking budgets for token-based providers.
     */
    set thinkingBudgets(value) {
        this._thinkingBudgets = value;
    }
    /**
     * Get the current preferred transport.
     */
    get transport() {
        return this._transport;
    }
    /**
     * Set the preferred transport.
     */
    setTransport(value) {
        this._transport = value;
    }
    /**
     * Get the current max retry delay in milliseconds.
     */
    get maxRetryDelayMs() {
        return this._maxRetryDelayMs;
    }
    /**
     * Set the maximum delay to wait for server-requested retries.
     * Set to 0 to disable the cap.
     */
    set maxRetryDelayMs(value) {
        this._maxRetryDelayMs = value;
    }
    /**
     * Install a hook called before each tool executes, after argument validation.
     * Return `{ block: true }` to prevent execution.
     */
    setBeforeToolCall(fn) {
        this._beforeToolCall = fn;
    }
    /**
     * Install a hook called after each tool executes, before results are emitted.
     * Return field overrides for content/details/isError.
     */
    setAfterToolCall(fn) {
        this._afterToolCall = fn;
    }
    get state() {
        return this._state;
    }
    subscribe(fn) {
        this.listeners.add(fn);
        return () => this.listeners.delete(fn);
    }
    // State mutators
    setSystemPrompt(v) {
        this._state.systemPrompt = v;
    }
    setModel(m) {
        this._state.model = m;
    }
    setThinkingLevel(l) {
        this._state.thinkingLevel = l;
    }
    setSteeringMode(mode) {
        this.steeringMode = mode;
    }
    getSteeringMode() {
        return this.steeringMode;
    }
    setFollowUpMode(mode) {
        this.followUpMode = mode;
    }
    getFollowUpMode() {
        return this.followUpMode;
    }
    setTools(t) {
        this._state.tools = t;
    }
    replaceMessages(ms) {
        this._state.messages = ms.slice();
    }
    appendMessage(m) {
        this._state.messages = [...this._state.messages, m];
    }
    /**
     * Queue a steering message to interrupt the agent mid-run.
     * Delivered after current tool execution, skips remaining tools.
     */
    steer(m, origin = "system") {
        this.steeringQueue.push({ message: m, origin });
    }
    /**
     * Queue a follow-up message to be processed after the agent finishes.
     * Delivered only when agent has no more tool calls or steering messages.
     */
    followUp(m, origin = "system") {
        this.followUpQueue.push({ message: m, origin });
    }
    clearSteeringQueue() {
        this.steeringQueue = [];
    }
    clearFollowUpQueue() {
        this.followUpQueue = [];
    }
    clearAllQueues() {
        this.steeringQueue = [];
        this.followUpQueue = [];
    }
    /**
     * Drain user-origin messages from queues, leaving system messages in place.
     * Used during abort to preserve messages the user explicitly typed.
     */
    drainUserMessages() {
        const userSteering = this.steeringQueue.filter((e) => e.origin === "user").map((e) => e.message);
        const userFollowUp = this.followUpQueue.filter((e) => e.origin === "user").map((e) => e.message);
        this.steeringQueue = this.steeringQueue.filter((e) => e.origin !== "user");
        this.followUpQueue = this.followUpQueue.filter((e) => e.origin !== "user");
        return { steering: userSteering, followUp: userFollowUp };
    }
    hasQueuedMessages() {
        return this.steeringQueue.length > 0 || this.followUpQueue.length > 0;
    }
    dequeueSteeringMessages() {
        if (this.steeringMode === "one-at-a-time") {
            if (this.steeringQueue.length > 0) {
                const first = this.steeringQueue[0];
                this.steeringQueue = this.steeringQueue.slice(1);
                return [first.message];
            }
            return [];
        }
        const steering = this.steeringQueue.map((e) => e.message);
        this.steeringQueue = [];
        return steering;
    }
    dequeueFollowUpMessages() {
        if (this.followUpMode === "one-at-a-time") {
            if (this.followUpQueue.length > 0) {
                const first = this.followUpQueue[0];
                this.followUpQueue = this.followUpQueue.slice(1);
                return [first.message];
            }
            return [];
        }
        const followUp = this.followUpQueue.map((e) => e.message);
        this.followUpQueue = [];
        return followUp;
    }
    clearMessages() {
        this._state.messages = [];
    }
    abort() {
        this.abortController?.abort();
    }
    waitForIdle() {
        return this.runningPrompt ?? Promise.resolve();
    }
    reset() {
        this._state.messages = [];
        this._state.isStreaming = false;
        this._state.streamMessage = null;
        this._state.pendingToolCalls = new Set();
        this._state.error = undefined;
        this.steeringQueue = [];
        this.followUpQueue = [];
    }
    async prompt(input, images) {
        if (this._state.isStreaming) {
            throw new Error("Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion.");
        }
        const model = this._state.model;
        if (!model)
            throw new Error("No model configured");
        let msgs;
        if (Array.isArray(input)) {
            msgs = input;
        }
        else if (typeof input === "string") {
            const content = [{ type: "text", text: input }];
            if (images && images.length > 0) {
                content.push(...images);
            }
            msgs = [
                {
                    role: "user",
                    content,
                    timestamp: Date.now(),
                },
            ];
        }
        else {
            msgs = [input];
        }
        await this._runLoop(msgs);
    }
    /**
     * Continue from current context (used for retries and resuming queued messages).
     */
    async continue() {
        if (this._state.isStreaming) {
            throw new Error("Agent is already processing. Wait for completion before continuing.");
        }
        const messages = this._state.messages;
        if (messages.length === 0) {
            throw new Error("No messages to continue from");
        }
        if (messages[messages.length - 1].role === "assistant") {
            const queuedSteering = this.dequeueSteeringMessages();
            if (queuedSteering.length > 0) {
                await this._runLoop(queuedSteering, { skipInitialSteeringPoll: true });
                return;
            }
            const queuedFollowUp = this.dequeueFollowUpMessages();
            if (queuedFollowUp.length > 0) {
                await this._runLoop(queuedFollowUp);
                return;
            }
            throw new Error("Cannot continue from message role: assistant");
        }
        await this._runLoop(undefined);
    }
    /**
     * Run the agent loop.
     * If messages are provided, starts a new conversation turn with those messages.
     * Otherwise, continues from existing context.
     */
    async _runLoop(messages, options) {
        const model = this._state.model;
        if (!model)
            throw new Error("No model configured");
        this._state.activeInferenceModel = model;
        this.runningPrompt = new Promise((resolve) => {
            this.resolveRunningPrompt = resolve;
        });
        this.abortController = new AbortController();
        this._state.isStreaming = true;
        this._state.streamMessage = null;
        this._state.error = undefined;
        const reasoning = this._state.thinkingLevel === "off" ? undefined : this._state.thinkingLevel;
        const context = {
            systemPrompt: this._state.systemPrompt,
            messages: this._state.messages.slice(),
            tools: this._state.tools,
        };
        let skipInitialSteeringPoll = options?.skipInitialSteeringPoll === true;
        const config = {
            model,
            reasoning,
            sessionId: this._sessionId,
            onPayload: this._onPayload,
            transport: this._transport,
            thinkingBudgets: this._thinkingBudgets,
            maxRetryDelayMs: this._maxRetryDelayMs,
            convertToLlm: this.convertToLlm,
            transformContext: this.transformContext,
            getApiKey: this.getApiKey,
            getSteeringMessages: async () => {
                if (skipInitialSteeringPoll) {
                    skipInitialSteeringPoll = false;
                    return [];
                }
                return this.dequeueSteeringMessages();
            },
            getFollowUpMessages: async () => this.dequeueFollowUpMessages(),
            beforeToolCall: this._beforeToolCall,
            afterToolCall: this._afterToolCall,
            externalToolExecution: this._externalToolExecution?.(model) ?? false,
        };
        let partial = null;
        try {
            const stream = messages
                ? agentLoop(messages, context, config, this.abortController.signal, this.streamFn)
                : agentLoopContinue(context, config, this.abortController.signal, this.streamFn);
            for await (const event of stream) {
                // Update internal state based on events
                switch (event.type) {
                    case "message_start":
                    case "message_update":
                        partial = event.message;
                        this._state.streamMessage = event.message;
                        break;
                    case "message_end":
                        partial = null;
                        this._state.streamMessage = null;
                        this.appendMessage(event.message);
                        break;
                    case "tool_execution_start":
                        this._updatePendingToolCalls("add", event.toolCallId);
                        break;
                    case "tool_execution_end":
                        this._updatePendingToolCalls("delete", event.toolCallId);
                        break;
                    case "turn_end":
                        if (event.message.role === "assistant" && event.message.errorMessage) {
                            this._state.error = event.message.errorMessage;
                        }
                        break;
                    case "agent_end":
                        this._state.isStreaming = false;
                        this._state.streamMessage = null;
                        break;
                }
                // Emit to listeners
                this.emit(event);
            }
            // Handle any remaining partial message
            if (partial && partial.role === "assistant" && partial.content.length > 0) {
                const onlyEmpty = !partial.content.some((c) => (c.type === "thinking" && c.thinking.trim().length > 0) ||
                    (c.type === "text" && c.text.trim().length > 0) ||
                    (c.type === "toolCall" && c.name.trim().length > 0));
                if (!onlyEmpty) {
                    this.appendMessage(partial);
                }
                else {
                    if (this.abortController?.signal.aborted) {
                        throw new Error("Request was aborted");
                    }
                }
            }
        }
        catch (err) {
            const errorMsg = {
                role: "assistant",
                content: [{ type: "text", text: "" }],
                api: model.api,
                provider: model.provider,
                model: model.id,
                usage: ZERO_USAGE,
                stopReason: this.abortController?.signal.aborted ? "aborted" : "error",
                errorMessage: err?.message || String(err),
                timestamp: Date.now(),
            };
            this.appendMessage(errorMsg);
            this._state.error = err?.message || String(err);
            this.emit({ type: "agent_end", messages: [errorMsg] });
        }
        finally {
            this._state.isStreaming = false;
            this._state.streamMessage = null;
            this._state.pendingToolCalls = new Set();
            this._state.activeInferenceModel = undefined;
            this.abortController = undefined;
            this.resolveRunningPrompt?.();
            this.runningPrompt = undefined;
            this.resolveRunningPrompt = undefined;
        }
    }
    _updatePendingToolCalls(action, id) {
        const s = new Set(this._state.pendingToolCalls);
        s[action](id);
        this._state.pendingToolCalls = s;
    }
    emit(e) {
        for (const listener of this.listeners) {
            listener(e);
        }
    }
}
//# sourceMappingURL=agent.js.map