import { streamChat } from '../providers/openrouter.js';
import { compactMessages, shouldCompact } from './compact.js';
const MAX_TOOL_RESULT_CHARS = 50_000;
export async function* runAgent(opts) {
    const maxTurns = opts.maxTurns ?? 40;
    const toolSpecs = opts.tools.map((t) => ({
        name: t.name,
        description: t.description,
        parameters: t.jsonSchema,
    }));
    for (let turn = 0; turn < maxTurns; turn++) {
        if (opts.signal.aborted) {
            yield { type: 'done', reason: 'cancelled' };
            return;
        }
        if (shouldCompact(opts.messages, opts.contextLength ?? null)) {
            try {
                const { messages: compacted, summarizedCount } = await compactMessages({
                    apiKey: opts.apiKey,
                    model: opts.model,
                    provider: opts.provider,
                    messages: opts.messages,
                });
                if (summarizedCount > 0) {
                    opts.messages.splice(0, opts.messages.length, ...compacted);
                    yield { type: 'compacted', summarizedCount };
                }
            }
            catch {
                // Compaction is best-effort; if it fails, proceed and let the
                // provider reject the request if the context truly overflows.
            }
        }
        let result;
        // Buffer deltas from the streaming callback and drain them from the
        // generator so the UI can render tokens as they arrive.
        const pending = [];
        let notify = null;
        let streamDone = false;
        let streamError = null;
        const streamPromise = streamChat({
            apiKey: opts.apiKey,
            model: opts.model,
            provider: opts.provider,
            messages: opts.messages,
            tools: toolSpecs,
            signal: opts.signal,
        }, (delta) => {
            pending.push(delta);
            notify?.();
        }).then((r) => {
            streamDone = true;
            notify?.();
            return r;
        }, (err) => {
            streamError = err instanceof Error ? err : new Error(String(err));
            streamDone = true;
            notify?.();
            return null;
        });
        while (!streamDone || pending.length > 0) {
            if (pending.length > 0) {
                yield { type: 'assistant-delta', text: pending.splice(0).join('') };
            }
            else {
                await new Promise((resolve) => {
                    notify = resolve;
                });
                notify = null;
            }
        }
        const streamResult = await streamPromise;
        if (streamError || !streamResult) {
            const err = streamError;
            if (opts.signal.aborted) {
                yield { type: 'done', reason: 'cancelled' };
            }
            else {
                yield { type: 'error', message: err?.message ?? 'unknown stream error' };
                yield { type: 'done', reason: 'error' };
            }
            return;
        }
        result = streamResult;
        if (result.usage)
            yield { type: 'usage', usage: result.usage };
        // Record the assistant message (with any tool calls) in history.
        opts.messages.push({
            role: 'assistant',
            content: result.content,
            ...(result.toolCalls.length > 0
                ? {
                    tool_calls: result.toolCalls.map((tc) => ({
                        id: tc.id,
                        type: 'function',
                        function: { name: tc.name, arguments: tc.arguments },
                    })),
                }
                : {}),
        });
        if (result.content)
            yield { type: 'assistant-message', text: result.content };
        if (result.toolCalls.length === 0) {
            yield { type: 'done', reason: 'complete' };
            return;
        }
        for (const call of result.toolCalls) {
            if (opts.signal.aborted) {
                opts.messages.push({
                    role: 'tool',
                    tool_call_id: call.id,
                    content: 'cancelled by user',
                });
                continue;
            }
            const tool = opts.tools.find((t) => t.name === call.name);
            let output;
            let isError = false;
            if (!tool) {
                output = `error: unknown tool "${call.name}"`;
                isError = true;
                yield { type: 'tool-start', id: call.id, name: call.name, summary: '' };
            }
            else {
                let parsedInput;
                try {
                    parsedInput = JSON.parse(call.arguments || '{}');
                }
                catch {
                    parsedInput = null;
                }
                const validated = parsedInput === null ? null : tool.schema.safeParse(parsedInput);
                yield {
                    type: 'tool-start',
                    id: call.id,
                    name: call.name,
                    summary: validated?.success ? tool.summarize(validated.data) : '(invalid arguments)',
                };
                if (!validated || !validated.success) {
                    const detail = validated
                        ? validated.error.issues
                            .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
                            .join('; ')
                        : 'arguments were not valid JSON';
                    const received = parsedInput && typeof parsedInput === 'object'
                        ? ` (received keys: ${Object.keys(parsedInput).join(', ') || 'none'})`
                        : '';
                    output = `error: invalid arguments for ${call.name}: ${detail}${received}`;
                    isError = true;
                }
                else {
                    const hookBlock = opts.hooks ? await opts.hooks.pre(tool.name, validated.data) : null;
                    const refusal = hookBlock ?? (await opts.gate.check(tool, validated.data));
                    if (refusal) {
                        output = refusal;
                        isError = true;
                    }
                    else {
                        try {
                            output = await tool.run(validated.data, opts.ctx);
                            isError = output.startsWith('error:');
                        }
                        catch (err) {
                            output = `error: ${err.message}`;
                            isError = true;
                        }
                        opts.hooks?.post(tool.name, validated.data, output);
                    }
                }
            }
            if (output.length > MAX_TOOL_RESULT_CHARS) {
                output =
                    output.slice(0, MAX_TOOL_RESULT_CHARS) +
                        `\n... [truncated ${output.length - MAX_TOOL_RESULT_CHARS} chars]`;
            }
            opts.messages.push({ role: 'tool', tool_call_id: call.id, content: output });
            yield { type: 'tool-result', id: call.id, name: call.name, result: output, isError };
        }
        if (opts.signal.aborted) {
            yield { type: 'done', reason: 'cancelled' };
            return;
        }
    }
    yield { type: 'error', message: `stopped after ${maxTurns} turns without finishing` };
    yield { type: 'done', reason: 'max-turns' };
}
