OpenProse
SDK API Reference

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 conversation always ends. The responsibility shouldn't have to.

On this page