/**
 * Runtime-message translation — converts the internal `AgentMessage[]`
 * transcript into the LLM-facing `Message[]` shape and the cross-boundary
 * `AgentSnapshot` consumed by host clients.
 */
import { AgentStopReason, RuntimeMessageRole } from '../contracts/agent-snapshot.js';
import { toStructuredCloneSafeValue } from '../utils/structured-clone-safe.js';
import { CANCELLATION_BOUNDARY_CUSTOM_TYPE } from './custom-messages.js';
function toRuntimeToolCalls(message) {
    if (message.role !== 'assistant') {
        return undefined;
    }
    const toolCalls = message.content.filter((item) => item.type === 'toolCall');
    if (toolCalls.length === 0) {
        return undefined;
    }
    return toolCalls.map((toolCall) => ({
        id: toolCall.id,
        name: toolCall.name,
        args: toStructuredCloneSafeValue(toolCall.arguments)
    }));
}
function toRuntimeThinking(message) {
    if (message.role !== 'assistant') {
        return undefined;
    }
    const thinkingBlocks = message.content.filter((item) => item.type === 'thinking' &&
        typeof item.thinking === 'string' &&
        item.thinking.trim().length > 0);
    if (thinkingBlocks.length === 0) {
        return undefined;
    }
    return thinkingBlocks.map((block) => block.thinking.trim()).join('\n\n');
}
function toRuntimeMessageRole(message) {
    switch (message.role) {
        case 'assistant':
            return RuntimeMessageRole.Assistant;
        case 'user':
            return RuntimeMessageRole.User;
        default:
            return RuntimeMessageRole.Tool;
    }
}
/**
 * Project the agent message's content array into a single UI-facing `text` field.
 *
 * **Role-specific rule** (intentional asymmetry — see §25 spec § Simplifying assumptions #9):
 * - `role === 'user'`: only `content[0].text` is user-visible. Subsequent blocks are
 *   per-turn enrichments (uploads, workbook summary, focused range, range restrictions,
 *   context-update warning) that exist for the LLM only. The UI snapshot must not show them.
 * - All other roles (`assistant`, `toolResult`, `custom`): join all blocks. Enrichments
 *   are a user-message concept; assistant/tool messages have no enrichments to hide.
 */
function projectAgentContentToSnapshotText(message) {
    if (message.role === 'user') {
        // §25 canonical shape: user `content` is always a non-empty array, content[0]
        // is the user query (TextContent), content[1+] are LLM-only enrichments.
        // String content is canonicalized to array form on hydrate by
        // `parseSessionEntriesStrict`. The string branch below is a defensive
        // owned AI-type fallback — unreachable in canonicalized flows but the owned AI
        // `UserMessage` type allows it, so we don't crash on a hand-built fixture.
        if (typeof message.content === 'string')
            return message.content;
        const head = message.content[0];
        if (head && head.type === 'text')
            return head.text;
        if (head && head.type === 'image')
            return '[image]';
        return '';
    }
    const content = Array.isArray(message.content) ? message.content : [];
    return content
        .map((item) => {
        if (item.type === 'text')
            return item.text;
        if (item.type === 'toolCall')
            return `[tool:${item.name}]`;
        if (item.type === 'image')
            return '[image]';
        return `[${item.type}]`;
    })
        .join('\n');
}
function toRuntimeMessage(message, index, entryId) {
    const text = projectAgentContentToSnapshotText(message);
    const thinking = toRuntimeThinking(message);
    const toolCalls = toRuntimeToolCalls(message);
    const toolCallId = 'toolCallId' in message ? message.toolCallId : undefined;
    const toolName = 'toolName' in message ? message.toolName : undefined;
    const explicitDetails = 'details' in message ? toStructuredCloneSafeValue(message.details) : undefined;
    const streamErrorDetails = message.role === 'assistant' && 'errorDetails' in message
        ? toStructuredCloneSafeValue(message.errorDetails)
        : undefined;
    const details = explicitDetails ?? streamErrorDetails;
    const isError = 'isError' in message ? message.isError : undefined;
    const uiMetadata = message.role === 'user' && 'uiMetadata' in message
        ? toStructuredCloneSafeValue(message.uiMetadata)
        : undefined;
    // Prefer the persisted session-entry id (stable UUID, 1:1 with the
    // message). Fall back to the assistant-message id minted by run-stats,
    // then to a positional placeholder for synthetic messages not yet
    // persisted (e.g. orphan-repair normalization).
    const id = entryId && entryId.length > 0
        ? entryId
        : 'id' in message && typeof message.id === 'string'
            ? message.id
            : `${message.role}-${index}`;
    return {
        id,
        role: toRuntimeMessageRole(message),
        text,
        ...(thinking ? { thinking } : {}),
        ...(toolCalls ? { toolCalls } : {}),
        ...(toolCallId ? { toolCallId } : {}),
        ...(toolName ? { toolName } : {}),
        ...(details !== undefined ? { details } : {}),
        ...(isError !== undefined ? { isError } : {}),
        ...(uiMetadata !== undefined ? { uiMetadata } : {})
    };
}
function isHiddenCustomMessage(message) {
    return message.role === 'custom' && message.display === false;
}
function isCancellationBoundaryMessage(message) {
    return (message.role === 'custom' &&
        message.customType === CANCELLATION_BOUNDARY_CUSTOM_TYPE &&
        message.display === false);
}
function normalizeIncompleteToolUseGroups(messages) {
    const normalized = [];
    for (let index = 0; index < messages.length; index += 1) {
        const message = messages[index];
        normalized.push(message);
        if (message.role !== 'assistant') {
            continue;
        }
        const toolCalls = message.content.filter((content) => content.type === 'toolCall');
        if (toolCalls.length === 0) {
            continue;
        }
        const expectedIds = new Set(toolCalls.map((toolCall) => toolCall.id));
        let nextIndex = index + 1;
        while (nextIndex < messages.length && messages[nextIndex]?.role === 'toolResult') {
            const toolResult = messages[nextIndex];
            normalized.push(toolResult);
            expectedIds.delete(toolResult.toolCallId);
            nextIndex += 1;
        }
        if (expectedIds.size > 0) {
            for (const toolCall of toolCalls) {
                if (!expectedIds.has(toolCall.id)) {
                    continue;
                }
                normalized.push({
                    role: 'toolResult',
                    toolCallId: toolCall.id,
                    toolName: toolCall.name,
                    content: [
                        {
                            type: 'text',
                            text: 'Tool call interrupted before a matching tool_result was recorded.'
                        }
                    ],
                    isError: true,
                    timestamp: Date.now()
                });
            }
        }
        index = nextIndex - 1;
    }
    return normalized;
}
function getAgentStopReason(messages, isStreaming, stopReasonOverride) {
    if (stopReasonOverride) {
        return stopReasonOverride;
    }
    if (isStreaming) {
        return AgentStopReason.Running;
    }
    const lastMessage = messages[messages.length - 1];
    if (lastMessage && isCancellationBoundaryMessage(lastMessage)) {
        return AgentStopReason.Cancelled;
    }
    for (let index = messages.length - 1; index >= 0; index -= 1) {
        const message = messages[index];
        if (message.role !== 'assistant') {
            continue;
        }
        if (message.stopReason === 'error') {
            return AgentStopReason.Failed;
        }
        if (message.stopReason === 'aborted') {
            return AgentStopReason.Cancelled;
        }
        return AgentStopReason.Completed;
    }
    return AgentStopReason.Running;
}
/**
 * Single source of truth: is this `AgentMessage` projected as a `role:'user'` LLM message?
 *
 * Used by both:
 *   - `convertAgentMessagesToLlm` (to decide what shape to emit)
 *   - `hasPriorUserEquivalent` in `embedded-agent/compose.ts` (to decide whether the
 *     context-update warning should fire)
 *
 * Per §25 spec § Simplifying assumption #6: the warning fires whenever the
 * transcript contains an entry that the projector treats as a user turn. Keeping
 * one predicate ensures the two callers can never drift.
 */
export function projectsAsUserRole(message) {
    return message.role === 'user' || message.role === 'custom';
}
/**
 * Slice `messages` at the most-recent reset boundary.
 *
 * A reset boundary is a user-role `AgentMessage` with `contextResetBoundary === true`.
 * If any boundary exists, return `messages` from the highest-index boundary forward;
 * otherwise return `messages` unchanged.
 *
 * Slice MUST happen before tool-pairing repair: pairing depends on local sequence
 * integrity, and repair across a boundary would re-attach a pre-boundary tool_use to
 * a post-boundary toolResult. See §25 spec § Simplifying assumptions #7.
 */
function sliceAtMostRecentResetBoundary(messages) {
    let boundaryIndex = null;
    for (let i = messages.length - 1; i >= 0; i -= 1) {
        const m = messages[i];
        if (m.role === 'user' && m.contextResetBoundary === true) {
            boundaryIndex = i;
            break;
        }
    }
    return boundaryIndex === null ? messages : messages.slice(boundaryIndex);
}
/**
 * Project a user `AgentMessage` to a clean owned AI `UserMessage`.
 *
 * Strips the SXL-only `contextResetBoundary` field so the LLM-facing `Message[]`
 * never carries non-owned AI shapes. See §25 spec § Simplifying assumptions #7a.
 */
function projectUserAgentMessageToLlm(message) {
    return {
        role: 'user',
        content: message.content,
        timestamp: message.timestamp
    };
}
function stripInternalMessageId(message) {
    const cleanMessage = { ...message };
    delete cleanMessage.id;
    return cleanMessage;
}
/** Project the agent transcript into an LLM-ready message list, repairing orphan tool calls. */
export function convertAgentMessagesToLlm(messages) {
    const sliced = sliceAtMostRecentResetBoundary(messages);
    return normalizeIncompleteToolUseGroups(sliced)
        .map((message) => {
        switch (message.role) {
            case 'assistant':
            case 'toolResult':
                return stripInternalMessageId(message);
            case 'user':
                return projectUserAgentMessageToLlm(message);
            case 'custom':
                return {
                    role: 'user',
                    content: typeof message.content === 'string'
                        ? [{ type: 'text', text: message.content }]
                        : message.content,
                    timestamp: message.timestamp
                };
            default: {
                const exhaustiveCheck = message;
                return exhaustiveCheck;
            }
        }
    })
        .filter((message) => message !== undefined);
}
function toRuntimeInFlightOp(op) {
    return {
        opId: op.opId,
        kind: op.kind,
        startedAt: op.startedAt,
        elapsedMs: op.elapsedMs,
        ...(op.label !== undefined ? { label: op.label } : {}),
        ...(op.toolName !== undefined ? { toolName: op.toolName } : {}),
        ...(op.toolCallId !== undefined ? { toolCallId: op.toolCallId } : {})
    };
}
/** Build a structured-clone-safe `AgentSnapshot` from the current transcript. */
export function createAgentSnapshotFromAgentMessages(inputs) {
    const stopReason = getAgentStopReason(inputs.messages, inputs.isStreaming, inputs.stopReasonOverride);
    const errorKind = stopReason === AgentStopReason.Failed && inputs.errorKind
        ? inputs.errorKind
        : undefined;
    const inFlight = (inputs.inFlight ?? []).map((op) => toRuntimeInFlightOp(op));
    const visibleMessages = inputs.messages
        .map((message, index) => ({ message, index }))
        .filter(({ message }) => !isHiddenCustomMessage(message));
    return {
        sessionId: inputs.sessionId,
        isStreaming: inputs.isStreaming,
        stopReason,
        ...(errorKind ? { errorKind } : {}),
        messages: visibleMessages.map(({ message, index }, visibleIndex) => toRuntimeMessage(message, visibleIndex, inputs.messageEntryIds?.[index])),
        inFlight,
        ...(inputs.runId !== undefined ? { runId: inputs.runId } : {})
    };
}
//# sourceMappingURL=agent-snapshot-builder.js.map