@sema-agent/core
Version:
Stateless, task-oriented AI agent core
108 lines (107 loc) • 6.88 kB
TypeScript
/**
* 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;