UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

186 lines (185 loc) • 11.6 kB
/** * The USER-ATTACHMENT SEAT — the one road a file the user hands the model takes: * * the host declares it on the objective (`TaskSpec.userAttachments`) → the engine screens the declaration at the * door → after the UserPromptSubmit hook lets the objective through, the engine reads a path entry with the Read * tool's own read (`ModelFileReader.readForModel`) under the run's synthetic-Read judge, or takes the host's bytes → * it mints ONE message per attachment that trails the human message: an engine note whose marked blocks hold only * static sentences and whose data (the name or path, then the contents or the refusal) sits between them → the * message's delivery record (`UserMessage.attachment`) is the one source of "what was delivered". * * This module owns the three steps no other module may repeat: the screen (`screenUserAttachments`), the mint * (`readUserAttachments` → the frame seeds, the `human_input` projection and the pending commits) and the settlement * (`createAttachmentDelivery`: a read's effects commit when its frame reaches the transcript, keyed by * `(inputId, ordinal)` — never at read time, never by text). * * The frame, per attachment (blocks in order): * <system-reminder mark="…">\n<opening sentence>\n</system-reminder>\n — static, the attachment's number only * Path: "<declared path>"\n or Name: "<name>"\n — data (lossless quoted label) * <data blocks> — the file's own blocks / the refusal * \n\n<system-reminder mark="…">\n<note sentence>\n</system-reminder> — one per engine note, static * \n\n<system-reminder mark="…">\nEnd of attachment <n>.\n</system-reminder> * Every marked block after data opens after a blank line (`MARKED_BLOCK_AFTER_DATA`): the completions lanes join a * message's text blocks with no separator. */ import type { ImageContent, TextContent, UserAttachmentSupply, UserMessageAttachment } from "../internal/llm.js"; import type { HumanInputAttachment } from "./task-event.js"; import { type ModelFileReader } from "../tools/fs/fs-read.js"; import type { SyntheticReadJudge } from "./synthetic-read-verdict.js"; import { type ReminderDisclosureCounts } from "./reminder-disclosure.js"; import type { ContextWindowOverlay } from "./context-settings.js"; /** At most this many attachments on one objective. */ export declare const USER_ATTACHMENTS_MAX = 20; /** The longest path or name an entry may carry (the platform path limit). */ export declare const USER_ATTACHMENT_LABEL_MAX = 4096; /** What the engine found an attachment to be (an output, never declared): the Read lanes, a directory, host bytes. */ export declare const USER_ATTACHMENT_KINDS: readonly ["text", "notebook", "image", "pdf", "directory", "client_text", "client_image"]; /** Who held the bytes — the entry's arm. */ export declare const USER_ATTACHMENT_SUPPLIES: readonly ["path", "content_text", "content_image"]; /** What reached the model of an attachment's contents — the Read layer's own outcome words, the same frozen array. */ export declare const USER_ATTACHMENT_OUTCOMES: readonly ["attached", "partial", "unchanged", "not_attached"]; /** * The seat's capability descriptor — what a host reads to decide, per attachment, whether to declare it here: its kind * is in `kinds` and its arm in `supplies` ⇒ declare it on the objective; otherwise (a kind this seat does not carry, or * an engine without the descriptor) the host carries it some other way. `entries`: the objective only — a steer carries * no attachments (they go with the next objective). */ export declare const USER_ATTACHMENT_SEAT: Readonly<{ entries: readonly ["objective"]; kinds: readonly ["text", "notebook", "image", "pdf", "directory", "client_text", "client_image"]; supplies: readonly ["path", "content_text", "content_image"]; limits: Readonly<{ maxEntries: 20; maxRenderedTextChars: 100000; }>; }>; /** One declaration as the door captured it: the caller's object read once, members copied. */ export type ScreenedUserAttachment = { readonly supply: "path"; readonly path: string; readonly lines?: { readonly start: number; readonly end?: number; }; } | { readonly supply: "content_text"; readonly name: string; readonly text: string; readonly startLine: number; readonly rendered: string; } | { readonly supply: "content_image"; readonly name: string; readonly data: string; readonly mimeType: string; }; /** * The field a refusal names, as one value: `path` is the field path the sentence opens with, and `index` the attachment's * position in the seat (0-based, the `[i]` of that path) — present exactly when the sentence names one attachment; a refusal of * the seat as a whole (not a list, too many, a resume, the window) has no index. Built only by {@link SEAT_REF} / * {@link entryRef} / {@link memberRef}, so the index and the path can never name two attachments. */ export interface UserAttachmentFieldRef { readonly path: string; readonly index?: number; } /** The door's refusal: one code, the field named first in the sentence and again in `detail` ({@link UserAttachmentFieldRef}). */ export declare function userAttachmentRefusal(at: UserAttachmentFieldRef, problem: string): Error & { code: "config.user_attachment_invalid"; detail: UserAttachmentFieldRef; }; /** * The door: screen the seat's value (synchronous; the caller's object is read once, member by member, before the * first await of the run). `undefined` / an empty array ⇒ no attachments. `resumeLeg`: a durable resume mints no new * human message, so any entry there is refused. Throws `config.user_attachment_invalid` naming the field. */ export declare function screenUserAttachments(seat: unknown, opts: { resumeLeg: boolean; }): readonly ScreenedUserAttachment[] | undefined; /** A path entry on a run with no workspace: there is nothing the engine could read it from. */ export declare function refuseUserAttachmentPathsWithoutWorkspace(screened: readonly ScreenedUserAttachment[] | undefined): void; /** One declared attachment as the UserPromptSubmit hook sees it (`UserPromptSubmitContext.userAttachments`). */ export interface UserAttachmentDeclaration { /** 1-based position among the objective's attachments. */ readonly ordinal: number; /** Who holds the bytes: `path` (the engine reads it), `content_text` / `content_image` (the host sent them). */ readonly supply: UserAttachmentSupply; readonly path?: string; readonly name?: string; /** A host entry's size in bytes (text as UTF-8, an image decoded). */ readonly bytes?: number; } /** The UserPromptSubmit hook's view of the declarations. */ export declare function userAttachmentDeclarations(screened: readonly ScreenedUserAttachment[]): readonly UserAttachmentDeclaration[]; /** * The host images' byte admission — the one function an image the Read tool serves passes. Asynchronous (it decodes), * run on the captured values before any attachment is read or delivered. Throws `config.user_attachment_invalid`. */ export declare function admitUserAttachmentImages(screened: readonly ScreenedUserAttachment[]): Promise<ReadonlyMap<number, ImageContent>>; /** One minted frame: the trailing message's seed, and the commit its delivery owes (absent ⇒ nothing to commit). */ export interface MintedAttachmentFrame { readonly seed: { readonly content: ReadonlyArray<TextContent | ImageContent>; readonly attachment: UserMessageAttachment; }; readonly commit?: () => void; } export interface ReadUserAttachmentsInput { screened: readonly ScreenedUserAttachment[]; /** The human input the attachments belong to (minted before the frames and the event, shared by both). */ inputId: string; /** The leg's one file reader (absent on a run without a workspace — then only host entries exist). */ reader: ModelFileReader | undefined; /** The run's synthetic-Read judge. */ judge: SyntheticReadJudge; /** The admitted host images, by ordinal. */ images: ReadonlyMap<number, ImageContent>; /** The tracked working directory a relative path resolves against (the Read tool's own). */ cwd: string | undefined; mark: string | undefined; counts: ReminderDisclosureCounts | undefined; signal: AbortSignal; } /** * Read every declared attachment and mint its frame. Side-effect free toward the run: nothing is committed here — each * frame carries the commit its delivery owes, run only when the frame is in the transcript. */ export declare function readUserAttachments(input: ReadUserAttachmentsInput): Promise<MintedAttachmentFrame[]>; /** The objective as the window check sees it: a reader of the live overlay over the model's window declarations (the * same seat the request guard reads) and the objective text's length — it rides the first request ahead of the * frames. The overlay is a GETTER because it is read when the frames exist, not when the call is made: the reads are * asynchronous and a deployment may swap its window declarations meanwhile, as it may before any request. */ export interface AttachmentWindowInput { readonly overlay: () => ContextWindowOverlay | undefined; readonly objectiveChars: number; } /** The objective lane's read: once the UserPromptSubmit hook let the objective through (a blocked objective reads * nothing), and before the pre-call gate, which prices the frames (`chars`). Refuses the whole objective when the frames * and the objective cannot ride one request ({@link refuseFramesOverWindow}). Commits nothing. */ export declare function readObjectiveAttachments(prepared: Pick<import("./runner/contracts.js").Prepared, "userAttachments" | "modelFileReader" | "syntheticReadVerdict" | "cwdRef" | "reminderMark" | "reminderDisclosureCounts" | "abortController" | "harness">, inputId: string, blocked: boolean, window: AttachmentWindowInput): Promise<{ frames: MintedAttachmentFrame[]; chars: number; }>; /** The last attachment frame of the group the message `id` opens, or `id` when it has none (a `resumeAt` that keeps the * message keeps its group). The frames of one input are appended one after another under its message (each the child of * the one before), all carrying its input id. */ export declare function lastAttachmentFrameOf(entries: ReadonlyArray<{ id: string; parentId: string | null; type: string; message?: unknown; }>, id: string): string; /** The `human_input` projection of the minted frames. */ export declare function humanInputAttachmentsOf(frames: readonly MintedAttachmentFrame[]): HumanInputAttachment[]; /** * The run's pending attachment commits, keyed by `(inputId, ordinal)`. A frame's commit runs exactly once, when the * harness reports that frame in the transcript; a frame that never got there (a refused pre-call budget, an abort, a * failed append) commits nothing — its entry is simply never settled. */ export interface AttachmentDelivery { /** Stage the minted frames of one input — called only once every gate before the append has passed. */ stage(inputId: string, frames: readonly MintedAttachmentFrame[]): void; /** The frame `(inputId, ordinal)` is in the transcript: run its commit (once) and forget it. */ settle(inputId: string, ordinal: number): void; } export declare function createAttachmentDelivery(): AttachmentDelivery;