@sema-agent/core
Version:
Stateless, task-oriented AI agent core
250 lines (249 loc) • 17.7 kB
TypeScript
/**
* PrepareTask's two GATE STATIONS, verbatim from the driver: the `tool_call` station (the final-roster
* effect sync, the write-protection judge compiled once per task, the arming predicate, the compliance and plan-mode
* short-circuits, the `runToolGate` call with every seat it is handed, the lifecycle sweep, the deferred sandbox-admission
* disclosure, the PreToolUse context stash, the settlement sideband write and the bare-human-rejection boundary) and the
* `tool_result` station's registration (its body, the PostToolUse / PostToolUseFailure seats and the PreToolUse context
* drain, lives in call-hook-stations.ts). The two module-scope
* helpers only these stations called moved with them (the settlement screen, the rejection-boundary judgment). The
* interface is the dependency list the stations had implicitly.
*
* The stations REGISTER handlers on the harness (the side effect this phase exists for) and report only two observation
* seats back; the order they register in — after the file-history boundary lease the driver registers, and in this order
* (call before result) — is the order handlers run in.
*
* SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (the handlers run per tool call / per tool
* result), so the phase adds no yield between the park closure before it and the context lane after it.
* Read-stability of the host handles for the call: {@link RunInternals} (`@contract prepare.deps-read-stable`).
*
* The `tool_call` station is built exactly when the ask lane and the park closure are (the same predicate as the whole
* gate-machinery block, kept as the block's one guard here); the `tool_result` station keys off the hook record alone, as
* it always did.
*
* Every station this file holds is a `deniedBy` / settlement WRITE site the decomposition's next slice maps onto one
* gate outcome; nothing here changes a word, a source or an order — the stations are moved, not re-adjudicated.
*/
import type { AgentHarness, AgentTool } from "../../internal/harness.js";
import type { AskClass } from "../ask-class.js";
import type { Arbitration, ToolMarkSource } from "../ask-origin.js";
import { type PermissionModeController } from "../mode-policy.js";
import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
import { type ComplianceCapability } from "../compliance.js";
import { type HookEnvCapabilities, type HookInvocationIdentity, type Hooks } from "../hooks.js";
import type { ToolPolicy } from "../tool-policy.js";
import { type GateOutcome } from "../gate-outcome.js";
import type { RunnerDeps, TaskSpec, ToolEffect } from "../types.js";
import { type WiringManifest } from "../wiring-manifest.js";
import type { WriteProtectionCheck } from "../write-protect.js";
import type { ShellReadCheck } from "../../tools/fs/shell-read-check.js";
import type { CwdRef } from "../../tools/fs/fs-shared.js";
import type { AskLane, GateSeatInputsFor, IrreversibilityTier, ParkAsk, Prepared, ReversibilityProbes, StopForDenialLimit } from "./contracts.js";
import type { PermissionRuleLanes } from "./permission-rule-lanes.js";
import { type ToolRosterDeltaSeat } from "../tool-roster.js";
import type { ToolCallLike } from "../tool-registry.js";
import type { FrameSettlement } from "../frame-settlement.js";
import { type ApprovedCallScreen } from "./call-hook-stations.js";
/**
* The parent-thread human-rejection boundary — judged ONCE here, from the pass's own record (CC parity:
* the reference CLI's reject path aborts exactly when there is no feedback and the run is not a
* subagent; a rejection WITH the decider's note, and every delegated child — forks included — keep the
* continue / adapt-or-report posture): the final block IS a person's refusal (`deniedBy: ask_resolution`
* and a `human_refused` settlement — not a later fold layer's deny that merely followed a human
* interaction), the refusal carried NO note (`settlement.note` absent — a fact composed at the refusal,
* never re-derived from message text), and the run is not a delegated child (the raw trusted delegation
* facet — the same fact that selects the refusal text's posture, so the boundary and the words the model
* was told always agree). Policy/hook/classifier denies and window/abort settlements never satisfy it.
*
* Returns the loop directive that settles every never-started same-batch sibling: honest
* known-not-executed wording plus the machine mark (`error`/`code` twin — the wire errorCode lift
* reads `details.code`; `details.error` is the in-band mark result consumers grep) naming the
* rejected call. `undefined` = no boundary (the block stands alone, current behavior).
*/
export declare function maybeHumanRejectionHalt(input: {
gate: GateOutcome | undefined;
isDelegatedChild: boolean;
toolName: string;
toolCallId: string;
}): {
reason: string;
details: Record<string, unknown>;
} | undefined;
export interface PrepareGateStationsInput {
/** borrowed-readonly — the in-stream ask lane, or undefined when the gate machinery is inactive (then no `tool_call`
* station is built). Read: `adjudicate`, `resolveAskBound`, `notifyPermissionDenied`, `notifyHookError`. */
askLane: AskLane | undefined;
/** borrowed-readonly — the park closure and the content-ask routing closure, or undefined (same law). Handed to the gate. */
parkAsk: ParkAsk | undefined;
/** borrowed-readonly — the FINAL mount roster: iterated once for the effect sync and the write-protection arming. Never
* mutated here. */
tools: readonly AgentTool[];
/** borrowed-readonly — the leg's delta seat: its current roster is the face source for every gated call. */
toolRosterDeltas: ToolRosterDeltaSeat;
/** borrowed-mutable — the effect collection. Writer here: the final-roster sync fills MISSING entries from each
* first-party tool's declared effect (never overrides a scanned one); the plan-mode deny reads it per call. */
toolEffects: Map<string, ToolEffect>;
/** borrowed-readonly — the deployment seats these stations read, as a Pick over the SAME `deps` object (receiver
* preserved for `deps.onError?.()`): the write-protection table and its data root, the tracer, the error face, and
* the notice sink (the gate's unresolvable-ask exit delivers `delegation.ask_unresolvable` through it). */
deps: Pick<RunnerDeps, "writeProtectedPaths" | "memoryEngineDir" | "tracer" | "onError" | "onNotice">;
/** borrowed-mutable — the protocol-tools phase's arming belt. Writer here: `armed` ← the registration predicate, the
* one write; the refresh seam reads it. */
toolCallGateArmedRef: {
armed: boolean;
};
/** borrowed-readonly — the policy chain's effective policy, or undefined; presence only (the arming predicate). */
effectivePolicy: ToolPolicy | undefined;
/** borrowed-readonly — the resolved hook record: `preToolUse` presence (arming), the two post seats and the failure seat
* (the `tool_result` station), called through the record so a class-instance keeps its receiver. */
hooks: Hooks | undefined;
/** borrowed-readonly — the egress vocabulary: arming, and the per-call `egress` flag handed to the gate. */
egressTools: ReadonlySet<string>;
/** borrowed-readonly — the irreversible vocabulary: arming only. */
irreversibleTools: ReadonlySet<string>;
/** borrowed-readonly — the REBOUND spec: `enablePlanMode` (arming) and `signal` (the failure seat's interrupt fact). */
spec: Pick<TaskSpec, "enablePlanMode" | "signal">;
/** borrowed-readonly — the compliance denials (the web_fetch veto's call-time lock, and its arming term). */
complianceDenies: ReadonlySet<ComplianceCapability>;
/** borrowed-mutable — the built harness. Writer here: `on("tool_call")` / `on("tool_result")` — the registrations this
* phase exists for, in this order, after the driver's boundary-lease handler. */
harness: Pick<AgentHarness, "on">;
/** borrowed-mutable — the blocked/suspended call ids (`Prepared.blockedToolCalls`). Writers here: the head-of-evaluation
* hygiene `delete`, the two short-circuits' and the gate outcome's `add`, the `tool_result` station's one-shot `delete`. */
blockedToolCalls: Set<string>;
/** borrowed-mutable — the duplicate-frame ask-grant record. Writer here: the head-of-evaluation and lifecycle-end `delete`. */
inheritedAskGrants: Map<string, unknown>;
/** borrowed-mutable — the inherited-unavailable marker set. Writer here: the lifecycle-end `delete`; `has` is handed to
* the gate as the read-only `isMarkedUnresolvable`. */
inheritedUnavailableAsks: Set<string>;
/** borrowed-mutable — the per-call ask-class record the sandbox admission reads. Writer here: the lifecycle-end `delete`. */
foldAskClasses: Map<string, Array<{
cls: AskClass;
layer: string;
}>>;
/** borrowed-mutable — the deferred ancestor sandbox admissions. Writer here: the unconditional read-and-`delete` after the
* fold settles. */
ancestorSandboxAdmissions: Map<string, Array<{
cls: AskClass;
layer: string;
}>>;
/** borrowed-mutable — PreToolUse `additionalContext` per call id. Writers here: the `tool_call` station's stash (`set`, only
* for calls that will execute) and the `tool_result` station's one-shot drain (`delete`). */
preToolContexts: Map<string, string[]>;
/** borrowed-mutable — the gate-outcome sideband (`Prepared.gateOutcomes`). Writer here: the single screened write per
* gated call (the two short-circuits and the gate's terminal exits); the frame mint reads and deletes. */
gateOutcomes: Prepared["gateOutcomes"];
/** borrowed-mutable — the turn halt fact (`Prepared.batchHaltRef`). Writer here: the bare-rejection boundary (`current`). */
batchHaltRef: Prepared["batchHaltRef"];
/** borrowed-readonly — whether blocked/suspended ids are tracked at all (the post-tool callbacks exist). */
blockedTracked: boolean;
/** borrowed-readonly — the leg's identity envelope every HookToolContext and PermissionDeniedPayload carries. */
hookIdentity: HookInvocationIdentity;
/** borrowed-readonly — the leg's reminder provenance mark (every model-facing deny reason and hook context carries it). */
reminderMark: string;
/** borrowed-readonly — the per-task identity (the trace rows' taskId). */
hostTaskId: string;
/** borrowed-readonly — the acquired session id (the settlement defects' error tag). */
sessionId: string;
/** borrowed-readonly — the screening face as the gate must see it (the observational wrapper or the face itself). */
ownGatePreToolUse: Hooks["preToolUse"] | undefined;
/** borrowed-readonly — the validated per-prepare hook seat bound. */
hookTimeoutMs: number;
/** borrowed-readonly — the hand's live tracked cwd, or undefined; `current` is read per screening / per post seat. Not
* written here. */
handsCwdRef: CwdRef | undefined;
/** borrowed-readonly — the leg's read-deny table in force, read at a release: the active built-in tiers and the rows
* beside them (the deployment's and the task's patterns, the read-face rule rows). Empty ⇒ no table. */
readDenyInForce: () => {
readonly tiers: readonly string[];
readonly rows: readonly string[];
};
/** borrowed-readonly — the read-only env capability face every hook context carries, or undefined. */
hookEnvFace: HookEnvCapabilities | undefined;
/** borrowed-readonly — the leg's write-protection gate input, compiled ONCE by the wiring-manifest phase
* (which runs first and has to report the judge's own target view), or undefined when the deployment
* emptied the table: no judge, and no tighten mounted at all. Carried rather than compiled a second time
* here — two compiles are two judges, free to disagree about the seats they were handed. */
writeProtectionCheck: WriteProtectionCheck | undefined;
/** borrowed-readonly — the leg's shell READ STATION (`ToolGateInput.shellReadCheck`), mounted by the hands phase, or
* undefined with no real shell: no station, nothing judged. */
shellReadCheck: ShellReadCheck | undefined;
/** borrowed-readonly — the per-tool irreversibility tier handed to the gate per call. */
irreversibilityTier: IrreversibilityTier;
/** borrowed-readonly — the per-tool reversibility probes handed to the gate per call. */
reversibilityProbes: ReversibilityProbes;
/** borrowed-readonly — prepare's abort seat; only `.signal` is read (the gate's probe bound, the post seats' bound, the
* failure seat's interrupt fact). */
abortController: {
readonly signal: AbortSignal;
};
/** borrowed-readonly — the shell-seat provenance set: the shell-running tools whose tier is the engine's shell seat's, not a
* deployment mark (the gate's `shellGated` attribution is the membership of the name the call's tier was read under). */
shellGatedTools: ReadonlySet<string>;
/** borrowed-readonly — the armed classifier's live seat (read per call: a leg steered into `auto` arms after prepare). */
autoModeDecider: {
readonly current: AutoModeDecider | undefined;
};
/** borrowed-readonly — the denial tracker's live seat beside the decider's. */
autoModeDenialTracking: {
readonly current: AutoModeDenialTracker | undefined;
};
/** borrowed-readonly — the leg's ONE ask derivation (`wiring_manifest.ask`: form + effective), read for the classifier's seat. */
askRoute: WiringManifest["ask"];
/** borrowed-readonly — the leg's live permission-mode seat: the gate's arbitration point reads `current` per call. */
permissionMode: PermissionModeController;
/** borrowed-readonly — WHO marked each tool's seat (the safety scan's map, the protocol fold's rows); read per call into
* the gate's class derivation. */
toolMarkSources: ReadonlyMap<string, ReadonlySet<ToolMarkSource>>;
/** borrowed-readonly — the deployment's A2A isolation switch (`RunnerDeps.a2a.isolatePeers`), when set. */
a2aIsolatePeers?: true;
/** borrowed-readonly — the run's invocation id (the released-ask account's detail). */
runId: string;
/** borrowed-mutable — the inherited stations' mode arbitrations, keyed by tool call (the policy-chain phase writes; the
* gate's exit reads and this station deletes at the call's end, the fold-record lifecycle). */
ancestorArbitrations: Map<string, Exclude<Arbitration, {
kind: "ask";
}>>;
/** borrowed-readonly — the headless denial-limit stop seat (wired unconditionally). */
stopForDenialLimit: StopForDenialLimit;
/** borrowed-readonly — the persisted allow-rule lane, or undefined (absent entirely with no store). */
permissionRuleLane: PermissionRuleLanes["personal"];
/** borrowed-readonly — the call's seat inputs: the permission seat (static, or the `Skill` tool's per-call reader) and the run's skill rows. */ seatInputsFor: GateSeatInputsFor;
/** borrowed-readonly — the org rule lane, or undefined (absent on an ungoverned deployment). */
permissionRuleOrgLane: PermissionRuleLanes["org"];
/** borrowed-readonly — whether the ENGINE's question tool is what is mounted under the reserved name (the org lane's
* content-ask exemption predicate). */
questionToolMounted: boolean;
/** borrowed-readonly — whether sandbox admission is armed (isolated env + park prerequisites). */
sandboxAdmissionArmed: boolean;
/** borrowed-readonly — the per-call boundary-capability fact the sandbox admission reads (the call's wire name and names). */
sandboxBoundaryCapable: (call: Pick<ToolCallLike, "toolName" | "face">) => boolean;
/** borrowed-readonly — the sandbox-admission trace sink (the deferred ancestor disclosure and the gate's own). */
emitSandboxAdmitted: (info: {
toolCallId: string;
toolName: string;
askClasses: ReadonlyArray<{
cls: AskClass;
layer: string;
}>;
boundary: "sandbox_internal";
}) => void;
/** borrowed-readonly — the effective delegation facts (the rejection boundary's isDelegatedChild). */
delegation: {
isDelegatedChild: boolean;
};
/** borrowed-readonly — the own hook-crash reporter (an expired post seat is reported through it). */
notifyOwnHookCrash: (err: unknown) => void;
/** borrowed-mutable — the leg's settlement seam. Writer here: the `tool_result` station marks a carrier whose output
* a host hook rewrote (`noteHostRewrite`), so a frame the rewrite removed settles as the host's decision. */
frameSettlement: FrameSettlement;
}
export interface PrepareGateStationsResult {
/** owned — whether the `tool_call` station registered (the arming predicate's value; observation seat only). */
toolCallStationArmed: boolean;
/** owned — whether the `tool_result` station registered (a post or screening hook is wired; observation seat only). */
toolResultStationArmed: boolean;
/** owned — the approved call's PreToolUse re-screen (the gate's face and seat), present exactly when a face is wired. */
approvedCallScreen?: ApprovedCallScreen;
}
/** The M20 phase body — prepareTask's two gate stations, verbatim (see the module header). */
export declare function prepareGateStations(input: PrepareGateStationsInput): PrepareGateStationsResult;