import { A as ChatTurnRequestPayload, B as ChatTurnPartInput, e as ChatMessagePart, D as ChatTurnFilePartInput } from '../parts-1_3y2JmR.js';
export { c as ChatMentionKind, E as ChatTurnInputError, G as ChatTurnTextPartInput, F as FileMention, H as FileMentionsToPartsOptions, I as INLINE_PARTS_MAX_BYTES, M as MENTION_MAX_COUNT, J as SandboxMentionPathCheck, K as assertPromptPartsWithinCap, L as buildMentionPromptBlock, N as chatTurnRequestInit, O as fileMentionsToParts, P as mediaTypeForMentionPath, Q as mentionKindForPath, R as parseChatTurnParts, T as parseFileMentions, U as promptPartsByteSize, V as validateSandboxMentionPath } from '../parts-1_3y2JmR.js';
import { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtime';
import { InteractionAnswerRoute, InteractionAnswerRouteOptions } from '../interactions/index.js';
import { PersistedChatMessageForTurn, TurnEventStore } from '../stream/index.js';
export { C as CreateSandboxFileIndexRouteOptions, F as FileIndexAuthorization, a as FileIndexCache, b as FileIndexReadyResponse, c as FileIndexResponse, d as FileIndexWarmingResponse, S as SandboxFileTreeSource, e as SandboxTreeFile, f as SandboxTreeResult, g as createSandboxFileIndexRoute } from '../file-index-Bn6sitKb.js';
import '@tangle-network/agent-interface';
import '../contract-KfqJh_au.js';
import '../plans/index.js';

/**
 * `createChatTurnRoutes` — the assembled server chat vertical (issue #188
 * Phase 1). One factory composing the pieces every product re-wired by hand:
 *
 *   body parse/validate      → `/web` `parseJsonObjectBody` + `./wire`
 *   turn identity            → `/stream` `resolveChatTurn` + agent-runtime
 *                              `deriveExecutionId`
 *   producer                 → injected seam (sandbox lane via
 *                              `createSandboxChatProducer`; router lane is the
 *                              product's own `ChatTurnProducer`)
 *   turn engine              → agent-runtime `handleChatTurn` (verbatim)
 *   durability               → `/stream` turn-buffer tap, wired BY DEFAULT
 *                              (tee + drain keeps the turn running after a
 *                              client drop; replay serves the buffered tail)
 *   persistence              → injected `/chat-store`-shaped store
 *                              (user row on send, assistant row on completion)
 *   interactions answer      → `/interactions` `createInteractionAnswerRoute`
 *
 * Handlers are web-standard `Request → Response` (Workers, Node 18+, Deno) —
 * no router import. Auth/access is one injected `authorize` seam, composable
 * with `/app-auth` guards but not coupled to them.
 *
 * Six optional product seams let a complex turn-orchestrator compose the
 * vertical instead of hand-rolling a generator — each omittable to the exact
 * behavior above: `turnLock` (single-flight acquire/release around the turn),
 * `contextGate` (pre-producer domain-readiness short-circuit), `beforeTurn`
 * (observe + augment the producer input), `lifecycle` (deterministic
 * start/complete/error telemetry), `heartbeat` (keepalive during silent
 * producer waits), plus `onRawEvent` (the raw producer events, for telemetry).
 * `handleChatTurn` stays the engine — the seams only wrap its input, its
 * producer stream, and its settle.
 *
 * Seam stability: `lifecycle` and `heartbeat` are generic and stable.
 * `turnLock`, `contextGate`, `beforeTurn`, and `onRawEvent` are `@experimental`
 * — proven by a single consumer (gtm's chat vertical, #200) and may change once
 * a second consumer exercises them. They stay FLAT top-level options (not
 * grouped under a `hooks` object): that grouping would break the shipped
 * consumer's call for no mechanism gain, and this package's exports are
 * additive-only.
 */

/** Usage receipt persisted onto the assistant message (the flattened
 *  `step-finish` shape `/chat-store`'s columns mirror). */
interface ChatTurnUsage {
    inputTokens?: number;
    outputTokens?: number;
    reasoningTokens?: number;
    cacheReadTokens?: number;
    cacheWriteTokens?: number;
    costUsd?: number;
}
/** What the route persists — a structural subset of `/chat-store`'s
 *  `ChatStore`, so `createChatStore(db, tables)` satisfies it directly and a
 *  product with its own persistence adapts without importing drizzle. */
interface ChatTurnMessageStore {
    listMessages(threadId: string): Promise<Array<{
        id: string;
        role: 'user' | 'assistant' | 'system' | 'tool';
        content: string;
        parts?: ChatMessagePart[] | null;
    }>>;
    appendMessage(input: {
        threadId: string;
        role: 'user' | 'assistant';
        content: string;
        parts?: ChatMessagePart[];
        model?: string | null;
        inputTokens?: number | null;
        outputTokens?: number | null;
        reasoningTokens?: number | null;
        cacheReadTokens?: number | null;
        cacheWriteTokens?: number | null;
        costUsd?: number | null;
    }): Promise<unknown>;
}
/** `ChatTurnProducer` plus the persisted projection the assembly reads after
 *  drain. `createSandboxChatProducer` returns this; a router-lane producer
 *  may omit the optional members (finalText persists as a single text part). */
interface ChatTurnRouteProducer extends ChatTurnProducer {
    assistantParts?(): Array<Record<string, unknown>>;
    usage?(): ChatTurnUsage;
    model?: string;
}
type ChatTurnAuthorization<TContext> = {
    ok: true;
    tenantId: string;
    userId: string;
    context: TContext;
    /** When `false`, skip the `role:'user'` message insert for this turn — for
     *  a product-dispatched / synthetic turn (e.g. a follow-up the product
     *  raised itself) that must not surface a new user row. Composes with —
     *  never overrides — the engine's retry-dedup: `authorize` runs before
     *  turn identity is resolved, so it cannot tell a retry from a fresh turn;
     *  a turn already deduped stays deduped. Omit / `true` → today's behavior.
     *  @experimental Single-consumer; shape may change. */
    insertUserMessage?: boolean;
} | {
    ok: false;
    response: Response;
};
interface ChatTurnAuthorizeArgs {
    request: Request;
    intent: 'turn' | 'replay' | 'running';
    /** Parsed, validated POST body (turn intent only). */
    body?: ChatTurnRequestPayload;
    /** The buffered turn id being replayed (replay intent only). */
    turnId?: string;
    /** The thread whose running turns are being discovered (running intent only). */
    threadId?: string;
}
interface ChatTurnProduceArgs<TContext> {
    request: Request;
    body: ChatTurnRequestPayload;
    identity: ChatTurnIdentity;
    context: TContext;
    /** The message to send: plain text, or parts when the client attached
     *  files (a text part is prepended from `content` when present). */
    prompt: string | ChatTurnPartInput[];
    /** Stable id for cross-process reconnect (`deriveExecutionId`). */
    executionId: string;
    /** The turn-buffer id announced to the client for replay. */
    turnStreamId: string;
    priorMessages: PersistedChatMessageForTurn[];
}
/** One event as it crosses the route: the producer's own vocabulary, or an
 *  injected keepalive. Same shape the engine forwards verbatim. */
type ChatRouteEvent = {
    type: string;
    data?: Record<string, unknown>;
};
/** Keepalive emitted while the producer is quiet (long tool calls, first-token
 *  wait) so client watchdogs stay re-armed. One is emitted each time
 *  `intervalMs` elapses with no producer event; the window resets on every real
 *  event, so a chatty producer never triggers one. The product owns the event
 *  shape (`type` + `data`). Omit → no keepalives (today's behavior). */
interface ChatTurnHeartbeat {
    intervalMs: number;
    event(info: {
        elapsedMs: number;
        tick: number;
    }): ChatRouteEvent;
}
/** Patch a `beforeTurn` hook returns to augment the producer's input. Omitted
 *  fields keep the route-assembled value; the product's `produce` still owns
 *  the system prompt. */
interface ChatTurnInputPatch {
    prompt?: string | ChatTurnPartInput[];
    priorMessages?: PersistedChatMessageForTurn[];
}
/** Pre-turn readiness verdict — proceed, or short-circuit with the product's
 *  own `Response` (e.g. a canned assistant reply asking for missing context).
 *  Distinct from `authorize`: this gates domain readiness, not access. */
type ChatTurnGateResult = {
    proceed: true;
} | {
    proceed: false;
    response: Response;
};
/** Single-flight lock verdict — acquired (with an opaque handle passed back to
 *  `release`), or already held (short-circuit with the product's 409-style
 *  `Response`). */
type ChatTurnLockResult = {
    acquired: true;
    handle?: unknown;
} | {
    acquired: false;
    response: Response;
};
/** Async acquire/release wrapped around the turn. `acquire` runs before any
 *  side effect; `release` runs once when the turn settles — including on a
 *  short-circuit or a throw. */
interface ChatTurnLock<TContext> {
    acquire(args: ChatTurnProduceArgs<TContext>): ChatTurnLockResult | Promise<ChatTurnLockResult>;
    release(handle: unknown): void | Promise<void>;
}
interface ChatTurnLifecycleBase<TContext> {
    identity: ChatTurnIdentity;
    executionId: string;
    turnStreamId: string;
    context: TContext;
}
interface ChatTurnLifecycleStart<TContext> extends ChatTurnLifecycleBase<TContext> {
    startedAt: number;
}
interface ChatTurnLifecycleComplete<TContext> extends ChatTurnLifecycleBase<TContext> {
    finalText: string;
    usage: ChatTurnUsage;
    durationMs: number;
}
interface ChatTurnLifecycleError<TContext> extends ChatTurnLifecycleBase<TContext> {
    error: unknown;
    durationMs: number;
}
/** Deterministic run telemetry: `onTurnStart` fires before the producer runs;
 *  exactly one of `onTurnComplete` / `onTurnError` fires after the turn
 *  settles, always after `onTurnStart`. Failure is derived from the turn's own
 *  `error` / `session.run.failed` events (or a drain throw), not the engine's
 *  lifecycle envelope. Hook errors are swallowed — telemetry never fails a
 *  turn. */
interface ChatTurnLifecycle<TContext> {
    onTurnStart?(info: ChatTurnLifecycleStart<TContext>): void | Promise<void>;
    onTurnComplete?(info: ChatTurnLifecycleComplete<TContext>): void | Promise<void>;
    onTurnError?(info: ChatTurnLifecycleError<TContext>): void | Promise<void>;
}
interface CreateChatTurnRoutesOptions<TContext = void> {
    /** Names the product in `deriveExecutionId` so retries land on the same
     *  substrate execution. */
    projectId: string;
    /** Authenticate + authorize the caller for a turn or a replay. The only
     *  product-supplied access step: session auth, thread/workspace access,
     *  seat/balance gates, rate limits all live here. */
    authorize(args: ChatTurnAuthorizeArgs): Promise<ChatTurnAuthorization<TContext>>;
    /** Thread/message persistence (`/chat-store`'s store or a product adapter). */
    store: ChatTurnMessageStore;
    /** Turn-event buffer (`createD1TurnEventStore(env.DB)` in production,
     *  `createMemoryTurnEventStore()` in tests). Wired by default — every turn
     *  is buffered and replayable. */
    turnStore: TurnEventStore;
    /** Build the turn's event stream. Sandbox lane: `streamSandboxPrompt(...)`
     *  wrapped in `createSandboxChatProducer`. Router/openai-compat lane: the
     *  product's own producer. May be async (box resolution). */
    produce(args: ChatTurnProduceArgs<TContext>): ChatTurnRouteProducer | Promise<ChatTurnRouteProducer>;
    /** Single-flight lock acquired before any side effect and released once when
     *  the turn settles (including short-circuit/throw). Omit → no lock.
     *  @experimental Single-consumer (gtm, #200); shape may change. */
    turnLock?: ChatTurnLock<TContext>;
    /** Pre-turn readiness gate that can short-circuit with a product `Response`
     *  before the producer runs (the user row is already persisted). Runs after
     *  `turnLock.acquire`, before `beforeTurn`. Omit → always proceed.
     *  @experimental Single-consumer (gtm, #200); shape may change. */
    contextGate?(args: ChatTurnProduceArgs<TContext>): ChatTurnGateResult | Promise<ChatTurnGateResult>;
    /** Observe the assembled producer input and optionally augment it (rewrite
     *  the prompt / prior messages) before the producer runs. Omit → no change.
     *  @experimental Single-consumer (gtm, #200); shape may change. */
    beforeTurn?(args: ChatTurnProduceArgs<TContext>): ChatTurnInputPatch | void | Promise<ChatTurnInputPatch | void>;
    /** Deterministic run telemetry (start / complete / error) with identity and
     *  timing. Omit → no telemetry. */
    lifecycle?: ChatTurnLifecycle<TContext>;
    /** Keepalive injected while the producer is quiet. Omit → no keepalives. */
    heartbeat?: ChatTurnHeartbeat;
    /** Observe each event the producer emits, before the engine frames it and
     *  before any heartbeat injection (the raw sidecar-producer events, for
     *  telemetry). Never alters the stream; errors are swallowed. Distinct from
     *  `onEvent`, which sees the engine-framed stream incl. lifecycle envelopes.
     *  @experimental Single-consumer (gtm, #200); shape may change. */
    onRawEvent?(event: ChatRouteEvent, context: TContext): void | Promise<void>;
    /** Pre-persist transform of the final text (e.g. `/redact`'s `redactPII`).
     *  Live stream is never altered. */
    transformFinalText?(text: string): string | Promise<string>;
    /** Post-processing after a turn settles (billing, titles, audit). Fires with
     *  `failed:true` + `failureReason` when the turn carried a terminal error
     *  event (model 402 / rate-limit / server error) instead of a clean
     *  completion, so products skip the deduct and render an error row rather
     *  than billing an empty turn and marking it done. A turn that THROWS never
     *  reaches this hook (the engine skips it on a producer throw). Errors are
     *  swallowed by the engine — they never fail a streamed turn. */
    onTurnComplete?(input: {
        identity: ChatTurnIdentity;
        finalText: string;
        context: TContext;
        failed: boolean;
        failureReason?: string;
    }): Promise<void>;
    /** Per-event side channel (product broadcast). The turn-buffer tap is
     *  already wired; this runs in addition. */
    onEvent?(event: {
        type: string;
        data?: Record<string, unknown>;
    }, context: TContext): void | Promise<void>;
    /** Trace flush handed to `waitUntil` (OTLP export). */
    traceFlush?(context: TContext): Promise<void>;
    /** Compose the interaction-answer endpoints (`/interactions`). Omit when the
     *  product has no sidecar ask channel. */
    interactions?: InteractionAnswerRouteOptions;
    /** Byte budget for inline prompt parts. Default `INLINE_PARTS_MAX_BYTES`. */
    maxInlinePartBytes?: number;
    /** Per-flush coalescer for the turn buffer. Default `coalesceDeltas` (this
     *  assembly streams the client vocabulary's `{type:'text'|'reasoning',
     *  text}` lines, which it merges). A producer streaming raw
     *  `message.part.updated` events passes `coalesceChatStreamEvents`. */
    coalesceTurnEvents?: (events: unknown[]) => unknown[];
    replay?: {
        pollMs?: number;
        timeoutMs?: number;
    };
    log?: (message: string, meta?: Record<string, unknown>) => void;
}
interface ChatTurnRoutes {
    /** POST — run one turn, streaming NDJSON. First line is
     *  `{type:'turn', turnId}` (the replay handle); the rest is the engine's
     *  event protocol. Pass the platform's `waitUntil` so the turn keeps
     *  running (and buffering) after a client disconnect. */
    turn(request: Request, ctx?: {
        waitUntil?(p: Promise<unknown>): void;
    }): Promise<Response>;
    /** GET — replay a buffered turn from `?fromSeq=` (0 = everything), then
     *  follow it live until it completes. */
    replay(request: Request, params: {
        turnId: string;
    }): Promise<Response>;
    /** GET `?threadId=` — the reconnect-discovery endpoint: the turn ids still
     *  running on a thread, so a client that reloaded mid-turn can re-attach to
     *  the live stream via {@link replay} instead of losing it. Returns `[]` when
     *  the turn store cannot enumerate running turns (`listRunning` unimplemented). */
    running(request: Request): Promise<Response>;
    /** list/answer endpoints from `/interactions`; null when not configured. */
    interactions: InteractionAnswerRoute | null;
}
declare function createChatTurnRoutes<TContext = void>(options: CreateChatTurnRoutesOptions<TContext>): ChatTurnRoutes;

/**
 * Recovery policy for a `ChatTurnLock` whose holder died.
 *
 * `createChatTurnRoutes` takes the lock as a seam (`acquire`/`release`) and a
 * lock is a single-flight guard: while it is held, a second turn on the same
 * scope is refused. Products give it a TTL measured in tens of minutes, so a
 * turn that dies without releasing wedges chat for that whole window. Every
 * app on the seam inherits that wedge, which is why the way OUT of it is
 * policy this package owns rather than something each app rediscovers.
 *
 * The policy takes PROBES, not clients: it imports no sandbox SDK, opens no
 * connection, and knows nothing about how a product finds its box or talks to
 * a sidecar. That is what makes the rules testable and what keeps the concrete
 * probes — which box key, which session id, which sidecar endpoint — in the
 * product.
 *
 * The rules, in precedence order:
 *
 * 1. The session probe answered and the execution is TERMINAL ⇒ release, once
 *    the lock is past a short grace period. The authority on "is this turn
 *    still running" is whatever is actually running it; a terminal verdict is
 *    proof the lock outlived its turn — but only if the verdict is about THIS
 *    turn, which is what the grace buys (see
 *    {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS}).
 * 2. The session probe answered and the execution is LIVE ⇒ hold, always.
 *    Nothing below may override this. The lock is doing exactly its job.
 * 3. The probes could not reach that authority at all — the sandbox could not
 *    be listed, is gone, is not running, or its session probe failed ⇒ fall
 *    back on the physical argument: an execution runs INSIDE the box, so a box
 *    that is not there is running nothing, and the lock is releasable. Without
 *    this fallback the recovery would depend on the very subsystem whose
 *    failure produced the stale lock.
 *
 * Rule 3 is gated on a grace period because it is an inference, not an
 * observation — see {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}.
 */
/** Where the box is, as far as the caller can see. `state` on `not-running`
 *  is the platform's own status string, carried through for the log. */
type StaleTurnLockSandboxProbeResult = {
    status: 'running';
} | {
    status: 'absent';
} | {
    status: 'not-running';
    state?: string;
};
/** What the thing running the turn says about it. `terminal: false` means an
 *  execution is LIVE — the strongest signal in the policy. `diagnostics` rides
 *  through to the result and the logs unread. */
type StaleTurnLockSessionProbeResult = {
    reachable: true;
    terminal: boolean;
    diagnostics?: Record<string, unknown>;
} | {
    reachable: false;
    reason?: string;
};
/**
 * Minimum age a lock must reach before the "sandbox unreachable ⇒ nothing can
 * be running" fallback may force-release it.
 *
 * The lock is acquired BEFORE the box is ensured, so during a cold workspace's
 * first turn there is a real window in which the lock is held and no box exists
 * yet — indistinguishable, from a peek, from a box that vanished. The grace
 * period has to outlast that window (create + bootstrap + whatever the product
 * hydrates) or a concurrent request steals the lock from a turn that is merely
 * still provisioning. Five minutes clears observed cold starts with room to
 * spare while cutting the worst case from a TTL-length wedge down to five
 * minutes. Raising it makes recovery slower; lowering it risks stealing a lock
 * mid-provision.
 */
declare const DEFAULT_STALE_TURN_LOCK_GRACE_MS: number;
/**
 * Minimum age a lock must reach before a TERMINAL session verdict may release
 * it.
 *
 * The session probe is keyed on the THREAD, not on the execution the lock
 * holds: a sidecar that has nothing running reports `terminal` with
 * `activeExecutionId: null`, so there is no id to match the lock against. The
 * lock, meanwhile, is acquired BEFORE the box is ensured and before the
 * execution registers with the sidecar. Between those two moments a second
 * request that reconciles the lock asks the sidecar about a turn it has not
 * heard of yet and gets back the PREVIOUS turn's terminal state — proof about
 * the wrong execution. Releasing on that verdict hands the second request a
 * lock the first one is still using, which is two concurrent turns on a scope
 * whose single-flight guard just voted for itself.
 *
 * One minute covers the acquire → box-ensure → sidecar-registration window on
 * a warm box (the cold-box case is Rule 3's, and has its own, much longer
 * grace). Deliberately NOT
 * {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}: this branch has a positive
 * observation behind it, so it should recover fast, and stretching it to five
 * minutes would leave a genuinely dead turn wedged for the whole window that
 * the session probe exists to shortcut. Raising it delays recovery from a
 * crashed turn; lowering it narrows the registration window it protects.
 */
declare const DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS: number;
interface ReconcileStaleTurnLockOptions {
    /** When the held lock was acquired (epoch ms). The grace period is measured
     *  from here, so it must be the LOCK's start, not the turn's. */
    lockStartedAt: number;
    /** Is the box there and running? Never provisions — a peek, not an ensure.
     *  A throw is treated as unreachable, same as `absent`. */
    probeSandbox(): Promise<StaleTurnLockSandboxProbeResult>;
    /** Ask the running box whether the execution is still live. Only called when
     *  `probeSandbox` reported `running`. A throw is treated as unreachable. */
    probeSession(): Promise<StaleTurnLockSessionProbeResult>;
    /** Release the lock, fenced by the instant the releasing evidence was
     *  observed. `fence.observedAt` is snapshotted BEFORE the probe that
     *  justified the release, so a store that can compare it against the held
     *  lock's start refuses to delete a SUCCESSOR lock acquired while the probe
     *  was in flight. A store that cannot make that comparison may ignore the
     *  fence, but must not substitute its own `Date.now()` — that timestamp is
     *  by construction newer than any successor and makes the check vacuous.
     *
     *  Returns whether the release actually landed — `false` when the lock was
     *  already gone (someone else got there first), which is reported, never
     *  treated as a release. */
    release(fence: {
        observedAt: number;
    }): boolean | Promise<boolean>;
    /** Override {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS} (Rule 3's fallback). */
    graceMs?: number;
    /** Override {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS} (Rule 1's release). */
    terminalGraceMs?: number;
    /** Identity fields merged into every log line (workspace, thread, execution
     *  id — whatever makes the entry findable in the product's logs). */
    context?: Record<string, unknown>;
    /** Defaults to `console.warn`. Both the withheld and the force-released
     *  branches log; a force-release is never silent. */
    log?(message: string, meta: Record<string, unknown>): void;
    /** Injectable clock, for tests. */
    now?(): number;
}
interface ReconcileStaleTurnLockResult {
    released: boolean;
    /** Why the policy decided what it did — the probe's own diagnostics on the
     *  reachable path, the unreachable reason and lock age on the fallback. */
    diagnostics: Record<string, unknown>;
}
/**
 * Decide whether a held lock is stale and, if so, release it.
 *
 * Never provisions and never mutates anything but the lock: a reconciliation
 * attempt on a cold workspace leaves it cold.
 */
declare function reconcileStaleTurnLock(options: ReconcileStaleTurnLockOptions): Promise<ReconcileStaleTurnLockResult>;

/**
 * Sandbox lane: bridge a raw sandbox event stream (`streamSandboxPrompt`) into
 * the `ChatTurnProducer` shape agent-runtime's `handleChatTurn` consumes AND
 * the client vocabulary `/web-react`'s `dispatchChatStreamLine` already parses
 * (`text` / `reasoning` / `tool_call` / `tool_result` / `usage` /
 * `interaction`). Legal and tax each hand-rolled this mapping differently;
 * this is that middle, composed from `/stream`'s normalizers — no new loop
 * logic, no SDK import (the event source is an injected `AsyncIterable`).
 *
 * Alongside the live mapping it accumulates the PERSISTED projection — the
 * `message.parts` rows `/chat-store` stores — via `normalizePersistedPart` /
 * `mergePersistedPart` / `finalizeAssistantParts`, plus the usage receipt from
 * `step-finish` parts. `createChatTurnRoutes` reads both after drain.
 */

interface SandboxChatProducerOptions {
    /** The raw sandbox event stream (e.g. `streamSandboxPrompt(...)`). */
    events: AsyncIterable<unknown>;
    /** Recorded on the persisted assistant message. */
    model?: string;
    /** Which ask kinds the product renders a card for. Anything else is
     *  auto-declined (see `declineInteraction`) so the run never hangs in the
     *  broker waiting on a card no client will show. Default: question/plan. */
    isRenderableInteraction?: (kind: string) => boolean;
    /** Resolve a non-renderable ask (wire `respondToSessionInteraction` with the
     *  session's sidecar connection). Without it, non-renderable asks are only
     *  logged — the run stays blocked until the broker times out. */
    declineInteraction?: (id: string) => Promise<void>;
    log?: (message: string, meta?: Record<string, unknown>) => void;
}
declare function createSandboxChatProducer(options: SandboxChatProducerOptions): ChatTurnRouteProducer;

interface ChatRouteDurableProjection {
    observe(event: unknown): void | Promise<void>;
    materialize(): Array<Record<string, unknown>> | Promise<Array<Record<string, unknown>>>;
}
type ChatRouteDurableProjectionLogger = (message: string, meta?: Record<string, unknown>) => void;
/** Adds durable lifecycle projection to any producer lane without moving its
 * transport into agent-app. The projection is observed inline and its
 * materialized parts replace same-key pending snapshots after the stream
 * drains. Projection persistence is best-effort for the live lane: a store
 * outage must not terminate an otherwise healthy sandbox stream. Failures are
 * reported through the optional logger so products can retain diagnostics. */
declare function withDurableChatProjection(producer: ChatTurnRouteProducer, projection: ChatRouteDurableProjection, log?: ChatRouteDurableProjectionLogger): ChatTurnRouteProducer;

/**
 * `createUploadRoute` — the multimodal middle. Accepts multipart file uploads
 * and returns `PromptInputPart`-shaped descriptors the client echoes back on
 * send (`ChatTurnRequestPayload.parts`):
 *
 *   ≤ inlineMaxBytes (700 KiB default) → inline `data:` URI part — rides the
 *     turn body directly, no sandbox round trip.
 *   > inlineMaxBytes → written into the sandbox workspace (base64 through the
 *     structural `write` seam — `box.fs` satisfies it) and referenced by
 *     `path`. Mandatory two-step: the gateway caps request bodies at ~1 MiB,
 *     so a large file can never ride the prompt POST.
 *
 * The sink is structural (no sandbox-SDK import); products pass `box.fs`.
 *
 * @remarks Sole consumer today is the `--chat` scaffold (`create-agent-app
 * --chat` → `template-chat/src/chat.ts`), the reference multimodal path. The
 * fleet apps (gtm/tax/legal/insurance) each keep their OWN upload route into a
 * durable vault (KV, or AES-GCM-encrypted R2) — a different persistence model
 * from this route's inline-`data:`-or-ephemeral-sandbox-workspace split, so
 * they don't (and shouldn't) route through it. This stays the scaffold's proven
 * upload pattern, not a fleet primitive; keep that distinction in mind before
 * widening its surface.
 */

/** 700 KiB: base64 inflates ~4/3, so an inline part stays comfortably under
 *  the ~1 MiB gateway body cap alongside the JSON envelope. */
declare const UPLOAD_INLINE_MAX_BYTES: number;
/** 8 MiB default ceiling per file — one base64 `write` call handles it. Raise
 *  it only with a sink that can take the bigger single write. */
declare const UPLOAD_MAX_FILE_BYTES: number;
/** Structural match of the sandbox SDK's `box.fs` write surface (v0.10.5+:
 *  `encoding: 'base64'` is the worker-safe binary path). */
interface SandboxUploadSink {
    write(path: string, content: string, options?: {
        encoding?: 'utf8' | 'base64';
    }): Promise<unknown>;
}
type UploadAuthorization = {
    ok: true;
    /** Where large files land. Absent/null: only inline uploads are
     *  accepted and an over-inline-cap file is rejected with 413. */
    sink?: SandboxUploadSink | null;
    /** Per-request override of the workspace directory large files go to. */
    uploadDir?: string;
} | {
    ok: false;
    response: Response;
};
interface CreateUploadRouteOptions {
    /** Authenticate the caller and resolve the sandbox file sink (usually
     *  `ensureWorkspaceSandbox(...)` → `box.fs`). */
    authorize(args: {
        request: Request;
    }): Promise<UploadAuthorization>;
    /** Inline-vs-sandbox threshold. Default {@link UPLOAD_INLINE_MAX_BYTES}. */
    inlineMaxBytes?: number;
    /** Hard per-file cap. Default {@link UPLOAD_MAX_FILE_BYTES}. */
    maxFileBytes?: number;
    /** Workspace directory for path-ref files. Default `'uploads'`. */
    uploadDir?: string;
}
/** One uploaded file, ready for the composer chip and the turn body. */
interface UploadedChatFile {
    id: string;
    name: string;
    size: number;
    mediaType: string;
    /** True when the part carries the bytes inline (`data:` URI). */
    inline: boolean;
    /** Echo this back verbatim in `ChatTurnRequestPayload.parts`. */
    part: ChatTurnFilePartInput;
}
/** Path-safe file name: basename only, conservative charset, length-capped. */
declare function sanitizeUploadFilename(name: string): string;
declare function bytesToBase64(bytes: Uint8Array): string;
declare function createUploadRoute(options: CreateUploadRouteOptions): (request: Request) => Promise<Response>;

export { type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, DEFAULT_STALE_TURN_LOCK_GRACE_MS, DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS, type ReconcileStaleTurnLockOptions, type ReconcileStaleTurnLockResult, type SandboxChatProducerOptions, type SandboxUploadSink, type StaleTurnLockSandboxProbeResult, type StaleTurnLockSessionProbeResult, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, bytesToBase64, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, reconcileStaleTurnLock, sanitizeUploadFilename, withDurableChatProjection };
