UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

120 lines (119 loc) • 7.11 kB
/** * Generic image downsample/resize pipeline (design/157 S3 — split out of `core/mcp.ts`, which historically * carried this alongside MCP-protocol plumbing even though the pipeline itself has no MCP dependency). * Two consumers share this ONE pipeline + ONE size yardstick: the MCP inline-image bound (`core/mcp.ts` * `boundedImageBlock`) and the Read tool's image branch (`tools/fs/index.ts`). `core/mcp.ts` re-exports * everything below (`export * from "./image-downsample.js"`) so existing importers (`tools/fs/index.ts`, * `core/types.ts`) needed no changes on this car. */ /** * design/116 W5-1 — the max base64 size of ONE inline image (5MB). Value = CC 2.1.187 * `API_IMAGE_MAX_BASE64_SIZE` (constants/apiLimits.ts:22, "5 * 1024 * 1024 // 5 MB" — the hard Anthropic API * limit on a base64 image block; CC resizes/compresses down to fit UNDER it, imageResizer.ts). An MCP server * returning a bigger image previously passed straight through = a context/API bomb. */ export declare const MCP_IMAGE_MAX_BASE64: number; /** * 批③ image-pipeline — CC 2.1.x `constants/apiLimits.ts` values, shared by the MCP inline-image bound * AND the Read tool's image branch (tools/fs — internal consistency: ONE yardstick per limit). That other * consumers are the file tools (`tools/fs/fs-read.ts`'s image branch, plus `fs-pdf`/`fs-bash`), which IMPORT * {@link MCP_IMAGE_MAX_BASE64} / {@link IMAGE_TARGET_RAW_SIZE} from here (through core/mcp.js's re-export) * rather than re-deriving them — one limit, every consumer reading it, no drift. * - `IMAGE_TARGET_RAW_SIZE` (apiLimits.ts:29): raw-byte target that guarantees the base64 encoding stays * under {@link MCP_IMAGE_MAX_BASE64} (raw × 4/3 = base64 → 3.75MB raw = 5MB base64). * - `IMAGE_MAX_WIDTH/HEIGHT` (apiLimits.ts:42-43): client-side resize box. The API internally resizes * above 1568px anyway; 2000px preserves a little extra quality while bounding token burn (~3x for a * full-resolution screenshot vs the box). */ export declare const IMAGE_TARGET_RAW_SIZE: number; export declare const IMAGE_MAX_WIDTH = 2000; export declare const IMAGE_MAX_HEIGHT = 2000; /** * design/116 W5-1 — bound an inline MCP image. HONEST DELTA vs CC: CC really resizes/downsamples an * oversized image with sharp (utils/imageResizer.ts `maybeResizeAndDownsampleImageBuffer`: ≤2000x2000px, * ≤3.75MB raw = 5MB base64, progressive JPEG/PNG quality ladder). Core deliberately takes NO image-processing * dependency (arch decision — sharp is a native module we won't force on every consumer), so instead of * resizing we BOUND: an image whose base64 exceeds {@link MCP_IMAGE_MAX_BASE64} is spilled to disk and * replaced by an explanatory text block with the size + path; a within-limit image passes through unchanged. */ /** * Optional image-resize seam (design/116 review CONFIRM-1, 裁定 2026-07-02 加 seam 保体验): given an * over-limit image, return a smaller re-encoded one — or undefined when it can't. CC resizes with sharp * (imageResizer.ts: fit within 2000x2000, JPEG quality ladder) so the model still SEES a degraded image; * core must not hard-depend on a native image library, so the capability is injected (or auto-detected). */ export type McpImageResizer = (base64: string, mimeType: string) => Promise<{ base64: string; mimeType: string; } | undefined>; /** * 批③ image-pipeline — original vs displayed size of a processed image (CC `imageResizer.ts` * `ImageDimensions`). `display*` = what the model actually sees; the Read tool renders the CC coordinate- * mapping meta text from the ratio, so vision coordinate reasoning survives a downsample. */ export interface ImageDimensions { originalWidth: number; originalHeight: number; displayWidth: number; displayHeight: number; } /** Result of {@link ImageDownsampler}: re-encoded (or passed-through) image + dimensions when known. */ export interface DownsampledImage { base64: string; /** Full mime form (`image/jpeg`), ready for an ImageContent block. */ mimeType: string; dimensions?: ImageDimensions; } /** * 批③ — buffer-level image downsampler shared by the MCP inline-image bound AND the Read tool's image * branch. Returns undefined when the input can't be processed (corrupt/unsupported) — the caller keeps * its non-sharp path (spill for MCP, original-bytes for Read). A within-limits image passes through * UNCHANGED (with dimensions when known) — pass-through is not a failure. */ export type ImageDownsampler = (input: Buffer, mimeType: string) => Promise<DownsampledImage | undefined>; /** Minimal structural slice of the sharp API the pipeline uses (fresh instance per operation — CC note: * reusing an instance after toBuffer() silently skips format conversions on some native builds). */ type SharpFactory = (input: Buffer) => SharpOps; interface SharpOps { metadata: () => Promise<{ width?: number; height?: number; format?: string; }>; resize: (w: number, h: number, o: { fit: "inside"; withoutEnlargement: boolean; }) => SharpOps; jpeg: (o: { quality: number; }) => SharpOps; png: (o: { compressionLevel: number; palette: boolean; }) => SharpOps; toBuffer: () => Promise<Buffer>; } /** * The CC downsample pipeline (imageResizer.ts `maybeResizeAndDownsampleImageBuffer`, called by * FileReadTool.ts:1097 `readImageWithTokenBudget`), ported over an injected sharp factory so the ladder * is unit-testable without the native dependency: * 1. fits already (raw ≤ 3.75MB AND ≤ 2000×2000) → pass through, dimensions attached; * 2. dims fit but bytes over → full-resolution compression first (PNG lossless/palette for PNGs, then * JPEG quality ladder 80/60/40/20) — preserves resolution when possible; * 3. dims over → constrain to the 2000×2000 box (aspect kept), then the same ladder on the resized * image if still over; last rung = ≤1000px wide JPEG q20 (returned without a further size check — * the caller's base64 gate stays the final arbiter, CC-identical shape); * 4. metadata without dimensions → JPEG q80 when over target, else pass through (CC branch). * Exported for tests (fake sharp factory); production entry = {@link sharpImageDownsampler}. */ export declare function createImageDownsampler(sharp: SharpFactory): ImageDownsampler; export declare function sharpImageDownsampler(): Promise<ImageDownsampler | undefined>; /** * Default resizer factory: dynamic `import("sharp")` — a turnkey deployment with sharp installed gets the * CC experience automatically; without it this resolves to undefined and the spill path applies. Since * 批③ this is a thin base64 adapter over {@link sharpImageDownsampler} (single pipeline, CC ladder); * the contract is unchanged: undefined when sharp is missing, the input is corrupt, or even the lowest * ladder rung stays over the API limit. */ export declare function sharpImageResizer(): Promise<McpImageResizer | undefined>; export {};