UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

178 lines (177 loc) • 12.9 kB
import { type TSchema } from "typebox"; import type { AgentTool } from "../../internal/harness-types.js"; import type { TerminalBlockedDetail } from "../terminal-cause.js"; import { type ResolvedSkillListingBudget } from "../skill-listing-budget.js"; import { type SkillGrantTable } from "../skill-grant.js"; /** Reserved name of the injected structured-output tool (CC 2.1.187 `StructuredOutput`; batch-A rename — old * "submit_output" is canonicalized for pre-rename durable state). */ export declare const OUTPUT_TOOL_NAME = "StructuredOutput"; /** Reserved name of the injected blocked-report signal tool (PascalCase: `report_blocked`→`ReportBlocked`). */ export declare const REPORT_BLOCKED_TOOL_NAME = "ReportBlocked"; /** Name of the injected code-review findings echo tool (CC 2.1.207 parity — 锚 pretty.js:361412 `BCe`). */ export declare const REPORT_FINDINGS_TOOL_NAME = "ReportFindings"; /** Default `submit_output` retries (council OS3); total attempts = retries + 1. Overridable via `TaskSpec.outputRetries`. */ export declare const OUTPUT_DEFAULT_RETRIES = 2; /** * Resolve the per-task cap on *consecutive failed* `submit_output` attempts. The task fails with * `output.invalid` when the failure streak EXCEEDS this value, so total attempts = cap + 1 * (default 2 → 3 attempts; `outputRetries: 0` → cap 0 → fail on the first invalid submit). Illegal * values (NaN/Infinity/negative) fall back to the default; fractional values floor down. */ export declare function resolveOutputRetries(outputRetries?: number): number; export interface BlockedRef { reason?: string; /** Written by the engine when a hook seat kept the prompt back (never by the `ReportBlocked` tool): the terminal's * `detail`. Absent = the model's own report. */ detail?: TerminalBlockedDetail; } /** Captures the validated object a `submit_output` call submitted (structured output, 1.41). `set` is * flipped so we can tell "model submitted {}" from "never called". Mirrors `BlockedRef`. */ export interface OutputRef { value?: unknown; set?: boolean; } /** * @param strictSchema CC 2.1.205 parity (strict-output-schema.ts): the STRICT derivation of `schema` when * it converted (`additionalProperties:false` stamped, subset-validated). It becomes the tool's * `parameters` — both the model-visible wire schema (CC sends it as `strictInputJSONSchema`) and the * validation schema (the strict form only TIGHTENS the caller's schema, so anything it accepts is valid * under the original — CC's Ajv-against-original validation agrees on every accepted submission). * Absent (fallback / legacy callers) → the caller's schema is served as-is, the pre-205 behavior. */ export declare function createOutputTool(ref: OutputRef, schema: TSchema, strictSchema?: TSchema): AgentTool; export declare function createReportBlockedTool(ref: BlockedRef): AgentTool; /** One reported finding (the CC 207 wire shape — see {@link createReportFindingsTool}). */ export interface ReportedFinding { file: string; line?: number; summary: string; failure_scenario: string; category?: string; verdict?: "CONFIRMED" | "PLAUSIBLE"; outcome?: "fixed" | "skipped" | "no_change_needed"; } /** Build the injected `ReportFindings` echo tool (CC 2.1.207 parity — description verbatim from 锚 * pretty.js:361413; `effect: "read"` mirrors CC `isReadOnly()`/`isConcurrencySafe()` → parallel-safe). */ export declare function createReportFindingsTool(): AgentTool; /** Reserved name of the injected skill-loading tool (present only when `spec.skills` is non-empty). */ export declare const SKILL_TOOL_NAME = "Skill"; /** Per-skill LOAD gate on `SkillSpec.content` (CC 2.1.207 parity: CC's only invoke-time size door is a 1MB file-load gate — an over-limit skill is REJECTED * WHOLE, never truncated, and the invoked body enters context untruncated). Sema skills are spec-supplied, so the equivalent gate sits at spec assembly: * an oversized skill is excluded from the disclosure block and the tool, with an operator-visible config error. The compaction-time retention caps are a * different door, deliberately. */ export declare const SKILL_CONTENT_MAX_CHARS: number; /** Total per-invoke cap for a skill's inlined attachments (`SkillSpec.files`) — an ATTACHMENT-face budget, distinct from the (gone) body truncation door: * the body is delivered untruncated at invoke time (CC parity), while attachments ride along bounded and truncation-disclosed. */ export declare const SKILL_ATTACHMENTS_MAX_CHARS = 50000; /** Per-attachment cap on the PATH rendered in its `###` header — one pathological path must not eat the budget. Overflow truncates with an ellipsis. */ export declare const SKILL_ATTACHMENT_PATH_MAX_CHARS = 512; interface SkillLike { name: string; description: string; content: string; /** The skill's two lists (`SkillSpec.allowedTools` / `disallowedTools`) — carried so a successful activation writes its rows into the run's grant table. */ allowedTools?: string[]; disallowedTools?: string[]; /** Optional attachments (SkillSpec.files): paths in the block, delivered inline on invoke under {@link SKILL_ATTACHMENTS_MAX_CHARS}. Same caller-trusted * contract as `content` — core never writes them to disk (deployment's half). */ files?: Array<{ path: string; content: string; }>; /** Optional base directory (SkillSpec.baseDir) — see `@contract skill.base_dir`. Absent ⇒ byte-identical result. */ baseDir?: string; } /** [c209-C] The metadata slice of a skill the LISTING faces consume (disclosure block + the * skills_listing turn attachment): name, description, attachment paths — never the body. The * Prepared seam projects `SkillSpec`s down to this shape so the run loop's announce lane holds no * skill content. `SkillLike` is structurally assignable (its `files` entries simply carry an extra * `content` the listing renderers never read). */ export interface SkillListingEntry { name: string; description: string; files?: Array<{ path: string; }>; /** The skill's rank in the DEPLOYMENT'S DECLARATION order (`TaskSpec.skills`, first-wins * dedupe applied, 0 = declared first). Lower = higher priority. Used ONLY to decide which skills keep * their descriptions when the block overflows its byte budget ({@link buildSkillsBlock} tier B); the * DISPLAY order stays name-sorted, so adding this never moves a line. * * Why declaration order: CC 220's `sbs` (@317547) takes a priority function and is called with * `uNt(name)` (@608552 → @402383) = usage count decayed by recency of last use, and additionally * pins every `bundled` skill's description unconditionally. Core has NEITHER axis — no cross-session * skill-usage store, and no bundled/user distinction (every skill arrives through the same * caller-supplied `TaskSpec.skills`). Declaration order is the one priority statement the deployment * can actually make here, and it is the same statement 220's bundled-pin encodes ("these matter most, * keep their descriptions"). Absent ⇒ the entry's array index is used, i.e. the prior behavior. */ declaredRank?: number; } /** Dedupe by name (first occurrence wins) and sort by name — one deterministic list shared by the disclosure block * and the tool, so what the prompt promises is exactly what the tool serves. Each field is READ ONCE, BY NAME, and * both consumers see the copy: the gate screens this list while the invoke substitutes from it, so a `get baseDir()` * answering those two reads differently passed a screen that measured a value the model never received — and a rest * copy would silently drop an INHERITED or non-enumerable list, i.e. lose a skill's grant or its refusals. */ export declare function normalizeSkills(skills: SkillLike[] | undefined): SkillLike[]; /** [c209-C] The skills listing's lead line — single-sourced so the run loop's durable-resume * transcript probe (MED-3② skills arm, runtask) matches EXACTLY what the initial frame carries * (probing the bare `<skills>` fence would be more collision-prone in model-quoted text). */ export declare const SKILLS_LISTING_PROBE_HEADER = "The following skills are available for this task."; /** [c209-C] One skill's disclosure line — `- name: description[(files 尾缀)]` with the per-skill * description cap. Single-sourced so the skills_listing DELTA frame (turn-attachments) renders adds * with byte-identical line shape to the initial `<skills>` block. */ export declare function skillListingLine(s: SkillListingEntry, descMaxChars?: number): string; /** What one {@link renderSkillsBlock} call produced: the block, and which skills the budget degraded. */ export interface SkillsBlockRender { /** The rendered block — what {@link buildSkillsBlock} returns. */ text: string; /** Skills listed WITHOUT their description (tier B's unspent candidates, or every listed name under * tier C), in display order. Raw names: this is host-facing structured data, not prompt text. */ nameOnly: readonly string[]; /** Skills the block could not list at all (tier C's overflow tail). The `Skill` tool still serves them. */ dropped: readonly string[]; } /** The text-only face of {@link renderSkillsBlock} — the `<skills>` BLOCK's face, not the listing * family's: the drift frame renders its own arms through the shared tier engine * ({@link renderSkillLinesWithin}), which is where "one ruler for every frame" lives. */ export declare function buildSkillsBlock(skills: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): string | undefined; /** Render the `<skills>` disclosure block (metadata only — bodies stay behind the tool). * [c209-C] REVERSAL: no longer composed into the stable system prompt — this is now the RENDERER for * the `skills_listing` initial frame (first-user-turn reminder / boundary re-announce), so the * cached prefix carries zero skill bytes (design/C §2.3; CC 209 classic ships the same listing as a * first-turn reminder). `budget` absent ⇒ the seat reader's no-knob answer ({@link SKILL_LISTING_DEFAULT_BUDGET}). * * The fence and its four header lines are this lane's frame; the budget arithmetic and the three * degradation tiers belong to {@link renderSkillLinesWithin}, which the drift frame renders through * as well. {@link buildSkillsBlock} is the text-only face; this one additionally reports what the * budget cost, for the disclosure lane. */ export declare function renderSkillsBlock(skills: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): SkillsBlockRender | undefined; /** * The skills VOCABULARY of the one folding engine (`listing-fold.ts` `foldListingWithin`): the full * `- name: description` line, the name-only line, the block's cap marker and its overflow count line, * measured byte-exact inside a caller-supplied FRAME — the engine both {@link renderSkillsBlock} and * the drift frame's ADD arm render through, so the initial `<skills>` block and a later "new skills are * now available" announcement degrade by the same rules against the same number — the number the seat * reader resolved (`@contract skills.listing_budget_enforced_as_declared`), which is the frame's whole * bound: the listing is an intact-class attachment, so no delivery lane measures it a second time. * * Tier B follows CC 220 `sbs` @317547-317586 (reserve the marker + every name-only line, then spend * the slack on descriptions in {@link SkillListingEntry.declaredRank} order); tier C closes with an * honest `(… +N more skills …)` count line — the tool still serves all of them. */ export declare function renderSkillLinesWithin(skills: ReadonlyArray<SkillListingEntry>, opts: { /** The whole frame's byte budget — honoured as given; the seat reader is the one place that decides it. */ blockMaxBytes: number; descMaxChars: number; frame: (lines: readonly string[]) => string; }): SkillsBlockRender | undefined; /** * The injected `skill` tool: returns a skill's full body as a tool result. * * A SUCCESSFUL activation writes the skill's two lists into the run's grant table (`grants`, origin `model`): its * `allowed-tools` then run without asking and its `disallowed-tools` are refused for the rest of the run; an unknown skill * throws first and writes nothing. Activating a skill that declares a list was itself a question (the gate reads this * tool's seat off the call's arguments — `Execute skill: <name>`), so a write here always follows a statement. The tool * runs ALONE in its batch: a call after it in the same message is judged after the rows exist. */ export declare function createSkillTool(skills: SkillLike[], grants?: SkillGrantTable): AgentTool; export {};