UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

290 lines (289 loc) • 23.1 kB
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>;