UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

142 lines (141 loc) • 10.5 kB
import type { Message } from "../llm/index.js"; import type { AgentMessage } from "../loop/types.js"; /** * design/388 A-18 — the FROZEN history table: tool names a persisted pre-rename transcript may still carry * (the upstream trio and the pre-CC-name core trio), mapped to the path access the live definition declares. * Never grows: a new name declares its access on the catalog instead. Registered with the name-table gate. */ export declare const DURABLE_HISTORY_NAMES: ReadonlyMap<string, "read" | "create" | "edit">; /** File paths touched by a session branch or compaction range. */ export interface FileOperations { /** Files read but not necessarily modified. */ read: Set<string>; /** Files written by full-file write operations. */ written: Set<string>; /** Files modified by edit operations. */ edited: Set<string>; /** * Cross-set touch order for modified files (written ∪ edited), oldest→newest — a re-touch moves * the path to the end (Set insertion-order semantics via delete-then-add). This is what lets * working-file attachments pick the MOST RECENTLY touched files instead of the first N * alphabetically (the LONGRUN-2b miss: orders.js sorted out of the top-3). */ modifiedOrder: Set<string>; } /** Create an empty file-operation accumulator. */ export declare function createFileOps(): FileOperations; /** Add file operations from assistant tool calls to an accumulator. */ export declare function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void; /** One retained invoked skill: the Skill tool result text as it stood in context (possibly * truncated with {@link SKILL_RETENTION_TRUNCATION_MARKER}). */ export interface InvokedSkillRetention { name: string; content: string; } /** Per-skill retained-content cap (chars; CC: 5000 tokens). Truncation is marker-inclusive: a * truncated entry is EXACTLY this long, marker included. */ export declare const SKILL_RETENTION_PER_SKILL_MAX_CHARS = 20000; /** Total retained-area budget (chars; CC: 25000 tokens). Skills that do not fit whole are skipped * individually — later (smaller) skills may still fit. */ export declare const SKILL_RETENTION_TOTAL_MAX_CHARS = 100000; /** In-band truncation marker. CC's marker points at `Read` on the skill's file path; sema skills * are spec-supplied content with no filesystem path, so the equivalent recovery route here is * re-invoking the Skill tool (which, post-parity, always serves the FULL text). */ export declare const SKILL_RETENTION_TRUNCATION_MARKER = "[... skill content truncated for compaction; invoke the Skill tool again if you need the full text]"; /** Strip the appended model-supplied `## Input` args section from a Skill tool result body (see * {@link SKILL_INPUT_SECTION_SEPARATOR}); returns the body unchanged when no section is present. */ export declare function stripSkillInputSection(text: string): string; /** * Scan dropped messages for successful Skill tool invocations and merge them over the previous * compaction's retained set. Returns the UNBUDGETED merged list, most recent invocation FIRST. * * - `prevRetained` (the prior compaction's `details.invokedSkills`, newest-first) seeds the set: * every prior entry is older than anything scanned here, and a re-invocation refreshes both the * content and the recency rank (delete-then-set Map insertion order). * - Only NON-error Skill results count (an unknown-skill error result carries no skill body). * - The skill NAME comes from the paired assistant toolCall's arguments (`skill`, legacy `name`); * the result message alone only knows the tool name. */ export declare function extractInvokedSkills(droppedMessages: AgentMessage[], prevRetained: InvokedSkillRetention[] | undefined): InvokedSkillRetention[]; /** Apply the CC-shaped retention budget to a newest-first invoked-skill list: per-skill truncation * (marker-inclusive), then the total budget with SKIP-not-break semantics for over-budget entries. * * `maxTotalChars` (window-aware clamp): the CC-shaped 20k/100k caps are UPPER BOUNDS tuned for * ≥100k-token windows, not guarantees — on a small-window model an area that big would push * 「summary + retention + kept tail」 past the window on every post-compaction request. A caller * that knows the consuming model's window passes its remaining chars headroom here; the effective * total budget is `min(100k, maxTotalChars)`, and the per-skill cap shrinks to the effective * total so the MOST RECENT skill is truncated-to-fit rather than skipped whole (skip-whole is the * right CC semantics between skills, but under a window clamp it would drop the newest skill in * favor of an older smaller one). A budget too small to hold even the truncation marker retains * nothing. Absent ⇒ the plain CC-shaped budget. */ export declare function budgetInvokedSkillsRetention(newestFirst: InvokedSkillRetention[], maxTotalChars?: number): InvokedSkillRetention[]; /** * Replace the bodies of successful Skill tool results with a bounded placeholder before the * summarizer sees them. Extraction copies those bodies into the retained invoked-skills area, so * leaving the originals in the summarization input would (a) double-charge the window budget and * (b) let multi-thousand-char skill texts crowd real conversation out of the summary input clamp. * The toolResult MESSAGE stays (shallow-copied, same toolCallId/toolName) so the call/result * pairing stays legal for serialization; only its content is swapped for a short note. `groups` * lets a split-turn boundary pass both legs at once so the call→name pairing map spans them. * `retainedNames` keeps the placeholder honest: a skill the retention budget skipped is disclosed * as omitted, not claimed preserved. Non-matching messages pass through by reference. */ export declare function replaceInvokedSkillBodiesForSummary(groups: AgentMessage[][], retainedNames: ReadonlySet<string>): AgentMessage[][]; /** Defensive reader for persisted `CompactionDetails.invokedSkills` (details is `unknown` on the * wire; sessions persisted before this field existed simply yield an empty list). */ export declare function readRetainedInvokedSkills(details: unknown): InvokedSkillRetention[]; /** Render the retained invoked-skills area appended to the compaction summary MESSAGE at context * build time (never baked into the persisted summary string — see the module note above). */ export declare function renderInvokedSkillsRetention(skills: InvokedSkillRetention[]): string; /** Bound on the retained ref list — write-side cap AND the import door's ceiling (import-validate * bounds against the producer's own constant so the two never drift). The list is oldest-first * (carry-forward seed, then this range's refs); over the cap the OLDEST refs are dropped — the * newest elisions are the likeliest to be paged back. */ export declare const PERSISTED_OUTPUT_REFS_MAX_ENTRIES = 50; /** Collect `<persisted-output ref="…">` handles from tool results in `messages` into `into` * (insertion-ordered Set = first appearance order; duplicates collapse). */ export declare function extractPersistedOutputRefs(messages: AgentMessage[], into: Set<string>): void; /** Defensive reader for persisted `CompactionDetails.elidedMessages` (RB-398-b①): a non-negative * safe integer, else absent (pre-field sessions / foreign shapes yield undefined ⇒ no wrapper * disclosure — the byte-identical legacy render). */ export declare function readElidedMessages(details: unknown): number | undefined; /** Defensive reader for persisted `CompactionDetails.unsummarizedMessages` (PTL-shortfall accounting): same contract as * {@link readElidedMessages} — a non-negative safe integer, else absent (pre-field sessions / * foreign shapes ⇒ no uncovered-scale clause, byte-identical wrapper). */ export declare function readUnsummarizedMessages(details: unknown): number | undefined; /** Defensive reader for persisted `CompactionDetails.activeTools` (RB-402): * the ToolSearch activation snapshot each landed compaction restates. details is * `unknown` on the wire — pre-field sessions and foreign shapes yield an empty list; non-string/ * empty members are dropped (the consumer additionally intersects with the current leg's deferred * registry, so an unknown name is inert either way). */ export declare function readCompactionActiveTools(details: unknown): string[]; /** Defensive reader for persisted `CompactionDetails.persistedOutputRefs` (details is `unknown` on * the wire; sessions persisted before this field existed simply yield an empty list). */ export declare function readPersistedOutputRefs(details: unknown): string[]; /** Render the retained handles as a summary footer section — the deterministic counterpart of the * `clearStaleToolResults` marker's "full text persisted; call ReadToolResult with ref …" note. * Empty list ⇒ empty string (pre-existing summaries stay byte-identical). */ export declare function formatPersistedOutputRefs(refs: string[]): string; /** * Compute the file lists from accumulated operations: the display lists stay SORTED (stable * summary-tag output), `modifiedFilesByRecency` carries the same modified set most-recent-FIRST * for consumers that pick a top-N (working-file attachments). */ export declare function computeFileLists(fileOps: FileOperations): { readFiles: string[]; modifiedFiles: string[]; modifiedFilesByRecency: string[]; }; /** Format file lists as summary metadata tags (each list capped at {@link FILE_LIST_MAX_ENTRIES}). */ export declare function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string; /** * Inverse of {@link formatFileOperations}: strip a TRAILING file-ops footer block from a stored summary. * Every summary persisted by `compact()` ends with this footer; when a prior summary is threaded verbatim * as the history of a later split-turn compaction (bug-hunt #1), the canonical superset footer is * re-appended — so the stale embedded one must be removed first to avoid a duplicate. Anchored at end-of- * string, so a `<read-files>`/`<modified-files>` string that legitimately appears mid-body is untouched. */ export declare function stripFileOperationsFooter(summary: string): string; /** Serialize LLM messages to plain text for summarization prompts. */ export declare function serializeConversation(messages: Message[]): string;