/**
 * Follow-up suggestion runner for the embedded (web) runtime.
 *
 * Owns prompt assembly + response parsing in the runtime. The actual
 * `/api/llm/invoke` HTTP call is dispatched through `bridge.request(...)`
 * and handled by the client side — same pattern as `web_search` etc. The
 * runtime never touches `fetch`, `baseUrl`, or `apiKey`.
 *
 * This module is reached by `composeEmbeddedAgentRuntime` callers.
 */
import { HostToolResultStatus } from '../contracts/host-tool.js';
import { convertAgentMessagesToLlm as convertToLlm } from '../core/agent-snapshot-builder.js';
import { withAbort } from '../utils/abort.js';
import { stringifyTruncatedJson } from '../utils/json-safe-truncate.js';
import { INVOKE_LLM_APPROVAL, INVOKE_LLM_KIND } from './host-tools/invoke-llm/index.js';
const SUGGESTIONS_MODEL = 'claude-haiku-4-5-20251001';
const MAX_CONTEXT_CHARS = 60_000;
const TOOL_CALL_ARGUMENTS_MAX_CHARS = 500;
let suggestionsRequestSeq = 0;
const SET_FOLLOWUP_SUGGESTIONS_TOOL = {
    name: 'set_followup_suggestions',
    description: 'Send follow-up suggestions to the client UI. Call this tool once you have a solid set of suggested follow-up messages.',
    parameters: {
        type: 'object',
        properties: {
            suggestions: {
                type: 'array',
                items: { type: 'string' },
                minItems: 1,
                maxItems: 3,
                description: 'Follow-up suggestions for the user'
            },
            skill_proposal: {
                type: 'object',
                properties: {
                    name: {
                        type: 'string',
                        description: 'Short kebab-case name for the reusable skill'
                    },
                    description: {
                        type: 'string',
                        description: 'One-sentence description of what the skill does'
                    },
                    prompt: {
                        type: 'string',
                        description: 'Prompt to create the reusable skill'
                    }
                },
                required: ['name', 'description', 'prompt'],
                description: 'Optional reusable skill proposal for the completed workflow'
            }
        },
        required: ['suggestions']
    }
};
/** Compose the static suggestions system prompt with today's date and the available tool descriptions. */
function buildSystemPrompt(toolDescriptions) {
    const currentDate = new Date().toISOString().slice(0, 10);
    return `You are Shortcut's Suggestions AgentController.
Today's date: ${currentDate}
You are running at the end of a conversation between the user and the main agent. You have inherited their interaction context.

Your goal: Generate follow-up prompts the user might want to send next.

Call the set_followup_suggestions tool with 1-3 follow-up prompts. If the completed workflow looks reusable, also include a skill_proposal with name, description, and prompt. The graph will terminate after this tool call.

Suggestions Guidelines:
- You MUST ALWAYS call the set_followup_suggestions tool. NEVER respond with text — your text output is discarded and will cause errors. Even if the conversation is vague or minimal, generate generic useful suggestions.
- Prompts should be specific to the user's current task/context when possible, or generic but useful when context is minimal
- Each prompt MUST be written as something the USER would say to the AI — a command, request, or instruction. NOT a question the AI would ask the user.
  - GOOD: "Build a budget tracker template", "Add a chart for monthly expenses", "Format column B as currency"
  - BAD: "What would you like to build?", "Do you have data to import?", "Would you like help with X?"
- Keep prompts concise (under 100 characters each)
- Make prompts diverse - cover different directions the user might want to go
- Consider what logical next steps the user might take based on their recent work
- Only include skill_proposal when there is a concrete repeatable workflow worth saving.

========================
START OF TOOL REFERENCES
========================

Below are all the tools that are available to the main agent, provided for your reference.

${toolDescriptions}

========================
END OF TOOL REFERENCES
========================
`;
}
/** Trim text to `maxChars`, appending `...` when truncation is required. */
function truncate(text, maxChars) {
    return text.length <= maxChars ? text : text.slice(0, maxChars) + '...';
}
function textFromContent(content) {
    if (typeof content === 'string')
        return content;
    const parts = [];
    for (const item of content) {
        if (item.type === 'text' && 'text' in item && typeof item.text === 'string') {
            parts.push(item.text);
        }
        else if (item.type === 'image') {
            parts.push('[image]');
        }
        else if (item.type === 'toolCall') {
            const name = 'name' in item && typeof item.name === 'string' ? item.name : 'unknown_tool';
            parts.push(`[tool call: ${name}]`);
        }
    }
    return parts.join('\n');
}
function renderTranscriptMessage(msg) {
    if (msg.role === 'user') {
        return `User:\n${textFromContent(msg.content)}`;
    }
    if (msg.role === 'assistant') {
        const parts = [];
        const text = textFromContent(msg.content);
        if (text.trim().length > 0)
            parts.push(text);
        for (const item of msg.content) {
            if (item.type !== 'toolCall')
                continue;
            parts.push(`[tool call: ${item.name} ${stringifyTruncatedJson(item.arguments, TOOL_CALL_ARGUMENTS_MAX_CHARS)}]`);
        }
        return `Assistant:\n${parts.join('\n')}`;
    }
    if (msg.role === 'toolResult') {
        const text = truncate(textFromContent(msg.content), 1000);
        return `Tool result (${msg.toolName ?? 'unknown_tool'}, ${msg.toolCallId}):\n${text}`;
    }
    return '';
}
function buildTranscriptPrompt(messages) {
    const llmMessages = convertToLlm([...messages]);
    const rendered = [];
    let totalChars = 0;
    let omitted = 0;
    for (let index = llmMessages.length - 1; index >= 0; index -= 1) {
        const entry = renderTranscriptMessage(llmMessages[index]);
        if (entry.trim().length === 0)
            continue;
        const nextTotal = totalChars + entry.length + 2;
        if (nextTotal > MAX_CONTEXT_CHARS) {
            omitted = index + 1;
            break;
        }
        rendered.unshift(entry);
        totalChars = nextTotal;
    }
    const omittedLine = omitted > 0 ? `[${omitted} earlier transcript messages omitted for length]\n\n` : '';
    const transcript = rendered.length > 0 ? `${omittedLine}${rendered.join('\n\n')}` : '[no prior transcript]';
    return `Conversation transcript:

${transcript}

Based on the transcript above, call the set_followup_suggestions tool now.`;
}
/** Build the owned-AI message list sent to the suggestions LLM, capped at MAX_CONTEXT_CHARS. */
function buildInvokeMessages(messages) {
    return [{ role: 'user', content: buildTranscriptPrompt(messages), timestamp: Date.now() }];
}
/** Extract the `set_followup_suggestions` tool call result and clamp it to at most five strings. */
function parseResult(details) {
    const toolCall = details?.tool_calls?.find((tc) => tc.name === 'set_followup_suggestions');
    if (!toolCall)
        return undefined;
    let args;
    try {
        args = JSON.parse(toolCall.arguments);
    }
    catch {
        return undefined;
    }
    const raw = args.suggestions;
    if (!Array.isArray(raw))
        return undefined;
    const suggestions = raw
        .filter((s) => typeof s === 'string' && s.trim().length > 0)
        .slice(0, 3);
    if (suggestions.length === 0)
        return undefined;
    const rawSkillProposal = args.skill_proposal;
    const skillProposal = rawSkillProposal &&
        typeof rawSkillProposal === 'object' &&
        typeof rawSkillProposal.name === 'string' &&
        typeof rawSkillProposal.description === 'string' &&
        typeof rawSkillProposal.prompt === 'string'
        ? {
            name: rawSkillProposal.name,
            description: rawSkillProposal.description,
            prompt: rawSkillProposal.prompt
        }
        : null;
    return { suggestions, skillProposal };
}
/**
 * Generate follow-up suggestions through pending host-tool requests. Best-effort
 * — never throws, never blocks. Returns a structured outcome so callers can emit
 * `_failed` events.
 */
/** Best-effort run of the follow-up suggestions LLM call through the host-tool bridge. */
export async function runSuggestionsViaBridge(messages, toolDescriptions, pendingHostToolRequests, signal) {
    const systemPrompt = buildSystemPrompt(toolDescriptions);
    const params = {
        model: SUGGESTIONS_MODEL,
        systemPrompt,
        messages: buildInvokeMessages(messages),
        tools: [SET_FOLLOWUP_SUGGESTIONS_TOOL],
        tool_choice: { type: 'tool', name: 'set_followup_suggestions' },
        effort_level: 'low',
        thinking_type: 'off'
    };
    suggestionsRequestSeq += 1;
    const request = {
        approval: INVOKE_LLM_APPROVAL,
        kind: INVOKE_LLM_KIND,
        params,
        requestId: `invokeLlm-suggestions-${suggestionsRequestSeq}`
    };
    try {
        const result = await withAbort(signal, () => pendingHostToolRequests.request(request));
        if (result.status !== HostToolResultStatus.Succeeded) {
            const reason = result.userMessage ?? `bridge result status: ${result.status}`;
            return { status: 'failed', reason };
        }
        return { status: 'ok', result: parseResult(result.details) };
    }
    catch (err) {
        return {
            status: 'failed',
            reason: err instanceof Error ? err.message : String(err)
        };
    }
}
//# sourceMappingURL=suggestions.js.map