@sema-agent/core
Version:
Stateless, task-oriented AI agent core
120 lines (119 loc) • 7.11 kB
TypeScript
/**
* 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 {};