/**
 * Agent loop that works with AgentMessage throughout.
 * Transforms to Message[] only at the LLM call boundary.
 */
import { EventStream, streamSimple, validateToolArguments, } from "@nerf/pi-ai";
export const ZERO_USAGE = {
    input: 0,
    output: 0,
    cacheRead: 0,
    cacheWrite: 0,
    totalTokens: 0,
    cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
/**
 * Build an AssistantMessage for an unhandled error caught outside runLoop.
 * Uses the model from config so the message satisfies the full interface.
 */
function createErrorMessage(error, config) {
    const msg = error instanceof Error ? error.message : String(error);
    return {
        role: "assistant",
        content: [{ type: "text", text: msg }],
        api: config.model.api,
        provider: config.model.provider,
        model: config.model.id,
        usage: ZERO_USAGE,
        stopReason: "error",
        errorMessage: msg,
        timestamp: Date.now(),
    };
}
/**
 * Emit a message_start + message_end pair for a single message.
 */
function emitMessagePair(stream, message) {
    stream.push({ type: "message_start", message });
    stream.push({ type: "message_end", message });
}
/**
 * Emit the standard error sequence when the outer agent loop catches an error.
 * Pushes message_start/end, turn_end, agent_end, then closes the stream.
 */
function emitErrorSequence(stream, errMsg, newMessages) {
    emitMessagePair(stream, errMsg);
    stream.push({ type: "turn_end", message: errMsg, toolResults: [] });
    stream.push({ type: "agent_end", messages: [...newMessages, errMsg] });
    stream.end([...newMessages, errMsg]);
}
/**
 * Start an agent loop with a new prompt message.
 * The prompt is added to the context and events are emitted for it.
 */
export function agentLoop(prompts, context, config, signal, streamFn) {
    const stream = createAgentStream();
    (async () => {
        const newMessages = [...prompts];
        const currentContext = {
            ...context,
            messages: [...context.messages, ...prompts],
        };
        stream.push({ type: "agent_start" });
        stream.push({ type: "turn_start" });
        for (const prompt of prompts) {
            emitMessagePair(stream, prompt);
        }
        try {
            await runLoop(currentContext, newMessages, config, signal, stream, streamFn);
        }
        catch (error) {
            emitErrorSequence(stream, createErrorMessage(error, config), newMessages);
        }
    })();
    return stream;
}
/**
 * Continue an agent loop from the current context without adding a new message.
 * Used for retries - context already has user message or tool results.
 *
 * **Important:** The last message in context must convert to a `user` or `toolResult` message
 * via `convertToLlm`. If it doesn't, the LLM provider will reject the request.
 * This cannot be validated here since `convertToLlm` is only called once per turn.
 */
export function agentLoopContinue(context, config, signal, streamFn) {
    if (context.messages.length === 0) {
        throw new Error("Cannot continue: no messages in context");
    }
    if (context.messages[context.messages.length - 1].role === "assistant") {
        throw new Error("Cannot continue from message role: assistant");
    }
    const stream = createAgentStream();
    (async () => {
        const newMessages = [];
        const currentContext = {
            ...context,
            messages: [...context.messages],
        };
        stream.push({ type: "agent_start" });
        stream.push({ type: "turn_start" });
        try {
            await runLoop(currentContext, newMessages, config, signal, stream, streamFn);
        }
        catch (error) {
            emitErrorSequence(stream, createErrorMessage(error, config), newMessages);
        }
    })();
    return stream;
}
function createAgentStream() {
    return new EventStream((event) => event.type === "agent_end", (event) => (event.type === "agent_end" ? event.messages : []));
}
/**
 * Main loop logic shared by agentLoop and agentLoopContinue.
 */
async function runLoop(currentContext, newMessages, config, signal, stream, streamFn) {
    let firstTurn = true;
    // Check for steering messages at start (user may have typed while waiting)
    let pendingMessages = (await config.getSteeringMessages?.()) || [];
    // Outer loop: continues when queued follow-up messages arrive after agent would stop
    while (true) {
        let hasMoreToolCalls = true;
        let steeringAfterTools = null;
        // Inner loop: process tool calls and steering messages
        while (hasMoreToolCalls || pendingMessages.length > 0) {
            if (!firstTurn) {
                stream.push({ type: "turn_start" });
            }
            else {
                firstTurn = false;
            }
            // Process pending messages (inject before next assistant response)
            if (pendingMessages.length > 0) {
                for (const message of pendingMessages) {
                    emitMessagePair(stream, message);
                    currentContext.messages.push(message);
                    newMessages.push(message);
                }
                pendingMessages = [];
            }
            // Stream assistant response
            let message;
            try {
                message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn);
            }
            catch (error) {
                // Critical failure before stream started (e.g. getApiKey threw, credentials in
                // backoff, network unavailable). Convert to a graceful error message so the
                // agent loop can end cleanly instead of crashing with an unhandled rejection.
                const errorText = error instanceof Error ? error.message : String(error);
                message = {
                    role: "assistant",
                    content: [],
                    api: config.model.api,
                    provider: config.model.provider,
                    model: config.model.id,
                    usage: ZERO_USAGE,
                    stopReason: signal?.aborted ? "aborted" : "error",
                    errorMessage: errorText,
                    timestamp: Date.now(),
                };
                stream.push({ type: "message_start", message: { ...message } });
                stream.push({ type: "message_end", message });
                currentContext.messages.push(message);
            }
            newMessages.push(message);
            if (message.stopReason === "error" || message.stopReason === "aborted") {
                stream.push({ type: "turn_end", message, toolResults: [] });
                stream.push({ type: "agent_end", messages: newMessages });
                stream.end(newMessages);
                return;
            }
            // Check for tool calls
            const toolCalls = message.content.filter((c) => c.type === "toolCall");
            hasMoreToolCalls = toolCalls.length > 0;
            const toolResults = [];
            if (hasMoreToolCalls && config.externalToolExecution) {
                // External execution mode: tools were handled by the provider
                // (e.g., Claude Code SDK). Emit tool_execution events for each
                // tool call. The TUI adds these as components after the message.
                for (const tc of toolCalls) {
                    stream.push({
                        type: "tool_execution_start",
                        toolCallId: tc.id,
                        toolName: tc.name,
                        args: tc.arguments,
                    });
                    stream.push({
                        type: "tool_execution_end",
                        toolCallId: tc.id,
                        toolName: tc.name,
                        result: {
                            content: [{ type: "text", text: "(executed by Claude Code)" }],
                            details: {},
                        },
                        isError: false,
                    });
                }
                // Don't add tool results to context or loop back — the streamSimple
                // call already ran the full multi-turn agentic loop.
                hasMoreToolCalls = false;
            }
            else if (hasMoreToolCalls) {
                const toolExecution = await executeToolCalls(currentContext, message, config, signal, stream);
                toolResults.push(...toolExecution.toolResults);
                steeringAfterTools = toolExecution.steeringMessages ?? null;
                for (const result of toolResults) {
                    currentContext.messages.push(result);
                    newMessages.push(result);
                }
            }
            stream.push({ type: "turn_end", message, toolResults });
            // Get steering messages after turn completes
            if (steeringAfterTools && steeringAfterTools.length > 0) {
                pendingMessages = steeringAfterTools;
                steeringAfterTools = null;
            }
            else {
                pendingMessages = (await config.getSteeringMessages?.()) || [];
            }
        }
        // Agent would stop here. Check for follow-up messages.
        const followUpMessages = (await config.getFollowUpMessages?.()) || [];
        if (followUpMessages.length > 0) {
            // Set as pending so inner loop processes them
            pendingMessages = followUpMessages;
            continue;
        }
        // No more messages, exit
        break;
    }
    stream.push({ type: "agent_end", messages: newMessages });
    stream.end(newMessages);
}
/**
 * Stream an assistant response from the LLM.
 * This is where AgentMessage[] gets transformed to Message[] for the LLM.
 */
async function streamAssistantResponse(context, config, signal, stream, streamFn) {
    // Apply context transform if configured (AgentMessage[] → AgentMessage[])
    let messages = context.messages;
    if (config.transformContext) {
        messages = await config.transformContext(messages, signal);
    }
    // Convert to LLM-compatible messages (AgentMessage[] → Message[])
    const llmMessages = await config.convertToLlm(messages);
    // Build LLM context
    const llmContext = {
        systemPrompt: context.systemPrompt,
        messages: llmMessages,
        tools: context.tools,
    };
    const streamFunction = streamFn || streamSimple;
    // Resolve API key (important for expiring tokens)
    const resolvedApiKey = (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
    const response = await streamFunction(config.model, llmContext, {
        ...config,
        apiKey: resolvedApiKey,
        signal,
    });
    let partialMessage = null;
    let addedPartial = false;
    for await (const event of response) {
        switch (event.type) {
            case "start":
                partialMessage = event.partial;
                context.messages.push(partialMessage);
                addedPartial = true;
                stream.push({ type: "message_start", message: { ...partialMessage } });
                break;
            case "text_start":
            case "text_delta":
            case "text_end":
            case "thinking_start":
            case "thinking_delta":
            case "thinking_end":
            case "toolcall_start":
            case "toolcall_delta":
            case "toolcall_end":
            case "server_tool_use":
            case "web_search_result":
                if (partialMessage) {
                    partialMessage = event.partial;
                    context.messages[context.messages.length - 1] = partialMessage;
                    stream.push({
                        type: "message_update",
                        assistantMessageEvent: event,
                        message: { ...partialMessage },
                    });
                }
                break;
            case "done":
            case "error": {
                const finalMessage = await response.result();
                if (addedPartial) {
                    context.messages[context.messages.length - 1] = finalMessage;
                }
                else {
                    context.messages.push(finalMessage);
                }
                if (!addedPartial) {
                    stream.push({ type: "message_start", message: { ...finalMessage } });
                }
                stream.push({ type: "message_end", message: finalMessage });
                return finalMessage;
            }
        }
    }
    return await response.result();
}
/**
 * Execute tool calls from an assistant message.
 */
async function executeToolCalls(currentContext, assistantMessage, config, signal, stream) {
    const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
    if (config.toolExecution === "sequential") {
        return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, stream);
    }
    return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, stream);
}
async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, stream) {
    const results = [];
    let steeringMessages;
    for (let index = 0; index < toolCalls.length; index++) {
        const toolCall = toolCalls[index];
        stream.push({
            type: "tool_execution_start",
            toolCallId: toolCall.id,
            toolName: toolCall.name,
            args: toolCall.arguments,
        });
        const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
        if (preparation.kind === "immediate") {
            results.push(emitToolCallOutcome(toolCall, preparation.result, preparation.isError, stream));
        }
        else {
            const executed = await executePreparedToolCall(preparation, signal, stream);
            results.push(await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal, stream));
        }
        if (config.getSteeringMessages) {
            const steering = await config.getSteeringMessages();
            if (steering.length > 0) {
                steeringMessages = steering;
                const remainingCalls = toolCalls.slice(index + 1);
                for (const skipped of remainingCalls) {
                    results.push(skipToolCall(skipped, stream));
                }
                break;
            }
        }
    }
    return { toolResults: results, steeringMessages };
}
async function executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, stream) {
    const results = [];
    const runnableCalls = [];
    let steeringMessages;
    for (let index = 0; index < toolCalls.length; index++) {
        const toolCall = toolCalls[index];
        stream.push({
            type: "tool_execution_start",
            toolCallId: toolCall.id,
            toolName: toolCall.name,
            args: toolCall.arguments,
        });
        const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
        if (preparation.kind === "immediate") {
            results.push(emitToolCallOutcome(toolCall, preparation.result, preparation.isError, stream));
        }
        else {
            runnableCalls.push(preparation);
        }
        if (config.getSteeringMessages) {
            const steering = await config.getSteeringMessages();
            if (steering.length > 0) {
                steeringMessages = steering;
                for (const runnable of runnableCalls) {
                    results.push(skipToolCall(runnable.toolCall, stream, { emitStart: false }));
                }
                const remainingCalls = toolCalls.slice(index + 1);
                for (const skipped of remainingCalls) {
                    results.push(skipToolCall(skipped, stream));
                }
                return { toolResults: results, steeringMessages };
            }
        }
    }
    const runningCalls = runnableCalls.map((prepared) => ({
        prepared,
        execution: executePreparedToolCall(prepared, signal, stream),
    }));
    for (const running of runningCalls) {
        const executed = await running.execution;
        results.push(await finalizeExecutedToolCall(currentContext, assistantMessage, running.prepared, executed, config, signal, stream));
    }
    if (!steeringMessages && config.getSteeringMessages) {
        const steering = await config.getSteeringMessages();
        if (steering.length > 0) {
            steeringMessages = steering;
        }
    }
    return { toolResults: results, steeringMessages };
}
async function prepareToolCall(currentContext, assistantMessage, toolCall, config, signal) {
    const tool = currentContext.tools?.find((t) => t.name === toolCall.name);
    if (!tool) {
        return {
            kind: "immediate",
            result: createErrorToolResult(`Tool ${toolCall.name} not found`),
            isError: true,
        };
    }
    try {
        // Allow tools to normalize/migrate arguments before schema validation
        if (tool.prepareArguments && toolCall.arguments) {
            toolCall = { ...toolCall, arguments: tool.prepareArguments(toolCall.arguments) };
        }
        const validatedArgs = validateToolArguments(tool, toolCall);
        if (config.beforeToolCall) {
            const beforeResult = await config.beforeToolCall({
                assistantMessage,
                toolCall,
                args: validatedArgs,
                context: currentContext,
            }, signal);
            if (beforeResult?.block) {
                return {
                    kind: "immediate",
                    result: createErrorToolResult(beforeResult.reason || "Tool execution was blocked"),
                    isError: true,
                };
            }
        }
        return {
            kind: "prepared",
            toolCall,
            tool,
            args: validatedArgs,
        };
    }
    catch (error) {
        return {
            kind: "immediate",
            result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
            isError: true,
        };
    }
}
async function executePreparedToolCall(prepared, signal, stream) {
    try {
        const result = await prepared.tool.execute(prepared.toolCall.id, prepared.args, signal, (partialResult) => {
            stream.push({
                type: "tool_execution_update",
                toolCallId: prepared.toolCall.id,
                toolName: prepared.toolCall.name,
                args: prepared.toolCall.arguments,
                partialResult,
            });
        });
        return { result, isError: false };
    }
    catch (error) {
        return {
            result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
            isError: true,
        };
    }
}
async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal, stream) {
    let result = executed.result;
    let isError = executed.isError;
    if (config.afterToolCall) {
        const afterResult = await config.afterToolCall({
            assistantMessage,
            toolCall: prepared.toolCall,
            args: prepared.args,
            result,
            isError,
            context: currentContext,
        }, signal);
        if (afterResult) {
            result = {
                content: afterResult.content !== undefined ? afterResult.content : result.content,
                details: afterResult.details !== undefined ? afterResult.details : result.details,
            };
            isError = afterResult.isError !== undefined ? afterResult.isError : isError;
        }
    }
    return emitToolCallOutcome(prepared.toolCall, result, isError, stream);
}
function createErrorToolResult(message) {
    return {
        content: [{ type: "text", text: message }],
        details: {},
    };
}
function emitToolCallOutcome(toolCall, result, isError, stream) {
    stream.push({
        type: "tool_execution_end",
        toolCallId: toolCall.id,
        toolName: toolCall.name,
        result,
        isError,
    });
    const toolResultMessage = {
        role: "toolResult",
        toolCallId: toolCall.id,
        toolName: toolCall.name,
        content: result.content,
        details: result.details,
        isError,
        timestamp: Date.now(),
    };
    emitMessagePair(stream, toolResultMessage);
    return toolResultMessage;
}
function skipToolCall(toolCall, stream, options) {
    const result = {
        content: [{ type: "text", text: "Skipped due to queued user message." }],
        details: {},
    };
    if (options?.emitStart !== false) {
        stream.push({
            type: "tool_execution_start",
            toolCallId: toolCall.id,
            toolName: toolCall.name,
            args: toolCall.arguments,
        });
    }
    return emitToolCallOutcome(toolCall, result, true, stream);
}
//# sourceMappingURL=agent-loop.js.map