import { DocumentExtraction, DocumentFormat } from './documentText'; import { AssistantPromptPart } from './types'; export declare const MAX_ATTACHMENTS = 5; /** Per-file ceiling before downscaling (images) / rejection (everything else). */ export declare const MAX_FILE_BYTES: number; /** * Media types a provider will accept as a URL-only `file` part. * * ★ An ALLOW-list on purpose. The provider SDK throws * `UnsupportedFunctionalityError: media type: ` for anything outside its * accepted set, and that throw kills the turn silently — so an unknown type * must be DROPPED, never forwarded hopefully. Widen this only once the * provider genuinely accepts the type. * * `text/plain` is here because it is the one text type providers take * verbatim. Every other text type (`text/markdown`, `text/csv`, …) is dropped * and reaches the model through the inlined prompt block instead — the same * content, by a channel that cannot fail. */ export declare const PROVIDER_READABLE_FILE_MEDIA_TYPES: readonly ["application/pdf", "text/plain"]; /** * Whether a `file` part carrying this media type is safe to send. * * Case- and parameter-insensitive, because a browser may report * `text/plain; charset=utf-8`. Images are accepted for completeness though * they normally travel as `image` parts with inline bytes. */ export declare function isProviderReadableFilePart(mimeType: string | undefined): boolean; /** * Per-file inline budget for text-like content, in characters — the DEFAULT. A * mode may set its own (`AssistantModeConfig.maxInlineCharsPerFile`); see * `resolveAssistantInlineLimits`. */ export declare const MAX_INLINE_CHARS_PER_FILE = 8000; /** * Total inline budget across all attachments in one turn — the DEFAULT. A mode * may set its own (`AssistantModeConfig.maxInlineCharsTotal`). */ export declare const MAX_INLINE_CHARS_TOTAL = 20000; /** * How much attachment TEXT one turn may carry inline: per file, and across all * of them (BOFF-7334). * * ★★ Per mode, because one size did not fit. 8,000 characters a file is right * for a question about a spreadsheet and far too little for a mode that BUILDS * from one — BigConsole's Build designs a data sink from the CSV it is handed, * and its own transport used to send 100,000 characters of it (decision D13: * "per-mode inline cap for BC Build"). Every other mode keeps the defaults, and * with them this module's behaviour byte for byte. */ export interface AssistantInlineLimits { /** Characters of any one file's text that may travel inline. */ readonly perFileChars: number; /** Characters of text, across every file in the turn, that may travel inline. */ readonly totalChars: number; } /** What every mode gets unless it says otherwise. */ export declare const DEFAULT_INLINE_LIMITS: AssistantInlineLimits; /** The fields of a mode's config that set its limits. */ export interface AssistantInlineLimitsConfig { readonly maxInlineCharsPerFile?: number; readonly maxInlineCharsTotal?: number; } export type AssistantAttachmentKind = "image" | "text" | "document"; export type AssistantAttachmentStatus = "pending" | "uploading" | "ready" | "error"; export type AttachmentRejection = "too-many" | "too-large" | "empty" | "unreadable" | "unsupported-image"; /** * Thrown while PREPARING a file (after it passed `validateFile`) to reject it * with a specific reason rather than the generic 'unreadable'. * * `validateFile` runs before the bytes have been touched, so it cannot know * whether an oversized image will actually survive the downscale — that verdict * only exists once the browser has tried to rasterise it. */ export declare class AttachmentRejectedError extends Error { readonly reason: AttachmentRejection; constructor(reason: AttachmentRejection); } export interface AssistantAttachment { /** Client-side id; stable for the lifetime of the draft + the sent message. */ id: string; filename: string; mimeType: string; /** Size of the bytes we actually kept (post-downscale for images). */ size: number; kind: AssistantAttachmentKind; status: AssistantAttachmentStatus; /** Object/data URL for the composer thumbnail. Images only; never persisted. */ previewUrl?: string; /** * A smaller `data:` URL of the same image, sized for the request body rather * than for the eye. Preferred over `previewUrl` when building the multimodal * parts; see `MAX_TURN_BODY_BYTES`. Images only; never persisted. */ inlineDataUrl?: string; /** wspace-files-svc id, once the upload lands. */ fileId?: string; /** Short-lived presigned URL, resolved at send time. */ downloadUrl?: string; /** * Inlined content for this attachment, already truncated to the per-file * limit it was prepared with (`largestInlineCharsPerFile` of the modes on * offer; `MAX_INLINE_CHARS_PER_FILE` by default). The send cuts it again to * the sending mode's limit. Text files carry the file itself; PDF/DOCX/XLSX * carry whatever `extractDocumentText` pulled out of them (BOFF-6291). */ textContent?: string; /** True when `textContent` is a prefix of a longer file. */ textTruncated?: boolean; /** * Outcome of the client-side document extraction. Present on every `document` * we ATTEMPTED to read — including the failures, which is the point: a scanned * or encrypted PDF has to reach the model as a sentence rather than as * nothing. Absent means no extractor exists for the type. */ extraction?: DocumentExtraction; /** * Per-page text for a PDF, kept so the SHARED inline budget can also cut on a * page boundary rather than mid-sentence. Already trimmed to the per-file * budget at prepare time, so this never holds a whole 50-page document. * Draft-only; `toMessageAttachment` never carries it to storage. */ documentPages?: string[]; /** Human-readable failure reason when `status === 'error'`. */ error?: string; /** * Why an upload that is still `uploading` is taking so long — e.g. "File service * is starting up — retry 2 of 3". * * wspace-files-svc runs at minReplicas 0 and needs >=51s to bind its port against * the gateway's 15s budget, so the first attachment after a quiet period * legitimately sits for about a minute while fe-libs walks its retry ladder. A * spinner alone reads as a hang. Draft-only, and NEVER a failure: `status` is still * `uploading` and `error` is still unset. */ progressNote?: string; } /** * The slice of an attachment that is safe to keep on a sent message. * * Conversations are persisted to (profile-scoped) localStorage by the assistant * store, so the heavy fields — the base64 `previewUrl` and the inlined * `textContent` — MUST NOT travel here: five 1280px JPEG data URLs would exhaust * the ~5 MB quota on their own and take the whole conversation history down with * them. The bubble re-resolves a thumbnail from `fileId` on demand instead, the * same way the pest-scan list does. */ export interface AssistantMessageAttachment { id: string; filename: string; mimeType: string; size: number; kind: AssistantAttachmentKind; fileId?: string; } export declare function toMessageAttachment(attachment: AssistantAttachment): AssistantMessageAttachment; export declare function getExtension(filename: string): string; export declare function classifyAttachment(mimeType: string, filename: string): AssistantAttachmentKind; /** * Document formats we can turn into text in the browser (BOFF-6291). * * Anything else stays a `document` with no extraction, and the prompt block * says so — the honest fallback that used to be the ONLY behaviour. */ export type { DocumentFormat } from './documentText'; /** * Which extractor (if any) can read this file. * * MIME first, extension second — same order as `classifyAttachment`, and for * the same reason: Windows reports `application/octet-stream` for plenty of * files whose extension is perfectly informative. */ export declare function detectDocumentFormat(mimeType: string, filename: string): DocumentFormat | null; /** * The composer's `` — DERIVED from what we can actually read. * * It used to be a hand-written list (`…,.pdf,.csv,.json,.md,.txt,.xlsx,.docx`) * that invited exactly the files the model could not see: a farmer picked a * soil-test PDF because the picker offered it, and the assistant then asked * them to paste it as text. Building the list from `TEXT_EXTENSIONS` and * `DOCUMENT_EXTENSIONS` makes that class of mismatch a compile-time * impossibility rather than a thing to remember. */ export declare const ATTACHMENT_ACCEPT: string; export declare function formatBytes(bytes: number): string; /** * Is there attachment work in flight that must block sending? * * `preparingCount` counts files the user has ALREADY dropped/picked but whose * image-downscale or text-read has not resolved yet. Those files have no id and * no row in `attachments`, so a check that looked only at `attachments` reported * "idle" during the whole preparation window — the composer stayed sendable, the * turn went out without the photo, and the photo then attached itself to the * user's NEXT message. Both halves must be counted. */ export declare function hasAttachmentWorkInFlight(preparingCount: number, attachments: AssistantAttachment[]): boolean; /** * The attachments a send would ACTUALLY carry to the model. * * This is the single definition of "sendable" — `buildAttachmentPromptBlock`, * `buildPromptParts`, `countInlineImageCandidates` and the composer's send * handler all route through it, so the set the user is shown and the set the * model receives cannot drift apart by one of them being updated and the others * not. * * `status` is the whole test, and that is deliberate rather than lazy: an * attachment whose content is already inlined does not need its upload to have * worked, so `useAssistantAttachments` marks a text file — and, since * BOFF-6291, a document whose text WAS extracted — `ready` even when the upload * failed. Such a file is sendable and does not block: its contents travel in * the prompt block, and all it lost is the URL. Only attachments with nothing * left to send (an image, or a document that could not be read) reach 'error', * and those are exactly the ones worth stopping a turn for. */ export declare function getSendableAttachments(attachments: AssistantAttachment[]): AssistantAttachment[]; /** Why a draft cannot be sent yet, as far as its attachments are concerned. */ export type AttachmentSendBlocker = "in-flight" | "failed"; /** * Whether the attachments block sending, and why — `null` when they do not. * * Two distinct reasons, deliberately not collapsed into one boolean: * * - `'in-flight'` is momentary and resolves itself; the composer shows a * spinner and the user just waits. * - `'failed'` never resolves on its own. It is the case this function was * added for: a failed attachment is omitted from the outgoing turn by * `getSendableAttachments`, but send used to stay enabled as long as there * was message text, and `clear()` then wiped the failed chip along with the * draft. So the user watched their photo's chip vanish into a sent message * that never contained it, and the assistant answered a question about an * image it was never given. Blocking is the honest response: the chip offers * both retry and remove, so the user picks whether the file matters, and * whichever they pick the UI and the model end up agreeing. * * In-flight is reported first: while an upload is still running the failure is * not yet final, and "wait" is better advice than "retry or remove". * * Nothing here needs to special-case text files or extracted documents: they * never reach 'error' in the first place (see `getSendableAttachments`), so a * soil-test PDF the browser read but the file library refused still sends, with * its text, and never blocks. */ export declare function getAttachmentSendBlocker(preparingCount: number, attachments: AssistantAttachment[]): AttachmentSendBlocker | null; /** * Gate a candidate file. Images over the ceiling are still accepted — the * composer downscales them before upload, so a 12 MB phone photo is fine while * a 12 MB PDF is not. * * That exemption is a PROMISE, not a fact: it holds only if the browser can * actually rasterise the image. When it cannot (HEIC), `prepareFile` re-applies * this ceiling to the original bytes and rejects there instead. */ export declare function validateFile(file: File, currentCount: number): AttachmentRejection | null; /** * Truncate on a line boundary where possible, so an inlined CSV/JSON prefix ends * at a row rather than mid-token (which reads as corrupt data to the model). */ export declare function truncateForInline(content: string, limit: number): { text: string; truncated: boolean; }; /** * What the modelled body genuinely misses: `buildPromptForMode` prepends its * preambles AFTER this block is built, and `parts[0]` duplicates the whole * prompt, so each is charged twice. * * api-calls preamble ~493 bytes x2 = ~986 * language directive ~290 bytes x2 = ~580 (BOFF-7107) * total ~1,566 * * The old 1,024 covered only the api-calls preamble, so adding the language * directive could push a turn that `fitInlineBudget` promised would keep its * pixels over the cap — and `buildPromptParts` would then demote the very image * the question was about. * * The GraphQL document and page context remain covered many times over by * `TURN_BODY_HEADROOM_BYTES`. Still deliberately tight: every byte here is a * byte of document text a farmer does not get. */ export declare const PROMPT_ENVELOPE_RESERVE_BYTES = 2048; /** * Render the attachment block appended to the outgoing prompt. Returns '' when * there is nothing to say, so callers can concatenate unconditionally. * * `userText` is the user's own message — the rest of the prompt this block will * be concatenated with. It is optional only so existing callers and tests keep * compiling; passing it is what lets the budget be measured against the real * turn rather than against the block alone. * * Attachments that failed to upload are omitted: the user already sees the * failure in the composer, and a half-described file only confuses the model. */ export declare function buildAttachmentPromptBlock(attachments: AssistantAttachment[], userText?: string, limits?: AssistantInlineLimits): string; /** * Compose the text actually sent to the agent: the user's words, then the * attachment block. The user-facing bubble keeps the raw text (attachments are * rendered as chips there), matching how `buildPromptForMode` already separates * agent-only preamble from displayed content. * * `limits` are the sending MODE's (`resolveAssistantInlineLimits`); the default * is what every mode had before modes could choose. */ export declare function buildPromptWithAttachments(content: string, attachments: AssistantAttachment[], limits?: AssistantInlineLimits): string; export type { AssistantPromptPart }; /** * How many images may travel INLINE in one turn. * * Inline bytes are what make vision work without the sandbox needing egress to * our storage. Images beyond the budget still travel, by URL only, and the * prompt block still describes them. */ export declare const MAX_INLINE_IMAGE_PARTS = 3; /** * Belt-and-braces per-turn ceiling on base64 characters. * * Kept as a cheap guard against a pathological single image, but it is NO * LONGER the operative limit — `MAX_TURN_BODY_BYTES` is. See below for why the * previous 1.5M-character budget was a fiction. */ export declare const MAX_INLINE_IMAGE_BASE64_CHARS = 1500000; /** * The real ceiling: what the gateway will accept as a request body. * * `wspace-public-gateway` enforces `MAX_REQUEST_BODY_SIZE`, and both the alpha * and prod configs set it to **256kb** (`burdenoff-gitops` * `apps/config-workspace{,-prod}/wspace-public-gateway-config.yaml`). It is * checked against `Content-Length` before authentication and before anything is * forwarded, and it answers 413 with a JSON body — which fe-libs' `graphqlFetch` * maps to the generic 4xx sentence, discarding the status. So an oversized turn * does not degrade: it dies, and the user is told to refresh. * * The old budget (1.5M base64 characters ≈ 1.43 MB) was ~5.7x that cap, so a * single ordinary 1280px photo — 150-400 KB, i.e. 200-530 KB of base64 — was * enough to lose the whole turn. Measure the body instead of guessing at it. */ export declare const MAX_TURN_BODY_BYTES = 262144; /** * Headroom left for everything `estimateTurnBodyBytes` does not model: the * GraphQL query document, the operation name, the sandbox id / path / method, * the context input and the page context. * * Measured, that overhead is under 1 KB — the ProxySandboxRequest document is * ~150 bytes and the page context a few hundred. 32 KB is therefore deliberately * ~30x the real figure, because the two ways of being wrong are not * symmetrical: reserve too much and one photo travels as a link instead of as * pixels; reserve too little and the entire turn is rejected and the farmer's * question is lost. */ export declare const TURN_BODY_HEADROOM_BYTES = 32768; /** What the modelled part of the body must fit inside. */ export declare const MAX_INLINE_TURN_BODY_BYTES: number; /** * The most text, per file or per turn, a mode may ask to inline: 200,000 * characters. * * ★ Bounded by the request body, which is where an over-large turn dies. Every * character is at least one byte, so more characters than the body has bytes * left after the prompt envelope can never travel. It is also no more than a * document extraction ever holds (`MAX_EXTRACTED_CHARS`), so a document can use * all of it. Within the ceiling, whether a given turn fits is still MEASURED — * non-ASCII text and JSON escaping cost more than a byte a character — by * `capTextToBody`. */ export declare const MAX_CONFIGURABLE_INLINE_CHARS: number; /** * The inline limits of the mode a turn is sent in (BOFF-7334). * * - Neither field set (or no config at all): `DEFAULT_INLINE_LIMITS`, the very * object, so every existing mode keeps today's budget. * - `maxInlineCharsPerFile` alone: that per file, and a total of at least the * default — never less than one such file. Asking for 100,000 a file should * not be silently held to a 20,000 total. * - `maxInlineCharsTotal` alone: that total, per file unchanged. * - Per file never exceeds the total; both are bounded by * `MAX_CONFIGURABLE_INLINE_CHARS`. * * ★ Pass the config for EXACTLY the mode being sent in (`findAssistantModeConfig`), * never a display fallback to another mode's — the same rule as * `resolveAssistantModeCapabilities`, for the same reason: one mode's budget must * not be lent to another. */ export declare function resolveAssistantInlineLimits(config?: AssistantInlineLimitsConfig | null): AssistantInlineLimits; /** * How much of one file to KEEP when it is attached, before anyone knows which * mode it will be sent in: the largest per-file limit among the modes offered. * * ★ Not the current mode's. A file attached in Ask and sent after switching to * a mode with a larger limit would otherwise arrive already cut to Ask's 8,000 * characters. Keeping the largest costs only memory — at most * `MAX_ATTACHMENTS` × `MAX_CONFIGURABLE_INLINE_CHARS` — because the send cuts * every file to the SENDING mode's limit (`applyInlineBudget`). A product whose * modes all use the defaults keeps exactly 8,000, as before. */ export declare function largestInlineCharsPerFile(modes: readonly AssistantInlineLimitsConfig[] | null | undefined): number; /** * Encode settings for the copy of an image that travels INSIDE the turn. * * Separate from the upload encode (1280px / q0.72) on purpose: the file that * lands in the farmer's file library should not be degraded to fit a transport * limit. At 768px / q0.6 a photo is typically 40-90 KB — 53-120 KB of base64 — * so one image fits inside `MAX_INLINE_TURN_BODY_BYTES` with room for the * prompt, and often two do. Vision models downsample to roughly this size * anyway, so the diagnostic detail that matters survives. */ export declare const INLINE_IMAGE_MAX_DIM = 768; export declare const INLINE_IMAGE_QUALITY = 0.6; /** * Estimate the bytes the prompt + parts will occupy in the POSTed body. * * Shaped like the JSON fe-libs actually builds (`{ mode, prompt, pageContext, * parts }` inside `proxySandboxRequest.body`), so the number tracks the thing * being limited rather than a proxy for it. Deliberately an UNDER-estimate of * the full request — `TURN_BODY_HEADROOM_BYTES` covers the difference. */ export declare function estimateTurnBodyBytes(promptText: string, parts?: AssistantPromptPart[]): number; /** * How many attachments COULD have travelled as pixels — i.e. are ready images * that have a `data:` URL to inline. * * Compared against how many actually did, this is what tells the user their * photo was demoted to a link. Without it the deterministic budget path is * silent, and only the transport-rejection path ever admits to anything. */ export declare function countInlineImageCandidates(attachments: AssistantAttachment[]): number; /** How many parts actually carry image bytes. */ export declare function countInlinedImageParts(parts: AssistantPromptPart[] | undefined): number; /** Split a `data:;base64,` URL. Returns null for anything else. */ export declare function parseDataUrl(dataUrl: string): { mimeType: string; base64: string; } | null; /** * Build the structured multimodal turn. * * `promptText` MUST be the exact string also sent as `prompt`, so the two * channels are equivalent: a bridge that understands `parts` sees everything a * text-only bridge sees, plus the images. Returns `undefined` when there is * nothing an image/file part could add, which keeps the wire body byte-identical * to a pre-multimodal client for every ordinary text turn. */ export declare function buildPromptParts(promptText: string, attachments: AssistantAttachment[]): AssistantPromptPart[] | undefined; //# sourceMappingURL=attachments.d.ts.map