import { streamSimple } from '../../../ai/stream.js';
import { AgentController } from '../../../core/agent-controller.js';
import { AgentEngine } from '../../../core/agent-engine.js';
import { convertAgentMessagesToLlm } from '../../../core/agent-snapshot-builder.js';
import { SessionState } from '../../../core/session-state.js';
import { getActiveUserMessageId } from '../../../core/user-message-id.js';
import { createShortcutLlmProxyClient } from '../../providers/shortcut-llm-proxy-client.js';
import { DEFAULT_SUBAGENT_MODEL } from '../../subagents/defaults.js';
import { createTaskSidechainArtifact, TaskSidechainWriter } from './sidechain.js';
import { emptyTaskUsage, TASK_OUTPUT_LINE_LIMIT } from './types.js';
/** Concatenate all text content blocks from an assistant message. */
function textFromAssistant(message) {
    if (!message)
        return '';
    return message.content
        .filter((part) => part.type === 'text')
        .map((part) => part.text)
        .join('');
}
/** Join the text parts of a tool result into a single string. */
function textFromToolResult(result) {
    return (result.content ?? [])
        .filter((part) => part.type === 'text')
        .map((part) => part.text)
        .join('\n');
}
/** Type guard narrowing an AgentMessage to an AssistantMessage. */
function isAssistantMessage(message) {
    return message.role === 'assistant';
}
/** Coerce unknown tool args into a plain record. */
function toToolArgs(args) {
    return args && typeof args === 'object' ? args : {};
}
/** Accumulate per-turn usage stats from an assistant message. */
function addUsage(total, message) {
    const usage = message.usage;
    total.turns++;
    total.input += usage.input;
    total.output += usage.output;
    total.cacheRead += usage.cacheRead;
    total.cacheWrite += usage.cacheWrite;
    total.cost += usage.cost.total;
    total.lastTurnInput = usage.input;
}
/** Stringify a value as compact JSON, truncating to `max` chars. */
function compactJson(value, max = 140) {
    let text;
    try {
        text = JSON.stringify(value);
    }
    catch {
        text = String(value);
    }
    return text.length > max ? `${text.slice(0, max - 3)}...` : text;
}
/** Append non-empty lines from `value` into `lines`, capped to `maxLines`. */
function pushOutputLines(lines, value, maxLines = TASK_OUTPUT_LINE_LIMIT) {
    for (const raw of value.split(/\r?\n/)) {
        const line = raw.trimEnd();
        if (line.length > 0)
            lines.push(line);
    }
    if (lines.length > maxLines) {
        lines.splice(0, lines.length - maxLines);
    }
}
/** Filter the runtime tool catalog down to the tools declared by the subagent. */
function getChildTools(allTools, agent) {
    const allowed = new Set(agent.tools);
    return allTools.filter((tool) => allowed.has(tool.name));
}
/** Split a `provider/modelId` reference into its components. */
function getModelParts(model) {
    const [provider, ...modelIdParts] = model.split('/');
    return { provider, modelId: modelIdParts.join('/') };
}
/** Construct a child session view that points subagent tools at the sidechain transcript. */
function createChildSessionView(parent, sidechain) {
    const sessionFile = sidechain?.sessionFile;
    const sessionId = sidechain?.sessionId ?? `task-${Date.now()}`;
    return {
        getCwd: () => parent.getCwd(),
        getSessionDir: () => parent.getSessionDir(),
        getSessionId: () => sessionId,
        getSessionFile: () => sessionFile,
        getLeafId: () => null,
        getLeafEntry: () => undefined,
        getEntry: () => undefined,
        getLabel: () => undefined,
        getBranch: () => [],
        getHeader: () => ({
            type: 'session',
            id: sessionId,
            parentSession: parent.getSessionFile(),
            isSubagent: true
        }),
        getEntries: () => [],
        getTree: () => [],
        getSessionName: () => undefined
    };
}
/** Run a task subagent to completion using the in-process AgentController. */
export async function runTaskSubagent(options) {
    const { agent, context, task, signal, onProgress } = options;
    const effectiveModel = agent.model ?? DEFAULT_SUBAGENT_MODEL;
    const { provider, modelId } = getModelParts(effectiveModel);
    const model = context.modelRegistry.find(provider, modelId);
    if (!model) {
        return {
            text: '',
            toolCalls: [],
            usage: emptyTaskUsage(),
            model: effectiveModel,
            exitCode: 1,
            stderr: '',
            stopReason: 'error',
            errorMessage: `Model '${effectiveModel}' is not available.`
        };
    }
    const childTools = getChildTools(options.tools, agent);
    const sidechain = options.sidechain ??
        createTaskSidechainArtifact(context.sessionManager, agent.name, options.agentId);
    const sidechainWriter = sidechain
        ? new TaskSidechainWriter({
            ...sidechain,
            cwd: context.cwd,
            systemPrompt: agent.systemPrompt,
            tools: childTools,
            append: options.appendSidechain
        })
        : undefined;
    const abortController = new AbortController();
    const abortChild = () => abortController.abort();
    if (signal?.aborted)
        abortChild();
    signal?.addEventListener('abort', abortChild, { once: true });
    const controllerRef = {};
    const childSessionView = createChildSessionView(context.sessionManager, sidechain);
    const childShortcutLlmProxy = createShortcutLlmProxyClient({
        modelRegistry: context.modelRegistry,
        getAttribution: () => ({
            sessionId: childSessionView.getSessionId(),
            userMessageId: getActiveUserMessageId(controllerRef.current?.state.messages ?? [])
        })
    });
    // Child tools get an explicit child-scoped ExtensionContext. Do not spread the
    // parent context here: parent messages, selected tools, session state, and
    // approval grants are not part of the subagent boundary.
    const childContext = {
        ui: context.ui,
        hasUI: context.hasUI,
        cwd: context.cwd,
        sessionManager: childSessionView,
        modelRegistry: context.modelRegistry,
        shortcutLlmProxy: childShortcutLlmProxy,
        get model() {
            return model;
        },
        isIdle: () => !controllerRef.current?.state.isStreaming,
        abort: () => abortController.abort(),
        hasPendingMessages: () => controllerRef.current?.hasQueuedMessages() ?? false,
        shutdown: () => context.shutdown(),
        getContextUsage: () => undefined,
        compact: (compactOptions) => {
            compactOptions?.onError?.(new Error('Subagent contexts do not support compaction.'));
        },
        getSystemPrompt: () => agent.systemPrompt,
        deductChildUsage: (childUsage) => context.deductChildUsage(childUsage),
        getMessages: () => controllerRef.current?.state.messages ?? [],
        getTools: () => childTools,
        permissionPolicy: context.permissionPolicy,
        sessionState: new SessionState(),
        approvalGrants: undefined,
        subagentSpawnGrant: undefined
    };
    const usage = emptyTaskUsage();
    const observedToolCalls = [];
    const activeToolCallIds = new Set();
    const outputLines = [];
    let lastAssistant;
    let lastText = '';
    let errorMessage;
    const emitProgress = () => {
        onProgress?.({
            text: lastText,
            toolCalls: [...observedToolCalls],
            outputLines: [...outputLines],
            activeToolUses: activeToolCallIds.size,
            usage: { ...usage },
            model: effectiveModel,
            sessionId: sidechain?.sessionId,
            sessionFile: sidechain?.sessionFile
        });
    };
    const controller = new AgentController({
        initialState: {
            model,
            systemPrompt: agent.systemPrompt,
            thinkingLevel: agent.thinkingLevel ?? 'low',
            tools: childTools,
            messages: options.initialMessages ?? []
        },
        runtime: new AgentEngine({
            convertToLlm: convertAgentMessagesToLlm,
            // Match the main AgentSession runtime: tests may inject streamFn, while
            // the shipped CLI uses the provider-backed default streamSimple path.
            streamFn: options.streamFn ?? streamSimple,
            getApiKey: () => context.modelRegistry.getApiKey(model),
            getToolContext: () => childContext
        })
    });
    controllerRef.current = controller;
    options.onReady?.({
        sendMessage: (message) => {
            if (!controller.state.isStreaming)
                return false;
            controller.steer({
                role: 'user',
                content: [{ type: 'text', text: message }],
                timestamp: Date.now()
            });
            return true;
        }
    });
    abortController.signal.addEventListener('abort', () => controller.abort(), { once: true });
    const unsubscribe = controller.subscribe((event) => {
        // Persist sparse, exporter-compatible sidechain entries. Token deltas are
        // intentionally excluded; /trace needs turns and messages, not stream spam.
        if (event.type === 'turn_start') {
            sidechainWriter?.writeTurnStart();
        }
        else if (event.type === 'message_end') {
            sidechainWriter?.writeMessage(event.message);
        }
        else if (event.type === 'turn_end') {
            sidechainWriter?.writeTurnEnd();
        }
        else if (event.type === 'agent_end') {
            const terminalAssistant = [...event.messages].reverse().find(isAssistantMessage);
            if (terminalAssistant?.errorMessage && !lastAssistant) {
                lastAssistant = terminalAssistant;
                lastText = textFromAssistant(terminalAssistant);
                errorMessage = terminalAssistant.errorMessage;
                sidechainWriter?.writeMessage(terminalAssistant);
            }
        }
        // Emit only stable milestones. Token deltas stay inside AgentController so
        // high-volume child streams do not flood the parent TUI. tool_execution_start
        // is the single source of truth for observed tool calls — every dispatched
        // tool call fires it exactly once with a stable toolCallId.
        if (event.type === 'tool_execution_start') {
            observedToolCalls.push({ name: event.toolName, args: toToolArgs(event.args) });
            activeToolCallIds.add(event.toolCallId);
            pushOutputLines(outputLines, `tool start ${event.toolCallId} ${event.toolName} ${compactJson(event.args)}`);
            emitProgress();
            return;
        }
        if (event.type === 'tool_execution_end') {
            activeToolCallIds.delete(event.toolCallId);
            sidechainWriter?.writeToolExecutionEnd({
                toolCallId: event.toolCallId,
                toolName: event.toolName,
                result: event.result,
                isError: event.isError
            });
            const resultText = textFromToolResult(event.result);
            const status = event.isError ? 'error' : 'done';
            pushOutputLines(outputLines, resultText
                ? `tool ${status} ${event.toolCallId} ${event.toolName}: ${resultText}`
                : `tool ${status} ${event.toolCallId} ${event.toolName}`);
            emitProgress();
            return;
        }
        if (event.type === 'message_end' && event.message.role === 'assistant') {
            lastAssistant = event.message;
            lastText = textFromAssistant(lastAssistant);
            addUsage(usage, lastAssistant);
            if (lastAssistant.errorMessage)
                errorMessage = lastAssistant.errorMessage;
            pushOutputLines(outputLines, lastAssistant.errorMessage ?? lastText);
            emitProgress();
        }
    });
    try {
        if (options.userMessageId) {
            await controller.prompt({
                id: options.userMessageId,
                role: 'user',
                content: [{ type: 'text', text: task }],
                timestamp: Date.now()
            });
        }
        else {
            await controller.prompt(task);
        }
        await controller.waitForIdle();
    }
    catch (error) {
        errorMessage = error instanceof Error ? error.message : String(error);
    }
    finally {
        unsubscribe();
        signal?.removeEventListener('abort', abortChild);
    }
    const aborted = signal?.aborted || lastAssistant?.stopReason === 'aborted';
    const failed = Boolean(errorMessage) || lastAssistant?.stopReason === 'error';
    return {
        text: lastText,
        toolCalls: observedToolCalls,
        outputLines,
        usage,
        model: effectiveModel,
        exitCode: aborted ? 130 : failed ? 1 : 0,
        stderr: '',
        sessionId: sidechain?.sessionId,
        sessionFile: sidechain?.sessionFile,
        stopReason: lastAssistant?.stopReason,
        errorMessage,
        aborted
    };
}
//# sourceMappingURL=runner.js.map