UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

233 lines (232 loc) • 16.6 kB
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>;