/**
 * Per-connection spectral SDK lifecycle.
 *
 * One `AgentBridge` instance per active WebSocket connection. Wraps:
 *  - `createAgentSession` (in-memory session manager — we own persistence in
 *    SQLite; spectral doesn't need to write its own JSONL files).
 *  - `subscribe` listener that translates spectral `AgentSessionEvent`s into our
 *    own `ServerEvent` wire format and pushes them through a caller-supplied
 *    sink.
 *  - `prompt(text)` to send user input.
 *  - `dispose()` for clean teardown on WS close.
 *
 * Event mapping (agent → wire):
 *  - `message_start` (assistant)        → emit our own `message_start` with a
 *    fresh UUID `messageId`. spectral's AssistantMessage has no stable id field, so
 *    we mint one per turn and use it for all subsequent deltas/tool events
 *    until `message_end`.
 *  - `message_update` w/ inner text_delta / thinking_delta → wire `text_delta`
 *    or `thinking_delta` carrying the `delta` and our `messageId`.
 *  - `tool_execution_start`             → wire `tool_call`.
 *  - `tool_execution_end`               → wire `tool_result`.
 *  - `message_end` (assistant)          → wire `message_end` and persist the
 *    final assembled text + JSONL of every WIRE event we emitted for this
 *    message into SQLite. Content is taken from the final
 *    `AssistantMessage.content` (concatenating `text` blocks); we fall back
 *    to the `text_delta` accumulator when the provider didn't populate
 *    final blocks (e.g. deepseek via openai-compatible).
 *  - `agent_end`                        → wire `agent_end`.
 *
 * Persistence shape:
 *  `events_jsonl` is the newline-delimited JSON of the wire-format
 *  `ServerEvent`s we emitted for this message — NOT raw spectral
 *  `AgentSessionEvent`s. This guarantees the client's `parseWireEvents`
 *  reducer can rehydrate the turn after a refresh using the exact same
 *  reducer it uses for the live broadcast.
 *
 * Errors thrown synchronously from `session.prompt()` (e.g. no model
 * configured) are caught by the caller in `routes.ts` and re-emitted as
 * `{type:"error"}`.
 *
 * History rehydration:
 *  On first attach to a previously-created session (e.g. after a server
 *  restart), the SessionStreamManager passes the full SQLite transcript
 *  to the AgentBridge via `AgentBridgeOptions.history`. Before
 *  `createAgentSession` is called, each message is appended to the
 *  in-memory SessionManager so the LLM sees the full conversation
 *  context from the very first prompt. Multi-turn conversations within
 *  a single spectral session also work normally (the same AgentSession
 *  instance is reused across `prompt()` calls).
 */
import { type AgentSessionEvent } from "../sdk/coding-agent/index.js";
import { fetchAllowedModels as defaultFetchAllowedModels } from "../relay/models-fetch.js";
import type { SessionMemorySnapshot } from "./storage.js";
import type { ImageAttachment, ServerEvent, WireMessage } from "./wire.js";
/** Optional override for fetchAllowedModels (tests inject a mock). */
export type FetchAllowedModelsFn = typeof defaultFetchAllowedModels;
export interface AgentBridgeOptions {
    /** Working directory spectral will operate in (used by built-in tools). */
    cwd: string;
    /** spectral config / agent dir. Defaults to ~/.spectral/agent (spectral default). */
    agentDir?: string;
    /**
     * Backend base URL (no trailing slash). All inference is proxied through
     * `${backendUrl}/v1` — spectral NEVER reads `~/.spectral/agent/auth.json` and NEVER
     * holds raw provider API keys; the backend authenticates the machine via
     * `machineJwt` and uses its own centralized provider credentials.
     */
    backendUrl: string;
    /**
     * Bearer token used to authenticate with the backend's `/v1/*` endpoints
     * AND with the GraphQL `availableAgentModels` query. Issued by
     * `ensureMachineRegistered` and persisted in `machine.json`.
     */
    machineJwt: string;
    /** Sink for outbound wire events. Synchronous; the bridge does not buffer. */
    emit: (event: ServerEvent) => void;
    /**
     * Called when an assistant message completes, after `message_end` is
     * emitted. The bridge passes the accumulated text content and the JSONL
     * of every agent events captured for this message. Caller is expected to
     * persist this to SQLite.
     */
    onAssistantMessageComplete: (msg: {
        messageId: string;
        content: string;
        eventsJsonl: string;
    }) => void;
    /**
     * Called when the bridge decides to skip persisting an empty intermediate
     * message. Caller should clean up any stub row inserted at message_start.
     */
    onAssistantMessageSkipped?: (messageId: string) => void;
    /**
     * Called when spectral or an extension throws asynchronously (e.g. from
     * `prompt`). Caller can choose how to surface to the client. The bridge
     * also emits `{type:"error"}` on the wire as a fallback.
     */
    onError?: (err: Error) => void;
    /**
     * Optional session history to rehydrate into spectral's in-memory session
     * before the first prompt. Populated from SQLite by the session-stream
     * manager when attaching to a previously-created session (so the LLM
     * sees the full conversation transcript from the beginning). Omitted for
     * brand-new sessions (no history) and for tests that don't need it.
     */
    history?: WireMessage[];
    /**
     * Optional persisted observational-memory snapshot restored after message
     * history rehydration so the next prompt still sees compacted memory after
     * reconnect/server restart.
     */
    memorySnapshot?: SessionMemorySnapshot | null;
    /**
     * Test seam — override the model whitelist fetch. Production code never
     * sets this; tests inject a stub returning a synthetic list so `start()`
     * can run without a live backend.
     */
    fetchAllowedModels?: FetchAllowedModelsFn;
}
export declare class AgentBridge {
    private session?;
    private sessionManager?;
    private unsubscribe?;
    private pending?;
    private disposed;
    private readonly opts;
    /**
     * spectral's model registry. Built lazily in `start()` so we can resolve a
     * `modelId` (envelope-supplied or SQLite-persisted) to a concrete `Model`
     * via `registry.getAll().find(m => m.id === modelId)` before invoking
     * `session.setModel()`. Phase 3 (Available Models whitelist).
     */
    private modelRegistry?;
    /**
     * Raw allowed models list from the backend, preserved in sortOrder.
     * Used by `getFirstAvailableModelId()` to return the backend-curated
     * top pick when no explicit model selection is made.
     */
    private allowedModels?;
    /**
     * Last `modelId` we successfully applied via `session.setModel()`, or
     * `undefined` if we never applied one (spectral falls back to its own settings
     * file in that case, matching pre-Phase-3 behaviour). Tracked so repeated
     * envelopes carrying the same modelId don't churn spectral's internal state.
     */
    private lastAppliedModelId?;
    /** Current model's credit rates (from availableAgentModels), used for token_usage. */
    private activeCreditRates;
    private readonly memoryRuntime;
    private memoryPhase;
    constructor(opts: AgentBridgeOptions);
    /**
     * Create the spectral session, wire up subscription, and return.
     * Throws on creation failure (caller should surface to client).
     */
    start(): Promise<void>;
    private proxyProviderForAllowedModel;
    private refreshAllowedModels;
    /**
     * Register one synthetic provider per upstream API shape. Anthropic models
     * go to `${backendUrl}/v1/messages` (Messages API); everything else (OpenAI,
     * Google, Cerebras, etc.) goes to `${backendUrl}/v1/chat/completions`
     * (OpenAI-compatible API). The backend supports both endpoints natively
     * (verified in F1).
     *
     * spectral will send `Authorization: Bearer ${apiKey}` (because `authHeader: true`)
     * which carries the machine JWT — the only credential the backend trusts.
     *
     * The `id` we register is the raw `modelId` (e.g. `claude-3-5-haiku-latest`),
     * which is exactly what the backend expects in `body.model`.
     */
    private registerSyntheticProviders;
    /**
     * Apply a sticky model selection to the underlying spectral session, if it
     * differs from what was last applied. No-ops when:
     *  - `modelId` is null/undefined (caller passed nothing to apply)
     *  - the same modelId was already applied to this session
     *  - the registry can't resolve the modelId (emits an `error` wire event
     *    so the client surfaces the failure, but does NOT throw — the caller
     *    is expected to drop the prompt)
     *
     * Returns true when the requested model is now in effect (either because
     * we just applied it or because it was already applied). Returns false
     * on resolution failure so the caller can skip `prompt()` and avoid
     * driving spectral against the wrong model.
     *
     * Phase 3 (Available Models whitelist).
     */
    /**
     * Return the modelId of the first available model from the backend
     * whitelist (preserving the backend's sortOrder, which is the same
     * ordering the frontend uses in its model picker). Returns `undefined`
     * when no models are available (e.g. backend unreachable at startup).
     *
     * Used by `SessionStreamManager.prompt()` as a defense-in-depth
     * default when neither the envelope nor SQLite supply a modelId.
     */
    getFirstAvailableModelId(): string | undefined;
    /**
     * Return current session context usage from spectral's built-in estimator.
     * Used after compaction and session start to push updated context-window
     * stats to the frontend without waiting for the next assistant turn.
     */
    getContextUsage(): {
        tokens: number | null;
        contextWindow: number;
        percent: number | null;
    } | undefined;
    getSessionBranch(): Array<{
        type: string;
        id: string;
        timestamp?: string;
        message?: unknown;
        content?: unknown;
        customType?: string;
        summary?: unknown;
        fromId?: string;
        data?: unknown;
        details?: unknown;
        firstKeptEntryId?: string;
    }>;
    getMemoryActivity(): {
        phase: "idle" | "observing" | "compacting" | "reflecting" | "pruning";
        inFlight: {
            observer: boolean;
            compaction: boolean;
            reflection: boolean;
            pruner: boolean;
        };
    };
    private updateMemoryPhase;
    setModel(modelId: string | null | undefined): Promise<boolean>;
    /**
     * Map a frontend reasoning-effort string to spectral's ThinkingLevel.
     * Frontend sends: xhigh | high | medium | low | minimal | none | undefined
     * spectral expects:   "high" | "medium" | "low" | "minimal" | "off"
     *
     * Mapping:
     *   xhigh   → high    (spectral doesn't have xhigh, default to max)
     *   high    → high
     *   medium  → medium
     *   low     → low
     *   minimal → minimal
     *   none    → off
     *   undefined → no-op (spectral keeps whatever it has currently)
     */
    private mapReasoningEffortToThinkingLevel;
    /**
     * Set the reasoning/thinking effort level for the next prompt.
     * Pass `undefined` to leave spectral's current level unchanged.
     *
     * The caller (SessionStreamManager) is responsible for persisting the
     * value to SQLite; this method only applies it to spectral's in-memory session.
     */
    setReasoningEffort(effort: string | undefined): void;
    /**
     * Forward a user message to ext. Resolves when the full turn ends.
     * The caller is responsible for persisting the user message to SQLite
     * BEFORE invoking this — we don't do it here because spectral's `prompt` may
     * fail and we still want the user message recorded.
     *
     * When `images` is non-empty, each base64-encoded attachment is converted
     * to a spectral `ImageContent` block and passed as `options.images` to
     * `session.prompt()`. If the current model does not support image inputs,
     * images are instead converted to text placeholders so the conversation
     * can continue without errors.
     */
    prompt(text: string, images?: ImageAttachment[]): Promise<void>;
    /**
     * Manually compact the session context via spectral's built-in compaction.
     * Pi generates a summary of older conversation history, preserving the
     * most recent ~20K tokens verbatim. Compaction events are forwarded to
     * the wire through `handleEvent()`.
     *
     * `customInstructions` can inject guidance into the compaction prompt
     * so the summary captures information relevant to the current task.
     */
    compact(customInstructions?: string): Promise<void>;
    dispose(): void;
    /**
     * Emit a wire event AND record it in the pending message's audit log so
     * the persisted JSONL exactly matches the live broadcast. Use this in
     * place of `this.opts.emit` for any event that should appear in
     * `events_jsonl` for the current assistant message.
     */
    private emitAndBuffer;
    /**
     * Finalize the current `pending` assistant message: persist its assembled
     * content + wire events (including any tool_call / tool_result events that
     * arrived after `message_end`) to SQLite via `onAssistantMessageComplete`.
     *
     * Idempotent: marks the pending as `finalized` on first call; subsequent
     * calls are no-ops (guards against double-persisting when both
     * `message_start` and `agent_end` would otherwise finalize the same pending).
     *
     * Skips empty framing-only messages (messages with no text, thinking, or
     * tool events that contribute nothing the client can render).
     */
    private finalizePendingMessage;
    /**
     * Subscriber callback. Public so tests can drive event flow without
     * spinning up a real spectral session — production code never calls this
     * directly; spectral's `subscribe()` does, via the closure registered in
     * `start()`.
     */
    handleEvent(ev: AgentSessionEvent): void;
}
//# sourceMappingURL=agent-bridge.d.ts.map