/**
 * Clawx agent loop — own implementation replacing pi-agent-core's agentLoop.
 *
 * Phase 4: The core execution engine.
 * Flow: user message → model call → tool calls → tool execution → next model call → end
 */
import { streamSimple } from "./types.js";
// --- EventStream: async iterable with a final result ---
class AgentEventStream {
    queue = [];
    waiting = null;
    done = false;
    finalResult = [];
    resultResolver;
    resultPromise;
    constructor() {
        this.resultPromise = new Promise((resolve) => {
            this.resultResolver = resolve;
        });
    }
    push(event) {
        if (this.waiting) {
            const resolve = this.waiting;
            this.waiting = null;
            resolve({ value: event, done: false });
        }
        else {
            this.queue.push(event);
        }
    }
    end(messages) {
        this.done = true;
        this.finalResult = messages;
        this.resultResolver(messages);
        if (this.waiting) {
            const resolve = this.waiting;
            this.waiting = null;
            resolve({ value: undefined, done: true });
        }
    }
    [Symbol.asyncIterator]() {
        return {
            next: () => {
                if (this.queue.length > 0) {
                    return Promise.resolve({ value: this.queue.shift(), done: false });
                }
                if (this.done) {
                    return Promise.resolve({
                        value: undefined,
                        done: true,
                    });
                }
                return new Promise((resolve) => {
                    this.waiting = resolve;
                });
            },
        };
    }
    result() {
        return this.resultPromise;
    }
}
// --- Validation ---
function validateToolArguments(tool, toolCall) {
    // Simple validation: trust LLM output, just ensure it's an object
    const args = toolCall.arguments;
    if (args === null || args === undefined) {
        return {};
    }
    if (typeof args !== "object") {
        throw new Error(`Tool "${toolCall.name}" expected object arguments, got ${typeof args}`);
    }
    return args;
}
// --- Tool execution ---
function createErrorToolResult(message) {
    return {
        content: [{ type: "text", text: message }],
        details: {},
    };
}
async function emitToolCallOutcome(toolCall, result, isError, emit) {
    await emit({
        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(),
    };
    await emit({ type: "message_start", message: toolResultMessage });
    await emit({ type: "message_end", message: toolResultMessage });
    return toolResultMessage;
}
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 {
        const validatedArgs = validateToolArguments(tool, toolCall);
        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, emit) {
    const updateEvents = [];
    try {
        const result = await prepared.tool.execute(prepared.toolCall.id, prepared.args, signal, (partialResult) => {
            updateEvents.push(Promise.resolve(emit({
                type: "tool_execution_update",
                toolCallId: prepared.toolCall.id,
                toolName: prepared.toolCall.name,
                args: prepared.toolCall.arguments,
                partialResult,
            })));
        });
        await Promise.all(updateEvents);
        return { result, isError: false };
    }
    catch (error) {
        await Promise.all(updateEvents);
        return {
            result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
            isError: true,
        };
    }
}
async function executeToolCalls(currentContext, assistantMessage, config, signal, emit) {
    const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
    const results = [];
    const runnableCalls = [];
    for (const toolCall of toolCalls) {
        await emit({
            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(await emitToolCallOutcome(toolCall, preparation.result, preparation.isError, emit));
        }
        else {
            runnableCalls.push(preparation);
        }
    }
    // Execute prepared calls in parallel (default) or sequential
    if (config.toolExecution === "sequential") {
        for (const prepared of runnableCalls) {
            const executed = await executePreparedToolCall(prepared, signal, emit);
            results.push(await emitToolCallOutcome(prepared.toolCall, executed.result, executed.isError, emit));
        }
    }
    else {
        const running = runnableCalls.map((prepared) => ({
            prepared,
            execution: executePreparedToolCall(prepared, signal, emit),
        }));
        for (const r of running) {
            const executed = await r.execution;
            results.push(await emitToolCallOutcome(r.prepared.toolCall, executed.result, executed.isError, emit));
        }
    }
    return results;
}
async function streamAssistantResponse(context, config, signal, emit, streamFn) {
    let messages = context.messages;
    if (config.transformContext) {
        messages = await config.transformContext(messages, signal);
    }
    const llmMessages = await config.convertToLlm(messages);
    const llmContext = {
        systemPrompt: context.systemPrompt,
        messages: llmMessages,
        tools: context.tools,
    };
    const fn = streamFn || streamSimple;
    const resolvedApiKey = (config.getApiKey
        ? await config.getApiKey(config.model.provider)
        : undefined) || config.apiKey;
    const response = fn(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;
                await emit({ 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":
                if (partialMessage) {
                    partialMessage = event.partial;
                    context.messages[context.messages.length - 1] = partialMessage;
                    await emit({
                        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) {
                    await emit({
                        type: "message_start",
                        message: { ...finalMessage },
                    });
                }
                await emit({ type: "message_end", message: finalMessage });
                return finalMessage;
            }
        }
    }
    const finalMessage = await response.result();
    if (addedPartial) {
        context.messages[context.messages.length - 1] = finalMessage;
    }
    else {
        context.messages.push(finalMessage);
        await emit({ type: "message_start", message: { ...finalMessage } });
    }
    await emit({ type: "message_end", message: finalMessage });
    return finalMessage;
}
// --- Main loop ---
async function runLoop(currentContext, newMessages, config, signal, emit, streamFn) {
    let firstTurn = true;
    let pendingMessages = (await config.getSteeringMessages?.()) || [];
    while (true) {
        let hasMoreToolCalls = true;
        while (hasMoreToolCalls || pendingMessages.length > 0) {
            if (!firstTurn) {
                await emit({ type: "turn_start" });
            }
            else {
                firstTurn = false;
            }
            if (pendingMessages.length > 0) {
                for (const message of pendingMessages) {
                    await emit({ type: "message_start", message });
                    await emit({ type: "message_end", message });
                    currentContext.messages.push(message);
                    newMessages.push(message);
                }
                pendingMessages = [];
            }
            const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
            newMessages.push(message);
            if (message.stopReason === "error" ||
                message.stopReason === "aborted") {
                await emit({ type: "turn_end", message, toolResults: [] });
                await emit({ type: "agent_end", messages: newMessages });
                return;
            }
            const toolCalls = message.content.filter((c) => c.type === "toolCall");
            hasMoreToolCalls = toolCalls.length > 0;
            const toolResults = [];
            if (hasMoreToolCalls) {
                toolResults.push(...(await executeToolCalls(currentContext, message, config, signal, emit)));
                for (const result of toolResults) {
                    currentContext.messages.push(result);
                    newMessages.push(result);
                }
            }
            await emit({ type: "turn_end", message, toolResults });
            pendingMessages = (await config.getSteeringMessages?.()) || [];
        }
        const followUpMessages = (await config.getFollowUpMessages?.()) || [];
        if (followUpMessages.length > 0) {
            pendingMessages = followUpMessages;
            continue;
        }
        break;
    }
    await emit({ type: "agent_end", messages: newMessages });
}
// --- Public API ---
export function agentLoop(prompts, context, config, signal, streamFn) {
    const stream = new AgentEventStream();
    void (async () => {
        const newMessages = [...prompts];
        const currentContext = {
            ...context,
            messages: [...context.messages, ...prompts],
        };
        await stream.push({ type: "agent_start" });
        await stream.push({ type: "turn_start" });
        for (const prompt of prompts) {
            stream.push({ type: "message_start", message: prompt });
            stream.push({ type: "message_end", message: prompt });
        }
        await runLoop(currentContext, newMessages, config, signal, async (event) => stream.push(event), streamFn);
        stream.end(newMessages);
    })();
    return stream;
}
//# sourceMappingURL=agent-loop.js.map