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