UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

1,576 lines • 65.7 kB
import { findToolByName, validateToolArguments } from "../llm/index.js";
import { truncateError } from "../../core/tool-errors.js";
import { INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER, INTERRUPTED_BY_USER_MARKER, } from "../../core/session-reconcile.js";
import { resolveAgentCoreStreamFn } from "./runtime-deps.js";
import { isErrorLike, readMember, thrownMarksOf, thrownText } from "../../core/thrown-value.js";
function appendTextDeltaToAssistantMessage(message, contentIndex, delta) {
    const content = [...message.content];
    const currentContent = content[contentIndex];
    content[contentIndex] =
        currentContent?.type === "text"
            ? { ...currentContent, text: currentContent.text + delta }
            : { type: "text", text: delta };
    return { ...message, content };
}
function resolveAssistantMessageUpdate(event, currentMessage) {
    if ("partial" in event && event.partial) {
        return event.partial;
    }
    if (event.type === "text_delta") {
        return appendTextDeltaToAssistantMessage(currentMessage, event.contentIndex, event.delta);
    }
    return currentMessage;
}
export function findUnansweredToolBatch(messages) {
    for (let i = messages.length - 1; i >= 0; i--) {
        const m = messages[i];
        if (m.role !== "assistant")
            continue;
        return unansweredToolCallsOf(m, messages.slice(i + 1)).length > 0 ? m : undefined;
    }
    return undefined;
}
function unansweredToolCallsOf(assistantMessage, after) {
    const answered = new Set(after.filter((m) => m.role === "toolResult").map((m) => m.toolCallId));
    const seen = new Set();
    const unanswered = [];
    for (const c of assistantMessage.content) {
        if (c.type !== "toolCall" || answered.has(c.id) || seen.has(c.id))
            continue;
        seen.add(c.id);
        unanswered.push(c);
    }
    return unanswered;
}
export async function settleUnansweredToolCalls(context, assistantMessage, config, signal, emit, halt) {
    const at = context.messages.lastIndexOf(assistantMessage);
    const toolCalls = unansweredToolCallsOf(assistantMessage, at >= 0 ? context.messages.slice(at + 1) : context.messages);
    if (toolCalls.length === 0)
        return { messages: [], terminate: false };
    if (halt !== undefined) {
        const messages = [];
        for (const toolCall of toolCalls) {
            if (signal?.aborted)
                break;
            messages.push((await settleHaltedToolCall(toolCall, halt, emit)).message);
        }
        return { messages, terminate: true };
    }
    return config.toolExecution === "sequential"
        ? executeToolCallsSequential(context, assistantMessage, toolCalls, config, signal, emit)
        : executeToolCallsPartitioned(context, assistantMessage, toolCalls, config, signal, emit);
}
export async function runAgentLoop(prompts, context, config, emit, signal, streamFn, runtime, trace, seams) {
    const state = await createLoopState({ ...context, messages: [...context.messages] }, config, []);
    await emit({ type: "agent_start" });
    await emit({ type: "turn_start" });
    if (seams?.settle !== undefined && (await settleTrailingBatchTurn(state, signal, emit, seams.settle)) === "ended") {
        return state.newMessages;
    }
    const delivered = typeof prompts === "function" ? await prompts() : prompts;
    if (delivered.length > 0)
        state.hasMoreToolCalls = true;
    state.context.messages.push(...delivered);
    state.newMessages.push(...delivered);
    seams?.onPromptsDelivered?.();
    for (const prompt of delivered) {
        await emit({ type: "message_start", message: prompt });
        await emit({ type: "message_end", message: prompt });
    }
    await runLoop(state, signal, emit, streamFn, runtime, trace);
    return state.newMessages;
}
async function settleTrailingBatchTurn(state, signal, emit, settle) {
    const tail = findUnansweredToolBatch(state.context.messages);
    if (tail === undefined)
        return "continue";
    const turnController = new AbortController();
    const onRunAbort = () => turnController.abort();
    if (signal !== undefined) {
        if (signal.aborted)
            turnController.abort();
        else
            signal.addEventListener("abort", onRunAbort, { once: true });
    }
    state.config.publishTurnInterruptSeat?.(turnController);
    let batch;
    try {
        batch = await settleUnansweredToolCalls(state.context, tail, state.config, turnController.signal, emit, settle.halt);
    }
    finally {
        state.config.publishTurnInterruptSeat?.(undefined);
        signal?.removeEventListener("abort", onRunAbort);
    }
    state.context.messages.push(...batch.messages);
    state.newMessages.push(...batch.messages);
    let ended = signal?.aborted === true;
    let terminalEmitted = false;
    if (!ended && turnController.signal.aborted) {
        state.config.noteTurnCutConsumed?.(turnController);
        const pendingBeforeSettlement = state.pendingMessages;
        const outcome = await settleInterruptedTurn(state, tail, new StreamToolExecutor(state.context, state.config, turnController.signal, emit), batch.messages, signal, emit);
        ended = outcome.kind === "terminal";
        if (!ended)
            state.pendingMessages.unshift(...pendingBeforeSettlement);
        terminalEmitted = ended;
    }
    else if (!ended) {
        const boundary = { message: tail, toolResults: batch.messages, context: state.context, newMessages: state.newMessages };
        adoptNextTurnSnapshot(state, await state.config.prepareNextTurn?.(boundary));
        ended = (await state.config.shouldStopAfterTurn?.(boundary)) === true || signal?.aborted === true;
        if (!ended)
            state.pendingMessages.push(...((await state.config.getSteeringMessages?.()) || []));
    }
    settle.report({ assistantMessage: tail, settled: batch.messages.length, aborted: ended });
    if (ended && !terminalEmitted)
        await emit({ type: "agent_end", messages: state.newMessages });
    return ended ? "ended" : "continue";
}
const PROMPT_TOO_LONG_RE = /context.{0,3}length|context_length|context window (?:limit|exceeded)|reached its context window|prompt is too long|too many tokens|maximum context|exceeds? .{0,30}(?:context|token)|input is too long|\b413\b/i;
function isBlankFailureContent(message) {
    return message.content.every((c) => c.type === "text" ? c.text.trim() === "" : c.type === "thinking" ? c.thinking.trim() === "" : false);
}
function defaultDetectPromptTooLong(message) {
    if (message.stopReason !== "error")
        return false;
    if (message.errorKind === "input_too_long")
        return true;
    if (message.inputTooLongRuledOut === true)
        return false;
    return PROMPT_TOO_LONG_RE.test(message.errorMessage ?? "");
}
function createPtlWithholdBuffer(emit, detect) {
    const held = [];
    let liveStartEmitted = false;
    const isCandidate = (message) => message.role === "assistant" &&
        message.stopReason === "error" &&
        detect(message) &&
        isBlankFailureContent(message);
    return {
        sink: async (event) => {
            if (event.type === "message_start" && isCandidate(event.message)) {
                held.push(event);
                return;
            }
            if (event.type === "message_end" && !liveStartEmitted && isCandidate(event.message)) {
                held.push(event);
                return;
            }
            if (event.type === "message_start")
                liveStartEmitted = true;
            await emit(event);
        },
        async flush() {
            for (const event of held.splice(0))
                await emit(event);
        },
        discard() {
            held.length = 0;
        },
    };
}
function createMalformedToolUseNudge() {
    return {
        role: "custom",
        customType: "loop:malformed-tool-use-retry",
        provenance: "engine-note",
        content: "Your previous response indicated tool use but contained no valid tool call. Re-issue the tool call(s) now, with complete and valid JSON arguments — provide exactly one JSON value per parameter, and do not truncate.",
        display: false,
        timestamp: Date.now(),
    };
}
function createThinkingOnlyNudge() {
    return {
        role: "custom",
        customType: "loop:thinking-only-retry",
        provenance: "engine-note",
        content: "Your previous response had no visible output — it contained only internal reasoning. Respond now with your answer as visible text (or make the required tool calls); do not end the turn with reasoning alone.",
        display: false,
        timestamp: Date.now(),
    };
}
function createMidstreamPartialContinueNudge() {
    return {
        role: "custom",
        customType: "loop:midstream-partial-continue",
        provenance: "engine-note",
        content: "Your previous response was cut off mid-stream by a connection failure — the content above may be incomplete. Continue EXACTLY where you left off; do not repeat any content you already produced, do not re-introduce or summarize.",
        display: false,
        timestamp: Date.now(),
    };
}
function createDegenerateRecoveryNudge() {
    return {
        role: "custom",
        customType: "loop:degenerate-output-recovery",
        provenance: "engine-note",
        content: "Your previous reply degenerated into repetition and was cut off. Do NOT repeat or continue that text. " +
            "Take a DIFFERENT approach: state your next concrete step or conclusion in one short sentence, then proceed normally.",
        display: false,
        timestamp: Date.now(),
    };
}
function createReasoningCutContinueNudge() {
    return {
        role: "custom",
        customType: "loop:truncated-output-continue",
        provenance: "engine-note",
        content: "Your previous response hit the output-token limit before producing any visible text (the budget went to internal reasoning). " +
            "Do NOT restart the long reasoning: state your current best answer or next concrete action directly as visible text (or make the required tool call) now.",
        display: false,
        timestamp: Date.now(),
    };
}
function createContinueNudge() {
    return {
        role: "custom",
        customType: "loop:truncated-output-continue",
        provenance: "engine-note",
        content: "Your previous response was cut off because it hit the output-token limit. Continue EXACTLY where you left off — do not repeat any content you already produced, do not re-introduce or summarize.",
        display: false,
        timestamp: Date.now(),
    };
}
async function createLoopState(context, config, newMessages) {
    return {
        context,
        config,
        newMessages,
        firstTurn: true,
        pendingMessages: (await config.getSteeringMessages?.()) || [],
        hasMoreToolCalls: true,
        truncatedOutputContinues: 0,
        staticReasoningCutRecoveries: context.messages.filter((m) => m.role === "assistant" && m.staticReasoningCut === true).length,
        degenerateContinues: 0,
        malformedToolUseRetries: 0,
        thinkingOnlyRetries: 0,
        midstreamPartialContinues: 0,
    };
}
async function runLoop(state, signal, emit, streamFn, runtime, trace) {
    while (true) {
        if (!state.hasMoreToolCalls && state.pendingMessages.length === 0) {
            const followUpMessages = (await state.config.getFollowUpMessages?.()) || [];
            if (followUpMessages.length === 0) {
                if ((state.config.pendingInjectionCount?.() ?? 0) > 0) {
                    state.pendingMessages = (await state.config.getSteeringMessages?.()) || [];
                    if (state.pendingMessages.length > 0) {
                        trace?.({ kind: "continue", reason: "steer_injected" });
                    }
                    continue;
                }
                trace?.({ kind: "terminal", reason: "completed" });
                break;
            }
            state.pendingMessages = followUpMessages;
            trace?.({ kind: "continue", reason: "follow_up_injected" });
        }
        const outcome = await runSingleTurn(state, signal, emit, streamFn, runtime, trace);
        if (outcome.kind === "terminal") {
            trace?.({ kind: "terminal", reason: outcome.reason });
            return;
        }
        if (outcome.recovered) {
            trace?.({ kind: "continue", reason: outcome.recovered });
        }
        else if (state.pendingMessages.length > 0) {
            trace?.({ kind: "continue", reason: "steer_injected" });
        }
        else if (state.hasMoreToolCalls) {
            trace?.({ kind: "continue", reason: "next_turn" });
        }
    }
    await emit({ type: "agent_end", messages: state.newMessages });
}
const MAX_STATIC_REASONING_CUT_RECOVERIES = 2;
async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
    if (signal?.aborted) {
        await emit({ type: "agent_end", messages: state.newMessages });
        return { kind: "terminal", reason: "aborted_before_stream" };
    }
    if (state.config.shouldHaltBeforeTurn?.() === true) {
        await emit({ type: "agent_end", messages: state.newMessages });
        return { kind: "terminal", reason: "stop_requested" };
    }
    if (!state.firstTurn) {
        await emit({ type: "turn_start" });
        if (state.config.shouldHaltBeforeTurn?.() === true) {
            await emit({ type: "agent_end", messages: state.newMessages });
            return { kind: "terminal", reason: "stop_requested" };
        }
    }
    else {
        state.firstTurn = false;
    }
    const turnController = new AbortController();
    const onRunAbort = () => turnController.abort();
    if (signal !== undefined) {
        if (signal.aborted)
            turnController.abort();
        else
            signal.addEventListener("abort", onRunAbort, { once: true });
    }
    let seatLive = false;
    const publishSeat = () => {
        seatLive = true;
        state.config.publishTurnInterruptSeat?.(turnController);
    };
    const retractSeat = () => {
        if (!seatLive)
            return;
        seatLive = false;
        state.config.publishTurnInterruptSeat?.(undefined);
    };
    try {
        return await runTurnPhases(state, signal, turnController, publishSeat, retractSeat, emit, streamFn, runtime, trace);
    }
    finally {
        retractSeat();
        signal?.removeEventListener("abort", onRunAbort);
    }
}
async function runTurnPhases(state, signal, turnController, publishSeat, retractSeat, emit, streamFn, runtime, trace) {
    publishSeat();
    if (state.config.recheckImmediateInjections) {
        state.pendingMessages = await state.config.recheckImmediateInjections(state.pendingMessages);
    }
    if (state.pendingMessages.length > 0) {
        for (const message of state.pendingMessages) {
            await emit({ type: "message_start", message });
            await emit({ type: "message_end", message });
            state.context.messages.push(message);
            state.newMessages.push(message);
        }
    }
    let executor = new StreamToolExecutor(state.context, state.config, turnController.signal, emit);
    const staticReasoningCutDowngrade = state.config.recovery?.truncatedOutput !== undefined &&
        state.staticReasoningCutRecoveries < MAX_STATIC_REASONING_CUT_RECOVERIES;
    const ptl = state.config.recovery?.promptTooLong;
    const ptlDetect = ptl?.detect ?? defaultDetectPromptTooLong;
    const withholdEnabled = ptl !== undefined && (ptl.withholdErrorEvents ?? true);
    let withhold = withholdEnabled ? createPtlWithholdBuffer(emit, ptlDetect) : undefined;
    let message;
    let cutAtFinalSettle = false;
    try {
        ({ message, cutAtSettle: cutAtFinalSettle } = await streamAssistantResponse(state.context, state.config, turnController.signal, withhold?.sink ?? emit, streamFn, runtime, executor, staticReasoningCutDowngrade));
    }
    finally {
        await executor.settle();
    }
    raiseFatalCancellation(executor);
    let interruptedInRecovery = false;
    {
        if (ptl) {
            const detect = ptlDetect;
            const maxRetries = ptl.maxRetries ?? 2;
            let attempt = 0;
            while (message.stopReason === "error" &&
                !signal?.aborted &&
                detect(message) &&
                isBlankFailureContent(message) &&
                executor.admittedCount === 0 &&
                attempt < maxRetries) {
                attempt++;
                if (state.context.messages[state.context.messages.length - 1] === message) {
                    state.context.messages.pop();
                }
                let replaced;
                try {
                    replaced = await ptl.recover(state.context.messages, attempt, turnController.signal);
                }
                catch {
                    replaced = undefined;
                }
                if (!replaced) {
                    state.context.messages.push(message);
                    interruptedInRecovery = turnController.signal.aborted && !signal?.aborted;
                    break;
                }
                withhold?.discard();
                withhold = withholdEnabled ? createPtlWithholdBuffer(emit, ptlDetect) : undefined;
                if (Array.isArray(replaced)) {
                    state.context.messages = replaced;
                }
                else {
                    adoptRebuiltContext(state.context, replaced);
                }
                if (turnController.signal.aborted && !signal?.aborted) {
                    state.context.messages.push(message);
                    interruptedInRecovery = true;
                    break;
                }
                trace?.({ kind: "continue", reason: "reactive_compact_retry" });
                executor = new StreamToolExecutor(state.context, state.config, turnController.signal, emit);
                try {
                    ({ message, cutAtSettle: cutAtFinalSettle } = await streamAssistantResponse(state.context, state.config, turnController.signal, withhold?.sink ?? emit, streamFn, runtime, executor, staticReasoningCutDowngrade));
                }
                finally {
                    await executor.settle();
                }
                raiseFatalCancellation(executor);
            }
        }
    }
    const unrecoveredPtlFailureUnderInterrupt = ptl !== undefined &&
        message.stopReason === "error" &&
        ptlDetect(message) &&
        isBlankFailureContent(message) &&
        executor.admittedCount === 0;
    const cutAtPtlLaneExit = turnController.signal.aborted;
    const interruptOwnsTurn = ((message.stopReason === "aborted" && cutAtFinalSettle) || (unrecoveredPtlFailureUnderInterrupt && cutAtPtlLaneExit) || interruptedInRecovery) &&
        !signal?.aborted;
    if (interruptOwnsTurn)
        state.config.noteTurnCutConsumed?.(turnController);
    if (interruptOwnsTurn && message.stopReason === "error")
        withhold?.discard();
    await withhold?.flush();
    state.newMessages.push(message);
    if (message.staticReasoningCut === true)
        state.staticReasoningCutRecoveries++;
    if (interruptOwnsTurn) {
        retractSeat();
        return await settleInterruptedTurn(state, message, executor, [], signal, emit);
    }
    {
        const degen = state.config.recovery?.degenerateOutput;
        if (degen &&
            message.stopReason === "error" &&
            !signal?.aborted &&
            degen.detect(message) &&
            executor.admittedCount === 0 &&
            message.content.every((b) => b.type !== "toolCall") &&
            state.degenerateContinues < (degen.maxContinues ?? 2)) {
            retractSeat();
            state.degenerateContinues++;
            await emit({ type: "turn_end", message, toolResults: [] });
            const drained = (await state.config.getSteeringMessages?.()) || [];
            state.pendingMessages = drained.length > 0 ? drained : [createDegenerateRecoveryNudge()];
            return { kind: "ran", recovered: "degenerate_output_recovery" };
        }
        if (message.stopReason !== "error")
            state.degenerateContinues = 0;
    }
    if (message.stopReason === "error" || message.stopReason === "aborted") {
        retractSeat();
        const errorFinalResults = await harvestExecutorOnErrorFinal(executor, state, message, turnController.signal, emit);
        await emit({ type: "turn_end", message, toolResults: errorFinalResults });
        await emit({ type: "agent_end", messages: state.newMessages });
        return { kind: "terminal", reason: "assistant_error" };
    }
    const toolCalls = message.content.filter((c) => c.type === "toolCall");
    const toolResults = [];
    state.hasMoreToolCalls = false;
    if (toolCalls.length > 0) {
        const executedToolBatch = await executeToolCalls(state.context, message, state.config, turnController.signal, emit, executor);
        toolResults.push(...executedToolBatch.messages);
        state.hasMoreToolCalls = !executedToolBatch.terminate;
        for (const result of toolResults) {
            state.context.messages.push(result);
            state.newMessages.push(result);
        }
    }
    else {
        for (const orphan of executor.remaining()) {
            executor.take(orphan.toolCall.id);
            await closeOrphanedStreamEntry(orphan, emit);
        }
    }
    const cutAtBatchSettle = turnController.signal.aborted;
    retractSeat();
    if (toolCalls.length > 0 && cutAtBatchSettle && !signal?.aborted) {
        state.config.noteTurnCutConsumed?.(turnController);
        return await settleInterruptedTurn(state, message, executor, toolResults, signal, emit);
    }
    await emit({ type: "turn_end", message, toolResults });
    const nextTurnContext = {
        message,
        toolResults,
        context: state.context,
        newMessages: state.newMessages,
    };
    const nextTurnSnapshot = await state.config.prepareNextTurn?.(nextTurnContext);
    adoptNextTurnSnapshot(state, nextTurnSnapshot);
    if (await state.config.shouldStopAfterTurn?.({
        message,
        toolResults,
        context: state.context,
        newMessages: state.newMessages,
    })) {
        await emit({ type: "agent_end", messages: state.newMessages });
        return { kind: "terminal", reason: "stop_requested" };
    }
    state.pendingMessages = (await state.config.getSteeringMessages?.()) || [];
    const truncated = state.config.recovery?.truncatedOutput;
    if (message.stopReason === "length") {
        if (truncated && toolCalls.length === 0 && state.pendingMessages.length === 0) {
            if (state.truncatedOutputContinues < (truncated.maxContinues ?? 3)) {
                state.truncatedOutputContinues++;
                state.midstreamPartialContinues = 0;
                const noVisibleText = !message.content.some((c) => c.type === "text" && c.text.trim() !== "");
                state.pendingMessages = [...state.pendingMessages, noVisibleText ? createReasoningCutContinueNudge() : createContinueNudge()];
                return { kind: "ran", recovered: "max_output_tokens_recovery" };
            }
            await emit({ type: "agent_end", messages: state.newMessages });
            return { kind: "terminal", reason: "truncated_output_exhausted" };
        }
    }
    else {
        state.truncatedOutputContinues = 0;
    }
    if (message.stopReason === "stop" && message.partialFinalized === true) {
        if (toolCalls.length === 0 &&
            state.pendingMessages.length === 0 &&
            state.midstreamPartialContinues < 3) {
            state.midstreamPartialContinues++;
            state.pendingMessages = [createMidstreamPartialContinueNudge()];
            return { kind: "ran", recovered: "midstream_partial_recovery" };
        }
    }
    else {
        state.midstreamPartialContinues = 0;
    }
    const malformed = state.config.recovery?.malformedToolUse;
    if (malformed && message.stopReason === "toolUse" && toolCalls.length === 0 && state.pendingMessages.length === 0) {
        if (state.malformedToolUseRetries < (malformed.maxRetries ?? 1)) {
            state.malformedToolUseRetries++;
            state.pendingMessages = [createMalformedToolUseNudge()];
            return { kind: "ran", recovered: "malformed_tool_use_retry" };
        }
        await emit({ type: "agent_end", messages: state.newMessages });
        return { kind: "terminal", reason: "malformed_tool_use_exhausted" };
    }
    const thinkingOnly = state.config.recovery?.thinkingOnly;
    if (thinkingOnly &&
        message.stopReason === "stop" &&
        toolCalls.length === 0 &&
        state.pendingMessages.length === 0 &&
        state.thinkingOnlyRetries < (thinkingOnly.maxRetries ?? 1) &&
        message.content.some((c) => c.type === "thinking") &&
        !message.content.some((c) => c.type === "text" && c.text.trim() !== "")) {
        state.thinkingOnlyRetries++;
        state.pendingMessages = [createThinkingOnlyNudge()];
        return { kind: "ran", recovered: "thinking_only_retry" };
    }
    return { kind: "ran" };
}
function adoptRebuiltContext(context, adopted) {
    context.messages = adopted.messages;
    if (adopted.systemPrompt === undefined)
        return;
    context.systemPrompt = adopted.systemPrompt;
    if (adopted.systemBlocks !== undefined) {
        context.systemBlocks = adopted.systemBlocks;
    }
    else {
        delete context.systemBlocks;
    }
}
function adoptNextTurnSnapshot(state, nextTurnSnapshot) {
    if (!nextTurnSnapshot)
        return;
    state.context = nextTurnSnapshot.context ?? state.context;
    state.config = Object.assign({}, state.config, {
        model: nextTurnSnapshot.model ?? state.config.model,
        reasoning: nextTurnSnapshot.thinkingLevel === undefined
            ? state.config.reasoning
            : nextTurnSnapshot.thinkingLevel,
    });
}
const TURN_INTERRUPTED_TOOL_TEXT = "[INTERRUPTED] The user interrupted this turn before this tool call started. It was never executed and " +
    "had no side effects. Address the user's interjection first; re-issue the call afterwards if it is still needed.";
async function settleInterruptedTurn(state, message, executor, committedResults, signal, emit) {
    const toolResults = [...committedResults];
    toolResults.push(...(await harvestExecutorOnErrorFinal(executor, state, message, signal, emit)));
    const turnCalls = message.content.filter((c) => c.type === "toolCall");
    const priorResults = state.context.messages.slice(state.context.messages.lastIndexOf(message) + 1).filter((m) => m.role === "toolResult");
    const answered = new Set([...priorResults, ...toolResults].map((r) => r.toolCallId));
    for (const call of turnCalls) {
        if (answered.has(call.id))
            continue;
        await emit({
            type: "tool_execution_start",
            toolCallId: call.id,
            toolName: call.name,
            args: call.arguments,
        });
        const finalized = {
            toolCall: call,
            result: {
                content: [{ type: "text", text: TURN_INTERRUPTED_TOOL_TEXT }],
                details: { errorKind: "interrupted_never_started" },
            },
            isError: true,
        };
        await emit({
            type: "tool_execution_end",
            toolCallId: call.id,
            toolName: call.name,
            result: finalized.result,
            isError: true,
            notExecuted: true,
        });
        const toolResultMessage = createToolResultMessage(finalized);
        await emitToolResultMessage(toolResultMessage, emit);
        state.context.messages.push(toolResultMessage);
        state.newMessages.push(toolResultMessage);
        toolResults.push(toolResultMessage);
    }
    if (turnCalls.length === 0 &&
        isBlankFailureContent(message) &&
        state.context.messages[state.context.messages.length - 1] === message) {
        state.context.messages.pop();
        if (state.newMessages[state.newMessages.length - 1] === message)
            state.newMessages.pop();
    }
    const marker = {
        role: "user",
        content: [
            {
                type: "text",
                text: turnCalls.length > 0 ? INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER : INTERRUPTED_BY_USER_MARKER,
            },
        ],
        provenance: "engine-note",
        timestamp: Date.now(),
    };
    await emit({ type: "message_start", message: marker });
    await emit({ type: "message_end", message: marker });
    state.context.messages.push(marker);
    state.newMessages.push(marker);
    state.hasMoreToolCalls = false;
    await emit({ type: "turn_end", message, toolResults });
    if (signal?.aborted) {
        await emit({ type: "agent_end", messages: state.newMessages });
        return { kind: "terminal", reason: "aborted_before_stream" };
    }
    const nextTurnSnapshot = await state.config.prepareNextTurn?.({
        message,
        toolResults,
        context: state.context,
        newMessages: state.newMessages,
    });
    adoptNextTurnSnapshot(state, nextTurnSnapshot);
    if (await state.config.shouldStopAfterTurn?.({
        message,
        toolResults,
        context: state.context,
        newMessages: state.newMessages,
    })) {
        await emit({ type: "agent_end", messages: state.newMessages });
        return { kind: "terminal", reason: "stop_requested" };
    }
    state.pendingMessages = (await state.config.getSteeringMessages?.()) || [];
    return { kind: "ran" };
}
async function streamAssistantResponse(context, config, signal, emit, streamFn, runtime, executor, staticReasoningCutDowngrade) {
    let messages = context.messages;
    if (config.transformContext) {
        const transformed = await config.transformContext(messages, signal);
        if (Array.isArray(transformed)) {
            messages = transformed;
        }
        else {
            if (transformed.adoptedContext !== undefined) {
                adoptRebuiltContext(context, transformed.adoptedContext);
            }
            messages = transformed.messages;
        }
    }
    const llmMessages = await config.convertToLlm(messages);
    const llmContext = {
        systemPrompt: context.systemPrompt,
        ...(context.systemBlocks ? { systemBlocks: context.systemBlocks } : {}),
        messages: llmMessages,
        tools: context.tools,
    };
    const streamFunction = resolveAgentCoreStreamFn(runtime, streamFn);
    const resolvedApiKey = (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
    const perCallMaxTokens = config.maxTokensPerCall?.();
    const perCallStallTimeouts = config.stallTimeoutsPerCall?.();
    if (signal?.aborted) {
        const abortedBeforeDispatch = {
            role: "assistant",
            content: [{ type: "text", text: "" }],
            api: config.model.api,
            provider: config.model.provider,
            model: config.model.id,
            stopReason: "aborted",
            timestamp: Date.now(),
            usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
            usageMissing: true,
        };
        context.messages.push(abortedBeforeDispatch);
        await emit({ type: "message_start", message: { ...abortedBeforeDispatch } });
        await emit({ type: "message_end", message: abortedBeforeDispatch });
        return { message: abortedBeforeDispatch, cutAtSettle: true };
    }
    const response = await streamFunction(config.model, llmContext, {
        ...config,
        ...(perCallMaxTokens !== undefined ? { maxTokens: perCallMaxTokens, maxTokensDynamic: true } : {}),
        ...(perCallStallTimeouts !== undefined ? { stallTimeouts: perCallStallTimeouts } : {}),
        ...(staticReasoningCutDowngrade === true ? { staticReasoningCutDowngrade: true } : {}),
        apiKey: resolvedApiKey,
        signal,
    });
    let partialMessage = null;
    let addedPartial = false;
    for await (const event of response) {
        switch (event.type) {
            case "start": {
                const message = event.partial;
                partialMessage = message;
                context.messages.push(message);
                addedPartial = true;
                await emit({ type: "message_start", message: { ...message } });
                break;
            }
            case "text_start":
            case "text_delta":
            case "text_end":
            case "thinking_start":
            case "thinking_delta":
            case "thinking_end":
            case "toolcall_start":
            case "toolcall_delta":
            case "toolcall_end":
                if (partialMessage) {
                    const message = resolveAssistantMessageUpdate(event, partialMessage);
                    partialMessage = message;
                    context.messages[context.messages.length - 1] = message;
                    await emit({
                        type: "message_update",
                        assistantMessageEvent: event,
                        message: { ...message },
                    });
                }
                if (event.type === "toolcall_end" && executor) {
                    executor.maybeAdmit(event.toolCall, partialMessage);
                }
                break;
            case "done":
            case "error": {
                const finalMessage = await response.result();
                const cutAtSettle = signal?.aborted === true;
                if (executor && (finalMessage.stopReason === "error" || finalMessage.stopReason === "aborted")) {
                    mergeBackAdmittedCalls(finalMessage, executor);
                }
                if (addedPartial) {
                    context.messages[context.messages.length - 1] = finalMessage;
                }
                else {
                    context.messages.push(finalMessage);
                }
                if (!addedPartial) {
                    await emit({ type: "message_start", message: { ...finalMessage } });
                }
                await emit({ type: "message_end", message: finalMessage });
                return { message: finalMessage, cutAtSettle };
            }
        }
    }
    const finalMessage = await response.result();
    const cutAtSettle = signal?.aborted === true;
    if (executor && (finalMessage.stopReason === "error" || finalMessage.stopReason === "aborted")) {
        mergeBackAdmittedCalls(finalMessage, executor);
    }
    if (addedPartial) {
        context.messages[context.messages.length - 1] = finalMessage;
    }
    else {
        context.messages.push(finalMessage);
        await emit({ type: "message_start", message: { ...finalMessage } });
    }
    await emit({ type: "message_end", message: finalMessage });
    return { message: finalMessage, cutAtSettle };
}
async function executeToolCalls(currentContext, assistantMessage, config, signal, emit, executor) {
    const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
    if (config.toolExecution === "sequential") {
        return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);
    }
    return executeToolCallsPartitioned(currentContext, assistantMessage, toolCalls, config, signal, emit, executor);
}
async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit) {
    const finalizedCalls = [];
    const messages = [];
    for (let i = 0; i < toolCalls.length; i++) {
        const toolCall = toolCalls[i];
        await emit({
            type: "tool_execution_start",
            toolCallId: toolCall.id,
            toolName: toolCall.name,
            args: toolCall.arguments,
        });
        const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
        let finalized;
        let halt;
        if (preparation.kind === "immediate") {
            finalized = {
                toolCall,
                result: preparation.result,
                isError: preparation.isError,
            };
            halt = preparation.halt;
        }
        else {
            finalized = await executePreparedWithDisclosure(currentContext, assistantMessage, preparation, config, signal, emit);
        }
        await emitToolExecutionEnd(finalized, emit);
        const toolResultMessage = createToolResultMessage(finalized);
        await emitToolResultMessage(toolResultMessage, emit);
        finalizedCalls.push(finalized);
        messages.push(toolResultMessage);
        if (halt !== undefined) {
            for (const remaining of toolCalls.slice(i + 1)) {
                if (signal?.aborted)
                    break;
                const settled = await settleHaltedToolCall(remaining, halt, emit);
                finalizedCalls.push(settled.finalized);
                messages.push(settled.message);
            }
            return { messages, terminate: true };
        }
        if (signal?.aborted) {
            break;
        }
    }
    return {
        messages,
        terminate: shouldTerminateToolBatch(finalizedCalls),
    };
}
async function settleHaltedToolCall(toolCall, halt, emit) {
    await emit({
        type: "tool_execution_start",
        toolCallId: toolCall.id,
        toolName: toolCall.name,
        args: toolCall.arguments,
    });
    const finalized = {
        toolCall,
        result: {
            content: [{ type: "text", text: truncateError(halt.reason) }],
            details: halt.details !== undefined ? { ...halt.details } : {},
        },
        isError: true,
    };
    await emit({
        type: "tool_execution_end",
        toolCallId: finalized.toolCall.id,
        toolName: finalized.toolCall.name,
        result: finalized.result,
        isError: true,
        notExecuted: true,
    });
    const message = createToolResultMessage(finalized);
    await emitToolResultMessage(message, emit);
    return { finalized, message };
}
function isCallConcurrencySafe(tool, args) {
    if (!tool)
        return false;
    if (tool.executionMode === "parallel")
        return true;
    if (!tool.isConcurrencySafe)
        return false;
    try {
        return tool.isConcurrencySafe(args) === true;
    }
    catch {
        return false;
    }
}
function isBatchSafeToolCall(tool, toolCall) {
    if (!tool)
        return false;
    let args;
    try {
        args = validateToolArguments(tool, prepareToolCallArguments(tool, toolCall));
    }
    catch {
        return false;
    }
    return isCallConcurrencySafe(tool, args);
}
function partitionToolCalls(toolCalls, tools) {
    const batches = [];
    for (const tc of toolCalls) {
        const safe = isBatchSafeToolCall(findToolByName(tools, tc.name), tc);
        const last = batches[batches.length - 1];
        if (safe && last?.safe) {
            last.calls.push(tc);
        }
        else {
            batches.push({ safe, calls: [tc] });
        }
    }
    return batches;
}
function resolveToolConcurrency(configured) {
    if (configured === undefined)
        return 10;
    return Number.isFinite(configured) ? Math.max(1, Math.floor(configured)) : 1;
}
async function runCapped(thunks, limit) {
    const results = new Array(thunks.length);
    const failures = new Array(thunks.length);
    let next = 0;
    const width = Number.isFinite(limit) ? Math.max(1, Math.min(limit, thunks.length)) : 1;
    const workers = Array.from({ length: width }, async () => {
        while (next < thunks.length) {
            const i = next++;
            try {
                results[i] = await thunks[i]();
            }
            catch (error) {
                failures[i] = { error };
            }
        }
    });
    await Promise.all(workers);
    const firstFailure = failures.find((failure) => failure !== undefined);
    if (firstFailure !== undefined) {
        throw firstFailure.error;
    }
    return results;
}
async function executeToolCallsPartitioned(currentContext, assistantMessage, toolCalls, config, signal, emit, executor) {
    const cap = resolveToolConcurrency(config.maxToolConcurrency);
    const allFinalized = [];
    const messages = [];
    const remainingCalls = [];
    let halt;
    for (const toolCall of toolCalls) {
        const held = executor?.take(toolCall.id);
        if (!held) {
            remainingCalls.push(toolCall);
            continue;
        }
        const finalized = await finalizeStreamEntry(currentContext, assistantMessage, held, config, signal);
        await emitToolExecutionEnd(finalized, emit);
        const toolResultMessage = createToolResultMessage(finalized);
        await emitToolResultMessage(toolResultMessage, emit);
        allFinalized.push(finalized);
        messages.push(toolResultMessage);
        if (held.immediate?.halt !== undefined)
            halt ??= held.immediate.halt;
    }
    if (executor) {
        for (const orphan of executor.remaining()) {
            executor.take(orphan.toolCall.id);
            await closeOrphanedStreamEntry(orphan, emit);
        }
    }
    if (halt === undefined) {
        const batches = partitionToolCalls(remainingCalls, currentContext.tools);
        outer: for (const batch of batches) {
            if (signal?.aborted)
                break;
            const entries = [];
            for (const toolCall of batch.calls) {
                await emit({
                    type: "tool_execution_start",
                    toolCallId: toolCall.id,
                    toolName: toolCall.name,
                    args: toolCall.arguments,
                });
                const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
                if (preparation.kind === "immediate") {
                    const finalized = { toolCall, result: preparation.result, isError: preparation.isError };
                    await emitToolExecutionEnd(finalized, emit);
                    entries.push({ finalized, toolCall });
                    if (preparation.halt !== undefined) {
                        halt = preparation.halt;
                        break;
                    }
                }
                else {
                    entries.push({ prepared: preparation, toolCall });
                }
                if (signal?.aborted) {
                    await settleBatch(entries, true);
                    break outer;
                }
            }
            await settleBatch(entries, batch.safe);
            if (halt !== undefined)
                break;
        }
    }
    if (halt !== undefined && !signal?.aborted) {
        const settledIds = new Set(messages.map((m) => m.toolCallId));
        for (const toolCall of toolCalls) {
            if (settledIds.has(toolCall.id))
                continue;
            if (signal?.aborted)
                break;
            const settled = await settleHaltedToolCall(toolCall, halt, emit);
            allFinalized.push(settled.finalized);
            messages.push(settled.message);
        }
        return { messages, terminate: true };
    }
    return { messages, terminate: shouldTerminateToolBatch(allFinalized) };
    async function settleBatch(entries, concurrent) {
        const runOne = (entry) => async () => {
            const prepared = entry.prepared;
            if (!prepared) {
                throw new Error(`tool call ${entry.toolCall.id} reached execution without a preparation`);
            }
            const finalized = await executePreparedWithDisclosure(currentContext, assistantMessage, prepared, config, signal, emit);
            await emitToolExecutionEnd(finalized, emit);
            return finalized;
        };
        const preparedEntries = entries.filter((e) => e.prepared);
        let executedResults;
        if (concurrent && preparedEntries.length > 1) {
            executedResults = await runCapped(preparedEntries.map((e) => runOne(e)), cap);
        }
        else {
            executedResults = [];
            for (const e of preparedEntries) {
                executedResults.push(await runOne(e)());
            }
        }
        let idx = 0;
        for (const e of entries) {
            const finalized = e.finalized ?? executedResults[idx++];
            if (!finalized)
                continue;
            allFinalized.push(finalized);
            const toolResultMessage = createToolResultMessage(finalized);
            await emitToolResultMessage(toolResultMessage, emit);
            messages.push(toolResultMessage);
        }
    }
}
class StreamToolExecutor {
    context;
    config;
    signal;
    emit;
    entries = new Map();
    admittedOrder = [];
    barrier = false;
    inFlight = 0;
    fatalCancellation;
    constructor(context, config, signal, emit) {
        this.context = context;
        this.config = config;
        this.signal = signal;
        this.emit = emit;
    }
    get admittedCount() {
        return this.admittedOrder.length;
    }
    admittedToolCalls() {
        return this.admittedOrder.map((id) => this.entries.get(id)?.toolCall).filter((tc) => tc !== undefined);
    }
    maybeAdmit(toolCall, partialAssistant) {
        if (this.barrier)
            return;
        if (this.config.streamingToolExecution !== true ||
            this.config.toolExecution === "sequential" ||
            this.signal?.aborted ||
            partialAssistant === null ||
            !isBatchSafeToolCall(findToolByName(this.context.tools, toolCall.name), toolCall) ||
            this.inFlight >= resolveToolConcurrency(this.config.maxToolConcurrency) ||
            this.entries.has(toolCall.id)) {
            this.barrier = true;
            return;
        }
        const entry = { toolCall, promise: Promise.resolve() };
        this.entries.set(toolCall.id, entry);
        this.admittedOrder.push(toolCall.id);
        entry.promise = this.runOne(entry, partialAssistant);
    }
    async runOne(entry, partialAssistant) {
        this.inFlight++;
        try {
            await this.emit({
                type: "tool_execution_start",
                toolCallId: entry.toolCall.id,
                toolName: entry.toolCall.name,
                args: entry.toolCall.arguments,
            });
            const preparation = await prepareToolCall(this.context, partialAssistant, entry.toolCall, this.config, this.signal);
            if (preparation.kind === "immediate") {
                entry.immediate = preparation;
                if (preparation.halt !== undefined)
                    this.barrier = true;
                return;
            }
            entry.prepared = preparation;
            entry.executed = await executePreparedToolCall(preparation, this.signal, this.emit, this.config.abortResultDetails, (error) => this.retainCancellation(error));
        }
        catch (error) {
            if (isAbortSemanticsError(error))
                this.retainCancellation(error);
            entry.immediate = {
                kind: "immediate",
                result: createErrorToolResult(thrownText(error), error),
                isError: true,
            };
        }
        finally {
            this.inFlight--;
        }
    }
    async settle() {
        this.barrier = true;
        await Promise.all([...this.entries.values()].map((e) => e.promise));
    }
    retainCancellation(error) {
        this.fatalCancellation ??= error;
        this.barrier = true;
    }
    takeFatalCancellation() {
        const cancellation = this.fatalCancellation;
        this.fatalCancellation = undefined;
        return cancellation;
    }
    take(id) {
        const entry = this.entries.get(id);
        if (entry)
            this.entries.delete(id);
        return entry;
    }
    remaining() {
        return this.admittedOrder
            .map((id) => this.entries.get(id))
            .filter((e) => e !== undefined);
    }
}
async function finalizeStreamEntry(currentContext, assistantMessage, entry, config, signal) {
    if (entry.prepared && entry.executed) {
        return finalizeExecutedToolCall(currentContext, assistantMessage, entry.prepared, entry.executed, config, signal);
    }
    const immediate = entry.immediate ?? {
        kind: "immediate",
        result: createErrorToolResult("in-stream execution produced no outcome"),
        isError: true,
    };
    return { toolCall: entry.toolCall, result: immediate.result, isError: immediate.isError };
}
async function closeOrphanedStreamEntry(entry, emit) {
    await emit({
        type: "tool_execution_end",
        toolCallId: entry.toolCall.id,
        toolName: entry.toolCall.name,
        result: createErrorToolResult("in-stream result dropped: the call is missing from the final assistant content (provider contract breach)"),
        isError: true,
    });
}
async function harvestExecutorOnErrorFinal(executor, state, finalMessage, signal, emit) {
    const held = executor.remaining();
    if (held.length === 0)
        return [];
    const results = [];
    const persistedCallIds = new Set(finalMessage.content.filter((c) => c.type === "toolCall").map((c) => c.id));
    for (const entry of held) {
        executor.take(entry.toolCall.id);
        if (!persistedCallIds.has(entry.toolCall.id)) {
            await closeOrphanedStreamEntry(entry, emit);
            continue;
        }
        const finalized = await finalizeStreamEntry(state.context, finalMessage, entry, state.config, signal);
        await emitToolExecutionEnd(finalized, emit);
        const toolResultMessage = createToolResultMessage(finalized);
        await emitToolResultMessage(toolResultMessage, emit);
        state.context.messages.push(toolResultMessage);
        state.newMessages.push(toolResultMessage);
        results.push(toolResultMessage);
    }
    return results;
}
function mergeBackAdmittedCalls(finalMessage, executor) {
    const admitted = executor.admittedToolCalls();
    if (admitted.length === 0)
        return;
    const present = new Set(finalMessage.content.filter((c) => c.type === "toolCall").map((c) => c.id));
    const missing = admitted.filter((tc) => !present.has(tc.id));
    if (missing.length === 0)
        return;
    finalMessage.content = [...finalMessage.content, ...missing];
}
function shouldTerminateToolBatch(finalizedCalls) {
    return (finalizedCalls.length > 0 &&
        finalizedCalls.every((finalized) => finalized.result.terminate === true));
}
function prepareToolCallArguments(tool, toolCall) {
    if (!tool.prepareArguments) {
        return toolCall;
    }
    const preparedArguments = tool.prepareArguments(toolCall.arguments);
    if (preparedArguments === toolCall.arguments) {
        return toolCall;
    }
    return {
        ...toolCall,
        arguments: preparedArguments,
    };
}
export const ROSTER_LISTING_MAX = 25;
export const ROSTER_SEARCH_HINT_NAME = "ToolSearch";
function formatRosterRecovery(availableTools) {
    if (availableTools.length === 0)
        return "";
    const shown = availableTools.slice(0, ROSTER_LISTING_MAX);
    const withheld = availableTools.length - shown.length;
    const listing = withheld > 0 ? `${shown.join(", ")} … and ${withheld} more` : shown.join(", ");
    const hint = availableTools.includes(ROSTER_SEARCH_HINT_NAME)
        ? `. Use ${ROSTER_SEARCH_HINT_NAME} to look up a tool by name.`
        : "";
    return ` Available tools: ${listing}${hint}`;
}
export const TOOL_VALIDATION_REFUSED_CODE = "tool.validation_refused";
export const TOOL_INPUT_VALIDATION_TIMEOUT_MS = 10_000;
async function runInputValidation(tool, args, toolCallId, signal, onFault) {
    if (tool.validateInput === undefined)
        return undefined;
    try {
        const verdict = await boundedValidatorCall(() => tool.validateInput(args, signal !== undefined ? { toolCallId, signal } : { toolCallId }), signal);
        if (verdict !== null && typeof verdict === "object" && verdict.ok === false && typeof verdict.message === "string") {
            return typeof verdict.code === "string" ? { message: verdict.message, code: verdict.code } : { message: verdict.message };
        }
        return undefined;
    }
    catch (error) {
        if (signal?.aborted === true)
            return undefined;
        try {
            const ret = onFault?.({ toolName: tool.name, toolCallId, error });
            if (ret !== null && typeof ret === "object" && typeof ret.then === "function") {
                ret.then(undefined, () => undefined);
            }
        }
        catch {
        }
        return undefined;
    }
}
function boundedValidatorCall(call, signal) {
    return new Promise((resolve, reject) => {
        let settled = false;
        const finish = (fn) => {
            if (settled)
                return;
            settled = true;
            clearTimeout(timer);
            signal?.removeEventListener("abort", onAbort);
            fn();
        };
        const onAbort = () => finish(() => reject(Object.assign(new Error("tool input pre-validation aborted"), { name: "AbortError" })));
        const timer = setTimeout(() => finish(() => reject(new Error(`tool input pre-validation did not answer within ${TOOL_INPUT_VALIDATION_TIMEOUT_MS}ms`))), TOOL_INPUT_VALIDATION_TIMEOUT_MS);
        if (signal?.aborted === true) {
            onAbort();
            return;
        }
        signal?.addEventListener("abort", onAbort, { once: true });
        let out;
        try {
            out = call();
        }
        catch (err) {
            finish(() => reject(err));
            return;
        }
        Promise.resolve(out).then((v) => finish(() => resolve(v)), (err) => finish(() => reject(err)));
    });
}
function sameToolArgs(a, b) {
    if (a === b)
        return true;
    const canon = (v) => JSON.stringify(v, (_k, x) => (x !== null && typeof x === "object" && !Array.isArray(x) ? Object.fromEntries(Object.keys(x).sort().map((k) => [k, x[k]])) : x));
    return canon(a) === canon(b);
}
function validationRefusedOutcome(tool, refusal) {
    return {
        kind: "immediate",
        result: createErrorToolResult(refusal.message, {
            details: { code: TOOL_VALIDATION_REFUSED_CODE, toolName: tool.name, ...(refusal.code !== undefined ? { reason: refusal.code } : {}) },
        }),
        isError: true,
    };
}
async function prepareToolCall(currentContext, assistantMessage, toolCall, config, signal) {
    const tool = findToolByName(currentContext.tools, toolCall.name);
    if (!tool) {
        const availableTools = (currentContext.tools ?? []).map((t) => t.name);
        return {
            kind: "immediate",
            result: createErrorToolResult(`Tool ${toolCall.name} not found.${formatRosterRecovery(availableTools)}`, { details: { code: "tool.not_found", toolName: toolCall.name, availableTools } }),
            isError: true,
        };
    }
    try {
        const preparedToolCall = prepareToolCallArguments(tool, toolCall);
        let finalArgs = validateToolArguments(tool, preparedToolCall);
        const refusal = await runInputValidation(tool, finalArgs, toolCall.id, signal, config.onToolInputValidationFault);
        if (refusal !== undefined)
            return validationRefusedOutcome(tool, refusal);
        const validatedArgs = finalArgs;
        if (config.beforeToolCall) {
            const beforeResult = await config.beforeToolCall({
                assistantMessage,
                toolCall,
                args: finalArgs,
                context: currentContext,
            }, signal);
            if (signal?.aborted) {
                return {
                    kind: "immediate",
                    result: createErrorToolResult("Operation aborted", { details: safeAbortDetails(config.abortResultDetails) }),
                    isError: true,
                };
            }
            if (beforeResult?.block) {
                const blocked = createErrorToolResult(beforeResult.reason || "Tool execution was blocked", beforeResult.details !== undefined ? { details: beforeResult.details } : undefined);
                if (beforeResult.haltRemaining !== undefined) {
                    return { kind: "immediate", result: blocked, isError: true, halt: beforeResult.haltRemaining };
                }
                return { kind: "immediate", result: blocked, isError: true };
            }
            if (beforeResult?.updatedInput !== undefined) {
                finalArgs = validateToolArguments(tool, {
                    ...preparedToolCall,
                    arguments: beforeResult.updatedInput,
                });
                if (!sameToolArgs(validatedArgs, finalArgs)) {
                    const rewrittenRefusal = await runInputValidation(tool, finalArgs, toolCall.id, signal, config.onToolInputValidationFault);
                    if (rewrittenRefusal !== undefined)
                        return validationRefusedOutcome(tool, rewrittenRefusal);
                }
            }
        }
        if (signal?.aborted) {
            return {
                kind: "immediate",
                result: createErrorToolResult("Operation aborted"),
                isError: true,
            };
        }
        return {
            kind: "prepared",
            toolCall,
            tool,
            args: finalArgs,
        };
    }
    catch (error) {
        return {
            kind: "immediate",
            result: createErrorToolResult(thrownText(error), error),
            isError: true,
        };
    }
}
async function executePreparedToolCall(prepared, signal, emit, abortResultDetails, onLateCancellation) {
    const updateEvents = [];
    let acceptingUpdates = true;
    if (signal?.aborted) {
        return { result: createErrorToolResult("operation aborted before execution", { details: safeAbortDetails(abortResultDetails) }), isError: true };
    }
    const work = (async () => {
        let outcome;
        try {
            const result = await prepared.tool.execute(prepared.toolCall.id, prepared.args, signal, (partialResult) => {
                if (!acceptingUpdates) {
                    return;
                }
                let delivery;
                try {
                    delivery = Promise.resolve(emit({
                        type: "tool_execution_update",
                        toolCallId: prepared.toolCall.id,
                        toolName: prepared.toolCall.name,
                        args: prepared.toolCall.arguments,
                        partialResult,
                    }));
                }
                catch (error) {
                    delivery = Promise.reject(error);
                }
                void delivery.catch((reason) => {
                    if (isAbortSemanticsError(reason))
                        onLateCancellation?.(reason);
                });
                updateEvents.push(delivery);
            });
            outcome = { result, isError: result.isError === true };
        }
        catch (error) {
            outcome = {
                result: createErrorToolResult(thrownText(error), error),
                isError: true,
            };
        }
        acceptingUpdates = false;
        const deliveryFailure = await settleUpdateDeliveries(updateEvents);
        if (!deliveryFailure)
            return outcome;
        const reason = deliveryFailure.reason;
        const details = annotateDeliveryFailure(outcome.result, {
            message: thrownText(reason),
        });
        return details === UNANNOTATED ? outcome : { result: { ...outcome.result, details }, isError: outcome.isError };
    })();
    return work;
}
const TOOL_PROGRESS_DELIVERY_FAILED = "toolProgressDeliveryFailed";
const UNANNOTATED = Symbol("unannotated");
function annotateDeliveryFailure(result, mark) {
    try {
        const raw = result.details;
        const prior = raw === undefined ? {} : raw;
        const proto = prior === null || typeof prior !== "object" ? undefined : Object.getPrototypeOf(prior);
        if (proto !== Object.prototype && proto !== null)
            return UNANNOTATED;
        const details = { ...prior };
        if (TOOL_PROGRESS_DELIVERY_FAILED in details)
            return UNANNOTATED;
        details[TOOL_PROGRESS_DELIVERY_FAILED] = mark;
        return details;
    }
    catch {
        return UNANNOTATED;
    }
}
function readDeliveryFailureMark(details) {
    try {
        if (details === null || typeof details !== "object")
            return undefined;
        const mark = details[TOOL_PROGRESS_DELIVERY_FAILED];
        return mark;
    }
    catch {
        return undefined;
    }
}
async function settleUpdateDeliveries(updateEvents) {
    if (updateEvents.length === 0)
        return undefined;
    let reportFailure;
    const firstFailure = new Promise((resolve) => {
        reportFailure = resolve;
    });
    for (const delivery of updateEvents) {
        void delivery.catch((reason) => reportFailure({ reason }));
    }
    const allSettled = Promise.allSettled(updateEvents).then((entries) => {
        for (const entry of entries) {
            if (entry.status === "rejected")
                return { reason: entry.reason };
        }
        return undefined;
    });
    const failure = await Promise.race([firstFailure, allSettled]);
    if (failure && isAbortSemanticsError(failure.reason))
        throw failure.reason;
    return failure;
}
function raiseFatalCancellation(executor) {
    const cancellation = executor.takeFatalCancellation();
    if (cancellation !== undefined)
        throw cancellation;
}
async function executePreparedWithDisclosure(currentContext, assistantMessage, prepared, config, signal, emit) {
    let executed;
    try {
        executed = await executePreparedToolCall(prepared, signal, emit, config.abortResultDetails);
    }
    catch (error) {
        if (isAbortSemanticsError(error))
            throw error;
        executed = {
            result: createErrorToolResult(`[tool execution harness failed: ${thrownText(error)}]`, error),
            isError: true,
        };
    }
    return finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal);
}
function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal) {
    return finalizeToolOutcome(prepared.toolCall, executed, config.afterToolCall === undefined
        ? undefined
        : (result, isError) => config.afterToolCall({ assistantMessage, toolCall: prepared.toolCall, args: prepared.args, result, isError, context: currentContext }, signal));
}
export async function finalizeToolOutcome(toolCall, executed, after) {
    let result = executed.result;
    let isError = executed.isError;
    const deliveryMark = readDeliveryFailureMark(executed.result.details);
    if (after) {
        try {
            const afterResult = await after(result, isError);
            if (afterResult) {
                result = {
                    content: afterResult.content ?? result.content,
                    details: afterResult.details ?? result.details,
                    terminate: afterResult.terminate ?? result.terminate,
                };
                isError = afterResult.isError ?? isError;
            }
        }
        catch (error) {
            const note = truncateError(`[post-tool processing failed (the tool already executed): ${thrownText(error)}]`);
            result = { ...result, content: [...result.content, { type: "text", text: note }] };
        }
        if (deliveryMark !== undefined && readDeliveryFailureMark(result.details) === undefined) {
            const details = annotateDeliveryFailure(result, deliveryMark);
            if (details !== UNANNOTATED)
                result = { ...result, details };
        }
    }
    return { toolCall, result, isError };
}
function safeAbortDetails(fn) {
    try {
        return fn?.();
    }
    catch {
        return undefined;
    }
}
function isAbortSemanticsError(error) {
    let cursor = error;
    for (let depth = 0; depth < 8 && isErrorLike(cursor); depth++) {
        if (readMember(cursor, "name") === "AbortError")
            return true;
        const next = readMember(cursor, "cause");
        if (next === cursor)
            break;
        cursor = next;
    }
    return false;
}
function createErrorToolResult(message, source) {
    const details = thrownMarksOf(source) ?? {};
    return {
        content: [{ type: "text", text: truncateError(message) }],
        details,
    };
}
async function emitToolExecutionEnd(finalized, emit) {
    await emit({
        type: "tool_execution_end",
        toolCallId: finalized.toolCall.id,
        toolName: finalized.toolCall.name,
        result: finalized.result,
        isError: finalized.isError,
    });
}
function createToolResultMessage(finalized) {
    return {
        role: "toolResult",
        toolCallId: finalized.toolCall.id,
        toolName: finalized.toolCall.name,
        content: finalized.result.content,
        details: finalized.result.details,
        isError: finalized.isError,
        timestamp: Date.now(),
    };
}
async function emitToolResultMessage(toolResultMessage, emit) {
    await emit({ type: "message_start", message: toolResultMessage });
    await emit({ type: "message_end", message: toolResultMessage });
}