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