UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

108 lines (107 loc) • 6.88 kB
/** * The skills-listing BUDGET SEAT as vocabulary: the two knobs a deployment may declare * (`TaskSpec.skillListingBudgetFraction` / `TaskSpec.skillListingMaxDescChars`), the ONE reader that * lowers them to the numbers the renderer measures against, and the disclosure announced when the * budget costs a skill its description. The reader is pure; the announcer's only effect is the notice. * `runner/synthetic-tools.ts` owns the renderer that consumes the numbers. * * The listing frame is an INTACT-class turn attachment (`runner/turn-attachments.ts` * `ATTACHMENT_DELIVERY_CLASS`): the delivery lane never clips it, so the number this reader produces is * the number the frame is rendered to — there is no lane ceiling above it. * * Sibling of `tool-search-setting.ts` in shape and in posture: a value outside its domain REFUSES * loudly rather than folding back to the default, because a knob that is silently dropped reads to the * deployment as a knob that took effect. */ import { type EngineNotice } from "./engine-notice.js"; /** Fraction of the model's context window the listing may spend when the deployment declares nothing. */ export declare const SKILL_LISTING_BUDGET_FRACTION_DEFAULT = 0.01; /** Per-skill description cap (code units, ellipsis included) when the deployment declares nothing. */ export declare const SKILL_LISTING_DESC_MAX_CHARS_DEFAULT = 1536; /** Context window assumed for a model that declares none. */ export declare const SKILL_LISTING_DEFAULT_CONTEXT_TOKENS = 200000; /** * The token→byte exchange rate this budget allocates at. * * @contract skills.listing_budget_exchange_rate — deliberately NOT `DEFAULT_CHARS_PER_TOKEN` (3), and * the difference is directional, not an inconsistency. That constant serves the compaction/context * estimators, which convert bytes→tokens and must OVER-estimate the token count to stay inside a * window; a low rate is the conservative choice there. This seat converts tokens→a byte ALLOWANCE, * where a low rate silently SHRINKS what the deployment may spend. Two directions, two numbers, both * named. At 3 the default allowance would be 6 000 bytes rather than 8 000 — a quarter of every * default deployment's listing, removed by an estimator's safety margin. */ export declare const SKILL_LISTING_BYTES_PER_TOKEN = 4; /** The typed refusal a budget seat outside its domain raises: a bad value never selects a smaller * listing silently, and never folds back to the default budget. */ export declare const SKILL_LISTING_BUDGET_INVALID_CODE = "config.skill_listing_budget_invalid"; /** The notice code minted when the budget cost at least one skill its description or its line. */ export declare const SKILL_LISTING_TRUNCATED_CODE = "skills.listing_truncated"; /** The agent-type listing's twin: minted when the budget cost at least one agent type its detail or its line. */ export declare const AGENT_LISTING_TRUNCATED_CODE = "agents.listing_truncated"; /** Which listing frame a degradation report describes. */ export type ListingKind = "skills" | "agents"; /** The lowered seat: the two numbers the renderer measures against. */ export interface ResolvedSkillListingBudget { /** The budget the renderer enforces over the whole block (fences, headers, markers, joiners * included), in UTF-8 bytes. */ blockMaxBytes: number; /** Per-skill description cap in code units, ellipsis included. */ descMaxChars: number; } /** * The ONE reader of the two listing-budget seats. * * @contract skills.listing_budget_enforced_as_declared — the resolved `blockMaxBytes` is * `floor(window × 4 × fraction)` with no ceiling above it: the `<skills>` frame is an intact-class * attachment the delivery lane never clips, so what the deployment declares is what the renderer * folds the listing into, on the initial block and on every drift frame alike. The fraction's own * domain `(0, 1]` is the only upper bound (a whole window at most), and a value outside it refuses. * * `skillListingBudgetFraction` must be a finite number in `(0, 1]` — zero is excluded deliberately: a * zero budget is not "spend nothing", it is a listing every skill loses its description to, which the * deployment expresses by declaring fewer skills. `skillListingMaxDescChars` must be a positive safe * integer. Both absent ⇒ the documented defaults, which reproduce the pre-seat render byte for byte * (`200 000 × 4 × 0.01` = 8 000 bytes). * * The window is `contextTokens ?? contextWindow`, the same physical coordinate the deferral gate * measures against. `contextTokens: 0` (or any other non-finite/non-positive declared value) does * NOT fall through `??` to `contextWindow` — `0` is not nullish — so it lands here as a * declared-but-unusable window, and this seat folds it to {@link SKILL_LISTING_DEFAULT_CONTEXT_TOKENS} * the same as a genuinely absent window: one rule, not a case per bad shape, because this seat's * job is to always hand a renderer a usable number. */ export declare function resolveSkillListingBudget(opts: { fraction?: unknown; maxDescChars?: unknown; model?: { contextTokens?: number; contextWindow?: number; } | undefined; }): ResolvedSkillListingBudget; /** The reader's answer to "no knobs declared, no window declared" — the budget a renderer measures * against when its caller hands it none. One rule: only the reader turns knobs into numbers. */ export declare const SKILL_LISTING_DEFAULT_BUDGET: Readonly<ResolvedSkillListingBudget>; /** * Announce a listing frame the budget degraded — delivered only when an entry actually lost its detail * (`nameOnly`) or its line (`dropped`), so a deployment whose roster fits hears nothing. One announcer * for both listing frames; the code names the frame (`skills.listing_truncated` / `agents.listing_truncated`). * * Called from the commit of the frame's receipt — the moment the carrying message is in the transcript — * never from the config door: the door cannot see the deployment's listing switches (resolved at run * start), it runs per prepared task rather than per delivered listing, and a resume leg reaches only * the boundary. * * Audience is `operator`: the person who can raise the fraction, shorten a description or remove an * entry runs the deployment, not the session. */ export declare function announceListingTruncation(onNotice: ((notice: EngineNotice) => void) | undefined, opts: { listing: ListingKind; /** The denominator: how many entries the DELIVERED frame set out to list — the whole roster on an * initial frame, the newly announced set on a drift frame. Read off that frame's own render. */ total: number; nameOnly: readonly string[]; dropped: readonly string[]; budget: ResolvedSkillListingBudget; sessionId?: string | undefined; }): void;