The offline boundary
The /run and /run/types entrypoints -- compileProject and runProject, the model-bearing run phase that stays off the keyless front door, plus the type-only mirror that types the handle without crossing the boundary.
The offline boundary: /run and /run/types
Two of the six SDK entrypoints exist for one reason: to keep the
model-bearing run phase off the keyless front door. A coding agent should be
able to inspect a project, replay a ledger, or type a run configuration without
ever loading a provider or spending a token. /run is where the live model
session actually gets pulled in; /run/types is the type-only mirror that lets
you describe the same shapes without crossing that line.
The keyless inspection or replay build never loads a provider.
That sentence is the whole design. compileProject and runProject deep-import
the live agent adapters (@openai/agents and zod, both optional peers), so
they are deliberately not re-exported from the curated . front door. The
facade reaches them only through a dynamic await import("../run")
inside its body, after it has decided it is actually going to run. A consumer who
only wants to read state imports nothing model-bearing.
This is the same boundary the DevTools keyless replay relies on. Inspection and replay are first-class postures, not afterthoughts: the package is structured so they cannot accidentally pull a model in.
/run -- the model-bearing run phase
The /run entrypoint exports exactly two functions and the type shapes that go
with them. Both functions are the dynamic-import target for the run phase -- you
import /run only at the point where a live render (or a compile session) is
about to happen.
// @openprose/reactor/run
export { compileProject, runProject } from "../sdk/run-project";
export type {
CompileProjectInput,
CompiledProject,
CompiledProjectNode,
NodeStepCompileOptions,
PerStepCompileOptions,
RunProjectInput,
RunProjectRender,
RunProjectResult,
} from "../sdk/run-project";The two phases mirror the harness's determinism boundary: compileProject is
the compile phase, run as sessions; runProject is the run phase, dumb
and deterministic. See the DAG and compile for
the conceptual model the two functions implement.
compileProject -- the compile phase as sessions
compileProject takes a directory of .prose.md contracts all the way to a
mountable shape, without any hand-authored topology and without a .prose
parser. Every model call inside it is an agent session: loadContractSet (plain
file loading) feeds compileForme (the topology session), then per node a
compileCanonicalizer and compilePostcondition session freeze each
### Maintains declaration into deterministic run-time code.
import { compileProject } from "@openprose/reactor/run";
const compiled = await compileProject({
contractsDir: "./my-project/src",
});export interface CompileProjectInput {
readonly contractsDir?: string; // a directory of .prose.md contracts
readonly contracts?: ContractSet; // OR an already-loaded set (skips loadContractSet)
readonly options?: CompileStepOptions; // per-call compile-session knobs (provider/model/skill/...)
readonly perStep?: PerStepCompileOptions;// per-step overrides (forme / canonicalizer / postcondition)
readonly skipPostconditions?: boolean; // synthesize an empty validator set (no model call)
}The result is the mountable project the run phase consumes:
export interface CompiledProject {
readonly reconcilerTopology: ReconcilerTopology; // Forme's output -- the DAG
readonly perNode: Readonly<Record<string, CompiledProjectNode>>; // compiled canonicalizers + validators
readonly contracts: ContractSet; // the source the sessions read
readonly contractFingerprints: Readonly<Record<string, Fingerprint>>; // the memo key's first half
readonly cost: Cost; // summed session cost across every step
}Each CompiledProjectNode carries the frozen materiality and the commit gate for
one node:
export interface CompiledProjectNode {
readonly compiled: CompiledNode; // the run-time canonicalizer (materiality frozen at compile)
readonly postconditions: CompilePostconditionsResult; // the deterministic commit-gate validators
}Because each compile step emits a different output schema (Forme vs
canonicalizer vs postcondition), a single shared fake provider cannot satisfy
all three at once. For an offline compile, hand a distinct provider per step
via perStep. The two per-node steps take the explicit
NodeStepCompileOptions { all?, byNode? } shape -- all applies one set of
options to every node, byNode overrides per node id, and byNode[node]
merges over all. There is no key-name heuristic deciding which one you meant.
runProject -- the dumb run phase
runProject mounts the compiled project over a substrate and
runs the boot cold-miss sweep. The render is built over the same world-model
store the reactor commits to, so a workspace write is visible to the harvest in
that render. Boot is the honest first render of a pure source: only sources are
seeded, and input-driven nodes wake via propagation. A second runProject over
the same directories boots to all-skips.
import { compileProject, runProject } from "@openprose/reactor/run";
import { fileSystemSubstrate } from "@openprose/reactor";
const compiled = await compileProject({ contractsDir: "./my-project/src" });
const { reactor, bootResults } = await runProject({
compiled,
substrate: fileSystemSubstrate({ directory: "./state" }),
render: {
// the model + full @openai/agents escape hatch, as one nested render config
render: { provider: myScopedProvider, model: "google/gemini-3.5-flash" },
},
});
// `reactor` IS the typed handle -- drive and observe it with no casts
console.log(reactor.view.cost); // { fresh, reused, byCause, byNode }The input bundles the compiled project, the run substrate, and the render wiring:
export interface RunProjectInput {
readonly compiled: CompiledProject; // the output of compileProject
readonly substrate?: Partial<Substrate>; // the blessed persistence primitive (clock/storage/worldModel/ledger)
readonly adapters?: { // the a-la-carte form, retained for back-compat
readonly clock: ClockAdapter;
readonly storage: StorageAdapter;
readonly worldModel?: WorldModelStore;
readonly ledger?: MutableReceiptLedger;
};
readonly directory?: string; // world-model directory when the store is defaulted
readonly render: RunProjectRender; // the render wiring (below)
}Prefer substrate (a whole fileSystemSubstrate / inMemorySubstrate, or a
Partial with missing pieces defaulted) over the a-la-carte adapters. See
adapters for the substrate primitive and the restart-survival
invariant.
RunProjectRender -- the render wiring
RunProjectRender is how runProject reaches the model. It threads the model
selection and the full @openai/agents escape hatch through to the live
render. The escape hatch is one nested render: RenderOptions, not a flat
re-declaration -- so maxTurns: number | null is preserved end-to-end and you
reach every SDK knob the render does. See the agents escape hatch
for the full RenderOptions tiers.
export interface RunProjectRender {
readonly contractFor?: (node: string) => CompiledContractView; // per-node compiled-contract view
readonly projectTruthFor?: (node: string) => TruthProjection; // per-node truth projection (GOTCHA-1 half)
readonly buildRender?: (store: WorldModelStore) => AsyncMountedRender; // the deepest render-body backstop
readonly skill?: string; // pre-read SKILL system prompt
readonly skillPath?: string; // path to the SKILL when `skill` is unset
readonly sandbox?: RenderSandboxRunner; // a caller-supplied runner reaches the live render's sandbox_exec; the SDK exports the TYPE but ships no concrete runner (the CLI builds one)
readonly shellTimeoutMs?: number; // per-command shell_exec timeout (default 300_000 ms)
readonly renderBackend?: RenderBackend; // the model-injection seam (record/replay, proxy, alternate model)
readonly render?: RenderOptions; // the model + full @openai/agents escape hatch, as one nested config
}projectTruthFor is load-bearing for any producer that maintains a named
facet other nodes subscribe to. If the compiled topology has any named-facet
edge and projectTruthFor is left undefined, runProject throws at boot
rather than ship a silently dead edge -- the producer's facet fingerprint
could otherwise never move, and propagation would never fire. This is the
honest-failure posture: a loud error at boot, never a quiet wrong answer at
run time.
The result hands back the typed handle and the boot sweep's receipts:
export interface RunProjectResult {
readonly reactor: Reactor; // the typed running handle (drive + observe, no casts)
readonly bootResults: readonly ReconcileResult[]; // the boot cold-miss sweep's results
}RunProjectResult.reactor is the one typed Reactor handle --
the same object the facade and createReactor return. There is no separate
nested .dag to cast into.
/run/types -- the type-only mirror
/run/types re-exports the same compile and run shapes as /run, but
carries no @openai/agents value import. A consumer can describe a run or
compile configuration -- and type the running handle it drives -- without ever
crossing the offline boundary into provider code.
// @openprose/reactor/run/types -- type-only, no @openai/agents value import
export type {
CompileProjectInput,
CompiledProject,
CompiledProjectNode,
NodeStepCompileOptions,
PerStepCompileOptions,
RunProjectInput,
RunProjectRender,
RunProjectResult,
} from "../sdk/run-project";
// The handle types, mirrored type-only:
export type {
Reactor, // RunProjectResult.reactor
SyncDriveSurface,
IngestInput,
} from "../sdk/reactor-handle";This is the entry the reference CLI types its handle against. The CLI drives a
reactor it ultimately runs through /run, but it types that handle off
/run/types -- so its type-checking never pulls @openai/agents and the offline
boundary stays clean. Before this entry existed, the CLI hand-mirrored about
twenty structural copies of these shapes (and an AssembledReactorLike for the
handle); /run/types erases all of them.
Rule of thumb for an agent wiring this up: import the types you need from
@openprose/reactor/run/types, and import the functions
(compileProject / runProject) from @openprose/reactor/run only at the
call site where you are actually about to run. If you find yourself importing
/run just to type a variable, switch that import to /run/types.
Where to go next
The front door
The reactor() facade and the one typed Reactor handle that runProject returns -- drive and observe with no casts.
The agents escape hatch
The full RenderOptions tiers that RunProjectRender.render threads through to the live model session.
Adapters
The Substrate primitive runProject mounts over, plus the gateway-ingress and record/replay seams.
Compile, run, serve (CLI)
The CLI's two-phase driver over this same boundary: compile once, then run or serve.
The conversation always ends. The responsibility shouldn't have to.
Adapters
The /adapters injection boundary -- the one Substrate primitive, the restart-survival invariant, the gateway-ingress and cursor toolkit, record/replay, and the port contracts a custom backend implements.
/internals
The engine room -- the honest deep door. The reconciler-construction spine, the deep domain shapes, the deprecated Reactor*-prefixed port aliases, and the receipt/projection helpers. Stable-but-deep, distinct from the curated front door.