@sema-agent/core
Version:
Stateless, task-oriented AI agent core
233 lines (232 loc) • 16.6 kB
TypeScript
import type { BeforeWriteHook, EffectiveMemoryScopes, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
import { type CaptureEntitlementInput, type MemoryCapturePosture } from "./memory-capture-optout.js";
import type { Prepared } from "./contracts.js";
import type { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
export interface PrepareMemoryInput {
/** borrowed-readonly — the REBOUND spec. Read: `memory` (the scope declaration this phase
* normalizes) and `principal` (the org-admission subject). Never mutated. */
spec: TaskSpec;
/** borrowed-readonly — the deployment seams this phase reads: the two operator lanes (`onError`,
* `onNotice`), the memory planes (`memoryBackend`, `memoryStore`, `memoryEngineDir`, `rootPath`,
* `deploymentMemoryScopes`, `memoryScopeAdmission`), `memoryCaptureRecordStore`,
* `memoryConsolidation`, `runtimeCapsResolver` and `onMemoryHarvestReport`. Receivers preserved
* (`deps.onError?.(…)`). */
deps: RunnerDeps;
/** borrowed-readonly — the acquired session's id: the engine session's key, the operator lines'
* context, and the capture record's subject. */
sessionId: string;
/** borrowed-readonly — the resolved task root: the containment boundary every derived memory dir
* is checked against, and the repo-root fallback when `deps.rootPath` is unset. */
taskRootPath: string;
/**
* borrowed-readonly — #181-F5: whether a tool NAMED `Write` (the tool the CC `# Memory` instruction names) is on the
* ASSEMBLED roster this run, not excluded, AND can actually REACH the host-side memory root:
* prepare-task's `tools.some(t => t.name === "Write") && !exclude.includes("Write")` plus the
* remote-env conjunct (a hand-band Write on a remote ExecutionEnv writes the sandbox filesystem,
* not the host memory root — it does not count unless the deployment declares the mount shared
* via `memoryPersistenceCapable: true`). Name occupancy IS the channel declaration for the local
* arms — a hands-less run that mounts its own `Write` counts, and an `excludeTools: ["Write"]`
* run reads as unmounted whatever the hands band did. Threaded into `engine.inject` so a run with
* a writable memory scope but no write channel is not instructed to call a tool that is not on
* its roster (or cannot reach the store); the RB-276 index seed follows the same gate. Since #691 this is
* the FIRST conjunct of the write channel; the second is the root fence's admission of the write plane's
* root through {@link advertisedWritableDirs} — the channel is open only when both hold.
*/
writeToolsMounted: boolean;
/**
* borrowed-mutable — #691: the root fence's advertised-writable-directory seat (its module header carries the
* one rule). Present iff the hands band mounts this run (a fence exists between the roster's `Write` and the
* store); absent ⇒ a `Write` on the roster is a caller tool running in the host process, which no fence
* stands before, so admission holds trivially. This phase admits the WRITE plane's root when the channel's
* first conjunct holds (`writeToolsMounted`) and a write face exists; a read-only layering admits nothing.
*/
advertisedWritableDirs?: AdvertisedWritableDirs;
/** borrowed-readonly — design/319 (A ticket): the run's reminder provenance mark: the memory engine's announcement
* block is an engine-authored `<system-reminder>`, so its open tag must carry the same mark the
* system-prompt declaration names. Threaded into every `engine.inject` call this phase builds. */
reminderMark: string;
/**
* borrowed-readonly — the raw `TaskSpec.memoryPersistenceCapable` DECLARATION, kept separate from the composed
* {@link rosterCanPersist} verdict because the two drive different arms:
* - `true` (declared) RETRACTS the engine's own read-only notice — the deployment vouches for a
* persistence channel the engine cannot see (a custom writer persisting through its closure),
* so "you cannot save" beside it would be a false claim. An INFERRED-true roster must NOT
* retract: over a writeScope-null layering the engine will refuse those very file writes
* (`read_only_layering`), so the roster's write tools prove nothing about THIS store and
* stripping the notice re-opens the silent-confabulation hole for exactly the state the
* notice was built for.
* - `false` (declared) also CLOSES the file-tool write channel into the writable memory root
* (the write gate refuses), so the mounted notice's "the engine will not accept writes into
* the memory store" stays a true statement instead of a disclosure the store then contradicts.
*/
memoryPersistenceDeclared?: boolean;
/**
* borrowed-readonly — design/336 §13-3: the memory-provenance master switch, screened at the config doors
* (`config.memory_provenance`, exact spellings, absent folded to `"carry"`). Every engine this
* phase constructs runs under it, and the pollution notices/tool wording state the mode-true
* behavior off it. See {@link RunnerDeps.memoryProvenance}.
*/
memoryProvenance: "off" | "carry";
/**
* borrowed-readonly — design/178 ②-1: whether the `memory_search`/`memory_get` pair PASSED its early mount conjuncts
* (exclusion + name occupancy, decided in prepare-task BEFORE this phase). True ⇒ this phase builds
* the pair's ToolSpecs ({@link PrepareMemoryResult.memoryTools}) and the injection's instruction
* teaches the retrieval discipline; false ⇒ neither happens, so the teaching and the roster agree
* (the #181-F5 config-face honesty rule). The later support-name counterfactual can still retract
* the pair — prepare-task then strips the taught paragraph back out of the block.
*/
memorySearchToolsPlanned: boolean;
/**
* borrowed-readonly — design/383 §3.1: the SCREENED capture posture (prepare-config-doors; an
* absent deps seat folded to `"open"`).
*/
memoryCapturePolicy?: MemoryCapturePosture;
/** borrowed-readonly — whether the deployment DECLARED a posture at all: the once-per-process
* "switch in use, no policy configured" operator line fires only on the undeclared arm. */
memoryCapturePolicyDeclared?: boolean;
/**
* borrowed-readonly — design/383 §3.1: the per-principal entitlement as resolved by prepare-task's ONE caps resolve
* (`value` = `runtimeCaps.allowMemoryOptOut` when boolean; `faulted` = the resolve threw or the
* member was non-boolean — per the member's F2 fault-transit contract a fault must never arrive
* as a coined explicit value). Consumed by the adjudicator on every ingress of this prepare.
*/
captureEntitlement?: CaptureEntitlementInput;
/**
* borrowed-readonly — design/383 §2.1b: the carrier-absence verdict, judged by prepare-task from the DECLARED
* deployment shape: a remote-execution deployment whose control plane does not follow the run
* (the per-run sandbox / stateless-replica form) with no
* {@link RunnerDeps.memoryCaptureRecordStore} supplied. TRUE + a `capture:"off"` declaration ⇒
* the declaration is refused (`config.memory_capture_unsupported`) — a promise the next resume
* replica cannot see is a false promise, and a loud refusal beats one.
*/
captureCarrierUnsupported?: boolean;
/**
* borrowed-readonly — design/383 §2.5: the IMMEDIATE spawner's session id (trusted internals seat, filled by the
* delegation lanes; absent on top-level runs). The record-query leg of the delegation floor: a
* child's harvest legs also read the PARENT's opt-out record off the shared write-plane control
* store, so a parent whose opt-out landed AFTER this child spawned (the mid-run flip) still gets
* zero admission from in-flight children — the spec-carried floor covers only spawn-time state.
* Honest bounds (registered): the query sees the CHILD's write-plane store — a parent whose
* record lives on a different plane (a dual-root split where the two write different planes) is
* outside its reach, and a lane that fills no parent seat (TIER-3 durable revival) is covered
* only insofar as the child's OWN record (same sessionId across revival) or spec carriage
* survives that lane.
*/
parentSessionId?: string;
/**
* borrowed-readonly — design/383 §2.5: the delegation FLOOR bit off the trusted internals chain
* (`internals.memoryCaptureFloor === true`): the spawning session was opted out at spawn time.
* A floored leg runs opted-out WITHOUT re-adjudication (the parent's granted opt-out tightens
* the tree; re-asking could only loosen) and mints its own one-way record (reason names the
* floor), so its grandchildren and its own resumes inherit mechanically.
*/
captureFloor?: boolean;
/**
* borrowed-readonly — design/383 §2.5 (rescan post-6.0.0-RC): the floor seat's THIRD state off the same trusted
* chain: the spawning session's capture state was INDETERMINATE at spawn time (its record store
* faulted at the getter read). This leg resolves it against the live record query (see
* `armCaptureOptOut`): ancestor record found ⇒ floor; still faulting ⇒ this session runs
* indeterminate itself (mechanical restrictions, no record mint); readable-and-clean ⇒ clean.
* Mutually exclusive with `captureFloor` at the mint site (the spread keys on optedOut !== true).
*/
captureFloorIndeterminate?: boolean;
/**
* borrowed-readonly — design/383 §2.5 (rescan post-6.0.0-RC): the HOST-SIDE FORK lane: the session this one was
* forked FROM (`SessionMetadata.forkedFrom`, read by prepare-task off the acquired session). A
* fork byte-copies the source's committed transcript, so the source's standing opt-out governs
* the copy: record found at prepare ⇒ the floor arm (mint, reason naming the fork, no
* re-adjudication — the source's granted opt-out tightens the lineage); source store faulting ⇒
* indeterminate (suppress, no mint); clean ⇒ ordinary run. Also seeded into the harvest
* record-query walk, so a source that flips WHILE the fork runs suppresses it live.
*/
captureForkOrigin?: string;
/** borrowed-readonly — design/383 §2.5 (rescan, codex adversarial round): the fork-lineage read itself FAULTED
* (session metadata unreadable): whether this session forks an opted-out source is unknowable,
* so the capture arm runs INDETERMINATE (suppress fail-closed, disclose, mint nothing) — a
* metadata outage must not read as "no lineage" (读错不许伪装 clean). */
captureLineageFault?: boolean;
/** borrowed-readonly — design/383 §2.5 (codex review adopted): the SPAWNING session's write-plane control dir
* (trusted internals chain), so the record-query leg reaches a parent on a DIFFERENT memory
* plane (project-writing parent, personal-writing child). Absent ⇒ same-plane query only. */
parentCaptureQueryDir?: string;
/** borrowed-readonly — design/383 §2.5 (codex round 3): the full ancestor chain's capture coordinates off the
* trusted internals seat; the harvest closure walks every row (deduped with the parent seat),
* so a mid-run flip anywhere up the tree suppresses this in-flight descendant. */
captureAncestors?: ReadonlyArray<{
sessionId: string;
controlDir?: string;
}>;
/**
* borrowed-readonly — the org-memory admission context (ruled 2026-08-05), assembled by
* prepare-task:
* - `orgMemoryDenied`/`complianceDegraded`: the compliance capability `org_memory_mount` verdict
* for this principal (the veto covers the request-origin plane);
* - `parentAdmittedOrgScopes`: the delegation chain's FROZEN admitted org set — `undefined` for a
* top-level task; for a delegated child the caller passes the fail-closed empty set when the
* chain carries no verdict (an older-shape chain must not read as "unconstrained");
* - `priorOwnVerdict`/`governedProvenance`: the session freeze and the monotonic governance bit.
* Read as a whole into the admission call; never mutated.
*/
admissionCtx: {
orgMemoryDenied: boolean;
complianceDegraded: boolean;
parentAdmittedOrgScopes: readonly string[] | undefined;
/** The SESSION freeze (resume legs): this session's own verdict recorded at suspend — the final
* admitted projection can only narrow relative to it, regardless of origin. */
priorOwnVerdict: import("../memory-admission.js").OwnOrgAdmissionVerdict | undefined;
/** The monotonic governance-provenance bit (deployment surface / inherited / prior verdict) —
* governance evidence at the door even when every admitted set en route is empty. */
governedProvenance: boolean;
};
}
/** #691 — what {@link PrepareMemoryResult.composeInjection} hands back: the composed `# Memory` block (undefined
* when nothing mounts), the recall-discipline segment EXACTLY as appended (prepare-task's retraction strips this
* returned string — #493 单铸), and the RB-276 index seed files whose full disk text rides in the block. */
export interface MemoryInjectionComposition {
memoryBlock: string | undefined;
recallDisciplineSegment?: string;
seedFiles?: ReadonlyArray<{
path: string;
content: string;
}>;
}
export interface PrepareMemoryResult {
memoryEngineSession: Prepared["memoryEngineSession"];
memoryTools?: ToolSpec[];
/** owned — the memory WRITE gate (Write/Edit/NotebookEdit run their FINAL text through MemoryEngine.gateWrite when
* the target lands inside the memory writable root; the read-only plane refuses its own domain). Present iff a memory
* engine mounted; the hands phase composes it into the band's beforeWrite (a memory-less run: every write passes). */
memoryWriteGate?: BeforeWriteHook;
/**
* owned — #691: the injection's COMPOSITION step, run by the driver AFTER the hands phase (the mount above runs
* BEFORE it). It performs the one destructive read this phase has — `inject()` drains the announcement queue —
* so a hands-phase refusal cannot consume announcements a later prepare should deliver, and it takes the
* roster-wide persistence verdict the driver can only read off the MOUNTED roster (`rosterCanPersist`: the
* `TaskSpec.memoryPersistenceCapable` declaration when set, else the known-store-path inference over the
* assembled tools). Call exactly once per prepare.
*/
composeInjection: (facts: {
rosterCanPersist: boolean;
}) => MemoryInjectionComposition;
/**
* The FROZEN org-admission verdict of this prepare: every org scope the task actually mounts
* (deployment-origin kept + request-origin admitted). Threaded into the run's child chain
* (`InheritedGate.admittedOrgScopes`) so a delegated child's request plane can only narrow it.
* Empty when the task mounts no org plane (including memory-less tasks).
*/
admittedOrgScopes: readonly string[];
/** The session's own frozen verdict to persist at suspend — present iff this leg ADJUDICATED an
* org plane (including adjudicated-empty); `undefined` for an org-less task. */
ownOrgVerdict: import("../memory-admission.js").OwnOrgAdmissionVerdict | undefined;
/**
* design/178 v2 §2.3 (件①) — the memory-visibility OBSERVATION this phase minted for
* `TaskResult.effectiveMemoryScopes` (see {@link EffectiveMemoryScopes} for the state law).
* ALWAYS present when the phase returns: the memory-less outcomes are their own values
* (`none` / `memoryless`), never an absent seat — absence on the RESULT means only "prepare
* never completed" (this phase threw). Minted AFTER the materialize outcome (the fail-open arm
* sits between the admission verdict and the mount — an earlier stamp would report a mount that
* never happened); the deliberate-refusal `config.memory_*` throws produce no value at all.
*/
effectiveMemoryScopes: EffectiveMemoryScopes;
}
export declare function prepareMemory(input: PrepareMemoryInput): Promise<PrepareMemoryResult>;