@sema-agent/core
Version:
Stateless, task-oriented AI agent core
290 lines (289 loc) • 23.1 kB
TypeScript
import { type BeforeWriteHook, type CwdRef, type PathRuleDenyRow, type ContainmentFace } from "../../tools/fs/index.js";
import { type ReadFileState } from "../../tools/fs/safety.js";
import { type PlacedReadDenyMatcher } from "../../tools/fs/read-deny-home.js";
import { type ShellReadAdmission, type ShellReadCheck } from "../../tools/fs/shell-read-check.js";
import type { ShellReadBoundary } from "../../tools/fs/shell-read-judge.js";
import type { RunnerDeps, ShellGateDoctrine, TaskSpec } from "../types.js";
import type { Model } from "../../internal/llm.js";
import type { AgentHarness, AgentTool, ExecutionEnv, WorkspaceState } from "../../internal/harness.js";
import type { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
import { type ArtifactStoreRef, type GrantReadRoot } from "./read-root-grant.js";
import type { StoredSession } from "../session.js";
import type { IrreversibilityTier, PrepareResume, ReversibilityProbes, RunInternals, ToolFaceSnapshot, ToolProgressLane, WriteFaceSeat } from "./contracts.js";
import { RosterBuilder } from "../tool-roster.js";
import type { PermissionMode } from "../ask-origin.js";
/** Test seam (mirrors `__resetMalformedNoticeSeatAnnouncement`): never called by production code.
* Deliberately asymmetric — it resets only the console latch: the WeakSet arm needs no seam
* because a test resets it by minting a fresh sink function (identity IS the ledger key), while
* the console arm's key is the process itself, which only this seam can refresh. Moved WITH the
* phase; prepare-task.ts keeps a re-export so the existing import sites stand. */
export declare function __resetReadFaceClampAnnouncement(): void;
export interface PrepareHandsReadFaceInput {
/** owned (value) — `ownedEnv !== undefined || deps.executionEnv !== undefined`, computed in the
* driver: the phase's own branch condition (hands mount vs the hands-less resolver arm). */
handsEnabled: boolean;
/** borrowed-readonly (the ownership-ref PROTOCOL stays with the driver — D-8 案① posture, same
* as B-4): the effective env (`ownedEnv ?? deps.executionEnv ?? stub`). This phase drives it
* (canonicalPath/joinPath/writeFileExclusive probe/instanceof screens) and closes tool bands
* over it, but NEVER rebinds or destroys it. */
executionEnv: ExecutionEnv;
/** borrowed-readonly — B-4's settled root (T6 FINAL semantics); the containment root every face
* in this phase canonicalizes from. */
taskRootFinal: string;
/** borrowed-readonly — the same root, resolved to the spelling the fence compares with, by the ONE
* resolver at the driver's root receiving point ({@link import("./task-root-canonical.js").resolveTaskRootCanonical}):
* taken as an input, never re-canonicalized here, so the fence and the root notice cannot disagree. */
taskRootCanonical: string;
/** borrowed-readonly — B-4's checkpoint-path rebaser (T7): identity unless a divergent restore
* armed it; applied to the readFileState seed keys and the handsCwd seed. */
rebaseRestoredPath: (p: string) => string;
/** borrowed-readonly — the durable-resume seat this phase reads: `seed` only (readFileState /
* handsCwd / readFace section). Presence also gates the leg forks (T11: replay never runs on a
* resume; the workspace_state rung is skipped; the face fold goes stricter-wins). */
resume: Pick<PrepareResume, "seed"> | undefined;
/** borrowed-readonly — the acquired session (B-3 output), as a Pick (相 API 规则件 R-4: receiver
* preserved): `getWorkspaceState` feeds the continuation rung; `buildContext` feeds
* the transcript replay; `getStorage`/`getEntry` feed the seat-anchor verification (one
* leaf read per prepare + a bounded parent walk on the inherit leg only). */
session: Pick<StoredSession, "getWorkspaceState" | "buildContext" | "getStorage" | "getEntry">;
/** borrowed-readonly — the acquired session's id, for error/notice tags and the band's session
* registration. */
sessionId: string;
/** borrowed-readonly — the host task id: trace frames + the band's task-registry owner. */
hostTaskId: string;
/** borrowed-readonly — the registry scope (`internals?.registryScope ?? spec.principal ??
* "default"`, computed in the driver); the band's task-registry scope. */
taskScope: string;
/** borrowed-readonly — the deny tables' permission-rule rows: the path deny rules of the leg's caller policy and of every
* ancestor's (`toolPolicyPathDenyRules`); joined to the read face's matcher and to the shell station's write face. */
pathDenyRules: readonly PathRuleDenyRow[];
/** borrowed-readonly — the two judges a shell command's write targets meet for the mode's file-command placement: the
* leg's write-protection judge (read per judgment — it is compiled at the manifest phase, before any gate is mounted)
* and the chain's write-target guard (`toolPolicyWriteTargetGuard`). Absent ⇒ neither (a standalone phase drive). */
writeGuards?: {
readonly protection: () => ShellReadBoundary["writeProtectionJudge"];
readonly target: ShellReadBoundary["writeTargetGuard"];
};
/** borrowed-readonly — the directories of the person's persisted `Read(//dir/**)` allow rows: read directories of this
* run, canonicalized and admitted exactly as `additionalReadDirectories` entries are. */
persistedReadDirectories?: readonly string[];
/** borrowed-readonly — the leg's mode seat, read LIVE by the write face ({@link writeFaceReader}): its mode's face
* is the resolver's row 3′ on every write. */
permissionMode: {
readonly current: PermissionMode;
};
/** borrowed-readonly — the D-6 spec-time full-shell reachability precomputation; an input to the
* face resolver on BOTH arms (the driver's post-exclusion roster assertion pins it to the
* mounted truth — that assertion is driver-side, after this phase). */
fullShellReachable: boolean;
/** borrowed-readonly — the operator's shell FLOOR (`"always"`: the spec's, or the chain's), or undefined (no floor:
* the shape tier installs on its own and the permission mode arbitrates its asks). */
effectiveShellGate: ShellGateDoctrine | undefined;
/** borrowed-readonly — B-1's frozen task-start face snapshot; only `.exclude` is read here (the
* Monitor-mount honesty bit for the gh rate-limit hint). */
toolFaceSnapshot: ToolFaceSnapshot;
/** borrowed-readonly — B-1's resolved main model; read ONLY for its pdf capability profile
* (the Read tool's document-degradation fork). */
model: Model;
/** borrowed-readonly — the task-spec seats this phase reads, as a Pick over the SAME spec object
* (R-4). `tools` feeds only the band shadow check; `envFacts` only the advertised scratchpad;
* `retainBackgroundProcesses` only the band's retain lane (Bash's lifetime sentence + row stamp). */
spec: Pick<TaskSpec, "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "handsReadOnly" | "readDenyPatterns" | "readFace" | "writeFace" | "oneShot" | "resumeAt" | "retainBackgroundProcesses" | "sessionId" | "tools">;
/** borrowed-readonly — the deployment seats this phase reads, as a Pick over the SAME deps
* object (R-4: `deps.onError?.()` keeps its receiver; a new deps read must widen this Pick).
* `readDenyBuiltinTiers`/`readDenyBuiltinExclude` are SNAPSHOTTED before the first await that
* precedes the toolkit compile (the caller retains these arrays by reference). */
deps: Pick<RunnerDeps, "onError" | "onNotice" | "tracer" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "readFace" | "writeFace" | "permissionRuleStore" | "hands">;
/** borrowed-readonly — the trusted spawn-side seats the band forwards: the finished-command
* notification lane, the mid-flight detach hub, and the Runner's per-session
* read-state seats (absent on a standalone prepare ⇒ the transcript-replay rung covers alone). */
internals: Pick<RunInternals, "onTaskNotification" | "detachHub" | "sessionReadStates"> | undefined;
/** borrowed-mutable — the harness seat, read LIVE by the band's queued-human-input predicate (the shell's
* auto-backgrounding leg asks it mid-command). Late-bound: the band is built here, the harness later. */
harnessRef: {
current?: AgentHarness;
};
/** borrowed-mutable — the tool-progress lane's cell, read LIVE by the band's progress seat; same
* late-binding contract as {@link harnessRef}. Unbound ⇒ every tick through it is a silent no-op. */
toolProgressRef: {
current?: ToolProgressLane;
};
/** borrowed-readonly — ABSOLUTE paths this prepare's restore wrote, deleted,
* or failed to write. Every read-state seed above describes the PRE-restore tree, so these keys are
* dropped from the map: the engine replaced those bytes itself, and a surviving entry makes the
* next refusal blame "the user or a linter" for the engine's own write. Absent (the common case)
* when no file restore ran. Fail-closed by construction — dropping a read record only ever
* produces refusals. */
restoredFilePaths?: readonly string[];
/** borrowed-readonly — the memory write gate the memory phase minted (it runs BEFORE this phase: the memory root
* must be known before the toolkit compiles), composed into the band's beforeWrite below; undefined for a
* memory-less run (every write passes the engine half). */
memoryWriteGate: BeforeWriteHook | undefined;
/** borrowed-mutable — the advertised-writable-directory seat (see its module header for the one
* rule). Present on the hands leg only (a fence exists). The memory phase, which runs BEFORE this one,
* has already admitted its write root through it; this phase admits the deployment's scratchpad and
* folds `roots` into the write-capable containment allowlist before the toolkit compiles. */
advertisedWritableDirs?: AdvertisedWritableDirs;
/** borrowed-readonly — the driver's per-tool offload wrapper (posture + policy); the
* band rides through it exactly as caller/MCP tools do. */
firstPartyOffload: (tool: AgentTool) => AgentTool;
/** borrowed-mutable — the SHARED mount array (§4.4: later refs alias it; identity is the
* contract, never map/copy). This phase pushes the hands band IN PLACE (after caller tools /
* built-ins / MCP / A2A, before the background-task mounts — T22 order). Mutation owners after
* this phase: P11 background/Monitor/worktree pushes, the driver's excludeTools splice, the
* refresh seam's runtime splice/push. The array is builder-private: this phase `mountAll`s the band. */
roster: RosterBuilder;
/** borrowed-readonly HERE (census divergence #5 vs the design row): this phase only reads
* `.has("Bash")`/`.has("Monitor")` for the provenance bits — its mutation owners are B-2/P8/P9
* (+refresh seam)/P11/P22, per the B-2 Result's mutation-owner table. */
egressTools: ReadonlySet<string>;
/** borrowed-mutable — the shellGate fold's tier write-back (`.get` for the provenance bit,
* monotonic `.set` — tighten-only). Same object B-2 returned; owner table lives on the B-2
* Result. */
irreversibilityTier: IrreversibilityTier;
/** borrowed-mutable — the shellGate fold `.add`s Bash/Monitor here. Same-object contract as
* above. */
irreversibleTools: Set<string>;
/** borrowed-mutable — the shape tier's probe installs (`.set`, only where the shell seat
* owns the tier). Same-object contract as above. */
reversibilityProbes: ReversibilityProbes;
/** borrowed-readonly — the run's reminder provenance mark (minted/adopted
* by the driver's adoption ladder); threaded into the hands band so the fs-side engine mints
* (the Read dedup/offset/empty reminders, the gh rate-limit hint) stamp the same mark the
* system-prompt declaration names. */
reminderMark: string;
/** borrowed-mutable — the leg's disclosure trigger counters; the hands
* band's Read/notebook/PDF trailer outlets bump keys on it at tool-execute time (the driver owns
* the object and folds it into result stats). */
reminderDisclosureCounts: import("../reminder-disclosure.js").ReminderDisclosureCounts;
/** borrowed-readonly — the first-touch history hook prepare built (present iff the run records file history into a
* wired store and mounts a real fs env); threaded to the write band's trackFileEdit seat. */
trackFileEdit?: import("../types.js").TrackFileEditHook;
/** borrowed-readonly — the mutation lane's LANDED observation seat (the accumulator behind
* `TaskResult.editedFiles`); threaded to the write band's onFileEdited seat. Unlike
* `trackFileEdit` it does NOT depend on a wired history store: it observes what this run's hands
* did, so it is present on every run that mounts the band. */
onFileEdited?: import("../types.js").FileEditedHook;
}
/** The phase's outputs (相 API 规则件 four-class form) — ALL settled before the return; the driver
* binds them as fresh consts (R-5) except the inverted-closure trio and the two shellGated bits,
* which write back into the driver's pre-existing `let`s (D-3 hold / driver-owned seats — see the
* module header). */
export interface PrepareHandsReadFaceResult {
/** owned — the per-task read-before-write state, exposed for the suspend closure's checkpoint
* serialization (§15.2 net-add #8) and the compaction faces. Undefined ⇔ hands-less. The MAP
* keeps being mutated by tool executes / compaction at run time — object identity carried. */
readFileStateForCheckpoint: ReadFileState | undefined;
/** owned — parity-204 (CC seededFromContext): seeds declared context files through the SAME
* resolveKey containment as a real Read. CREATED here; CALLED by the driver's memory-seed and
* loadProjectMemory blocks (T10 — the forever-cross-phase edge; this module does not claim the
* call order). Undefined on a hands-less task (nothing to seed). Reads the phase-local
* `resolvedReadFace` call-time — settled before this function returns. */
seedContextFiles: ((files: ReadonlyArray<{
path: string;
content: string;
}>) => Promise<void>) | undefined;
/** owned — the per-task logical cwd ref (observable `cd`), minted when a real
* writable shell mounts. The driver stamps it into checkpoints, threads it into worktree tools
* and gate cwd reads; tools mutate `.current` at run time. Undefined when no shell / readOnly. */
handsCwdRef: CwdRef | undefined;
/** owned — the settle-side snapshot base (what the cwd/worktree refs were RESTORED
* to), so runtask's settle write diffs against it. The driver's G4 worktree mount writes
* `.restoredWorktreeDir` after this phase — object identity carried. */
workspaceStateSettle: {
rootCanonical: string;
restoredHandsCwd?: string;
restoredWorktreeDir?: string;
baselineUnknown?: boolean;
} | undefined;
/** owned — the branch-nearest `workspace_state` entry, fetched ONCE here and shared
* with the driver's worktree seed (the G4 mount reads `.activeWorktree` through it). */
wsSnapshot: WorkspaceState | undefined;
/** owned — the wsSnapshot path rebaser (identity when no snapshot / same root); the G4 worktree
* seed applies it to the snapshot's worktree paths. */
rebaseWsPath: (p: string) => string;
/** owned — real shell + write hands + background-capable env: gates the driver's TaskOutput/
* TaskStop/Monitor/SendMessage mounts (P11) and the Monitor arm of the shellGate fold here. */
backgroundTaskToolsActive: boolean;
/** owned — this run's READ STATIONS as the one gate input (`ToolGateInput.shellReadCheck`): the shell's (ONE judgment of
* where a shell command reads, for every shell face this leg mounts, under every mode and floor) and the file tools'
* (an out-of-root Read / Grep / Glob target is a question). ⇔ hands with a real shell, or hands with an approval face. */
shellReadCheck: ShellReadCheck | undefined;
/** owned — the shell read station is mounted (⇔ a real shell): the wiring manifest's cell. */
shellReadStationArmed: boolean;
/** owned — the same station's pre-spawn ADMISSION, published for the P11 face that spawns a shell command
* and mounts LATER than the band (`Monitor`): the SAME function the band's shell faces got. ⇔ a real shell. */
shellReadAdmission: ShellReadAdmission | undefined;
/** write-back (D-3 hold) — the run's RESOLVED read face where hands mount; undefined ⇔ no hands
* (the hands-less value lives in `handsLessResolvedFace`). The driver assigns this onto its
* outer `let` for the call-time closures (ctx enrichment / checkpoint mint / envFacts). */
resolvedReadFace: ContainmentFace | undefined;
/** write-back — the run's WRITE face in force where hands mount, read per call ({@link writeFaceReader}); undefined ⇔
* no hands (the hands-less reader is `handsLessResolvedWriteFace`). The read seat's twin, resolved by the SAME order on
* its OWN seats — the two never read each other. */
resolvedWriteFace: WriteFaceSeat | undefined;
/** write-back — the hands-LESS legs' write face in force (the posture is a property of the RUN, not the mount): the
* delegation carriers and the checkpoint mint read it on every leg. */
handsLessResolvedWriteFace: WriteFaceSeat | undefined;
/** write-back (D-3 hold) — the NORMALIZED deny-set additions in force (deployment ∪ task ∪
* resume-seed; zero-builtin compile — the persisted/carrier form, never the judged union's
* post-dedup remainder). Driver assigns onto its outer `let`. */
readDenyAdditionsNormalized: ReadonlyArray<{
pattern: string;
caseSensitive: boolean;
}>;
/** owned — the built-in deny TIERS this leg MATERIALLY activated, off the same snapshot the compile
* judged with; empty ⇒ none. Relayed to the wiring manifest, never re-derived from `deps` there. */
readDenyBuiltinTiersActive: readonly string[];
/** write-back (D-3 hold) — the hands-LESS legs' resolved face (the read posture is a
* property of the RUN, not the mount). Driver assigns onto its outer `let`. */
handsLessResolvedFace: ContainmentFace | undefined;
/** owned — the canonicalized write-capable extra roots (additionalDirectories + the advertised
* scratchpad), deduped; the driver's envFacts advertisement and attachment readers share the
* ARRAY identity. Settled here (no mutation after return). */
additionalRootsCanonical: string[];
/** owned — the read-only arm of the fence widening (2026-08-05 both-arms ruling); same identity
* contract as above, and the run's LIVE read-root set: {@link grantReadRoot} is its one writer after prepare,
* and every read face reads it per call (never a copy). */
additionalReadRootsCanonical: string[];
/** owned — widen {@link additionalReadRootsCanonical} by one granted candidate (an approver's in-flight
* `readRootGrant`); undefined ⇔ hands-less (no read face to widen). */
grantReadRoot: GrantReadRoot | undefined;
/** owned — the store cell {@link grantReadRoot} judges against; the driver threads it to its one writer, the Artifact phase. */
artifactStoreRef: ArtifactStoreRef | undefined;
/** owned — the canonical containment root, published so the working-file ATTACHMENT
* reader applies the same containment the hands tools do. Undefined ⇔ hands-less. */
attachmentRootCanonical: string | undefined;
/** owned — the ONE compiled deny judge per prepare (built-ins ∪ deployment ∪
* task additions), carrying the home views of its placement. Undefined ⇔ hands-less (its consumers only exist with hands). */
readDenyMatcher: PlacedReadDenyMatcher | undefined;
/** owned — the shell-seat PROVENANCE set: the shell-running tools whose tier the engine's shell seat (read
* boundary + shape tier + shellGate floor) is the SOLE POSITIVE source of — no explicit deployment mark on that name.
* The one seat every consumer reads (risk grading, the gate's tighten attribution, the persisted-rule mandate
* boundary): `shellGatedTools.has(toolName)`. Minted here, never grown afterwards; empty when the seat fold
* did not run (no real writable shell). */
shellGatedTools: ReadonlySet<string>;
/** owned — the ENV-HAND tool names, minted AT THE MOUNT: every tool this
* phase mounted over the ExecutionEnv (the fs/bash band), later grown by the driver's own env
* mounts (Monitor, the worktree pair — the SAME Set identity, mutated at those mount sites).
* Consumed by the O9a external-content-target fold: these are the tools whose RESULTS are the
* target's bytes, so membership must be what actually mounted on the env — never a static table
* (the wake/resume reconciliation table lists TaskOutput/TaskStop, which are orchestration, and
* misses Monitor/worktree, which are hands — both directions wrong for this consumer). Empty on
* a hands-less leg (nothing mounts on the env; the driver's env mounts are hands-gated too). */
envHandToolNames: Set<string>;
/** owned — the settle-time SEAT SEAL: re-anchors the session seat to the
* task's OWN last-committed entry (see {@link import("./session-read-file-states.js").SessionReadFileStates.reanchor}). The driver calls
* it once at the run's settle (every terminal, suspend included), under the session lock, passing
* the last entryId ITS OWN run committed (the message_committed lane) — never a re-read of the
* shared session leaf, which a concurrent writer on another Runner could have moved onto a branch
* this task's reads were never on. No commits observed ⇒ no-op (the adopt-time anchor
* stands). Synchronous, never throws. Undefined ⇔ hands-less / no seat channel. */
sealReadStateSeat: ((ownCommittedTailEntryId: string | undefined) => void) | undefined;
}
export declare function resolveHandsLessReadFace(input: PrepareHandsReadFaceInput): PrepareHandsReadFaceResult;
/** The hands-MOUNT branch — the async half of the phase (see the module header for the contract).
* The driver awaits this ONLY on the hands leg (`handsEnabled === true`); the hands-less leg goes
* through the synchronous {@link resolveHandsLessReadFace} instead. Two seams, one boolean
* dispatch — no `Promise`-identity guard (r3). */
export declare function prepareHandsMount(input: PrepareHandsReadFaceInput): Promise<PrepareHandsReadFaceResult>;