UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

169 lines (168 loc) • 12.8 kB
import { type TracerHook } from "../trace.js"; import type { MaybeCompactOptions } from "../auto-compaction.js"; import type { StaleToolResultOffloadOptions, TaskSpec } from "../types.js"; import type { Context } from "../../internal/llm.js"; import { type ToolResultStore } from "../tool-result-store.js"; import type { Prepared, RunnerDepsSeat } from "./contracts.js"; import { type ContextWindowOverlay } from "../context-settings.js"; import { type CompactionSettings } from "../../internal/harness.js"; /** * The maybeCompact call arguments that MUST be assembled identically on all three * of runtask's compaction lanes (turn boundary / prompt-too-long recovery / end-of-task). * * Those lanes carried three hand-copied assemblies whose own comments said "must not silently * diverge" — a discipline enforced only by human diffing. This module makes the identity structural. * It is deliberately NARROW: only the parts that are the same invariant live here. Everything else * the three call sites pass (model source, brain, minTokens, force, trigger, window safety, and the * end-of-task lane's deliberate ABSENCE of `signal`) is a legitimate per-lane difference and stays at * the call site, where its reason is written down. * * Every export is a free function: the turn-boundary lane assembles its call inside `createTurnBoundary` * (a module-level function with no `this`), so a method would not be reachable from all three sites. * Nothing here WRITES runner state; one export ({@link legContextOverlayOption}) READS the live deps seat, * because reading it at the moment of the pass — not at prepare — is the whole of what it states. */ /** * Opt-in working-file attachments (LONGRUN-2 / CC 198 post-compact restore parity). * * CC parity: post-compact file re-read is DEFAULT ON (CC hard-codes it; * LONGRUN-2 measured ≈2.3 extra read round-trips per compaction without it). `false` opts out; an * object customizes caps. Selection set = most recently READ (CC readFileState semantics) with a * modified-files fallback inside maybeCompact. Returns `undefined` when attachments are off or the * task has no hands (no `readTaskFile` ⇒ nothing can be re-read). */ export declare function buildWorkingFileAttachments(spec: TaskSpec, prepared: Prepared): MaybeCompactOptions["workingFileAttachments"]; /** * `MaybeCompactOptions.contextInstructionFiles`: the deployment's instruction-file content (the CLAUDE.md * family) delivered to the SUMMARIZER, assembled once here and spread on all three lanes — the compaction * boundary is where the summary REPLACES the transcript that carried those instructions. * * Contract: * - Bytes = `Prepared.projectInstructionContent`, the RAW loader text captured under the same non-blank * predicate that composes the project layer. Not the assembled `memoryBlock` (model-authored in-band * bytes would gain an instructing envelope), not the COMPOSED project block (`contextInstructionFilesSection` * neutralizes the whole authority family, `user_memory`/`scope` included), not `instructionSources` * (path + hash only). Raw text in, one fence at the mint. * - Provenance: REPOSITORY-CONTROLLED text delivered through a host callback — delivery is not authorship; * the mint's neutralization stops tag breakout, not a sentence asking for an omission. Two no-code exits * for a deployment that does not want it: do not wire the loader, or return vouched content. A separate * deployment-vouched summarization-instructions seat is an open design candidate, not built here. * - Both summary forms, no per-form split: fork eligibility is decided per pass inside maybeCompact, and the * form NOT covered by a `compModel`-gated split is exactly the fork that degrades mid-pass to the * independent form; the section rides the APPENDED instruction message, never the cached prefix. * - No new size knob: the bound is `fitContextInstructionFilesSection` (independent form only). Its three * known limits are accepted knowingly: a token-dense payload can pass a chars-domain fit and still overflow; * an unknown `contextWindow` leaves the section unbounded; the FORK form cannot fit and degrades ONCE. When * a limit bites the boundary FAILS (the section is a fixed request part the retry loop cannot drop) — the * task loses that compaction rather than sending a shorter one; making the section participate in the * retry loop is a library-face contract change, tracked there. * - Cost: instruction-file content + exactly 716 chars of framing on every summarization request, both * forms, once per pass (pinned in the REF-A2 suite). Deployments that never wired the loader pay nothing: * the key is ABSENT, so their requests are byte-identical to the pre-wiring shape. */ export declare function contextInstructionFilesOption(prepared: Prepared): Partial<Pick<MaybeCompactOptions, "contextInstructionFiles">>; /** * The CC-form fork seam, assembled identically on all three lanes (REF-A2): the * summary request forks the main lane's latest real request (recorded by prepare-task's provider * shim) instead of re-serializing the conversation into an independent full-price request. * `maybeCompact` consults the accessor per pass and only when the summary model IS the main model; * before the run's first main-loop call the accessor returns `undefined` and the independent form * runs unchanged. * * Lossy-projection guard (SEMA-ONLY — CC has no request-layer reducers): the recorded request is a * PROJECTION of the session, and when ANY default reducer made it lossy (guard-trim message drops, * clearStale content blanking, the aggregate tool-result/media caps, the orphan sweep), that * projection is missing history the summary would PERMANENTLY replace — measured live: a fork of a * trimmed request loses the fact-survival suite's fact; a fork of a clearStale'd request summarizes * `[tool result cleared …]` markers. That guard lives at the RECORDING side * (prepare-task arms `requestLossyRef` per request build; a lossy request records `undefined`), so * every lane degrades to the independent form — which reads the FULL session — automatically. * * `disable` — the PTL-recovery lane's structural off-switch: the provider just rejected the very * prefix a fork would resend, so a fork attempt there is a guaranteed wasted round-trip before the * internal fallback. The other lanes pass `false`. */ export declare function forkContextOption(prepared: Prepared, disable: boolean): Pick<MaybeCompactOptions, "forkContext">; /** * Compaction-boundary center-candidate adoption. Absent (an empty object, NOT a * `centerAdoption: undefined` key) = restate as before; a boundary re-pin hashes the same artifact * identity on every lane. */ export declare function centerAdoptionOption(prepared: Prepared): Partial<Pick<MaybeCompactOptions, "centerAdoption">>; /** Defaults (object-present, field-absent). Named per the magic-number rule. */ export declare const STALE_OFFLOAD_DEFAULT_KEEP_RECENT_PER_TOOL = 3; export declare const STALE_OFFLOAD_DEFAULT_MIN_SAVINGS_CHARS = 2000; /** Resolved (validated, defaulted) knob — see {@link resolveStaleToolResultOffload}. */ export interface ResolvedStaleToolResultOffload { keepRecentPerTool: number; minSavingsChars: number; } /** * Validate + default the spec knob. Fail-loud on non-finite/negative/fractional * values (a computed NaN silently re-defaulting is the ||0-fold this repo's engineering code bans); * `undefined` knob ⇒ `undefined` (feature completely off). */ export declare function resolveStaleToolResultOffload(knob: StaleToolResultOffloadOptions | undefined): ResolvedStaleToolResultOffload | undefined; /** The pointer that replaces a stale result's text in the request projection. PURE function of * (toolName, ref, chars) — that determinism is the DECISION-stability guarantee: a result already * replaced maps to the same pointer bytes on every later request (no flip-flop, no re-decision * churn). It is NOT full prefix stability: the verbatim→pointer TRANSITION itself (a result * crossing the staleness line as newer same-tool results arrive) rewrites that message's bytes * once, invalidating the provider cache from that position for one request — a bounded, * once-per-result break the `minSavingsChars` floor is there to amortize. * Deliberately NOT `offloadPagebackHint` (the preview's hint): that hint's wording is a * function of the LIVE reachable-tool set, and this pointer is re-derived per request — a mid-run * ToolSearch activation would flip its bytes and break the very prefix stability this file pins. * The reader is named by its real mounted name (`read_tool_result` is a * retired alias — a model following it burned a rejected call first). */ export declare function buildStaleOffloadPointer(toolName: string, ref: string, chars: number): string; /** * The request-side projection: replace STALE same-tool results with byte-stable * pointers. "Stale" = has ≥ `keepRecentPerTool` NEWER results of the same tool name in this request * — a monotone predicate (a result never gains "recency" back), so a replacement decision made on * one request holds on every later one and re-derivation is deterministic: no run-scoped mutable * decision state is needed for byte stability, only the pointer's purity (see * {@link buildStaleOffloadPointer}) and the store's write-once `put`. * * The session transcript is NEVER touched — this runs on the outgoing {@link Context} only. * Error results, image/document-bearing blocks' non-text parts, already-offloaded previews, and * replacements that would save < `minSavingsChars` are left verbatim. The full text is persisted * under a deterministic content-digested ref (`buildToolResultRef(sessionId, toolCallId, * toolResultContentSegment(text))` — a `~`-separated three-segment mint whose digest is its OWN * segment; write-once, so re-projection on every turn re-puts a no-op; the digest exists because * tool-call ids carry no cross-turn uniqueness contract), readable back via `read_tool_result`. */ export declare function projectStaleToolResults(context: Context, cfg: ResolvedStaleToolResultOffload, store: ToolResultStore, sessionId: string, /** Run-scoped cache of refs already persisted by THIS run: without it the * write-once `put` is still re-issued every request — on the file backend that is a * write-then-discard round-trip per stale result per turn. Caller owns the Set's lifetime. */ writtenRefs: Set<string>): Promise<Context>; /** * MF-18 修② — fidelity disclosure: the summary INPUT was clipped to the compaction model's * window. Fires pre-call, so the disclosure survives a failed attempt. All three lanes emit the SAME * frame; the end-of-task lane passes `spec.tracer ?? deps.tracer` / `spec.taskId ?? sessionId` * explicitly because `RunState.telemetry` (which holds exactly those two resolved values) is not in * scope inside `finish()`. */ export declare function emitInputTruncated(tracer: TracerHook | undefined, taskId: string): NonNullable<MaybeCompactOptions["onInputTruncated"]>; /** * The deployment's per-model window overlay in force for THIS pass — the one option of the three lanes that * is deliberately a LIVE read rather than a prepared value. * * Two reads, both at the moment of the pass: the leg's own catalog word (the harness's current ref, so a * mid-task degrade moves the key along with the model) and the Runner's current deps seat (so a `swapDeps` * reaches a leg already running). An object-pinned leg has no word and therefore no overlay — the key is * simply absent, which `maybeCompact` reads as "the model card's own declarations", the pre-seat behavior. */ export declare function legContextOverlayOption(prepared: Prepared, runner: RunnerDepsSeat): Pick<MaybeCompactOptions, "contextOverlay">; /** * The compaction SETTINGS a pass runs under — the assembly all three lanes share. * * `{ ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction }` was hand-copied at each lane, and it is where the * deployment's live `enabled` declaration belongs: `maybeCompact` has exactly one enablement gate (it reads * `settings.enabled`), so folding the overlay in HERE makes the deployment's switch reach the boundary pass, * the recovery pass and the end-of-task summary by one edit instead of three, and keeps "who decides whether * compaction runs" a single expression ({@link compactionEnabled}: the request authored it > the deployment * declared it > on). */ export declare function legCompactionSettings(spec: TaskSpec, overlay: ContextWindowOverlay | undefined): CompactionSettings;