UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

1,483 lines • 60.1 kB
import { createAssistantMessageEventStream, ensureToolTurnInvariants, snapshotActorAssertion, stripEngineMetadata } from "../llm/index.js";
import { finalizeToolOutcome, runAgentLoop } from "../loop/agent-loop.js";
import { resolveAgentCoreStreamFn } from "../loop/runtime-deps.js";
import { normalizeEngineSegments } from "../../core/untrusted-text.js";
import { AUTH_CARRIER_NAMES } from "../../brain/request-params.js";
import { isProviderEchoShape } from "../../brain/errors.js";
import { convertToLlm } from "./messages.js";
import { readMember, thrownText } from "../../core/thrown-value.js";
import { AgentHarnessError, CompactionError, SessionError, SYNTHETIC_MESSAGE_MODEL, toError, } from "./types.js";
const API_FAILURE_RECORD_MAX_CHARS = 2000;
function createUserMessage(text, images, provenance) {
    const content = text.length > 0 ? [{ type: "text", text }] : [];
    if (images) {
        content.push(...images);
    }
    const engineNote = provenance?.provenance === "engine-note";
    const segments = normalizeEngineSegments(text, provenance?.engineSegments);
    return {
        role: "user",
        content,
        timestamp: Date.now(),
        ...(segments.state === "valid" ? { engineSegments: segments.segments } : {}),
        ...(segments.state === "absent" &&
            provenance?.enginePrefixChars !== undefined &&
            Number.isInteger(provenance.enginePrefixChars) &&
            provenance.enginePrefixChars > 0 &&
            provenance.enginePrefixChars <= text.length
            ? { enginePrefixChars: provenance.enginePrefixChars }
            : {}),
        ...(provenance?.engineMinted === true && !engineNote ? { engineMinted: true } : {}),
        ...(engineNote ? { provenance: "engine-note" } : {}),
        ...(engineNote && typeof provenance?.engineNoteKey === "string" && provenance.engineNoteKey.length > 0 ? { engineNoteKey: provenance.engineNoteKey } : {}),
        ...(provenance?.actor !== undefined &&
            typeof provenance.actor.id === "string" &&
            provenance.actor.id.length > 0 &&
            typeof provenance.actor.hostAsserted === "boolean"
            ? { actor: snapshotActorAssertion(provenance.actor) }
            : {}),
    };
}
const engineNotePayloads = new WeakMap();
const engineAuthoredInjections = new WeakSet();
const userInputParkRecords = new WeakMap();
const drainedFromLane = new WeakMap();
const immediateClassInjections = new WeakSet();
const capPreferredInjections = new WeakSet();
function isEngineAuthoredInjection(options) {
    if (options === undefined)
        return false;
    if (options.engineMinted === true || options.enginePayload !== undefined)
        return true;
    return options.provenance === "engine-note" && options.callerAuthored !== true;
}
const ENGINE_NOTE_STEER_BACKLOG_CAP = 50;
function createFailureMessage(model, error, aborted) {
    return {
        role: "assistant",
        content: [{ type: "text", text: "" }],
        api: model.api,
        provider: model.provider,
        model: model.id,
        stopReason: aborted ? "aborted" : "error",
        errorMessage: thrownText(error),
        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 },
        },
    };
}
function cloneStreamOptions(streamOptions) {
    return {
        ...streamOptions,
        headers: streamOptions?.headers ? { ...streamOptions.headers } : undefined,
        metadata: streamOptions?.metadata ? { ...streamOptions.metadata } : undefined,
    };
}
function mergeHeaders(...headers) {
    const merged = {};
    let hasHeaders = false;
    for (const entry of headers) {
        if (!entry) {
            continue;
        }
        Object.assign(merged, entry);
        hasHeaders = true;
    }
    return hasHeaders ? merged : undefined;
}
function dropAuthCarriers(bag) {
    if (!bag)
        return bag;
    const out = {};
    for (const [k, v] of Object.entries(bag)) {
        if (!AUTH_CARRIER_NAMES.has(k.toLowerCase()))
            out[k] = v;
    }
    return out;
}
function refuseAuthCarriersInStreamOptions(bag) {
    if (!bag)
        return;
    for (const k of Object.keys(bag)) {
        if (AUTH_CARRIER_NAMES.has(k.toLowerCase())) {
            throw new Error(`streamOptions.headers must not carry an auth header ("${k}") — the run-static bag is not an auth channel ` +
                `(the route pairing law reads options-borne carriers as per-model credentials, which a run-wide bag is not). ` +
                `Put a deployment credential on the brain config (apiKey / headers, where its URL half is judged), or serve ` +
                `per-model credentials through getApiKeyAndHeaders.`);
        }
    }
}
function applyStreamOptionsPatch(base, patch) {
    const result = cloneStreamOptions(base);
    if (!patch) {
        return result;
    }
    if (Object.hasOwn(patch, "transport")) {
        result.transport = patch.transport;
    }
    if (Object.hasOwn(patch, "timeoutMs")) {
        result.timeoutMs = patch.timeoutMs;
    }
    if (Object.hasOwn(patch, "maxRetries")) {
        result.maxRetries = patch.maxRetries;
    }
    if (Object.hasOwn(patch, "maxRetryDelayMs")) {
        result.maxRetryDelayMs = patch.maxRetryDelayMs;
    }
    if (Object.hasOwn(patch, "cacheRetention")) {
        result.cacheRetention = patch.cacheRetention;
    }
    if (Object.hasOwn(patch, "headers")) {
        if (patch.headers === undefined) {
            result.headers = undefined;
        }
        else {
            const headers = { ...result.headers };
            for (const [key, value] of Object.entries(patch.headers)) {
                if (value === undefined) {
                    delete headers[key];
                }
                else {
                    headers[key] = value;
                }
            }
            result.headers = Object.keys(headers).length > 0 ? headers : undefined;
        }
    }
    if (Object.hasOwn(patch, "metadata")) {
        if (patch.metadata === undefined) {
            result.metadata = undefined;
        }
        else {
            const metadata = { ...result.metadata };
            for (const [key, value] of Object.entries(patch.metadata)) {
                if (value === undefined) {
                    delete metadata[key];
                }
                else {
                    metadata[key] = value;
                }
            }
            result.metadata = Object.keys(metadata).length > 0 ? metadata : undefined;
        }
    }
    return result;
}
const SUBSCRIBER_EVENT_TYPE = "*";
function normalizeHarnessError(error, fallbackCode) {
    if (error instanceof AgentHarnessError) {
        return error;
    }
    const cause = toError(error);
    if (cause instanceof SessionError) {
        return new AgentHarnessError("session", cause.message, cause);
    }
    if (cause instanceof CompactionError) {
        return new AgentHarnessError("compaction", cause.message, cause);
    }
    return new AgentHarnessError(fallbackCode, cause.message, cause);
}
function normalizeHookError(error) {
    return normalizeHarnessError(error, "hook");
}
export class AgentHarness {
    env;
    session;
    phase = "idle";
    get acceptsInjections() {
        return this.phase !== "idle" && !this.aborting && !this.stopRequested;
    }
    stopRequestSources = new Set();
    get stopRequested() {
        return this.stopRequestSources.size > 0;
    }
    boundaryStopConsumed;
    lastPublishedSeat;
    seatCutSources = new WeakMap();
    trailingBatchEndedOn;
    turnCutConsumed;
    aborting = false;
    drainedPendingInjection = [];
    runAbortController;
    runPromise;
    pendingSessionWrites = [];
    model;
    modelRef;
    thinkingLevel;
    onUndrainedEngineNotes;
    onToolTurnRepaired;
    runHistory;
    onUndrainedUserInputs;
    onEngineNoteConsumed;
    onUserInputConsumed;
    onFrameCommitted;
    engineInjectionsHeld;
    holdToolTurnLaw(messages) {
        const view = ensureToolTurnInvariants(messages, this.runHistory);
        if (view.repairs.length > 0) {
            try {
                this.onToolTurnRepaired?.(view.repairs);
            }
            catch {
            }
        }
        return view.messages;
    }
    recoverUndrainedEngineNotes() {
        this.sweepUndrainedEngineNotes([this.drainedPendingInjection, this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
    }
    sweepUndrainedEngineNotes(queues) {
        const undrained = [];
        for (const q of queues) {
            for (let i = 0; i < q.length; i++) {
                const payload = engineNotePayloads.get(q[i]);
                if (payload !== undefined) {
                    undrained.push(payload);
                    q.splice(i, 1);
                    i--;
                }
            }
        }
        if (undrained.length > 0 && this.onUndrainedEngineNotes) {
            try {
                this.onUndrainedEngineNotes(undrained);
            }
            catch {
            }
        }
    }
    pendingUserInputsByLane() {
        const inFlight = (lane) => this.drainedPendingInjection.filter((m) => !engineAuthoredInjections.has(m) && drainedFromLane.get(m) === lane).length;
        return {
            steer: this.steerQueue.filter((m) => !engineAuthoredInjections.has(m)).length + inFlight("steer"),
            followUp: this.followUpQueue.filter((m) => !engineAuthoredInjections.has(m)).length + inFlight("followUp"),
        };
    }
    hasPendingUserInput() {
        if (this.aborting)
            return false;
        return this.pendingUserInputsByLane().steer > 0;
    }
    announceUndrainedUserInputs() {
        const counts = this.pendingUserInputsByLane();
        if ((counts.steer > 0 || counts.followUp > 0) && this.onUndrainedUserInputs) {
            try {
                this.onUndrainedUserInputs(counts);
            }
            catch {
            }
        }
        return counts;
    }
    systemPrompt;
    systemBlocks;
    streamOptions;
    getApiKeyAndHeaders;
    runtime;
    resources;
    tools = new Map();
    activeToolNames;
    steerQueue = [];
    steeringQueueMode;
    maxOutputTokens;
    maxOutputTokensPerCall;
    stallTimeoutsPerCall;
    abortResultDetails;
    loopTrace;
    onToolInputValidationFault;
    resilience;
    maxToolConcurrency;
    streamingToolExecution;
    followUpQueue = [];
    followUpQueueMode;
    nextTurnQueue = [];
    loopRecovery;
    stopGate;
    turnInterruptSeat;
    handlers = new Map();
    constructor(options) {
        this.env = options.env;
        this.session = options.session;
        this.resources = options.resources ?? {};
        refuseAuthCarriersInStreamOptions(options.streamOptions?.headers);
        this.streamOptions = cloneStreamOptions(options.streamOptions);
        this.systemPrompt = options.systemPrompt;
        this.systemBlocks = options.systemBlocks;
        this.getApiKeyAndHeaders = options.getApiKeyAndHeaders;
        this.runtime = options.runtime;
        for (const tool of options.tools ?? []) {
            this.tools.set(tool.name, tool);
        }
        this.model = options.model;
        this.modelRef = options.modelRef ?? options.model;
        this.thinkingLevel = options.thinkingLevel;
        this.activeToolNames =
            options.activeToolNames ?? (options.tools ?? []).map((tool) => tool.name);
        this.steeringQueueMode = options.steeringMode ?? "one-at-a-time";
        this.maxOutputTokens = options.maxOutputTokens;
        this.maxOutputTokensPerCall = options.maxOutputTokensPerCall;
        this.stallTimeoutsPerCall = options.stallTimeoutsPerCall;
        this.abortResultDetails = options.abortResultDetails;
        this.loopTrace = options.loopTrace;
        this.onToolInputValidationFault = options.onToolInputValidationFault;
        this.resilience = options.resilience;
        this.maxToolConcurrency = options.maxToolConcurrency;
        this.streamingToolExecution = options.streamingToolExecution;
        this.followUpQueueMode = options.followUpMode ?? "one-at-a-time";
    }
    getHandlers(type) {
        return this.handlers.get(type);
    }
    async emitOwn(event, signal) {
        for (const listener of this.getHandlers(SUBSCRIBER_EVENT_TYPE) ?? []) {
            try {
                await listener(event, signal);
            }
            catch (error) {
                throw normalizeHookError(error);
            }
        }
    }
    async emitAny(event, signal) {
        for (const listener of this.getHandlers(SUBSCRIBER_EVENT_TYPE) ?? []) {
            try {
                await listener(event, signal);
            }
            catch (error) {
                throw normalizeHookError(error);
            }
        }
    }
    async emitHook(event) {
        const handlers = this.getHandlers(event.type);
        if (!handlers || handlers.size === 0) {
            return undefined;
        }
        let lastResult;
        for (const handler of handlers) {
            try {
                const result = (await handler(event));
                if (result !== undefined) {
                    lastResult = result;
                }
            }
            catch (error) {
                throw normalizeHookError(error);
            }
        }
        return lastResult;
    }
    async emitBeforeProviderRequest(model, sessionId, streamOptions) {
        const handlers = this.getHandlers("before_provider_request");
        let current = cloneStreamOptions(streamOptions);
        if (!handlers || handlers.size === 0) {
            return current;
        }
        for (const handler of handlers) {
            try {
                const result = (await handler({
                    type: "before_provider_request",
                    model,
                    sessionId,
                    streamOptions: cloneStreamOptions(current),
                }));
                if (result?.streamOptions) {
                    current = applyStreamOptionsPatch(current, result.streamOptions);
                }
            }
            catch (error) {
                throw normalizeHookError(error);
            }
        }
        return current;
    }
    async emitBeforeProviderPayload(model, payload) {
        const handlers = this.getHandlers("before_provider_payload");
        let current = payload;
        if (!handlers || handlers.size === 0) {
            return current;
        }
        for (const handler of handlers) {
            try {
                const result = (await handler({
                    type: "before_provider_payload",
                    model,
                    payload: current,
                }));
                if (result !== undefined) {
                    current = result.payload;
                }
            }
            catch (error) {
                throw normalizeHookError(error);
            }
        }
        return current;
    }
    async emitQueueUpdate() {
        await this.emitOwn({
            type: "queue_update",
            steer: [...this.steerQueue],
            followUp: [...this.followUpQueue],
            nextTurn: [...this.nextTurnQueue],
        });
    }
    startRunPromise() {
        let finish = () => { };
        this.runPromise = new Promise((resolve) => {
            finish = resolve;
        });
        return () => {
            this.runPromise = undefined;
            finish();
        };
    }
    async createTurnState() {
        const windowTokens = this.model.contextTokens ?? this.model.contextWindow;
        const context = await this.session.buildContext({
            ...(typeof windowTokens === "number" && windowTokens > 0 ? { windowTokens } : {}),
            ...(this.model.charsPerToken !== undefined ? { charsPerToken: this.model.charsPerToken } : {}),
        });
        const resources = this.getResources();
        const sessionMetadata = await this.session.getMetadata();
        const tools = [...this.tools.values()];
        const activeTools = this.activeToolNames
            .map((name) => this.tools.get(name))
            .filter((tool) => tool !== undefined);
        let systemPrompt = "You are a helpful assistant.";
        if (typeof this.systemPrompt === "string") {
            systemPrompt = this.systemPrompt;
        }
        else if (this.systemPrompt) {
            systemPrompt = await this.systemPrompt({
                env: this.env,
                session: this.session,
                model: this.model,
                thinkingLevel: this.thinkingLevel,
                activeTools,
                resources,
            });
        }
        return {
            messages: context.messages,
            resources,
            streamOptions: cloneStreamOptions(this.streamOptions),
            sessionId: sessionMetadata.id,
            systemPrompt,
            model: this.model,
            thinkingLevel: this.thinkingLevel,
            tools,
            activeTools,
        };
    }
    createContext(turnState, systemPrompt) {
        const effectivePrompt = systemPrompt ?? turnState.systemPrompt;
        const blocksSafe = this.systemBlocks !== undefined &&
            systemPrompt === undefined &&
            typeof this.systemPrompt === "string" &&
            this.systemBlocks.map((b) => b.text).join("\n\n") === effectivePrompt;
        return {
            systemPrompt: effectivePrompt,
            ...(blocksSafe ? { systemBlocks: this.systemBlocks } : {}),
            messages: turnState.messages.slice(),
            tools: turnState.activeTools.slice(),
        };
    }
    createStreamFn(getTurnState) {
        return async (model, context, streamOptions) => {
            const turnState = getTurnState();
            const auth = await this.getApiKeyAndHeaders?.(model);
            const snapshotOptions = {
                ...turnState.streamOptions,
                headers: mergeHeaders(dropAuthCarriers(turnState.streamOptions.headers), auth?.headers),
            };
            const requestOptions = await this.emitBeforeProviderRequest(model, turnState.sessionId, snapshotOptions);
            if (streamOptions?.signal?.aborted) {
                const out = createAssistantMessageEventStream();
                out.push({
                    type: "error",
                    reason: "aborted",
                    error: {
                        role: "assistant",
                        content: [{ type: "text", text: "" }],
                        api: model.api,
                        provider: model.provider,
                        model: 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,
                    },
                });
                return out;
            }
            return resolveAgentCoreStreamFn(this.runtime)(model, context, {
                ...(auth?.apiKey !== undefined ? { apiKey: auth.apiKey } : {}),
                cacheRetention: requestOptions.cacheRetention,
                headers: requestOptions.headers,
                maxRetries: requestOptions.maxRetries,
                maxRetryDelayMs: requestOptions.maxRetryDelayMs,
                metadata: requestOptions.metadata,
                onPayload: async (payload) => await this.emitBeforeProviderPayload(model, payload),
                onResponse: async (response) => {
                    const headers = { ...response.headers };
                    await this.emitOwn({ type: "after_provider_response", status: response.status, headers }, streamOptions?.signal);
                },
                reasoning: streamOptions?.reasoning,
                maxTokens: streamOptions?.maxTokens,
                stallTimeouts: streamOptions?.stallTimeouts,
                maxTokensDynamic: streamOptions?.maxTokensDynamic,
                staticReasoningCutDowngrade: streamOptions?.staticReasoningCutDowngrade,
                resilience: streamOptions?.resilience,
                signal: streamOptions?.signal,
                sessionId: turnState.sessionId,
                timeoutMs: requestOptions.timeoutMs,
                transport: requestOptions.transport,
                apiKey: auth?.apiKey,
            });
        };
    }
    async drainQueuedMessages(queue, mode) {
        let messages;
        if (this.aborting)
            return [];
        if (this.engineInjectionsHeld?.() === true) {
            messages = [];
            const limit = mode === "all" ? Number.POSITIVE_INFINITY : 1;
            for (let i = 0; i < queue.length && messages.length < limit; i++) {
                if (engineAuthoredInjections.has(queue[i]))
                    continue;
                messages.push(...queue.splice(i, 1));
                i--;
            }
        }
        else {
            let count = mode === "all" ? queue.length : 1;
            if (mode !== "all" && queue.length > 1 && engineNotePayloads.has(queue[0])) {
                count = 1;
                while (count < queue.length && engineNotePayloads.has(queue[count]))
                    count++;
            }
            messages = queue.splice(0, count);
        }
        if (messages.length === 0) {
            return messages;
        }
        const lane = queue === this.steerQueue ? "steer" : queue === this.followUpQueue ? "followUp" : undefined;
        if (lane !== undefined) {
            for (const m of messages)
                drainedFromLane.set(m, lane);
            this.drainedPendingInjection.push(...messages);
        }
        try {
            await this.emitQueueUpdate();
            for (const m of messages) {
                const payload = engineNotePayloads.get(m);
                if (payload !== undefined) {
                    try {
                        this.onEngineNoteConsumed?.(payload);
                    }
                    catch {
                    }
                }
            }
            return messages;
        }
        catch (error) {
            queue.unshift(...messages);
            this.unbookInFlight(messages);
            throw normalizeHookError(error);
        }
    }
    unbookInFlight(messages) {
        if (messages.length === 0 || this.drainedPendingInjection.length === 0)
            return;
        const gone = new Set(messages);
        this.drainedPendingInjection = this.drainedPendingInjection.filter((m) => !gone.has(m));
    }
    settleInjectedFrame(message, entryId) {
        if (!this.drainedPendingInjection.includes(message))
            return;
        this.unbookInFlight([message]);
        const parkRecord = userInputParkRecords.get(message);
        userInputParkRecords.delete(message);
        if (parkRecord !== undefined) {
            try {
                this.onUserInputConsumed?.(parkRecord);
            }
            catch {
            }
        }
        if (entryId !== undefined) {
            const payload = engineNotePayloads.get(message);
            if (payload !== undefined) {
                try {
                    this.onFrameCommitted?.({ kind: "engine_note", payload }, entryId);
                }
                catch {
                }
            }
        }
        engineNotePayloads.delete(message);
    }
    createLoopConfig(getTurnState, setTurnState, runPromptOverride) {
        const turnState = getTurnState();
        const self = this;
        return {
            model: turnState.model,
            reasoning: turnState.thinkingLevel,
            ...(this.maxOutputTokens !== undefined ? { maxTokens: this.maxOutputTokens } : {}),
            ...(this.maxOutputTokensPerCall !== undefined ? { maxTokensPerCall: this.maxOutputTokensPerCall } : {}),
            ...(this.stallTimeoutsPerCall !== undefined ? { stallTimeoutsPerCall: this.stallTimeoutsPerCall } : {}),
            ...(this.abortResultDetails !== undefined ? { abortResultDetails: this.abortResultDetails } : {}),
            ...(this.onToolInputValidationFault !== undefined ? { onToolInputValidationFault: this.onToolInputValidationFault } : {}),
            ...(this.resilience !== undefined ? { resilience: this.resilience } : {}),
            ...(this.maxToolConcurrency !== undefined ? { maxToolConcurrency: this.maxToolConcurrency } : {}),
            ...(this.streamingToolExecution === true && (this.getHandlers("tool_call")?.size ?? 0) === 0
                ? { streamingToolExecution: true }
                : {}),
            convertToLlm: (messages) => this.holdToolTurnLaw(stripEngineMetadata(convertToLlm(messages))),
            shouldStopAfterTurn: () => this.consultStopLatch("post_turn"),
            shouldHaltBeforeTurn: () => this.consultStopLatch("pre_turn"),
            transformContext: async (messages, signal) => {
                const result = await this.emitHook({ type: "context", messages: [...messages], ...(signal !== undefined ? { signal } : {}) });
                if (result?.adoptSessionRebuild !== true) {
                    return result?.messages ?? messages;
                }
                await this.flushPendingSessionWrites();
                const nextTurnState = await this.createTurnState();
                setTurnState(nextTurnState);
                const rebuiltContext = this.createContext(nextTurnState, runPromptOverride);
                const recheck = await this.emitHook({ type: "context", messages: [...rebuiltContext.messages], ...(signal !== undefined ? { signal } : {}), recheck: true });
                return {
                    messages: recheck?.messages ?? rebuiltContext.messages,
                    adoptedContext: {
                        messages: rebuiltContext.messages,
                        systemPrompt: rebuiltContext.systemPrompt,
                        ...(rebuiltContext.systemBlocks !== undefined ? { systemBlocks: rebuiltContext.systemBlocks } : {}),
                    },
                };
            },
            beforeToolCall: async ({ toolCall, args }, signal) => {
                const result = await this.emitHook({
                    type: "tool_call",
                    toolCallId: toolCall.id,
                    toolName: toolCall.name,
                    input: args,
                    ...(signal !== undefined ? { signal } : {}),
                });
                return result;
            },
            afterToolCall: ({ toolCall, args, result, isError }) => this.afterToolCallHook(toolCall, args, result, isError),
            prepareNextTurn: async () => {
                await this.flushPendingSessionWrites();
                await this.emitHook({ type: "turn_boundary", model: this.model });
                const nextTurnState = await this.createTurnState();
                setTurnState(nextTurnState);
                return {
                    context: this.createContext(nextTurnState, runPromptOverride),
                    model: nextTurnState.model,
                    thinkingLevel: nextTurnState.thinkingLevel,
                };
            },
            getSteeringMessages: async () => this.drainQueuedMessages(this.steerQueue, this.steeringQueueMode),
            publishTurnInterruptSeat: (seat) => {
                if (seat !== undefined) {
                    this.lastPublishedSeat = seat;
                    this.turnCutConsumed = undefined;
                }
                this.turnInterruptSeat = seat;
            },
            noteTurnCutConsumed: (seat) => this.noteTurnCutConsumed(seat),
            recheckImmediateInjections: async (pending) => this.absorbImmediateSteering(pending),
            pendingInjectionCount: () => this.pendingInjectionCount(),
            getFollowUpMessages: async () => {
                const queued = await this.drainQueuedMessages(this.followUpQueue, this.followUpQueueMode);
                if (queued.length > 0)
                    return queued;
                return this.stopGate ? await this.stopGate() : queued;
            },
            recovery: {
                get truncatedOutput() {
                    return self.loopRecovery?.truncatedOutput;
                },
                get malformedToolUse() {
                    return self.loopRecovery?.malformedToolUse;
                },
                get thinkingOnly() {
                    return self.loopRecovery?.thinkingOnly;
                },
                get degenerateOutput() {
                    return self.loopRecovery?.degenerateOutput;
                },
                get promptTooLong() {
                    const ptl = self.loopRecovery?.promptTooLong;
                    if (!ptl)
                        return undefined;
                    return {
                        detect: ptl.detect,
                        maxRetries: ptl.maxRetries,
                        withholdErrorEvents: ptl.withholdErrorEvents,
                        recover: async (_messages, attempt, turnSignal) => {
                            let shrank = false;
                            try {
                                shrank = await ptl.recover(attempt, turnSignal);
                            }
                            catch {
                                shrank = false;
                            }
                            if (!shrank)
                                return undefined;
                            const nextTurnState = await self.createTurnState();
                            setTurnState(nextTurnState);
                            const rebuilt = self.createContext(nextTurnState, runPromptOverride);
                            return {
                                messages: rebuilt.messages,
                                systemPrompt: rebuilt.systemPrompt,
                                ...(rebuilt.systemBlocks !== undefined ? { systemBlocks: rebuilt.systemBlocks } : {}),
                            };
                        },
                    };
                },
            },
        };
    }
    setLoopRecovery(recovery) {
        this.loopRecovery = recovery;
    }
    setStopGate(gate) {
        this.stopGate = gate;
    }
    validateToolNames(toolNames, tools = this.tools) {
        const missing = toolNames.filter((name) => !tools.has(name));
        if (missing.length > 0) {
            throw new AgentHarnessError("invalid_argument", `Unknown tool(s): ${missing.join(", ")}`);
        }
    }
    async flushPendingSessionWrites() {
        if (this.runAbortController?.signal.aborted) {
            return;
        }
        while (this.pendingSessionWrites.length > 0) {
            const write = this.pendingSessionWrites[0];
            if (write.type === "message") {
                await this.session.appendMessage(write.message);
            }
            else if (write.type === "model_change") {
                await this.session.appendModelChange(write.provider, write.modelId);
            }
            else if (write.type === "thinking_level_change") {
                await this.session.appendThinkingLevelChange(write.thinkingLevel);
            }
            else {
                const unreachable = write;
                throw new AgentHarnessError("invalid_argument", `flushPendingSessionWrites: unhandled queued write type ${unreachable.type}`);
            }
            this.pendingSessionWrites.shift();
        }
    }
    async recordApiFailure(failure) {
        const errorMessage = readMember(failure, "errorMessage");
        const apiErrorStatus = readMember(failure, "apiErrorStatus");
        const requestId = readMember(failure, "requestId");
        const errorKind = readMember(failure, "errorKind");
        const raw = typeof errorMessage === "string" ? errorMessage.trim() : undefined;
        const detail = raw !== undefined && raw.length > API_FAILURE_RECORD_MAX_CHARS
            ? `${raw.slice(0, API_FAILURE_RECORD_MAX_CHARS)}… (truncated)`
            : raw;
        const record = {
            role: "assistant",
            content: [{ type: "text", text: detail ? `API Error: ${detail}` : "API Error" }],
            api: failure.api,
            provider: failure.provider,
            model: SYNTHETIC_MESSAGE_MODEL,
            isApiErrorMessage: true,
            ...(typeof apiErrorStatus === "number" ? { apiErrorStatus } : {}),
            ...(typeof requestId === "string" && isProviderEchoShape(requestId) ? { requestId } : {}),
            usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
            usageMissing: true,
            stopReason: "error",
            ...(typeof errorMessage === "string" ? { errorMessage } : {}),
            ...(errorKind === "length_empty" || errorKind === "degenerate" || errorKind === "input_too_long" ? { errorKind } : {}),
            timestamp: failure.timestamp,
        };
        try {
            await this.session.appendMessage(record);
        }
        catch {
        }
    }
    async handleAgentEvent(event, signal) {
        if (event.type === "message_end") {
            let entryId;
            if (!isEmptyFailureAssistant(event.message)) {
                entryId = await this.session.appendMessage(event.message);
                this.settleInjectedFrame(event.message, entryId);
                if (entryId !== undefined && event.message.role === "toolResult" && typeof event.message.toolCallId === "string") {
                    try {
                        this.onFrameCommitted?.({ kind: "tool_result", toolCallId: event.message.toolCallId, content: event.message.content }, entryId);
                    }
                    catch {
                    }
                }
            }
            else if (event.message.isApiErrorMessage === true) {
                await this.recordApiFailure(event.message);
            }
            await this.emitAny(entryId !== undefined ? { ...event, entryId } : event, signal);
            return;
        }
        if (event.type === "turn_end") {
            let eventError;
            try {
                await this.emitAny(event, signal);
            }
            catch (error) {
                eventError = error;
            }
            const queued = this.pendingSessionWrites.length > 0;
            await this.flushPendingSessionWrites();
            const hadPendingMutations = queued && this.pendingSessionWrites.length === 0;
            if (eventError) {
                throw toLintErrorObject(eventError, "Non-Error thrown");
            }
            await this.emitOwn({ type: "save_point", hadPendingMutations });
            return;
        }
        if (event.type === "agent_end") {
            await this.flushPendingSessionWrites();
            this.sweepUndrainedEngineNotes([this.drainedPendingInjection, this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
            const undrainedUser = this.announceUndrainedUserInputs();
            this.phase = "idle";
            await this.emitAny(event, signal);
            await this.emitOwn({
                type: "settled",
                nextTurnCount: this.nextTurnQueue.length,
                ...(undrainedUser.steer > 0 ? { undrainedSteerCount: undrainedUser.steer } : {}),
                ...(undrainedUser.followUp > 0 ? { undrainedFollowUpCount: undrainedUser.followUp } : {}),
            }, signal);
            return;
        }
        await this.emitAny(event, signal);
    }
    async emitRunFailure(model, error, aborted, signal) {
        const failureMessage = createFailureMessage(model, error, aborted);
        await this.handleAgentEvent({ type: "message_start", message: failureMessage }, signal);
        await this.handleAgentEvent({ type: "message_end", message: failureMessage }, signal);
        await this.handleAgentEvent({ type: "turn_end", message: failureMessage, toolResults: [] }, signal);
        await this.handleAgentEvent({ type: "agent_end", messages: [failureMessage] }, signal);
        return [failureMessage];
    }
    async executeTurn(turnState, text, options, abortController) {
        let activeTurnState = turnState;
        const userMessage = createUserMessage(text, options?.images, options);
        this.runHistory = new WeakSet(turnState.messages.filter((m) => m.role === "assistant"));
        const beforeResult = await this.emitHook({
            type: "before_agent_start",
            prompt: text,
            images: options?.images,
            systemPrompt: turnState.systemPrompt,
            resources: turnState.resources,
        });
        const takePrompts = async () => {
            let messages = [userMessage];
            if (this.nextTurnQueue.length > 0) {
                const queuedMessages = this.nextTurnQueue.splice(0);
                try {
                    await this.emitQueueUpdate();
                }
                catch (error) {
                    this.nextTurnQueue.unshift(...queuedMessages);
                    throw normalizeHookError(error);
                }
                for (const m of queuedMessages) {
                    const payload = engineNotePayloads.get(m);
                    if (payload !== undefined) {
                        try {
                            this.onEngineNoteConsumed?.(payload);
                        }
                        catch {
                        }
                    }
                    engineNotePayloads.delete(m);
                }
                messages = [...queuedMessages, messages[0]];
            }
            if (beforeResult?.messages) {
                messages = [...messages, ...beforeResult.messages];
            }
            return messages;
        };
        const getTurnState = () => activeTurnState;
        const setTurnState = (nextTurnState) => {
            activeTurnState = nextTurnState;
        };
        const runResultPromise = (async () => {
            try {
                return await runAgentLoop(takePrompts, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState, beforeResult?.systemPrompt), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace, {
                    ...(options?.settleUnansweredToolCalls !== undefined
                        ? {
                            settle: {
                                ...(options.settleUnansweredToolCalls.halt !== undefined ? { halt: options.settleUnansweredToolCalls.halt } : {}),
                                report: (outcome) => {
                                    if (outcome.aborted)
                                        this.trailingBatchEndedOn = outcome.assistantMessage;
                                },
                            },
                        }
                        : {}),
                    ...(options?.onDelivered !== undefined ? { onPromptsDelivered: options.onDelivered } : {}),
                });
            }
            catch (error) {
                try {
                    return await this.emitRunFailure(activeTurnState.model, error, abortController.signal.aborted, abortController.signal);
                }
                catch (failureError) {
                    const cause = new AggregateError([toError(error), toError(failureError)], "Agent run failed and failure reporting failed");
                    throw new AgentHarnessError("unknown", cause.message, cause);
                }
            }
        })();
        try {
            const newMessages = await runResultPromise;
            for (let i = newMessages.length - 1; i >= 0; i--) {
                const message = newMessages[i];
                if (message.role === "assistant") {
                    return mergeTruncatedOutputChain(newMessages, i);
                }
            }
            if (this.stopRequested || abortController.signal.aborted)
                return undefined;
            if (this.trailingBatchEndedOn !== undefined)
                return this.trailingBatchEndedOn;
            throw new AgentHarnessError("invalid_state", "AgentHarness prompt completed without an assistant message");
        }
        finally {
            try {
                await this.flushPendingSessionWrites();
            }
            finally {
                this.runAbortController = undefined;
            }
        }
    }
    async prompt(text, options) {
        if (this.phase !== "idle") {
            throw new AgentHarnessError("busy", "AgentHarness is busy");
        }
        this.trailingBatchEndedOn = undefined;
        this.stopRequestSources.clear();
        this.boundaryStopConsumed = undefined;
        this.turnCutConsumed = undefined;
        this.lastPublishedSeat = undefined;
        this.aborting = false;
        if (this.drainedPendingInjection.length > 0) {
            const stranded = this.drainedPendingInjection;
            this.drainedPendingInjection = [];
            for (let i = stranded.length - 1; i >= 0; i--) {
                const frame = stranded[i];
                const lane = drainedFromLane.get(frame);
                if (lane === "steer")
                    this.steerQueue.unshift(frame);
                else if (lane === "followUp")
                    this.followUpQueue.unshift(frame);
            }
        }
        this.phase = "turn";
        const abortController = new AbortController();
        this.runAbortController = abortController;
        const finishRunPromise = this.startRunPromise();
        try {
            await this.flushPendingSessionWrites();
            const turnState = await this.createTurnState();
            return await this.executeTurn(turnState, text, options, abortController);
        }
        catch (error) {
            this.phase = "idle";
            throw normalizeHarnessError(error, "unknown");
        }
        finally {
            if (this.runAbortController === abortController)
                this.runAbortController = undefined;
            finishRunPromise();
        }
    }
    static emptyInjection(text, options) {
        return text.trim().length === 0 && (options?.images === undefined || options.images.length === 0);
    }
    async enqueueInjection(queue, text, options) {
        if (AgentHarness.emptyInjection(text, options))
            return undefined;
        let displaced;
        if (options?.enginePayload !== undefined && queue.filter((q) => engineNotePayloads.has(q)).length >= ENGINE_NOTE_STEER_BACKLOG_CAP) {
            const victimIdx = options.capPreferred === true
                ? queue.findIndex((q) => engineNotePayloads.has(q) && !capPreferredInjections.has(q))
                : -1;
            if (victimIdx === -1) {
                throw new AgentHarnessError("invalid_state", `engine-note backlog at cap (${ENGINE_NOTE_STEER_BACKLOG_CAP}) — park the payload for the session's next run`);
            }
            displaced = { frame: queue.splice(victimIdx, 1)[0], at: victimIdx };
        }
        const m = createUserMessage(text, options?.images, options);
        if (options?.enginePayload !== undefined)
            engineNotePayloads.set(m, options.enginePayload);
        if (isEngineAuthoredInjection(options))
            engineAuthoredInjections.add(m);
        if (options?.parkRecord !== undefined)
            userInputParkRecords.set(m, options.parkRecord);
        if (options?.immediate === true)
            immediateClassInjections.add(m);
        if (options?.capPreferred === true && options?.enginePayload !== undefined)
            capPreferredInjections.add(m);
        if (options?.immediate === true && queue === this.steerQueue) {
            let at = 0;
            while (at < queue.length && immediateClassInjections.has(queue[at]))
                at++;
            queue.splice(at, 0, m);
        }
        else {
            queue.push(m);
        }
        try {
            await this.emitQueueUpdate();
        }
        catch (error) {
            const at = queue.indexOf(m);
            if (at !== -1) {
                queue.splice(at, 1);
                if (displaced !== undefined)
                    queue.splice(Math.min(displaced.at, queue.length), 0, displaced.frame);
                throw error;
            }
        }
        if (displaced !== undefined) {
            const victimPayload = engineNotePayloads.get(displaced.frame);
            engineNotePayloads.delete(displaced.frame);
            if (victimPayload !== undefined && this.onUndrainedEngineNotes) {
                try {
                    this.onUndrainedEngineNotes([victimPayload]);
                }
                catch {
                }
            }
        }
        return m;
    }
    refuseHeldEngineInjection(options) {
        if (isEngineAuthoredInjection(options) && this.engineInjectionsHeld?.() === true) {
            throw new AgentHarnessError("invalid_state", "engine-authored injection refused: the run is halted awaiting the user's own direction — park the payload for the session's next run");
        }
    }
    async steer(text, options) {
        if (this.phase === "idle") {
            throw new AgentHarnessError("invalid_state", "Cannot steer while idle");
        }
        this.refuseHeldEngineInjection(options);
        return await this.enqueueInjection(this.steerQueue, text, options);
    }
    async followUp(text, options) {
        if (this.phase === "idle") {
            throw new AgentHarnessError("invalid_state", "Cannot follow up while idle");
        }
        this.refuseHeldEngineInjection(options);
        await this.enqueueInjection(this.followUpQueue, text, options);
    }
    interruptTurn(frame) {
        if (this.aborting)
            return false;
        if (!this.steerQueue.includes(frame))
            return false;
        const seat = this.turnInterruptSeat;
        if (seat === undefined || seat.signal.aborted)
            return false;
        this.cutSeat(seat, "steer");
        return true;
    }
    halt() {
        if (this.phase === "idle") {
            throw new AgentHarnessError("invalid_state", "Cannot halt while idle");
        }
        if (this.aborting)
            return { turnCut: false, accepted: false };
        this.stopRequestSources.add("halt");
        const seat = this.turnInterruptSeat;
        if (seat === undefined || seat.signal.aborted)
            return { turnCut: false, accepted: true };
        this.cutSeat(seat, "halt");
        return { turnCut: true, accepted: true };
    }
    cutSeat(seat, source) {
        this.seatCutSources.set(seat, source);
        const runQuietBefore = this.runAbortController?.signal.aborted !== true;
        seat.abort();
        if (runQuietBefore && this.runAbortController?.signal.aborted === true)
            this.turnCutConsumed = { source };
    }
    haltConsumption() {
        const turnCut = this.turnCutConsumed?.source;
        return {
            ...(turnCut !== undefined ? { turnCut } : {}),
            ...(this.boundaryStopConsumed !== undefined ? { boundaryStop: this.boundaryStopConsumed } : {}),
        };
    }
    consultStopLatch(face) {
        if (!this.stopRequested)
            return false;
        if (this.runAbortController?.signal.aborted !== true) {
            this.boundaryStopConsumed ??= { face, sources: [...this.stopRequestSources] };
        }
        return true;
    }
    noteTurnCutConsumed(seat) {
        if (seat !== this.lastPublishedSeat) {
            throw new AgentHarnessError("invalid_state", "turn-cut consumption reported for a seat the harness did not publish last");
        }
        this.turnCutConsumed = { source: this.seatCutSources.get(seat) };
    }
    pendingInjectionCount() {
        if (this.aborting)
            return 0;
        const held = this.engineInjectionsHeld?.() === true;
        const drainable = (q) => held ? q.filter((m) => !engineAuthoredInjections.has(m)).length : q.length;
        return drainable(this.steerQueue) + drainable(this.followUpQueue);
    }
    async absorbImmediateSteering(pending) {
        if (this.aborting)
            return pending;
        const held = this.engineInjectionsHeld?.() === true;
        const taken = [];
        for (let i = 0; i < this.steerQueue.length; i++) {
            const m = this.steerQueue[i];
            if (!immediateClassInjections.has(m))
                break;
            if (held && engineAuthoredInjections.has(m))
                continue;
            taken.push(...this.steerQueue.splice(i, 1));
            i--;
        }
        if (taken.length === 0)
            return pending;
        for (const m of taken)
            drainedFromLane.set(m, "steer");
        this.drainedPendingInjection.push(...taken);
        try {
            await this.emitQueueUpdate();
            for (const m of taken) {
                const payload = engineNotePayloads.get(m);
                if (payload !== undefined) {
                    try {
                        this.onEngineNoteConsumed?.(payload);
                    }
                    catch {
                    }
                }
            }
        }
        catch (error) {
            this.steerQueue.unshift(...taken);
            this.unbookInFlight(taken);
            throw normalizeHookError(error);
        }
        let at = 0;
        while (at < pending.length && immediateClassInjections.has(pending[at]))
            at++;
        return [...pending.slice(0, at), ...taken, ...pending.slice(at)];
    }
    async nextTurn(text, options) {
        await this.enqueueInjection(this.nextTurnQueue, text, options);
    }
    async appendMessage(message) {
        try {
            if (this.phase === "idle") {
                await this.session.appendMessage(message);
            }
            else {
                this.pendingSessionWrites.push({ type: "message", message });
            }
        }
        catch (error) {
            throw normalizeHarnessError(error, "session");
        }
    }
    async afterToolCallHook(toolCall, args, result, isError) {
        const patch = await this.emitHook({
            type: "tool_result",
            toolCallId: toolCall.id,
            toolName: toolCall.name,
            input: args,
            content: result.content,
            details: result.details,
            isError,
        });
        return patch;
    }
    finalizeToolCall(toolCall, args, executed) {
        return finalizeToolOutcome(toolCall, executed, (result, isError) => this.afterToolCallHook(toolCall, args, result, isError));
    }
    getModel() {
        return this.model;
    }
    getModelRef() {
        return this.modelRef;
    }
    getThinkingLevel() {
        return this.thinkingLevel;
    }
    async setModel(model, ref) {
        try {
            const previousModel = this.model;
            if (this.phase === "idle") {
                await this.session.appendModelChange(model.provider, model.id);
            }
            else {
                this.pendingSessionWrites.push({
                    type: "model_change",
                    provider: model.provider,
                    modelId: model.id,
                });
            }
            this.model = model;
            this.modelRef = ref ?? model;
            await this.emitOwn({ type: "model_select", model, previousModel, source: "set" });
        }
        catch (error) {
            throw normalizeHarnessError(error, "session");
        }
    }
    async setThinkingLevel(level) {
        try {
            const previousLevel = this.thinkingLevel;
            if (this.phase === "idle") {
                await this.session.appendThinkingLevelChange(level);
            }
            else {
                this.pendingSessionWrites.push({ type: "thinking_level_change", thinkingLevel: level });
            }
            this.thinkingLevel = level;
            await this.emitOwn({ type: "thinking_level_select", level, previousLevel });
        }
        catch (error) {
            throw normalizeHarnessError(error, "session");
        }
    }
    async setActiveTools(toolNames) {
        try {
            this.validateToolNames(toolNames);
            this.activeToolNames = [...toolNames];
        }
        catch (error) {
            throw normalizeHarnessError(error, "invalid_argument");
        }
    }
    getSteeringMode() {
        return this.steeringQueueMode;
    }
    async setSteeringMode(mode) {
        this.steeringQueueMode = mode;
    }
    getFollowUpMode() {
        return this.followUpQueueMode;
    }
    async setFollowUpMode(mode) {
        this.followUpQueueMode = mode;
    }
    getResources() {
        return {
            skills: this.resources.skills?.slice(),
            promptTemplates: this.resources.promptTemplates?.slice(),
        };
    }
    async setResources(resources) {
        const previousResources = this.getResources();
        this.resources = {
            skills: resources.skills?.slice(),
            promptTemplates: resources.promptTemplates?.slice(),
        };
        await this.emitOwn({
            type: "resources_update",
            resources: this.getResources(),
            previousResources,
        });
    }
    getStreamOptions() {
        return cloneStreamOptions(this.streamOptions);
    }
    async setStreamOptions(streamOptions) {
        refuseAuthCarriersInStreamOptions(streamOptions?.headers);
        this.streamOptions = cloneStreamOptions(streamOptions);
    }
    setSystemPrompt(prompt, blocks) {
        this.systemPrompt = prompt;
        this.systemBlocks = blocks;
    }
    async setTools(tools, activeToolNames) {
        try {
            const nextTools = new Map(tools.map((tool) => [tool.name, tool]));
            const nextActiveToolNames = activeToolNames ? [...activeToolNames] : this.activeToolNames;
            this.validateToolNames(nextActiveToolNames, nextTools);
            this.tools = nextTools;
            this.activeToolNames = [...nextActiveToolNames];
        }
        catch (error) {
            throw normalizeHarnessError(error, "invalid_argument");
        }
    }
    requestStopAfterTurn() {
        this.stopRequestSources.add("soft");
    }
    async flushQueuedSessionWrites() {
        try {
            await this.flushPendingSessionWrites();
        }
        catch (error) {
            throw normalizeHarnessError(error, "session");
        }
    }
    readParkableUserInputs() {
        const out = [];
        for (const q of [this.drainedPendingInjection, this.steerQueue, this.followUpQueue]) {
            for (const m of q) {
                const record = userInputParkRecords.get(m);
                if (record !== undefined)
                    out.push(record);
            }
        }
        return out;
    }
    dropParkedUserInputs(records) {
        const dropped = { steer: 0, followUp: 0 };
        if (records.length === 0)
            return dropped;
        const migrated = new Set(records);
        for (const q of [this.drainedPendingInjection, this.steerQueue, this.followUpQueue]) {
            const fixedLane = q === this.steerQueue ? "steer" : q === this.followUpQueue ? "followUp" : undefined;
            for (let i = 0; i < q.length; i++) {
                const frame = q[i];
                const record = userInputParkRecords.get(frame);
                if (record === undefined || !migrated.has(record))
                    continue;
                const lane = fixedLane ?? drainedFromLane.get(frame);
                userInputParkRecords.delete(frame);
                q.splice(i, 1);
                i--;
                if (lane !== undefined)
                    dropped[lane] += 1;
            }
        }
        return dropped;
    }
    async abort() {
        this.aborting = true;
        this.runAbortController?.abort();
        const errors = [];
        try {
            await this.emitQueueUpdate();
        }
        catch (error) {
            errors.push(toError(error));
        }
        try {
            await this.waitForIdle();
        }
        catch (error) {
            errors.push(toError(error));
        }
        this.sweepUndrainedEngineNotes([this.drainedPendingInjection, this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
        const userAuthored = (m) => !engineAuthoredInjections.has(m);
        const inFlightOf = (lane) => this.drainedPendingInjection.filter((m) => userAuthored(m) && drainedFromLane.get(m) === lane);
        const undrainedSteer = [...inFlightOf("steer"), ...this.steerQueue.filter(userAuthored)];
        const undrainedFollowUp = [...inFlightOf("followUp"), ...this.followUpQueue.filter(userAuthored)];
        try {
            await this.emitOwn({ type: "abort", undrainedSteer, undrainedFollowUp });
        }
        catch (error) {
            errors.push(toError(error));
        }
        if (errors.length > 0) {
            const cause = errors.length === 1 ? errors[0] : new AggregateError(errors, "Abort completed with errors");
            throw normalizeHarnessError(cause, "hook");
        }
        return { undrainedSteer, undrainedFollowUp };
    }
    async waitForIdle() {
        await this.runPromise;
    }
    subscribe(listener) {
        let handlers = this.handlers.get(SUBSCRIBER_EVENT_TYPE);
        if (!handlers) {
            handlers = new Set();
            this.handlers.set(SUBSCRIBER_EVENT_TYPE, handlers);
        }
        handlers.add(listener);
        return () => handlers.delete(listener);
    }
    on(type, handler) {
        let handlers = this.handlers.get(type);
        if (!handlers) {
            handlers = new Set();
            this.handlers.set(type, handlers);
        }
        handlers.add(handler);
        return () => handlers.delete(handler);
    }
}
function isEmptyFailureAssistant(message) {
    if (message.role !== "assistant")
        return false;
    const m = message;
    if (m.stopReason !== "aborted" && m.stopReason !== "error")
        return false;
    return m.content.every((c) => c.type === "text" ? c.text.trim() === "" : c.type === "thinking" ? c.thinking.trim() === "" : false);
}
function mergeTruncatedOutputChain(messages, lastAssistantIndex) {
    const last = messages[lastAssistantIndex];
    const lastText = last.content.map((c) => (c.type === "text" ? c.text : "")).join("");
    const parts = [];
    let i = lastAssistantIndex;
    while (i - 2 >= 0) {
        const mid = messages[i - 1];
        if (mid.role !== "custom")
            break;
        const h = messages[i - 2];
        if (h.role !== "assistant")
            break;
        const head = h;
        const step = (mid.customType === "loop:truncated-output-continue" && head.stopReason === "length") ||
            (mid.customType === "loop:midstream-partial-continue" && head.stopReason === "stop");
        if (!step)
            break;
        parts.unshift(head.content.map((c) => (c.type === "text" ? c.text : "")).join(""));
        i = i - 2;
    }
    if (parts.length === 0) {
        return last;
    }
    return {
        ...last,
        content: [
            { type: "text", text: parts.join("") + lastText },
            ...last.content.filter((c) => c.type !== "text"),
        ],
    };
}
function toLintErrorObject(value, fallbackMessage) {
    if (value instanceof Error) {
        return value;
    }
    if (typeof value === "string") {
        return new Error(value);
    }
    const error = new Error(fallbackMessage, { cause: value });
    if ((typeof value === "object" && value !== null) || typeof value === "function") {
        Object.assign(error, value);
    }
    return error;
}