UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

136 lines (135 loc) • 8.82 kB
import type { AutoModeClassifyInput } from "./auto-mode.js"; import type { Message } from "../internal/llm.js"; /** The sentinel a deployment puts INSIDE a rule list to splice the CC default rules back in at that * position (CC `XYt = "$defaults"`). Without it, a non-empty user list REPLACES the default section. * DECLARED in `./auto-mode-defaults.js` (the leaf the #503 recipe canonicalizer can reach without * loading these assets) and re-exported here, where every caller already looks for it. */ export { AUTO_MODE_DEFAULTS_SENTINEL } from "./auto-mode-defaults.js"; /** Deployment-authored rule overrides for the four paired sections of the permissions document. * Each entry is ONE rule (rendered as a `- ` bullet); omit a field (or pass an empty array) to keep * the CC default rules for that section verbatim. */ export interface AutoModeRules { allow?: readonly string[]; softDeny?: readonly string[]; hardDeny?: readonly string[]; environment?: readonly string[]; } export interface BuildAutoModePromptOptions { rules?: AutoModeRules; /** Settings-plane deny rules (tool[/argument-pattern] strings) — rendered into the * `<settings_deny_rules>` slot as the CC circumvention-catching paragraph, after the engine's own lines. */ settingsDenyRules?: readonly string[]; /** The ENGINE's deny lines — its effective read-deny table as rules ({@link readDenyClassifierRules}). * Rendered in the same paragraph, ahead of `settingsDenyRules`. A separate member (not pre-merged by the * caller) because a persisted arming records the two halves separately: the deployment's half is compared * against the redeeming deployment's own settings, the engine's half against the row's own table. */ derivedDenyRules?: readonly DerivedDenyRule[]; /** Extra session-context facts (e.g. the CC user-identity line) — appended as a * `## Session Context` bullet block after the assembled document. */ sessionContext?: readonly string[]; /** design/385 §4.5 — the text spliced into the `<cross_session_messages_rule>` slot. Absent (the * default, and every deployment without the cross-session lane) ⇒ the slot is blanked exactly as * before; the lane mount passes `CROSS_SESSION_CLASSIFIER_RULE`. Callback form at the splice (the * text may carry `$`). */ crossSessionMessagesRule?: string; } /** * One engine deny line: a `Read(...)` rule in the engine's own path-rule grammar (`//` = the filesystem root, * `*` within one segment, a whole-segment `**` across segments, and a rule reaches the path it names and * everything under it — `parseRuleText` / `pathRuleReachOf`), plus whether ASCII letter case is ignored, which * that grammar cannot spell and the paragraph states beside the rule. */ export interface DerivedDenyRule { readonly rule: string; readonly anyCase?: true; } /** * The effective READ-deny table as the classifier's deny lines — one line per row, the row's reach in the * engine's own `Read(...)` path-rule grammar. * * A read-deny row is a run of path SEGMENTS matched anywhere in a path — the entry itself and everything under * it — and its `*` never crosses a `/` (read-deny.ts: `.ssh` refuses `~/.ssh/id_rsa` and `/srv/x/.ssh/config` * alike; a `**` segment matches exactly one segment). The rule grammar reaches the path a pattern names and * everything under it, so the line is `//**` + `/` + the row with every run of stars written `*` (one segment, * as in the row): `.ssh` ⇒ `Read(//**\/.ssh)`, `**\/secrets/**` ⇒ `Read(//**\/*\/secrets/*)`, `*` (every * path) ⇒ `Read(//**\/*)`. A row that names no segment at all (`""`, `/`) is not stated. A HOME-ANCHORED row * (`~/<run>`, read-deny.ts `READ_DENY_HOME_WORD`) is the rule grammar's home word with the same reach: * `~/.sema/settings.json` ⇒ `Read(~/.sema/settings.json)`. * * The text of a row can come from a task (a spawned task's author included) and it is spliced into a trusted * prompt, so a row is stated as a rule and nothing else: every character the grammar or the prompt must not * carry (see NOT_STATED) is written `*`, and an over-long pattern is cut and closed with `*` — both wider than * the row, never narrower, and the result still parses as a rule. No row can start a line, leave its code * span, form a tag or a second rule, or read as a sentence; the engine enforces the row as compiled either way. * * A row matched without regard to ASCII letter case (the deny set's default) carries `anyCase` when its stated * pattern has an ASCII letter. Duplicates are folded AFTER this encoding, first seen. */ export declare function readDenyClassifierRules(entries: readonly { readonly pattern: string; readonly caseSensitive?: boolean; }[], rules?: readonly { readonly tool: string; readonly pattern: string; }[]): DerivedDenyRule[]; /** * The `<settings_deny_rules>` paragraph for one arming, from the engine's facts — the effective deny table * (`readDeny`: the compiled matcher's entries, built-in tiers + deployment + task additions; `readDenyRules`: its * permission-rule rows, both faces) and the deployment's `settingsDenyRules` — exactly as {@link buildAutoModePrompt} * splices it. Empty inputs ⇒ "". */ export declare function renderClassifierDenyRules(input: { readonly readDeny?: readonly { readonly pattern: string; readonly caseSensitive?: boolean; }[]; readonly readDenyRules?: readonly { readonly tool: string; readonly pattern: string; }[]; readonly settingsDenyRules?: readonly string[]; }): string; /** * Assemble the classifier SYSTEM prompt (CC `KRg`, content-equivalent single string — CC splits the * session-context tail into a second uncached block purely as a prompt-cache optimization). * Every `.replace` uses the CALLBACK form: the assets and user rules are full of `$`-patterns * (`$USER/...`) that a string replacement would corrupt via `$&`/`$'` substitution (CC does the same). */ export declare function buildAutoModePrompt(options?: BuildAutoModePromptOptions): string; export interface AutoModeWindowOptions { /** Newest transcript entries INCLUDED (default 40). Zero includes none — the classifier judges the * action with no run history, and the omission line still names every entry left out. */ maxEntries?: number; /** Per-entry excerpt cap in characters (default 2_000 — tool results and long turns are truncated * head-first with a `[… N chars truncated]` marker; the classifier weighs shape over bulk). */ maxCharsPerEntry?: number; } /** Whose words the window's user lane carries. A delegated run (a sub-agent the engine started on another agent's * behalf, forks included) has no user turn of its own: every user-lane entry reached it through the agent that started * it — its objective, a later message, or (for a fork) the history it was handed — and the engine vouches for none of * them as the user's own message, so they render under `[launching agent]`, never with user authority (#1166). */ export interface AutoModeWindowRun { readonly delegated?: boolean; } /** * Render the transcript window half of the classifier's user prompt: the newest `maxEntries` messages, * oldest→newest, each labeled by lane. Plain data — see the module header for the declared v1 gap vs * CC's entry-formatter stack. * * `maxEntries` counts the entries INCLUDED, so a bound of zero includes none and says so. That case is * handled explicitly rather than left to `slice`: `slice(-0)` is `slice(0)`, i.e. the WHOLE transcript, * which inverts a declared bound into "unbounded" — and silently, because the omitted count would then * be zero and the omission header would not render either. The recipe canonicalizer admits zero for both * window knobs deliberately (its sibling knobs use a minimum of one), so a deployment can genuinely * declare "classify the action with no transcript context"; a negative bound is not a coherent window * either and takes the same arm rather than `slice`'s "drop the oldest N". * * `run` is the engine's own fact about the run whose transcript this is ({@link AutoModeWindowRun}) — never deployment * configuration, which is why it is not a member of the window options a deployment declares. */ export declare function renderAutoModeWindow(messages: readonly Message[], options?: AutoModeWindowOptions, run?: AutoModeWindowRun): string; /** * Render the pending action (the classify subject). The 207 process's step 1 keys on "the last tool * call in the transcript" — this block IS that entry for our assembly, placed last in the user prompt. */ export declare function renderAutoModeAction(input: AutoModeClassifyInput): string;