@sema-agent/core
Version:
Stateless, task-oriented AI agent core
132 lines (131 loc) • 7.53 kB
TypeScript
/**
* PDF read pipeline (CC FileReadTool parity).
*
* CC ground truth (original source code):
* - Whole-PDF path (`FileReadTool.ts:987-1016` + `utils/pdf.ts:88-103`): the file bytes are base64'd and
* sent as a native `{type:"document",source:{type:"base64",media_type:"application/pdf",data}}` content
* block — the model reads the text layer directly (no local text extraction).
* - Page-range path (`utils/pdf.ts:179-230`): CC does NOT split the PDF itself — it shells out to
* poppler's `pdftoppm -jpeg -r 100 -f F -l L` and returns the rendered pages as IMAGE blocks.
* - Page counting (`utils/pdf.ts:119-135`): poppler's `pdfinfo`, parsing `/^Pages:\s+(\d+)/m`; `null`
* (tool absent/failed) fails OPEN. We add a zero-dependency byte-scan fallback for uncompressed PDFs.
* - Limits (`constants/apiLimits.ts:46-84`): 20MB whole-send target, 100MB extraction ceiling, 20 pages
* per request, >10 pages ⇒ the `pages` parameter is REQUIRED.
*
* sema follows the same shape with the ExecutionEnv seam (works against remote envs too) and honest
* degradation when poppler is absent — never a fake success.
*/
import type { ExecutionEnv } from "../../internal/harness-types.js";
/** Max pages a single `pages` request may span (CC `PDF_MAX_PAGES_PER_READ`). */
export declare const PDF_MAX_PAGES_PER_READ = 20;
/** Above this page count a whole-PDF read is refused and `pages` is required (CC `PDF_AT_MENTION_INLINE_THRESHOLD`). */
export declare const PDF_INLINE_PAGE_THRESHOLD = 10;
/** Max raw size the page-extraction (pdftoppm) path accepts (CC `PDF_MAX_EXTRACT_SIZE`). The whole-send target
* (`PDF_TARGET_RAW_SIZE`) and the `%PDF-` signature test (`pdfMagicMatches`) are the document admission's own
* (core/image-admission.ts). */
export declare const PDF_MAX_EXTRACT_SIZE: number;
/** A parsed 1-indexed inclusive page range. `last === Infinity` for an open range ("10-"). */
export interface PdfPageRange {
first: number;
last: number;
}
/** Parse the CC `pages` formats — "3", "1-5", "10-" (open range). `undefined` = invalid. */
export declare function parsePdfPageRange(pages: string): PdfPageRange | undefined;
/**
* Zero-dependency page-count estimate: count `/Type /Page` leaf objects in the raw bytes. Exact for
* classically-serialized PDFs (incl. linearized — objects stay uncompressed); returns `undefined` when
* nothing matches (page tree inside compressed object streams — modern writers), NEVER a guess.
*/
export declare function countPdfPagesFromBytes(bytes: Uint8Array): number | undefined;
/** Max chars of a failed poppler call's stderr/stdout kept in the reported error. */
export declare const PDF_EXEC_ERROR_PREVIEW_CHARS = 400;
/** Bound a poppler exec failure's captured stream to {@link PDF_EXEC_ERROR_PREVIEW_CHARS}, marking the cut
* (a bare slice reads identical to a short capture that just happened to end there — the model has no way
* to tell "that's everything" from "that's all we kept"). Exactly-at-the-cap text is unmarked. */
export declare function previewExecStream(s: string): string;
/**
* Total page count via poppler `pdfinfo` (CC parity), falling back to {@link countPdfPagesFromBytes}
* when the tool is absent/fails and the bytes were already read. `undefined` = unknown (fail-open,
* matching CC's `getPDFPageCount → null`).
*/
export declare function getPdfPageCount(env: ExecutionEnv, absPath: string, bytes: Uint8Array | undefined, signal?: AbortSignal): Promise<number | undefined>;
/** How many leading pages the vision fallback (level 2) renders for a whole-PDF read on a model without
* document input — conservative per-request budget; the note directs the model to `pages` for more. */
export declare const PDF_FALLBACK_RENDER_PAGES = 5;
/**
* PDF capability profile of the SERVING model, consumed by the Read tool's degradation chain (v2):
* - `document` — the brain path can forward a native `{type:"document"}` block (today: anthropic-messages
* WITH vision — anthropic.ts drops document blocks for no-vision models — or an explicit `input`
* declaration containing "document" for a future capability bit).
* - `vision` — `Model.input` contains "image" (absent metadata fails open = today's behavior).
* Structural parameter (no deep Model import): `prepare-task` passes the task's resolved Model.
*/
export interface PdfModelCapabilities {
document: boolean;
vision: boolean;
}
/** Derive {@link PdfModelCapabilities} from a Model-shaped object. `undefined` model = fully capable
* (fail-open: no metadata ⇒ preserve the native document-block behavior, brain-level placeholder guards). */
export declare function pdfModelCapabilitiesOf(model?: {
api?: string;
input?: readonly string[];
}): PdfModelCapabilities;
/** Timeout for the poppler extraction commands (pdftotext / pdftoppm), in seconds. */
export declare const PDF_EXEC_TIMEOUT_SEC = 120;
/** What cut a run short: the wall-clock budget (always a number), or the env's silence window (a number only when the env states it — otherwise the budget did not elapse and no number is honest). */
export type PdfCut = {
readonly kind: "budget";
readonly sec: number;
} | {
readonly kind: "silence";
readonly sec?: number;
};
/** A cut in words: the clause that says it (`timed out after 120s` / `was cut after producing no output for 30s` / `was cut for producing no output`) and the noun a later clause calls it. */
export declare function pdfCutWords(cut: PdfCut): {
clause: string;
noun: string;
};
export type PdfTextResult = {
ok: true;
text: string;
/** Set when pdftotext was cut by a time bound but had already written partial text: the extraction is
* INCOMPLETE (covers only the leading part of the requested range) and the caller must say so. */
cut?: PdfCut;
} | {
ok: false;
toolMissing: boolean;
timedOut?: boolean;
error: string;
};
/**
* Extract the PDF text layer via poppler `pdftotext -layout` (degradation-chain level 1: models without
* native document input get the text instead of a placeholder). Optional 1-indexed inclusive page range
* (`-f`/`-l`). Output goes through a temp FILE (not exec stdout — env exec seams may cap/mangle stdout),
* read back over the env seam so it works against remote envs too. `toolMissing` distinguishes
* "install poppler" from a real extraction failure.
*/
export declare function extractPdfTextLayer(env: ExecutionEnv, absPath: string, range?: {
first: number;
last: number;
}, signal?: AbortSignal): Promise<PdfTextResult>;
export type PdfExtractResult = {
ok: true;
pages: Array<{
pageNumber: number;
jpeg: Uint8Array;
}>;
/** Set when pdftoppm was cut by a time bound but had already rendered complete leading pages: `pages`
* covers only a PREFIX of the requested range and the caller must label the delivery as partial. */
cut?: PdfCut;
} | {
ok: false;
toolMissing: boolean;
timedOut?: boolean;
error: string;
};
/**
* Render pages [first..last] to JPEGs via poppler `pdftoppm` (CC `extractPDFPages` parity: `-jpeg -r 100`,
* 120s timeout). Runs through the ExecutionEnv seam (temp dir + exec + readBinaryFile), so it works
* wherever the env does. `toolMissing` distinguishes "install poppler" from a real extraction failure.
*/
export declare function extractPdfPagesAsImages(env: ExecutionEnv, absPath: string, first: number, last: number, signal?: AbortSignal): Promise<PdfExtractResult>;