/**
 * Forked AgentController — lightweight in-process side-agent that reuses a caller-owned
 * prompt snapshot and dedicated tool surface.
 *
 * Unlike task-style subagents, a forked agent does not create its own session
 * transcript. It runs in the same process, uses the caller's resolved prompt
 * snapshot, appends one user instruction, and is bounded by a caller-provided
 * max turn count.
 *
 * Scheduling is per kind:
 * - one active fork per kind
 * - one latest pending fork per kind
 * - different kinds may run in parallel
 *
 * Global concurrency is still capped to avoid overwhelming the LLM proxy.
 */
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { agentLoop, streamAssistantResponse } from '../../core/agent-loop.js';
import { CURRENT_SESSION_VERSION } from '../../core/session-schema.js';
import { log } from '../../shared/logging/agent-log.js';
import { createShortcutInvoke } from '../providers/shortcut-invoke.js';
export const MAX_CONCURRENT_FORKS = 2;
const FORK_LIMIT_RESULT_PREFIX = '[Fork reached maxTurns=';
let _activeForks = 0;
const _waitQueue = [];
const _forkKinds = new Map();
/** Render the per-fork reminder block appended to the parent system prompt. */
function buildForkReminder(maxTurns) {
    if (maxTurns === undefined) {
        return [
            '<forked-agent-reminder>',
            'You are a forked helper.',
            'You may use only the tools provided in this fork.',
            'Complete the task as efficiently as possible.',
            '</forked-agent-reminder>'
        ].join('\n');
    }
    return [
        '<forked-agent-reminder>',
        'You are a forked helper.',
        `You may take at most ${maxTurns} turns.`,
        'You may use only the tools provided in this fork.',
        'Complete the task as efficiently as possible.',
        '</forked-agent-reminder>'
    ].join('\n');
}
/** Append the fork reminder to the caller's resolved system prompt. */
function buildForkSystemPrompt(systemPrompt, maxTurns) {
    return `${systemPrompt.trimEnd()}\n\n${buildForkReminder(maxTurns)}`;
}
/** Format the synthetic assistant response emitted when the fork hits its maxTurns cap. */
function buildTurnLimitResponse(maxTurns) {
    return `${FORK_LIMIT_RESULT_PREFIX}${maxTurns}]`;
}
/** Filter agent messages down to the user/assistant/toolResult subset the LLM accepts. */
function defaultConvertToLlm(messages) {
    return messages.filter((message) => message.role === 'user' || message.role === 'assistant' || message.role === 'toolResult');
}
/** Build a synthetic assistant message used to terminate the fork loop when maxTurns is exceeded. */
function createTurnLimitAssistantMessage(model, maxTurns) {
    return {
        role: 'assistant',
        content: [{ type: 'text', text: buildTurnLimitResponse(maxTurns) }],
        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: 'stop',
        timestamp: Date.now()
    };
}
/** Build a turn-bounded `requestAssistantResponse` that injects a synthetic stop after `maxTurns`. */
function createBoundedRequestAssistantResponse(maxTurns) {
    let modelTurns = 0;
    return async (context, config, signal, stream, streamFn, onToolCallReady) => {
        if (modelTurns >= maxTurns) {
            const message = createTurnLimitAssistantMessage(config.model, maxTurns);
            context.messages.push(message);
            stream.push({ type: 'message_start', message: { ...message } });
            stream.push({ type: 'message_end', message });
            return message;
        }
        modelTurns++;
        return streamAssistantResponse(context, config, signal, stream, streamFn, onToolCallReady);
    };
}
/** Throw if the fork options are missing required fields or use an invalid maxTurns. */
function validateForkOptions(options) {
    if (options.kind.trim().length === 0) {
        throw new Error('Fork kind is required');
    }
    if (options.maxTurns !== undefined &&
        (!Number.isInteger(options.maxTurns) || options.maxTurns < 1)) {
        throw new Error('Fork maxTurns must be a positive integer');
    }
}
/** Concatenate the text segments of a tool-result message into a single string. */
function buildToolResultText(message) {
    return message.content
        .filter((item) => item.type === 'text')
        .map((item) => item.text)
        .join('\n');
}
/** Serialize a fork trace to a JSONL session transcript that /trace can render. */
function buildForkTraceSessionJsonl(trace) {
    const lines = [];
    const baseMs = new Date(trace.startedAt).getTime();
    const sessionId = `fork-${trace.kind}-${baseMs}`;
    let sequence = 0;
    let parentId = null;
    lines.push(JSON.stringify({
        type: 'session',
        version: CURRENT_SESSION_VERSION,
        id: sessionId,
        timestamp: trace.startedAt,
        cwd: process.cwd(),
        isSubagent: true
    }));
    const nextTimestamp = () => new Date(baseMs + sequence * 1000).toISOString();
    const appendEntry = (entry) => {
        sequence++;
        const id = `fork-entry-${sequence}`;
        lines.push(JSON.stringify({
            ...entry,
            id,
            parentId,
            timestamp: nextTimestamp()
        }));
        parentId = id;
    };
    appendEntry({
        type: 'model_change',
        provider: trace.model.provider,
        modelId: trace.model.id
    });
    appendEntry({
        type: 'thinking_level_change',
        thinkingLevel: trace.reasoning
    });
    appendEntry({
        type: 'system_prompt',
        content: trace.systemPrompt
    });
    appendEntry({
        type: 'tool_definitions',
        tools: trace.toolDefinitions
    });
    for (const message of trace.parentMessages) {
        appendEntry({
            type: 'message',
            message
        });
    }
    appendEntry({
        type: 'message',
        message: {
            role: 'user',
            content: trace.instruction,
            timestamp: baseMs + (sequence + 1) * 1000
        }
    });
    for (const round of trace.rounds) {
        appendEntry({
            type: 'turn_start',
            turnIndex: round.round
        });
        const assistantContent = [];
        if (round.text.length > 0) {
            assistantContent.push({ type: 'text', text: round.text });
        }
        for (const toolCall of round.toolCalls) {
            assistantContent.push({
                type: 'toolCall',
                id: toolCall.id,
                name: toolCall.name,
                arguments: (toolCall.arguments ?? {})
            });
        }
        appendEntry({
            type: 'message',
            message: {
                role: 'assistant',
                content: assistantContent,
                api: trace.model.api ?? 'shortcut-llm',
                provider: trace.model.provider,
                model: trace.model.id,
                usage: round.usage,
                stopReason: round.stopReason,
                timestamp: baseMs + (sequence + 1) * 1000
            },
            durationMs: 0
        });
        for (const toolResult of round.toolResults) {
            appendEntry({
                type: 'message',
                message: {
                    role: 'toolResult',
                    toolCallId: toolResult.toolCallId,
                    toolName: toolResult.toolName,
                    content: toolResult.text.length > 0 ? [{ type: 'text', text: toolResult.text }] : [],
                    details: {},
                    isError: toolResult.isError,
                    timestamp: baseMs + (sequence + 1) * 1000
                },
                durationMs: 0
            });
        }
        appendEntry({
            type: 'turn_end',
            turnIndex: round.round,
            durationMs: 0
        });
    }
    return `${lines.join('\n')}\n`;
}
/** Persist the fork trace as both a JSON dump and a parallel JSONL transcript. */
function writeForkTrace(traceDir, trace) {
    if (!traceDir)
        return undefined;
    try {
        mkdirSync(traceDir, { recursive: true });
        const safeTimestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const baseName = `fork-${safeTimestamp}-${process.pid}`;
        const filePath = join(traceDir, `${baseName}.json`);
        const sessionFilePath = join(traceDir, `${baseName}.jsonl`);
        writeFileSync(filePath, JSON.stringify(trace, null, 2) + '\n', 'utf-8');
        writeFileSync(sessionFilePath, buildForkTraceSessionJsonl(trace), 'utf-8');
        return {
            traceFile: filePath,
            sessionFile: sessionFilePath
        };
    }
    catch (error) {
        log.warn('forked-agent', 'Failed to write fork trace artifact', error);
        return undefined;
    }
}
/** Wait until a global concurrency slot is available, honoring abort while queued. */
function acquireForkSlot(signal) {
    if (signal.aborted) {
        return Promise.reject(new Error('Fork aborted before acquiring slot'));
    }
    if (_activeForks < MAX_CONCURRENT_FORKS) {
        _activeForks++;
        return Promise.resolve();
    }
    return new Promise((resolve, reject) => {
        const entry = { resolve, reject };
        _waitQueue.push(entry);
        const onAbort = () => {
            const idx = _waitQueue.indexOf(entry);
            if (idx !== -1) {
                _waitQueue.splice(idx, 1);
                reject(new Error('Fork aborted while waiting for slot'));
            }
        };
        signal.addEventListener('abort', onAbort, { once: true });
        const originalResolve = entry.resolve;
        entry.resolve = () => {
            signal.removeEventListener('abort', onAbort);
            originalResolve();
        };
    });
}
/** Release a global concurrency slot, handing it directly to the next waiter when present. */
function releaseForkSlot() {
    if (_waitQueue.length > 0) {
        const next = _waitQueue.shift();
        next.resolve();
    }
    else if (_activeForks > 0) {
        _activeForks--;
    }
}
/** Test-only: drop all scheduler state, rejecting any waiters and pending requests. */
export function _resetForkSchedulerForTests() {
    _activeForks = 0;
    for (const entry of _waitQueue) {
        entry.reject(new Error('Fork scheduler reset'));
    }
    _waitQueue.length = 0;
    for (const [kind, state] of _forkKinds.entries()) {
        state.pending?.cleanupPendingAbort?.();
        state.pending?.reject(new Error(`Pending fork reset before execution: ${kind}`));
    }
    _forkKinds.clear();
}
/** Remove a pending fork request from its kind state if it still matches, cleaning abort listeners. */
function removePendingRequestIfPresent(kind, request) {
    const state = _forkKinds.get(kind);
    if (!state || state.pending !== request)
        return;
    state.pending = undefined;
    request.cleanupPendingAbort?.();
    if (!state.active) {
        _forkKinds.delete(kind);
    }
}
/** Replace any existing pending fork of this kind with `request`, wiring abort handling. */
function enqueuePendingRequest(state, request) {
    const { kind, signal } = request.options;
    if (state.pending) {
        state.pending.cleanupPendingAbort?.();
        state.pending.reject(new Error(`Fork superseded by newer pending request of kind "${kind}"`));
    }
    state.pending = request;
    const onAbort = () => {
        removePendingRequestIfPresent(kind, request);
        request.reject(new Error('Fork aborted while pending'));
    };
    signal.addEventListener('abort', onAbort, { once: true });
    request.cleanupPendingAbort = () => signal.removeEventListener('abort', onAbort);
}
/** Clear the active slot for a fork kind and promote any pending request into it. */
function finalizeKindRequest(request) {
    const kind = request.options.kind;
    const state = _forkKinds.get(kind);
    if (!state)
        return;
    if (state.active === request) {
        state.active = undefined;
    }
    if (state.pending) {
        const next = state.pending;
        state.pending = undefined;
        next.cleanupPendingAbort?.();
        next.cleanupPendingAbort = undefined;
        state.active = next;
        void runScheduledFork(next);
        return;
    }
    _forkKinds.delete(kind);
}
/** Acquire a slot, run the fork, and unwind scheduler state regardless of outcome. */
async function runScheduledFork(request) {
    let acquiredSlot = false;
    try {
        await acquireForkSlot(request.options.signal);
        acquiredSlot = true;
        const result = await executeFork(request.parentContext, request.options);
        request.resolve(result);
    }
    catch (error) {
        request.reject(error instanceof Error ? error : new Error(String(error)));
    }
    finally {
        if (acquiredSlot) {
            releaseForkSlot();
        }
        finalizeKindRequest(request);
    }
}
/** Schedule the request as either the active fork for its kind or as the latest pending one. */
function executePendingOrActiveRequest(parentContext, options) {
    if (options.signal.aborted) {
        return Promise.reject(new Error('Fork aborted before scheduling'));
    }
    return new Promise((resolve, reject) => {
        const request = {
            parentContext,
            options,
            resolve,
            reject
        };
        const state = _forkKinds.get(options.kind) ?? {};
        _forkKinds.set(options.kind, state);
        if (!state.active) {
            state.active = request;
            void runScheduledFork(request);
            return;
        }
        enqueuePendingRequest(state, request);
    });
}
/** Return a zero-initialized usage record for accumulating fork token totals. */
function emptyUsage() {
    return {
        input: 0,
        output: 0,
        cacheRead: 0,
        cacheWrite: 0,
        totalTokens: 0,
        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
    };
}
/** Add a single assistant message's token/cost usage into the running fork totals. */
function accumulateUsage(total, message) {
    const u = message.usage;
    total.input += u.input;
    total.output += u.output;
    total.cacheRead += u.cacheRead;
    total.cacheWrite += u.cacheWrite;
    total.totalTokens += u.totalTokens;
    total.cost.input += u.cost.input;
    total.cost.output += u.cost.output;
    total.cost.cacheRead += u.cost.cacheRead;
    total.cost.cacheWrite += u.cost.cacheWrite;
    total.cost.total += u.cost.total;
}
/** Schedule and run a forked agent, returning its final response, tool calls, and usage. */
export async function runForkedAgent(parentContext, options) {
    validateForkOptions(options);
    return executePendingOrActiveRequest(parentContext, options);
}
/** Run the fork's agent loop end-to-end, capturing rounds, usage, and trace artifacts. */
async function executeFork(parentContext, options) {
    const { systemPrompt: callerSystemPrompt, messages: parentMessages, tools, model, baseUrl, apiKey, getExtraHeaders, recoverAuth, getToolContext } = parentContext;
    const { instruction, signal, effortLevel, maxTurns, kind } = options;
    const reasoning = effortLevel ?? 'low';
    const traceDir = options.traceDir;
    const systemPrompt = buildForkSystemPrompt(callerSystemPrompt, maxTurns);
    const startedAt = new Date().toISOString();
    const trace = {
        startedAt,
        model: { provider: model.provider, id: model.id, api: model.api },
        reasoning,
        kind,
        maxTurns,
        instruction,
        parentMessageCount: parentMessages.length,
        parentMessages,
        systemPrompt,
        toolDefinitions: tools.map((tool) => ({
            name: tool.name,
            description: tool.description,
            parameters: tool.parameters
        })),
        rounds: []
    };
    const totalUsage = emptyUsage();
    const allToolCalls = [];
    let finalResult;
    let finalAssistantMessage;
    try {
        if (signal.aborted) {
            throw new Error('Fork aborted');
        }
        const invoke = createShortcutInvoke(baseUrl, getExtraHeaders, recoverAuth);
        const context = {
            systemPrompt,
            messages: [...parentMessages],
            tools
        };
        const promptMessage = {
            role: 'user',
            content: instruction,
            timestamp: Date.now()
        };
        const boundedRequest = maxTurns === undefined ? undefined : createBoundedRequestAssistantResponse(maxTurns);
        const emitToolCallReady = (toolCall, onToolCallReady) => {
            options.onToolCall?.(toolCall);
            onToolCallReady?.(toolCall);
        };
        const loopConfig = {
            model,
            maxTokens: model.maxTokens,
            reasoning,
            apiKey,
            convertToLlm: defaultConvertToLlm,
            getToolContext,
            requestAssistantResponse: (loopContext, config, loopSignal, stream, _streamFn, onToolCallReady) => {
                const notifiedToolCallIds = new Set();
                const nextToolCallReady = (toolCall) => {
                    notifiedToolCallIds.add(toolCall.id);
                    emitToolCallReady(toolCall, onToolCallReady);
                };
                const response = boundedRequest
                    ? boundedRequest(loopContext, config, loopSignal, stream, invoke, nextToolCallReady)
                    : streamAssistantResponse(loopContext, config, loopSignal, stream, invoke, nextToolCallReady);
                return response.then((message) => {
                    for (const content of message.content) {
                        if (content.type === 'toolCall' && !notifiedToolCallIds.has(content.id)) {
                            nextToolCallReady(content);
                        }
                    }
                    return message;
                });
            },
            getSteeringMessages: async () => [],
            getFollowUpMessages: async () => []
        };
        const stream = agentLoop([promptMessage], context, loopConfig, signal);
        for await (const event of stream) {
            if (event.type !== 'turn_end')
                continue;
            if (event.message.role !== 'assistant')
                continue;
            const assistantMessage = event.message;
            finalAssistantMessage = assistantMessage;
            accumulateUsage(totalUsage, assistantMessage);
            const toolCalls = assistantMessage.content.filter((c) => c.type === 'toolCall');
            const textContent = assistantMessage.content
                .filter((c) => c.type === 'text')
                .map((c) => c.text)
                .join('');
            allToolCalls.push(...toolCalls);
            trace.rounds.push({
                round: trace.rounds.length,
                stopReason: assistantMessage.stopReason,
                text: textContent,
                toolCalls: toolCalls.map((toolCall) => ({
                    id: toolCall.id,
                    name: toolCall.name,
                    arguments: toolCall.arguments
                })),
                toolResults: event.toolResults.map((toolResult) => ({
                    toolCallId: toolResult.toolCallId,
                    toolName: toolResult.toolName,
                    isError: toolResult.isError,
                    text: buildToolResultText(toolResult)
                })),
                usage: assistantMessage.usage
            });
        }
        if (!finalAssistantMessage) {
            throw new Error('Fork ended without a final assistant message');
        }
        if (finalAssistantMessage.stopReason === 'error' ||
            finalAssistantMessage.stopReason === 'aborted') {
            throw new Error(finalAssistantMessage.errorMessage ?? 'Fork failed');
        }
        finalResult = {
            response: finalAssistantMessage.content
                .filter((c) => c.type === 'text')
                .map((c) => c.text)
                .join(''),
            toolCalls: allToolCalls,
            inputTokens: totalUsage.input,
            outputTokens: totalUsage.output,
            cacheHit: totalUsage.cacheRead > 0,
            usage: totalUsage
        };
        trace.result = {
            response: finalResult.response,
            toolCalls: finalResult.toolCalls.map((toolCall) => ({
                id: toolCall.id,
                name: toolCall.name,
                arguments: toolCall.arguments
            })),
            inputTokens: finalResult.inputTokens,
            outputTokens: finalResult.outputTokens,
            cacheHit: finalResult.cacheHit,
            usage: finalResult.usage
        };
        return finalResult;
    }
    catch (error) {
        trace.error = error instanceof Error ? error.message : String(error);
        throw error;
    }
    finally {
        trace.endedAt = new Date().toISOString();
        trace.durationMs = new Date(trace.endedAt).getTime() - new Date(trace.startedAt).getTime();
        const traceFiles = writeForkTrace(traceDir, trace);
        if (traceFiles && finalResult) {
            finalResult.traceFile = traceFiles.traceFile;
            finalResult.traceSessionFile = traceFiles.sessionFile;
        }
    }
}
//# sourceMappingURL=forked-agent.js.map