import { L as ChatTurnRequestPayload, M as ChatTurnPartInput, g as ChatMessagePart, N as ChatTurnFilePartInput, a as ChatAttachmentPart, C as ChatAttachmentKind, f as ChatMentionPart } from '../parts-IB-Kbb7z.js';
export { O as ChatAttachmentInput, e as ChatMentionKind, P as ChatTurnInputError, Q as ChatTurnTextPartInput, R as DISPATCH_MAX_MEDIA_PARTS, T as DISPATCH_MAX_PARTS, U as DISPATCH_REQUEST_MAX_BYTES, V as DISPATCH_STRUCTURAL_RESERVE_BYTES, K as FileMention, W as FileMentionsToPartsOptions, X as INLINE_PARTS_MAX_BYTES, Y as MENTION_MAX_COUNT, Z as SandboxMentionPathCheck, _ as assertPromptPartsWithinCap, $ as base64WireLen, a0 as buildMentionPromptBlock, a1 as chatTurnRequestInit, a2 as fileMentionsToParts, a3 as formatBytes, a4 as mediaTypeForMentionPath, a5 as mentionKindForPath, a6 as parseChatTurnParts, a7 as parseFileMentions, a8 as promptPartsByteSize, a9 as validateSandboxMentionPath } from '../parts-IB-Kbb7z.js';
export { A as ALLOWED_ATTACHMENT_SNIFFED_MIMES, a as ATTACHMENT_ACCEPT, b as ATTACHMENT_MAX_COUNT, c as AttachmentTypeCheckResult, C as CreateSandboxFileIndexRouteOptions, F as FileIndexAuthorization, d as FileIndexCache, e as FileIndexReadyResponse, f as FileIndexResponse, g as FileIndexWarmingResponse, M as MAX_ATTACHMENT_TOTAL_BYTES, h as MAX_BINARY_ATTACHMENT_BYTES, i as MAX_TEXT_ATTACHMENT_BYTES, S as SandboxFileTreeSource, j as SandboxTreeFile, k as SandboxTreeResult, l as SniffResult, m as attachmentSizeErrorMessage, n as attachmentTotalSizeErrorMessage, o as checkAttachmentType, p as createSandboxFileIndexRoute, s as sanitizeAttachmentFileName, q as sniffBinary } from '../attachment-validation-DX2KIzMC.js';
import { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtime';
import { InteractionAnswerRoute, InteractionAnswerRouteOptions } from '../interactions/index.js';
import { PersistedChatMessageForTurn } from '../stream/index.js';
import { d as TurnEventStore } from '../turn-buffer-C9mEgoop.js';
export { D as DEFAULT_STALE_TURN_LOCK_GRACE_MS, a as DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS, R as ReconcileStaleTurnLockOptions, b as ReconcileStaleTurnLockResult, S as StaleTurnLockSandboxProbeResult, c as StaleTurnLockSessionProbeResult, r as reconcileStaleTurnLock } from '../stale-turn-lock-C8Na1cFZ.js';
import { J as JsonRecord } from '../stream-normalizer-DWvtmY6F.js';
import { SandboxExecChannel, PromptInputPart } from '../sandbox/index.js';
import '@tangle-network/agent-interface';
import '../contract-KfqJh_au.js';
import '../plans/index.js';
import '@tangle-network/sandbox';
import '../auth-DuptSkWh.js';
import '../types-BEOvc_ue.js';
import '../harness/index.js';
import '../model-dF2h4xT9.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`, `heartbeat`, and `turnLock` are generic and
 * stable (`turnLock` graduated with `/turn-stream`'s shared DO adapter, #221).
 * `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)` or `/turn-stream`'s
     *  `createDurableObjectTurnEventStore(env.TURN_STREAM_DO)` 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). `/turn-stream`'s
     *  `createDurableTurnLock` is the shared DO-backed implementation. Omit →
     *  no lock. */
    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;

/**
 * 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.
 */

/** Outcome of a `promoteFilePart` attempt. `key`, when given, becomes the
 *  persisted part's row key (e.g. `attachment:<path>`) so repeat promotions
 *  of the same underlying file fold into one segment instead of appending;
 *  omitted, the default `getPartKey` keying applies.
 *
 *  On failure, `part` is an OPTIONAL substitute part to persist in place of
 *  the raw url-bearing one — this is how a product swaps in a transcript
 *  notice (gtm persists a `warning` notice part, never the transient url) for
 *  a failed promotion instead of baking a `data:`/sandbox-path url into the
 *  durable row. When `part` is present it is persisted (via the same
 *  `recordPersistedPart` path as a success, honoring the optional `key`);
 *  when absent, the existing raw-part fallback applies unchanged — so a
 *  caller that only returns `{ succeeded: false, reason }` keeps today's
 *  behavior verbatim. */
type FilePartPromotionOutcome = {
    succeeded: true;
    part: Record<string, unknown>;
    key?: string;
} | {
    succeeded: false;
    reason: string;
    part?: Record<string, unknown>;
    key?: string;
};
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>;
    /** Opt-in eager promotion of harness-emitted `file` parts. Unset, a `file`
     *  part persists exactly as the harness sent it — a transient `url` (a
     *  `data:` URI or in-sandbox path) baked into the transcript, which is
     *  today's behavior and stays byte-identical if this is never wired. Set,
     *  EVERY `file` part (never `image`, never any other kind) is routed
     *  through this callback instead of `recordPersistedPart`'s default
     *  fallback — including a part with NEITHER `id` NOR `url` (gtm always
     *  attempts promotion; such a part simply fails "carries no url" and
     *  resolves through the same failure path as any other rejection, rather
     *  than being persisted raw and unpromoted) — so the product can durably
     *  write the bytes and swap in a path-bearing part before the raw url ever
     *  reaches the persisted transcript. Keyed per source-prefixed `id:<id>` /
     *  `url:<url>` (an `id` and a `url` sharing the same text must never collide
     *  onto one memo entry) and memoized by PROMISE (not result), so re-emitted
     *  snapshot events for the same part —
     *  the harness resends the whole part on every update, not just deltas —
     *  fold onto the one in-flight or settled attempt rather than promoting
     *  twice or racing two concurrent writes; a raw part with neither `id` nor
     *  `url` cannot be keyed, so it is invoked UN-memoized (once per event) —
     *  each occurrence is its own attempt. A rejecting promise is caught,
     *  logged via `log`, and treated as `succeeded: false`. On `succeeded:
     *  false` the outcome's optional `part` (a substitute — e.g. a warning
     *  notice — see {@link FilePartPromotionOutcome}) persists in its place when
     *  given; otherwise the raw part persists exactly as it does today — this
     *  seam only decides whether to call the promoter and what to do with its
     *  outcomes; the promotion mechanics (vault write, key derivation, notice
     *  construction) live in the caller's callback, not here. */
    promoteFilePart?: (raw: JsonRecord) => Promise<FilePartPromotionOutcome>;
    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 — its
 * inline-`data:`-or-ephemeral-sandbox-workspace split stays the scaffold's
 * proven upload pattern, not a fleet primitive; keep that distinction in mind
 * before widening its surface. Fleet apps with a durable store of their own
 * (KV, or AES-GCM-encrypted R2) no longer need to hand-roll a vault upload
 * route: `createAttachmentUploadRoute` (`./attachment-upload`, agent-app#234)
 * is the shared hardened path for that persistence model — a content-sniffed
 * type gate, two-phase atomic batch writes, and per-kind/aggregate size caps,
 * all seamed through an injected `WriteAttachmentFn`. Point readers there
 * instead of widening this route to cover both models.
 */

/** 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>;

/**
 * Storage seams for the chat-attachment vertical (`resolveChatAttachments`,
 * `buildDispatchParts`, `promoteAgentFilePart`). Structural function ports in
 * the same style as `upload.ts`'s `SandboxUploadSink`: REQUIRED injection, no
 * default implementation — agent-app owns the size/budget/idempotency
 * mechanism, the product owns where the bytes actually live.
 *
 * One reader, two callers: `resolveChatAttachments` reads only the
 * authoritative `size` (never trusting the client-reported size), while
 * `buildDispatchParts` reads the inline `content` (base64 or raw bytes) to
 * build a `data:` URI. Both fit behind ONE `ReadAttachmentFn` so a product
 * wires a single vault/object-store adapter, not two. gtm's KV vault (stores
 * base64 bodies) and `/object-store`'s `ObjectStore` (hands back raw bytes)
 * both satisfy this shape.
 */
/**
 * The result of reading one stored attachment. `ok:true` MUST carry the
 * authoritative decoded byte `size` (the cap is meaningless against a
 * client-controlled number) and, when the caller needs to inline the file,
 * its content as `base64` and/or raw `bytes`. `mediaType` is the stored
 * content type when the store knows it (used as the fallback when the wire
 * part carried none). `ok:false` carries a human `reason` that SHOULD name the
 * offending path — it is surfaced verbatim in the caller's typed outcome.
 */
type AttachmentReadResult = {
    ok: true;
    /** Authoritative decoded byte length of the stored content. */
    size: number;
    /** Inline content as base64 — reused verbatim for a `data:` URI, so a
     *  store holding already-base64 bodies never decodes-and-re-encodes. */
    base64?: string;
    /** Inline content as raw bytes — base64-encoded by the caller when a
     *  `data:` URI is needed. Ignored when `base64` is present. */
    bytes?: Uint8Array;
    /** Stored content type, when the store tracks one. */
    mediaType?: string;
} | {
    ok: false;
    reason: string;
};
/**
 * Read one stored attachment for `scopeId` (the product's workspace/tenant
 * key) at its store-relative `path`. Missing, deleted, or unreadable content
 * MUST resolve to `{ ok: false, reason }`, never throw — a store failure is a
 * per-attachment outcome the caller folds into its own typed result, not a
 * turn-level exception.
 */
type ReadAttachmentFn = (scopeId: string, path: string) => Promise<AttachmentReadResult>;
/** Outcome of persisting one attachment. Mirrors `AttachmentReadResult`'s
 *  `ok`/`reason` shape and `upload.ts`'s `{ ok }` convention. */
type AttachmentWriteResult = {
    ok: true;
} | {
    ok: false;
    reason: string;
};
/**
 * Persist `content` for `scopeId` at `path`. `content` is either raw `bytes`
 * or a base64 `string` — a string argument is ALWAYS base64 (never utf8), so
 * a store that speaks base64 (gtm's vault) writes it verbatim and one that
 * speaks bytes decodes once. Like the reader, failures resolve to
 * `{ ok: false, reason }` rather than throwing.
 *
 * `opts` mirrors the vault frontmatter gtm's `writeAttachmentVaultFile`
 * persists alongside the body (promote-file-parts.ts:181-190), so a product
 * reimplementing that vault writer through this seam can reproduce it
 * exactly:
 * - `mediaType` — the resolved MIME type; gtm's frontmatter key `mime`.
 * - `name` — the sanitized (store-path-safe) display filename; gtm passes
 *   this only to shape its oversize message, not into frontmatter.
 * - `originalName` — the filename as the harness/browser reported it, BEFORE
 *   sanitization (`raw.filename ?? filename` — falls back to the sanitized
 *   name when the source carried none); gtm's frontmatter key `originalName`.
 *   This is the one field with no other recovery path once sanitization has
 *   run, so it must ride the write, not be re-derived after the fact.
 * - `size` — the authoritative decoded byte length being written; gtm's
 *   frontmatter key `size`.
 */
type WriteAttachmentFn = (scopeId: string, path: string, content: Uint8Array | string, opts: {
    mediaType?: string;
    name?: string;
    originalName?: string;
    size?: number;
}) => Promise<AttachmentWriteResult>;

/**
 * `resolveChatAttachments` — validate a turn body's `attachments` field into
 * persistable {@link ChatAttachmentPart}s. Every path is re-validated (a path
 * off the wire is never trusted to stay inside the store root) and every size
 * is re-derived from the STORED body via the injected {@link ReadAttachmentFn},
 * never the client-reported `size` — the upload path lets a caller rewrite its
 * own frontmatter, so a stored size cannot bound anything and the wire size can
 * be anything. Both the aggregate cap and the size carried on the returned part
 * come from the authoritative read.
 *
 * Storage-parameterized: the frontmatter parsing / base64 sizing that derives
 * the authoritative size lives BEHIND `readAttachment` (a product's vault or
 * object-store adapter), so this module is a pure validator + budget gate with
 * no store knowledge. Lifted from gtm-agent's `resolve-attachments.ts`
 * (workspaceId → scopeId, the vault read → the injected reader) and kept
 * behavior-identical for gtm-agent#618 adoption.
 */

type ResolveChatAttachmentsResult = {
    succeeded: true;
    value: ChatAttachmentPart[];
} | {
    succeeded: false;
    error: string;
};
/** Verdict of a path check: OK, or a rejection naming why. Mirrors
 *  `SandboxMentionPathCheck` in `./wire`. */
type AttachmentPathCheck = {
    succeeded: true;
} | {
    succeeded: false;
    error: string;
};
/**
 * Default path validator when a caller supplies none. Rejects the ways a path
 * picked in a client can escape the store root — traversal (`..` segment),
 * absolute (leading `/`), backslashes, null bytes, control characters (see
 * {@link CONTROL_CHARS} — a path also feeds {@link buildAttachmentPromptBlock}'s
 * `(vault: ${path})` pointer, so it is exposed to the same injection surface as
 * `name`) — plus a dotfile/hidden segment (a leading `.` on any segment).
 * Generalized from gtm's `validateVaultFilePath`, in the spirit of
 * `validateSandboxMentionPath` (`/chat-routes`'s wire mention-path validator) —
 * but the dotfile rejection here is INTENTIONALLY stricter than that sibling:
 * an uploaded attachment path is sanitized store-relative storage the product
 * itself assigned, whereas a mention path points at a file that already exists
 * in the sandbox and may legitimately live under a dotfile segment. A caller
 * that needs gtm's exact (looser) rule can supply `validatePath` to override
 * this default entirely.
 */
declare function defaultValidateAttachmentPath(path: string): AttachmentPathCheck;
interface ResolveChatAttachmentsOptions {
    /** The product's workspace/tenant key, passed to `readAttachment`. */
    scopeId: string;
    /** Authoritative size + content reader — see {@link ReadAttachmentFn}. */
    readAttachment: ReadAttachmentFn;
    /** Most attachments one request may carry. Default {@link ATTACHMENT_MAX_COUNT}. */
    maxCount?: number;
    /** Aggregate raw-byte ceiling. Default {@link MAX_ATTACHMENT_TOTAL_BYTES}. */
    maxTotalBytes?: number;
    /** Path validator override. Default {@link defaultValidateAttachmentPath}. */
    validatePath?: (path: string) => AttachmentPathCheck;
}
/**
 * Validate and resolve a turn body's `attachments` field into persistable
 * parts. Every path is confirmed present (and not deleted) in the caller's own
 * store by `readAttachment` before it is trusted, and size is derived from the
 * authoritative read for both the aggregate cap and the returned part's size.
 */
declare function resolveChatAttachments(value: unknown, options: ResolveChatAttachmentsOptions): Promise<ResolveChatAttachmentsResult>;

/**
 * `createAttachmentUploadRoute` — the fleet-primitive durable-store upload
 * route: a two-phase atomic batch (every file is validated before any file is
 * written — a batch never partially lands), a content-sniffed type gate
 * (`checkAttachmentType` over `sniffBinary`'s magic-byte read, not the
 * extension or the browser-reported MIME), per-kind + aggregate byte caps,
 * and sanitized filenames. Storage is fully seamed through the injected
 * `WriteAttachmentFn` (`./attachment-store`) — no default store, the product
 * owns where bytes actually live (vault, object store, …) — and auth/rate
 * limiting is entirely the injected `authorize` seam's job: this factory
 * never invents a 401 or 429 response, it only returns `auth.response`
 * verbatim on failure.
 *
 * Lifted from gtm-agent's `src/routes/api.vault.upload.ts` (the hardening
 * lineage other lifted modules in this vertical cite: gtm#584 binary
 * corruption, gtm#592 sniff gate/caps, gtm#593 batch-atomic writes) and
 * generalized the way `resolve-attachments.ts` generalized gtm's read path —
 * the vault-specific pieces (KV vault paths, frontmatter, per-user rate
 * limiting) are all injected seams here, while the validate-then-write phase
 * split and the type/size gate ordering survive byte-for-byte.
 *
 * @remarks Sibling to, NOT an extension of, `./upload.ts`'s
 * `createUploadRoute` — a different persistence model (durable product store
 * vs. inline-`data:`-or-ephemeral-sandbox-workspace). See that module's doc
 * comment for the up-to-date framing between the two.
 */

/** Outcome of the injected `authorize` seam: auth + rate limiting +
 *  scope resolution, all in one place so a 429 rides `{ok:false, response}`
 *  exactly like a 401 does — this factory has no rate-limit opinion of its
 *  own. `writeAttachment` lets a single request override the option-level
 *  store (e.g. routing per-tenant), defaulting to `options.writeAttachment`
 *  when absent. */
type AttachmentUploadAuthorization = {
    ok: true;
    scopeId: string;
    writeAttachment?: WriteAttachmentFn;
} | {
    ok: false;
    response: Response;
};
interface CreateAttachmentUploadRouteOptions {
    /** Authenticate the caller, rate-limit, and resolve the store scope
     *  (workspace/tenant id) — never a query param. */
    authorize(args: {
        request: Request;
    }): Promise<AttachmentUploadAuthorization>;
    /** Default store writer. `authorize` may override it per-request. */
    writeAttachment: WriteAttachmentFn;
    /** Overridable caps. Defaults come from `./attachment-validation`. */
    limits?: {
        /** Most files one request may carry. Default {@link ATTACHMENT_MAX_COUNT}. */
        maxCount?: number;
        /** Ceiling on a binary file's raw size. Default {@link MAX_BINARY_ATTACHMENT_BYTES}. */
        maxBinaryBytes?: number;
        /** Ceiling on a text file's raw size. Default {@link MAX_TEXT_ATTACHMENT_BYTES}. */
        maxTextBytes?: number;
        /** Aggregate raw-byte ceiling across the batch. Default {@link MAX_ATTACHMENT_TOTAL_BYTES}. */
        maxTotalBytes?: number;
    };
    /** Attachment kinds this route accepts. Default `['image', 'file']`. */
    allowedKinds?: ChatAttachmentKind[];
    /** Sniffed-mime allowlist fed to `checkAttachmentType`. Default
     *  {@link ALLOWED_ATTACHMENT_SNIFFED_MIMES}. */
    allowedSniffedMimes?: ReadonlySet<string>;
    /** Sanitized-name → store path. Default identity (the sanitized name IS
     *  the path); gtm passes `vaultFolderForFileName`, a tenant product a
     *  scope prefix. */
    pathFor?: (name: string) => string;
    /** Store-path validator. Default {@link defaultValidateAttachmentPath}. */
    validatePath?: (path: string) => AttachmentPathCheck;
    /** Last-resort media-type hook for text content the sniffer can't type.
     *  Default {@link sniffMimeFromName}. */
    sniffMime?: (name: string) => string;
}
declare function createAttachmentUploadRoute(options: CreateAttachmentUploadRouteOptions): (request: Request) => Promise<Response>;

/**
 * `buildDispatchParts` — assemble the `PromptInputPart[]` a turn carrying
 * attachments and/or `@`-mentions dispatches to the sandbox. `parts[0]` is
 * always the full prompt text (typed text plus the attachment + mention pointer
 * blocks); each attachment or mention becomes one media part. An attachment
 * (read from the product store via the injected reader) draws the inline byte
 * budget first; a mention (read from the LIVE box) takes what is left. A file
 * inlines as a `data:` URI when it fits the remaining budget, otherwise demotes
 * to an in-box path part so the whole request stays under the proxy cap. Every
 * media part is deduped by its resolved absolute path. This module only
 * produces the parts array; the caller decides when a turn dispatches parts
 * instead of a plain string.
 *
 * Storage-parameterized port of gtm-agent's `dispatch-parts.ts`: the vault
 * default reader is dropped (`readAttachment` is REQUIRED — the product supplies
 * its store adapter), the `GTM_SANDBOX_VAULT_DIR` prefixing becomes the required
 * `resolveAttachmentPath` seam, the `GTM_MULTIMODAL_FORCE_PATH` env fallback
 * becomes an explicit `forcePath` flag, and every budget cap reads an overridable
 * `./wire` constant. Kept behavior-identical for gtm-agent#618 adoption (the
 * demotion math and emitted part shapes reproduce its dispatched prompt bytes).
 */

type DispatchPartsOutcome = {
    succeeded: true;
    value: PromptInputPart[];
} | {
    succeeded: false;
    error: string;
};
/** One mention file's size (always) and inline bytes (only when the caller
 *  asked for them — a path-only mention never reads its bytes). */
type SandboxMentionReadOutcome = {
    succeeded: true;
    value: {
        size: number;
        base64?: string;
    };
} | {
    succeeded: false;
    error: string;
};
type ReadSandboxMentionFn = (box: SandboxExecChannel, absolutePath: string, options: {
    readBytes: boolean;
}) => Promise<SandboxMentionReadOutcome>;
interface BuildDispatchPartsInput {
    text: string;
    attachments: ChatAttachmentPart[];
    mentions?: ChatMentionPart[];
    history: Array<{
        role: 'user' | 'assistant';
        content: string;
    }>;
    systemPrompt: string;
    /** Serialized size of the backend profile the SDK inlines into the same
     *  prompt request body — a large, non-negotiable rider that must come out of
     *  the inline budget or near-cap attachments 413 at the proxy instead of
     *  demoting to path parts. */
    profileWireBytes: number;
    /** The product's workspace/tenant key, passed to `readAttachment`. */
    scopeId: string;
    /** Maps an attachment's store-relative path to the in-box absolute path a
     *  path-based part references (same seam style as `fileMentionsToParts`'s
     *  `resolvePath`). */
    resolveAttachmentPath: (path: string) => string;
    /** Maps a mention's workspace-relative path to its in-box absolute path.
     *  Default: {@link BuildDispatchPartsInput.resolveAttachmentPath} — in gtm the
     *  vault mount roots both; a product that mounts them apart overrides this. */
    resolveMentionPath?: (path: string) => string;
    /** The turn's already-ensured box — required when `mentions` is non-empty
     *  (mention bytes are read from the live box, not the store). */
    box?: SandboxExecChannel;
    /** Force every media part to a path reference, skipping all inlining. */
    forcePath?: boolean;
    /** REQUIRED store reader for attachment content — no default (the product
     *  owns its store; see {@link ReadAttachmentFn}). */
    readAttachment: ReadAttachmentFn;
    readSandboxMention?: ReadSandboxMentionFn;
    /** Whole-request proxy cap. Default {@link DISPATCH_REQUEST_MAX_BYTES}. */
    requestMaxBytes?: number;
    /** JSON-envelope reserve off the top of the request cap. Default
     *  {@link DISPATCH_STRUCTURAL_RESERVE_BYTES}. */
    structuralReserveBytes?: number;
    /** Sidecar per-request parts-array cap. Default {@link DISPATCH_MAX_PARTS}. */
    maxParts?: number;
}
declare function buildDispatchParts(input: BuildDispatchPartsInput): Promise<DispatchPartsOutcome>;

/**
 * `promoteAgentFilePart` — turn a harness-emitted `type:"file"` stream part
 * into a store-backed {@link ChatAttachmentPart}. The harness hands back a URL
 * pointing at bytes it produced (a `data:` URI, or a path inside the sandbox);
 * nothing durable survives past the turn unless it is written into the
 * product's store, the same way a user upload is. Typed outcomes throughout:
 * every failure mode (unsupported scheme, no sandbox, oversize, store-write
 * failure, malformed part) resolves to `{ succeeded: false, filename, reason }`
 * rather than throwing past this boundary, so the caller folds a visible notice
 * instead of losing the file silently.
 *
 * Storage-parameterized port of gtm-agent's `promote-file-parts.ts` with the
 * refactor gtm never made: persistence goes through the injected
 * {@link WriteAttachmentFn} (gtm hard-wired its vault writer), the path strategy
 * is the injected `buildAttachmentPath` (neutral `uploads/agent/<date>/` default,
 * no domain bucket taxonomy baked), the MIME map is an injectable hook, and the
 * date segment reads an injectable clock. The idempotent `hash8(id ?? url ??
 * filename)` naming is preserved so re-promoting the same source part resolves
 * to the same path.
 */

/** Default ceiling on a promoted file's raw (pre-encoding) byte size. */
declare const PROMOTE_MAX_FILE_BYTES: number;
interface RawAgentFilePart {
    type: 'file';
    id?: string;
    filename?: string;
    /** AI-SDK-shaped parts carry the MIME type here… */
    mediaType?: string;
    /** …but OpenCode's native FilePart calls the same field `mime`. */
    mime?: string;
    url?: string;
}
type PromoteFilePartResult = {
    succeeded: true;
    part: ChatAttachmentPart;
} | {
    succeeded: false;
    filename: string;
    reason: string;
};
/** Arguments handed to a {@link PromoteAgentFilePartOptions.buildAttachmentPath}
 *  override — everything needed to place the file deterministically. */
interface AttachmentPathArgs {
    /** Sanitized display filename (basename, safe charset). */
    filename: string;
    /** First 8 hex chars of the SHA-256 idempotency digest. */
    hash8: string;
    /** `YYYY-MM-DD` from the injected clock. */
    date: string;
    /** Resolved media type. */
    mediaType: string;
    /** `image`/`file` split of the media type. */
    kind: ChatAttachmentKind;
}
/** Default MIME hook: extension → mime, or `text/plain` for the unknown. */
declare function sniffMimeFromName(filename: string): string;
interface PromoteAgentFilePartOptions {
    raw: RawAgentFilePart;
    /** The turn's box — required only to promote a sandbox-path part; a `data:`
     *  URI needs none. */
    box?: SandboxExecChannel;
    /** The product's workspace/tenant key, passed to `writeAttachment`. */
    scopeId: string;
    /** The turn's session id, used for the sandbox stat/read exec calls. */
    sessionId: string;
    /** REQUIRED store writer — no default (the product owns its store). */
    writeAttachment: WriteAttachmentFn;
    /** Store-path strategy. Default {@link defaultBuildAttachmentPath}. */
    buildAttachmentPath?: (args: AttachmentPathArgs) => string;
    /** Raw-byte ceiling. Default {@link PROMOTE_MAX_FILE_BYTES}. */
    maxBytes?: number;
    /** Last-resort media-type hook. Default {@link sniffMimeFromName}. */
    sniffMime?: (filename: string) => string;
    /** Clock for the date path segment. Default `() => new Date()`. */
    now?: () => Date;
}
declare function promoteAgentFilePart(options: PromoteAgentFilePartOptions): Promise<PromoteFilePartResult>;

export { type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentUploadAuthorization, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, 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 CreateAttachmentUploadRouteOptions, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DispatchPartsOutcome, type FilePartPromotionOutcome, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, buildDispatchParts, bytesToBase64, createAttachmentUploadRoute, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, promoteAgentFilePart, resolveChatAttachments, sanitizeUploadFilename, sniffMimeFromName, withDurableChatProjection };
