diff --git a/.cspell.json b/.cspell.json index cf9fe78..ea75ff9 100644 --- a/.cspell.json +++ b/.cspell.json @@ -45,7 +45,59 @@ "antipatterns", "unwired", "dogfood", - "diffable" + "diffable", + "canonicalizer", + "recompiles", + "dedups", + "reconciler", + "idempotency", + "subagents", + "canonicalizers", + "canonicalization", + "fingerprint", + "fingerprints", + "fingerprinted", + "memoization", + "memoize", + "memoized", + "topology", + "subscribable", + "reconciliation", + "recheck", + "rechecks", + "reconverges", + "deduped", + "dollarized", + "mtimes", + "ETags", + "downstreams", + "Acyclicity", + "statefulness", + "agentically", + "canonicalizable", + "authorable", + "DevTools", + "devtools", + "replayable", + "sparkline", + "scrubber", + "monorepo", + "Chromium", + "Playwright", + "ffmpeg", + "webm", + "dedup", + "prev", + "dep", + "fixpoint", + "wakeups", + "desugars", + "footguns", + "greppable", + "headlessly", + "screenshotting", + "unparseable", + "bucketings" ], "ignoreRegExpList": ["https?://[^\\s)]+", "`[^`]*`"] } diff --git a/__tests__/prose-program-srcs.test.ts b/__tests__/prose-program-srcs.test.ts index 3040e2f..6e60903 100644 --- a/__tests__/prose-program-srcs.test.ts +++ b/__tests__/prose-program-srcs.test.ts @@ -8,8 +8,12 @@ const REPO_ROOT = resolve(__dirname, ".."); describe(" references", () => { const uses = findProseProgramUses(REPO_ROOT); - it("finds at least one ProseProgram use across content/docs (sanity)", () => { - expect(uses.length).toBeGreaterThan(0); + // The Reactor-forward docs no longer embed vendor `.prose` programs via + // , so an empty set is valid. The real guard is the per-use + // resolution check below: any that *is* authored must + // point at a file that exists. + it("every ProseProgram use found resolves (none is fine)", () => { + expect(uses.length).toBeGreaterThanOrEqual(0); }); it.each(uses)("resolves $src referenced from $mdxPath:$line", ({ src }) => { diff --git a/content/docs/agents/for-ai-agents.mdx b/content/docs/agents/for-ai-agents.mdx deleted file mode 100644 index 20f1c82..0000000 --- a/content/docs/agents/for-ai-agents.mdx +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: For AI agents -description: Read this before running, editing, or explaining OpenProse programs. ---- - -# For AI agents - -If you are an AI agent, this page is a signpost, not the execution spec. - -Load the `open-prose` skill before running, editing, linting, testing, migrating, or explaining an OpenProse program. - -The skill and its linked specs are the source of truth for execution behavior. These docs are for human orientation. - -## First move - -When you see: - -- `prose run ...` -- `prose lint ...` -- `prose test ...` -- a `.md` file with `kind:` frontmatter -- a `.prose` file -- a user asking for reusable multi-agent orchestration - -load the `open-prose` skill and follow its path table. - -Do not infer execution semantics from this docs site. - -## Command routing - -Inside an agent session, treat `prose ...` as an in-session command language. - -For `prose run file.md`: - -1. Read the Contract Markdown program. -2. Load the narrowest required specs. -3. Wire services through Forme if the program declares services. -4. Execute through the Prose VM behavior using host primitives. -5. Persist run state under `.prose/runs/`. - -Do not run `prose`, `npx prose`, or `npx @openprose/prose-cli` from inside the VM unless the user or harness explicitly tells you that you are using the CLI as an outer wrapper. - -The shell executable is a bridge into an agent harness. It is not the VM. - -## Host primitives - -Map the abstract OpenProse operations onto the current host: - -| Primitive | What it means | -| --- | --- | -| `spawn_session` | run a service or branch in an isolated agent session | -| `ask_user` | pause for missing caller input | -| `read_state` / `write_state` | read and write `.prose/runs/{id}/` files | -| `copy_binding` | publish declared outputs from a workspace to bindings | -| `check_env` | verify that an environment variable exists without revealing it | - -If the host lacks a required primitive, say so. Do not fake a multi-agent run as if isolation happened. - -## Fit check - -Decline OpenProse for one-shot work. - -Use it when the task has a reusable workflow shape: roles, handoffs, retries, parallel exploration, durable constraints, or a run trace the user will care about later. - -If the user says "just do it", respect that. - -## State and artifacts - -Prefer paths over pasted content. - -Services do their work in private workspaces. Declared outputs are copied into bindings. Downstream services inspect the artifact paths they need. - -Keep scratch out of bindings. Do not publish undeclared files as outputs. - -## Canonical files - -Use the files linked from [canonical specs](/reference/specs): - -- `SKILL.md` -- `contract-markdown.md` -- `forme.md` -- `prose.md` -- `prosescript.md` - -If this docs site and the skill disagree, trust the skill. - -Next: [reference overview](/reference/overview). diff --git a/content/docs/agents/meta.json b/content/docs/agents/meta.json deleted file mode 100644 index 096e383..0000000 --- a/content/docs/agents/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Agents", - "pages": ["for-ai-agents"] -} diff --git a/content/docs/cli/command-reference.mdx b/content/docs/cli/command-reference.mdx new file mode 100644 index 0000000..020ed64 --- /dev/null +++ b/content/docs/cli/command-reference.mdx @@ -0,0 +1,178 @@ +--- +title: Command reference +description: All twelve reactor commands, every flag (including serve --host), and the documented exit codes -- verified against the shipped 0.2.0 binary. +--- + +# Command reference + +The command is `reactor`. Run `reactor --help` for the full options of any command, and `reactor --version` (or `-v`) for the CLI version. + + +`reactor --version` prints the **CLI** version (`0.2.0`), not the SDK version (`@openprose/reactor@0.3.0`). The two packages version independently -- that is expected, not a mismatch. + + +The CLI is the reference [driver of the SDK](/sdk): it configures `@openprose/reactor` and never re-implements the reconciler. Everything below is verified against the shipped binary's argument parser, so an agent can treat this page as the contract. + +## Global flags + +These four flags are honored by **every** command and override `reactor.yml`. Absent flags are omitted rather than set, so they never clobber a config default. + +| Flag | Meaning | +| --- | --- | +| `--state-dir ` | The durable state directory (default `./.reactor`). | +| `--project ` | The project directory containing `reactor.yml` (default `.`). | +| `--json` | Machine-readable JSON output. | +| `--offline` | Force offline mode (sets `REACTOR_OFFLINE=1`). | + +## Commands + +The "Live?" column marks which commands reach the model surface. Live commands need `OPENROUTER_API_KEY` plus the `@openai/agents` and `zod` peer deps; they reach the model only via a dynamic `import()` inside the handler, so requiring the CLI entrypoint stays keyless. Every other command runs fully offline. + +| Command | Live? | What it does | +| --- | --- | --- | +| `reactor init [dir]` | offline | Scaffold a minimal `.prose` project (gateway + responsibility) + `reactor.yml`. | +| `reactor doctor` | offline (`--live` probes) | Report environment health: node, SDK, live key/deps, offline mode, sandbox, state-dir, IR. | +| `reactor compile` | live (cache hit and `--check` are offline) | Run the compile sessions and refresh the content-addressed IR cache. | +| `reactor run` | live | Ensure the IR is fresh, boot the reactor, drain to quiescence, and report. | +| `reactor serve` | live | Boot the durable host (one or many reactors) and run the continuity driver loop. | +| `reactor trigger ` | live | Trigger a node with an external wake (one-shot mount). | +| `reactor status` | offline | The standing compile cost beside the live run cost and dispositions. | +| `reactor topology` | offline | Print the compiled DAG: nodes (and wake source) and resolved edges. | +| `reactor inspect ` | offline | A node's topology position, fingerprints, last receipt, and chain. | +| `reactor logs` | offline | The receipt stream, optionally filtered to one node. | +| `reactor trace [node]` | offline | Each node's receipt chain: wake to disposition, in chain order. | +| `reactor receipts [sub]` | offline | Audit the receipt trail: `list` \| `verify` \| `cost` (default `list`). | + +## Per-command flags + +Each command spreads the four global flags on top of the local flags below. + +### `reactor init [dir]` + +Scaffold a minimal project: a gateway, a responsibility, `reactor.yml`, `.gitignore`, and a `README.md`. `[dir]` is the target directory (default `.`). + +| Flag | Meaning | +| --- | --- | +| `--force` | Overwrite existing scaffold files. The default is to refuse rather than clobber. | + +### `reactor doctor` + +Report environment health: node, SDK, live key/deps, offline mode, and sandbox. Offline by default; only `--live` reaches the model surface. + +| Flag | Meaning | +| --- | --- | +| `--live` | Additionally probe one live smoke render. Requires a key plus the live peer deps. | + +### `reactor compile` + +Run the compile phase as sessions and refresh the content-addressed IR cache. A compile against an unchanged contract set is a cache hit at zero session cost (and is offline). + +| Flag | Meaning | +| --- | --- | +| `--force` | Recompile regardless of cache freshness. | +| `--check` | Exit non-zero if the cache is stale; do not compile. Offline; intended for CI. | + +### `reactor run` + +Ensure the IR is fresh, boot the reactor, drain to quiescence, and report. One-shot, no flags beyond the globals. + + +A static gateway (no scheduled wake) does not fire on `run`. Bring it up with `serve`, then deliver a wake via `reactor trigger ` or an HTTP `POST /trigger/`. + + +### `reactor serve` + +Boot the durable reactor host (one or many reactors) and run the continuity driver loop. Stays up until `Ctrl-C` (`SIGINT`/`SIGTERM`), then drains in-flight work and exits. + +| Flag | Meaning | +| --- | --- | +| `--poll-interval ` | Continuity poll cadence ceiling, in milliseconds (default `60000`). | +| `--concurrency ` | Across-reactor worker-pool bound (default `1`). Within-reactor parallelism is a future enhancement. | +| `--http ` | Bind the built-in HTTP server on `` (trigger / status / health / cost). | +| `--host ` | HTTP bind address (default `127.0.0.1`, loopback only). | + + +The v1 HTTP server has **no auth**. The default `--host 127.0.0.1` is loopback-only by design. Bind `0.0.0.0` only behind a proxy that terminates auth. + + +### `reactor trigger ` + +Trigger a node with an external wake (a one-shot mount, or a `POST` to a running daemon). `` is the node id. + +| Flag | Meaning | +| --- | --- | +| `--data ` | A JSON payload inline, or `@path` to a JSON file. | + +### `reactor inspect ` + +Inspect a node: its topology position, fingerprints, last receipt, and chain. `` is the node id. + +| Flag | Meaning | +| --- | --- | +| `--strict` | Exit non-zero if the node's receipt chain does not verify. For CI. | + +### `reactor logs` + +Print the receipt stream. + +| Flag | Meaning | +| --- | --- | +| `--node ` | Filter the stream to a single node. | + +### `reactor trace [node]` + +Trace each node's receipt chain (wake to disposition, in chain order). `[node]` traces a single node; the default traces every node with receipts. No flags beyond the globals. + +### `reactor receipts [sub]` + +Audit the receipt trail. `[sub]` is `list` | `verify` | `cost` (default `list`). `verify` exits non-zero on a broken chain. + +| Flag | Meaning | +| --- | --- | +| `--node ` | Filter to a single node (`list` / `cost`). | +| `--rate ` | Price the cost rollup: `$/Mtok` (dollars per million tokens, e.g. `3` or `$3/Mtok`) or `tokens-per-dollar` (e.g. `500000tpd`). Fills the dollar column on `cost`. | + + +An unknown `receipts` subcommand (for example `receipts verifyy`) is rejected to stderr and exits `2`, rather than silently falling through to `list` -- a trust hazard a CI gate must not inherit. + + +### `reactor status` and `reactor topology` + +Read-only over the populated state directory; no flags beyond the globals. + +## Documented exit codes + +The CLI uses stable, documented exit codes so it composes in CI and scripts. + +| Code | Meaning | +| --- | --- | +| `0` | Success, or healthy. A clean help or version display also exits `0`. | +| `1` | A reported failure with an actionable message on stderr (an action handler set it): a stale cache (`compile --check`), a broken receipt chain (`receipts verify`, `inspect --strict`), no contracts found, a bad config, an unhealthy environment (`doctor`), a missing live key or dep (`--live`), or a connector or render error. | +| `2` | A usage error: an unknown command or flag, a missing argument, or an unknown `receipts` subcommand, surfaced by the arg parser. | + +Failure modes carry actionable messages. A missing live key points you to set `OPENROUTER_API_KEY`. A `mode: docker` config with no daemon tells you to install or start Docker, or that renders fall back to the bounded shell. A stale cache tells you to run `reactor compile`. Under `--json`, a thrown operational failure mirrors a `{ ok: false, error }` envelope to stdout, so a machine consumer is never left with empty, unparseable output. + +## See also + + + + + + + diff --git a/content/docs/cli/compile-run-serve.mdx b/content/docs/cli/compile-run-serve.mdx new file mode 100644 index 0000000..68acda6 --- /dev/null +++ b/content/docs/cli/compile-run-serve.mdx @@ -0,0 +1,122 @@ +--- +title: Compile, run, serve +description: The three core verbs in depth, the content-addressed compile cache, and the durable daemon's seven HTTP routes. +--- + +# Compile, run, serve + +These are the three core verbs. `compile` freezes intelligence into deterministic artifacts. `run` drains once. `serve` runs the durable daemon. All three reach the model surface and need a live key (`OPENROUTER_API_KEY`) plus the `@openai/agents` and `zod` peer deps. The keyless inspection commands ([observability](/cli/observability), [DevTools replay](/reactor-devtools/quickstart)) read what these verbs leave behind, so a single compiled state directory is the seam between the live side and the offline side. + +## compile + +```sh +reactor compile [--force] [--check] +``` + +`compile` runs the intelligent compile sessions (Forme topology, the per-node canonicalizer, postconditions) and freezes them into a content-addressed IR cache under `/compile/`. + +### The content-addressed cache + +The cache key is `(contract-set fingerprint, SDK version, model id)`. Cost is never part of cache identity. An unchanged contract set recompiles at zero session cost, a cache hit. + +The IR persists a serializable spec. A fresh process re-lowers each node's canonicalizer with the keyless `compileNode(spec)` call, with no model and no network, to mount it. That re-lowering is why the offline observability commands work after a compile with no key present. The programmatic equivalents are [`compileProject` and `runProject`](/sdk/run), the model-bearing boundary the CLI drives. + +### Flags + +| Flag | Meaning | +| --- | --- | +| `--force` | Recompile regardless of cache freshness. | +| `--check` | Exit non-zero if the cache is stale, and do not compile. The `--check` path is offline-safe and meant for CI. | + +Wire `--check` into CI to catch un-compiled contract changes: + +```sh +reactor compile --check # exit 1 when the cache is stale +``` + +## run + +```sh +reactor run +``` + +`run` is the one-shot verb. It ensures the IR is fresh (compiling if stale), boots the reactor, drains to quiescence, prints per-node dispositions plus cost, then exits. Use it for batch jobs and CI, where you want the system to settle once and report. + +## serve + +```sh +reactor serve [--http ] [--host ] [--concurrency ] [--poll-interval ] +``` + +`serve` boots the durable host and blocks on the continuity driver loop until it receives `SIGINT` or `SIGTERM`. + +### The durable substrate + +The host builds a durable substrate: a flat append-only receipt trail at `/receipts.json` and a filesystem world-model under `/world-models`. The reactor self-mounts and runs a boot cold-miss sweep, so a restart resumes from durable state rather than re-ingesting the backlog. `reactor run` and `reactor trigger` persist to the same flat layout, so any of them produces a state directory you can replay directly with [`reactor-devtools `](/reactor-devtools/quickstart). + +### The continuity loop + +Each tick, the driver polls gateways for ingress, then polls continuity across every reactor, surfaces a live cost line, and sleeps to the cadence ceiling. Gateways poll before continuity each tick so a freshly-staged arrival is visible to the same tick's continuity sweep. + +`--poll-interval ` sets the continuity cadence (default `60000`). In v1 the loop sleeps this fixed interval between ticks. (Sleeping adaptively to the soonest armed self-recheck is deferred along with the default `valid_until` freshness projector -- until that ships, no self-recheck instants are armed, so the loop polls on the flat interval.) + +### The HTTP surface + +`--http ` binds a zero-framework `node:http` server. It ships seven routes: one ingress and six read-only projections off the reactor's already-booted substrate. The GET routes never touch the model surface, so the surface is safe to poll for liveness and cost without spend. + +| Method and path | Purpose | Shape | +| --- | --- | --- | +| `POST /trigger/` | An external wake of `` (the webhook or manual ingress), serialized behind that reactor's queue. | `{ reactor, triggered, receiptsAdded, data?, dataDelivered? }` | +| `GET /health` | Liveness: boot done, reactor count. | `{ ok, reactors, reactor }` | +| `GET /status` | The cost rollup plus node count and queue depth. | `{ reactor, nodes, queueDepth, cost }` | +| `GET /cost` | The cost rollup (the headline observability). | `{ reactor, ...costRollup }` | +| `GET /topology` | The node ids only -- a thin id list, not the wired DAG. | `{ reactor, nodes: NodeId[] }` | +| `GET /receipts` | The full ledger receipt stream. | `{ reactor, receipts: Receipt[] }` | +| `GET /nodes/` | A node's published fingerprints, its last receipt, and a receipt count -- thin, not the node's full history. | `{ reactor, node, fingerprints, lastReceipt, receipts }` | + +Two of the GET routes are deliberately **thin**. `GET /topology` returns the node ids as a flat list, not the wired DAG with edges (use [`reactor topology`](/cli/observability) for the rendered graph). `GET /nodes/` returns the node's published fingerprints, its single last receipt, and a count -- not the full per-node receipt history. `GET /receipts` is the one full projection: it streams the entire ledger. + +The HTTP surface is namespaced per reactor under `//...`, with the prefix omitted for a single-reactor host. So a single-reactor host answers `GET /cost`, while a multi-reactor host answers `GET /sales/cost` and rejects an unprefixed `GET /cost` with a 404 telling you to prefix the path. There is no auth in v1: the host is one process for one operator. + +`POST /trigger/` accepts an optional JSON body and goes through the reactor's serialization queue, so an HTTP trigger never overlaps an in-flight drain. The body is validated as JSON (a malformed body is a 400) and, when the node is a configured gateway, staged into the node's ingress so it actually reaches the render. The response's `dataDelivered` reports whether that staging happened. + +### Bind address and safety + +`--host ` sets the bind address. The default is `127.0.0.1` -- loopback only. + + +v1 has no auth, and an unauthenticated `POST /trigger/` can cause model spend. The server binds loopback by default for exactly this reason. Pass `--host 0.0.0.0` only behind a trusted proxy that adds its own auth; `serve` prints a warning when it binds to a non-loopback address. + + +### Graceful shutdown + +On `SIGINT` or `SIGTERM`, the host stops arming new work, drains the in-flight queue for every reactor, closes the HTTP server, and exits `0`. The SDK keeps no process alive; the CLI owns the loop. + +## The multi-reactor host and --concurrency + +A `reactors:` list in `reactor.yml` hosts N isolated reactors, each with its own state directory, substrate, schedule, and cursors. The single-reactor case is just N=1: the host synthesizes one `default` reactor and the HTTP surface omits the `/` prefix. + +`--concurrency N` is an across-reactor worker-pool bound (default `1`). Independent reactors render in parallel up to N; the v1 default of 1 means no cross-reactor parallelism unless you raise it. + +Within a single reactor, drains stay strictly serial. At most one drain is in flight per reactor, behind a per-reactor serialization queue, because the SDK's single-flight atomicity requires it. + + +Within-reactor parallelism is a future enhancement. The current SDK has no within-reactor concurrency option, so `--concurrency` parallelizes reactors, not nodes within a reactor. + + +## trigger + +```sh +reactor trigger [--data |@file] +``` + +`trigger` fires an external wake at one node. In v1 it is a one-shot mount: it boots a transient reactor over the durable substrate, ingests the named node with a full external wake, drains to quiescence, and reports the dispositions. Like `run` and `serve`, it persists to the same flat `/receipts.json` trail, so the wake it injects is durable and replayable. + +`--data` accepts inline JSON or `@path` to a JSON file. The wake itself carries no payload slot, so the parsed data is validated and surfaced in the report. For a running daemon, `POST /trigger/` is the equivalent ingress. + +There is no separate `pull` command. Ingest happens through the serve continuity cadence and through `POST /trigger/`. + + + + + diff --git a/content/docs/cli/configuration.mdx b/content/docs/cli/configuration.mdx new file mode 100644 index 0000000..ae8f83e --- /dev/null +++ b/content/docs/cli/configuration.mdx @@ -0,0 +1,111 @@ +--- +title: Configuration +description: The reactor.yml schema, environment variables, and the global flags every command honors. +--- + +# Configuration + +`reactor init` writes a fully-commented `reactor.yml` at the project root. Every command reads it from `/reactor.yml`. When the file is absent, the documented defaults apply. + +## The reactor.yml schema + +```yaml +state: + dir: ./.reactor # durable state (receipts, world-models, IR cache) + +model: + provider: openrouter + render_model: google/gemini-3.5-flash + compile_model: google/gemini-3.5-flash + temperature: 0 + max_turns: 200 + +sandbox: + mode: none # none (default) | docker + shell_timeout_ms: 300000 + +gateways: # external-driven entry points + - node: inbox + source_id: inbox + connector: + type: static # static | http | file (or a connectors.{cjs,js} plugin) + id_field: id + items: [{ id: item-1, body: "the first item" }] + +reactors: [] # optional: a multi-reactor host (see below) +``` + +## state + +| Key | Default | Meaning | +| --- | --- | --- | +| `state.dir` | `./.reactor` | The durable state directory: receipts, world-models, and the compiled IR cache. Resolved to an absolute path rooted at the project dir, so every command agrees on one location regardless of cwd. | + +## model + +| Key | Default | Meaning | +| --- | --- | --- | +| `model.provider` | `openrouter` | The model provider. | +| `model.render_model` | `google/gemini-3.5-flash` | The model used for renders at run/serve time. | +| `model.compile_model` | `google/gemini-3.5-flash` | The model used for the compile sessions. It is part of the IR cache key. | +| `model.temperature` | `0` | Sampling temperature. | +| `model.max_turns` | `200` | The per-session turn ceiling. | + +The compile model is a component of the content-addressed cache key `(contract-set fingerprint, SDK version, model id)`. Changing it invalidates the cache and forces a recompile. + +## sandbox + +The `sandbox` block is the render threat-model knob. + +| Key | Default | Meaning | +| --- | --- | --- | +| `sandbox.mode` | `none` | `none` runs renders in the SDK's cwd-scoped, bounded shell. `docker` runs each render command in a throwaway, network-disabled container. (A third value, `unix-local`, is accepted by the config parser but is not yet realized -- it currently behaves as `none`.) | +| `sandbox.shell_timeout_ms` | `300000` | The per-command time bound (300 seconds) for the bounded shell. | +| `sandbox.image` | `node:22-bookworm-slim` | The container image, when `mode: docker`. Falls back to `node:22-bookworm-slim` when unset. | +| `sandbox.network` | (forced off) | Accepted for forward-compatibility but **not yet honored**: the docker runner currently forces `--network=none` on every render command regardless of this value (network isolation is the threat-model default). | + +See [connectors and sandbox](/cli/connectors-and-sandbox) for the full render-isolation behavior, including the Docker-absent fallback. + +## gateways + +Each `gateways` entry is an external-driven entry point bound to a connector. + +| Key | Meaning | +| --- | --- | +| `node` | The gateway node id (must match a `kind: gateway` contract). | +| `source_id` | The connector source id (defaults to the node id). Keys the durable idempotency cursor. | +| `poll` | An optional poll cadence for the gateway. | +| `connector` | The connector definition: its `type` plus type-specific fields such as `id_field` and `items`. | + +See [connectors and sandbox](/cli/connectors-and-sandbox) for the built-in connector types (`static`, `http`, `file`) and the `connectors.{cjs,js}` plugin shape. + +## reactors + +A `reactors` list hosts N isolated reactors in one `serve` process. When the list is empty (the default), the project is single-reactor and the top-level `state` and `gateways` are that one reactor. + +| Key | Default | Meaning | +| --- | --- | --- | +| `name` | `reactor-` | The reactor's name, which becomes its HTTP namespace prefix `//...`. | +| `project` | the project dir | The contracts directory for this reactor. | +| `state_dir` | `/` | This reactor's isolated durable state directory. | +| `gateways` | `[]` | This reactor's gateways. | + +Each entry is isolated: its own contracts directory, state directory, substrate, schedule, and cursors, so one reactor never corrupts another. See [the multi-reactor host](/cli/compile-run-serve) for how `--concurrency` parallelizes across reactors. + +## Environment variables + +| Variable | Meaning | +| --- | --- | +| `OPENROUTER_API_KEY` | The live key for the model surface. Required by `compile`, `run`, `serve`, and `trigger`. Read from the process env first, then a `.env` file discovered from the working directory upward. | +| `REACTOR_OFFLINE` | Set to `1` (or `true`) to force offline mode. Equivalent to the `--offline` flag. | + +## Global flags + +These flags are honored by every command and override the file: + +| Flag | Meaning | +| --- | --- | +| `--state-dir ` | Override `state.dir`. | +| `--project ` | The project directory containing `reactor.yml` (default `.`). | +| `--json` | Machine-readable JSON output. | +| `--offline` | Force offline mode (sets `REACTOR_OFFLINE=1`). | diff --git a/content/docs/cli/connectors-and-sandbox.mdx b/content/docs/cli/connectors-and-sandbox.mdx new file mode 100644 index 0000000..c7b6674 --- /dev/null +++ b/content/docs/cli/connectors-and-sandbox.mdx @@ -0,0 +1,114 @@ +--- +title: Connectors and sandbox +description: Gateways and connectors with durable idempotency cursors, and the render sandbox threat model. +--- + +# Connectors and sandbox + +This page covers the two boundaries where a reactor touches the outside world: connectors, which bring external data in through gateways, and the sandbox, which bounds what a render can do. + +The CLI wires both of these from `reactor.yml`. The mechanism underneath is the SDK's ingress toolkit. If you are driving a reactor programmatically instead of through the CLI, the same gateway-poll and idempotency-cursor primitives are documented in the [SDK adapters reference](/sdk/adapters). + +## Gateways and connectors + +A gateway is an external-driven entry point: a `kind: gateway` contract that accepts arrivals and materializes them as a subscribable set. A connector is what feeds a gateway. It is three pieces: + +- **`fetch`** does the source I/O. +- **`extract`** turns the payload into arrivals keyed by `id_field`. +- **`stage`** writes each arrival into the gateway's truth before the wake. + +You wire a connector to a gateway in `reactor.yml`. `reactor init` scaffolds exactly this shape, with the built-in `static` connector so a first `serve`/`run` has a deterministic arrival to ingest: + +```yaml +gateways: + - node: inbox + source_id: inbox + connector: + type: static + id_field: id + items: [{ id: item-1, body: "the first item" }] +``` + +### Built-in connector types + +| `type` | Behavior | +| --- | --- | +| `static` | A fixed `items` list. Great for `init`, examples, and tests. | +| `http` | `GET ` (substituting `{cursor}`), with the JSON array becoming arrivals. | +| `file` | Read a `dir` of `.json` files, re-scanned on each gateway poll (a per-poll `readdir`, not a filesystem watcher). | + +### Connector plugins + +A project may also ship a `connectors.cjs` or `connectors.js` plugin that exports `{ connectors: { [source_id]: { fetch, extract? } } }`. The plugin file is loaded once per project. + +### Durable idempotency + +Idempotency is durable. A per-source cursor dedups arrivals, so a restart never re-ingests the backlog. Poll a gateway again and nothing re-ingests, because the cursor already saw those ids. Add a new item and only that new arrival is staged. + +The cursor round-trips the same storage registry the reactor already persists to. There is no second state store. + + +The cursor is not a CLI-only construct. It is the SDK's `createIdempotencyCursor` (with `cursorRegistryPatch` to round-trip through the storage registry), the same primitive a hand-mounted reactor uses. The CLI just configures it for you. See [SDK adapters](/sdk/adapters) for the `pollGateway` / `createPollConnectorAdapter` / `GatewayArrival` surface. + + +### Ingress at run time + +When `serve` boots, the topology is augmented with a phantom-ingress edge per configured gateway, so a staged arrival moves the gateway's input fingerprint. Each tick the driver polls every gateway before the continuity sweep: fetch, extract, stage each new arrival, wake the gateway, then persist the advanced cursor. Every step runs behind the reactor's serialization queue, so a gateway poll never overlaps a continuity poll or a trigger. + +You can also drive ingress manually with `POST /trigger/` against a running daemon, or with `reactor trigger ` as a one-shot. There is no `reactor pull` command. + + +The SDK `Wake` shape carries no payload slot (`{ source, refs }` only), so a payload cannot be smuggled into a wake. `reactor trigger --data ` therefore uses the *same* staging mechanism as connector ingress: it augments the node's topology with a phantom-ingress edge, stages the `--data` into that inbox (moving the input fingerprint), then ingests. The wake is a memo-miss and the node re-renders reading the staged payload. With no `--data`, the trigger is a bare external wake. + + +## The render sandbox + +The `sandbox` block is the render threat-model knob. It bounds what a render command can reach. + +### mode: none + +`mode: none` is the locked default and the trusted posture. Renders run in the SDK's cwd-scoped, time and output bounded shell. `shell_timeout_ms` tunes the per-command time bound (default 300 seconds). + +```yaml +sandbox: + mode: none + shell_timeout_ms: 300000 +``` + +### mode: docker + +`mode: docker` runs each render command inside a throwaway, network-disabled container, bind-mounting only the workspace: + +```text +docker run --rm --network=none -v : -w ... +``` + +```yaml +sandbox: + mode: docker + image: node:22 +``` + +The `image` defaults to a built-in image when omitted. The bind-mount root is the per-project workspace; each node's render working dir lives beneath it, and the harness harvests results on the host side, so the determinism boundary is unaffected. + +### The Docker-absent fallback + +If Docker is absent when `mode: docker`, the run degrades to the bounded shell with a surfaced note. It never crashes. `reactor doctor` reports Docker availability when `mode: docker` is configured: + +```text + sandbox mode docker (Docker NOT available -- renders fall back to the bounded shell) +``` + +This keeps `mode: docker` safe to commit: a teammate without Docker still gets a working run, just at the `none` posture, with the downgrade made visible rather than silent. + + +**`none` and `docker` are the two modes that are realized.** A third mode, `mode: unix-local`, is accepted by the config parser but is not yet implemented: at run time it falls back to the bounded `none` shell with a surfaced note, exactly like the Docker-absent fallback. Treat it as deferred -- if you need real isolation today, use `docker`. The fallback is honest and never silent, but `unix-local` does not bound a render beyond what `none` already does. + + +## Where to go next + + + + + + diff --git a/content/docs/cli/meta.json b/content/docs/cli/meta.json new file mode 100644 index 0000000..9a4a032 --- /dev/null +++ b/content/docs/cli/meta.json @@ -0,0 +1,12 @@ +{ + "title": "Reactor CLI", + "pages": [ + "overview", + "quickstart", + "configuration", + "compile-run-serve", + "connectors-and-sandbox", + "observability", + "command-reference" + ] +} diff --git a/content/docs/cli/observability.mdx b/content/docs/cli/observability.mdx new file mode 100644 index 0000000..0cfff61 --- /dev/null +++ b/content/docs/cli/observability.mdx @@ -0,0 +1,94 @@ +--- +title: Observability +description: Read the compiled DAG, the receipt trail, and the cost rollup with status, inspect, topology, logs, trace, and receipts. +--- + +# Observability + +The observability commands are model-free. They open a read-only view over the populated state directory (the durable receipt trail, world-model truth, and cached topology) and print projections. They run fully offline, with no key and with the model deps absent. + +A read-only projection exits `0` even over an empty state directory: that is the honest quiet view. The two audit paths, `receipts verify` and `inspect --strict`, exit non-zero on a tampered or broken chain. + +## status + +```sh +reactor status +``` + +Reports the standing compile cost beside the live run cost, plus the per-node dispositions. This is the at-a-glance answer to "what did this system cost, and what is it doing?" + +## topology + +```sh +reactor topology +``` + +Prints the compiled DAG: the nodes (each with its wake source) and the resolved edges. It requires a compiled IR; without one it tells you to run `reactor compile` first. + +## inspect + +```sh +reactor inspect [--strict] +``` + +Inspects a single node: its topology position, its fingerprints, its last receipt, and its chain. A plain `inspect` is a pure read. With `--strict`, the command exits non-zero if the node's receipt chain does not verify, which makes it a CI gate. + +## logs + +```sh +reactor logs [--node ] +``` + +Prints the receipt stream as compact log entries, optionally filtered to one node with `--node`. + +## trace + +```sh +reactor trace [] +``` + +Traces each node's receipt chain in chain order, from wake to disposition. With no argument it traces every node that has receipts; pass a node id to trace just one. + +## receipts + +```sh +reactor receipts [list|verify|cost] [--node ] +``` + +Audits the receipt trail. The default subcommand is `list`. + +| Subcommand | Behavior | +| --- | --- | +| `list` | The receipt stream as compact entries (filter with `--node`). | +| `verify` | Walk the chain and check it. Exits non-zero on a tampered or broken chain. | +| `cost` | The cost rollup. | + +### Chain verification and tamper detection + +`receipts verify` is the integrity check. It walks the receipt chain and exits non-zero the moment it finds a break, which is exactly what tampering or corruption looks like. The same chain check backs `inspect --strict`. Both are designed to drop straight into CI: + +```sh +reactor receipts verify # exit 1 on a broken chain +reactor inspect digest --strict +``` + +## Reading cost + +Every receipt carries a `surprise_cause`. `reactor receipts cost` and `reactor status` roll cost up by that cause, so a cost figure is always attributable to a specific change. + +Because a node that re-wakes with unmoved inputs memo-skips at zero render cost, the standing cost of a quiet system trends to zero. A spike therefore means a real change propagated, and the rollup tells you which one. See [cost scales with surprise](/cli/overview#cost-scales-with-surprise) for the model. + +## JSON output + +Every observability command honors `--json` for machine-readable output, so you can pipe a projection into a script or a dashboard. + +```sh +reactor status --json +reactor receipts cost --json +``` + +When a running daemon is bound with `--http`, the same projections are available over HTTP at `/status`, `/cost`, `/topology`, `/receipts`, and `/nodes/`. See [the HTTP surface](/cli/compile-run-serve#the-http-surface). + +## Visualizing a run + +The receipts, world-models, and topology a state-dir holds are exactly what [Reactor DevTools](/reactor-devtools) replays. Point it at the same directory to watch the run animate -- nodes flashing on render, dim-pulsing on memo-skip, the cost meter spiking on a real change -- or read it headlessly with `reactor-devtools --describe`. diff --git a/content/docs/cli/overview.mdx b/content/docs/cli/overview.mdx new file mode 100644 index 0000000..c8c1ecc --- /dev/null +++ b/content/docs/cli/overview.mdx @@ -0,0 +1,159 @@ +--- +title: Reactor CLI +description: The reference driver for the @openprose/reactor SDK. It compiles a .prose project and serves it as a durable, cost-observable daemon -- twelve commands, four global flags, three exit codes. +--- + +# Reactor CLI + +`@openprose/reactor-cli` is the deterministic command-line **driver** for the [`@openprose/reactor`](/reactor) SDK. The command is `reactor`. + +The CLI is one client of the SDK, not its only face. It does a single job: it **configures** the SDK. It never re-implements the reconciler and it never parses `.prose` itself. Compile freezes intelligence (model sessions) into deterministic, content-addressed artifacts. Run and serve execute those frozen artifacts with a dumb reconciler. Everything the CLI does, you can do yourself in code. + + +The CLI is the recommended fast path. If you are embedding the harness in your own process -- mounting the DAG by hand, injecting a custom backend, driving the reactor handle from a server -- reach for the [SDK API reference](/sdk) instead. Same engine, programmatic surface. + + +## Install + +All three packages are live on npm: `@openprose/reactor-cli@0.2.0`, `@openprose/reactor@0.3.0`, `@openprose/reactor-devtools@0.2.0`. Prefer a **project-local** install -- no root, no global binary collisions -- and call the binary through `npx`: + +```sh +npm install --save-dev @openprose/reactor-cli @openprose/reactor @openai/agents zod +# then: npx reactor ... +``` + +To touch the keyless replay with **no install at all**, see [DevTools](/reactor-devtools): + +```sh +npx -p @openprose/reactor-devtools reactor-devtools --example masked-relay --describe +``` + +A global install (`npm i -g`) is an alternative, but `-g` can collide with other tools' binaries and is `EACCES`-prone on Linux/WSL. Reactor requires **Node >=20** (the SDK's `engines` floor). The SDK core has zero runtime deps; the live render needs two peers (`@openai/agents`, `zod`), and `doctor`, `init`, and the whole observability suite need neither. + + +`reactor --version` prints the **CLI** version (0.2.0), not the SDK version (0.3.0). That is expected, not a mismatch -- the two packages version independently. + + +## The reference client: compile, run, serve + +The CLI is the reference client for the SDK's three-phase lifecycle. + +1. **`compile`** runs the intelligent compile sessions (Forme topology, per-node canonicalizer, postconditions) and freezes them into a content-addressed IR cache under `/compile/`. An unchanged contract set recompiles at zero session cost. +2. **`run`** ensures the IR is fresh, boots the reactor, drains to quiescence, prints per-node dispositions plus cost, and exits. One-shot. +3. **`serve`** boots the durable host (filesystem receipts and world-models), runs the continuity driver loop, and exposes an HTTP surface. It stays up until `SIGINT` or `SIGTERM`, then drains in-flight work and exits. + +```sh +npx reactor init my-project # scaffold a gateway + responsibility + reactor.yml +cd my-project +npx reactor doctor # check node, SDK, key/deps, sandbox, state-dir, IR +npx reactor compile # run the compile sessions -> IR cache +npx reactor run # boot, drain to quiescence, print dispositions + cost +npx reactor serve --http 8080 # boot the durable host + continuity loop + HTTP surface +``` + + +A static gateway (one with no scheduled wake) does not fire on `run`. Bring it up with `serve`, then deliver a wake -- `reactor trigger ` or an HTTP `POST /trigger/`. See [Connectors and sandbox](/cli/connectors-and-sandbox). + + +## The twelve commands + +Every command spreads the four global flags on top of its own. The lifecycle verbs (`init`, `doctor`, `compile`, `run`, `serve`, `trigger`) drive the project; the observability suite (`status`, `topology`, `inspect`, `logs`, `trace`, `receipts`) reads the populated state directory. + +| Command | What it does | +| --- | --- | +| `init [dir]` | Scaffold a minimal `.prose` project (gateway + responsibility) + `reactor.yml`. | +| `doctor` | Report environment health: node, SDK, live key/deps, offline mode, sandbox, state-dir, IR. | +| `compile` | Run the compile sessions and refresh the content-addressed IR cache. | +| `run` | Ensure the IR is fresh, boot the reactor, drain to quiescence, and report. | +| `serve` | Boot the durable host (one or many reactors) and run the continuity driver loop. | +| `trigger ` | Trigger a node with an external wake (one-shot mount). | +| `status` | Report the standing compile cost beside the live run cost and dispositions. | +| `topology` | Print the compiled DAG: nodes (and wake source) and resolved edges. | +| `inspect ` | Inspect a node: topology position, fingerprints, last receipt, chain. | +| `logs` | Print the receipt stream, optionally filtered to one node. | +| `trace [node]` | Trace each node's receipt chain: wake to disposition, in chain order. | +| `receipts [sub]` | Audit the receipt trail: `list` \| `verify` \| `cost` (default `list`). | + +The full per-command flag tables live in the [command reference](/cli/command-reference). + +## Four global flags + +Every command honors these four flags: + +| Flag | Meaning | +| --- | --- | +| `--state-dir ` | Durable state directory (default `./.reactor`). | +| `--project ` | Project directory containing `reactor.yml` (default `.`). | +| `--json` | Machine-readable JSON output. | +| `--offline` | Force offline mode (equivalent to `REACTOR_OFFLINE=1`). | + +## Three exit codes + +The CLI is built to be driven by agents and CI, so its exit codes are a contract, not a side effect. + +| Code | Meaning | +| --- | --- | +| `0` | Success, or a clean help/version display. | +| `1` | A reported failure with an actionable message on stderr (a handler set it). | +| `2` | A usage error: an unknown command or flag, a missing argument, or an unknown `receipts` subcommand. | + +An unknown `receipts` subcommand (for example `receipts verifyy`) is rejected to stderr and exits `2` rather than silently falling through to `list` -- a trust hazard a CI gate must not inherit. Under `--json`, an operational failure mirrors a `{ ok: false, error }` envelope to stdout so a machine consumer is never left with empty output. + +## Cost scales with surprise + +Every receipt carries a `surprise_cause`. A node that re-wakes but whose inputs did not move memo-skips at zero render cost. A node renders, and spends tokens, only when its `(contract_fp, input_fps)` memo key actually moves. + +So the standing cost of a quiet system trends to zero, and a cost spike is always a real change propagating. `reactor receipts cost` and `reactor status` roll cost up by `surprise_cause` so you can see exactly what surprised the system. See [observability](/cli/observability) for the details. + +## The offline boundary + +The default import surface and every model-free command are keyless. Requiring the CLI entrypoint loads neither `@openai/agents` nor `zod`. + +`compile`, `run`, `serve`, `trigger`, and the connector and render paths reach the model surface only via a dynamic `import()` inside the handler. They need a live key (`OPENROUTER_API_KEY`) plus the optional peer deps. Every other command, including `doctor`, `init`, and the whole observability suite, runs fully offline, with no key and with the model deps absent. + +You can force offline mode on any command with `--offline` (or `REACTOR_OFFLINE=1`). + +## Where to go next + + + + + + + + + + + diff --git a/content/docs/cli/quickstart.mdx b/content/docs/cli/quickstart.mdx new file mode 100644 index 0000000..c24ddbe --- /dev/null +++ b/content/docs/cli/quickstart.mdx @@ -0,0 +1,163 @@ +--- +title: Quickstart +description: Scaffold, compile, and run a reactor project end to end. +--- + +# Quickstart + +This walks the full path: `reactor init` to scaffold a project, `reactor doctor` to check the environment offline, `reactor compile` to freeze the intelligence, then `reactor serve` to drive the scaffold's static gateway to a real receipt. + +If you are an agent onboarding on behalf of a user, the centralized, ordered setup path -- keyless proof first, then init/doctor/compile, then go live, then where contracts live -- is in [OpenProse Setup](/openprose/setup). This page is the CLI-local version of the same flow. + +## Install + +Prefer a **project-local** install. No root, no global binary collisions, and the live render peers (`@openai/agents`, `zod`) resolve from the project tree. Call the binaries through `npx`: + +```sh +npm install --save-dev @openprose/reactor-cli @openprose/reactor @openai/agents zod +# then: `npx reactor …` / `npx reactor-devtools …` +``` + +`@openprose/reactor` is the SDK engine the CLI drives, and a real dependency of `@openprose/reactor-cli` -- the line above just makes it explicit alongside the two live-render peers. None of these pull a model provider or a key. Zero runtime deps live in the SDK core; `doctor`, `init`, the whole observability suite, and the `@openprose/reactor-devtools` replay viewer need neither key nor peers. + +To touch the keyless replay with **no install at all** -- the fastest proof that the receipts are real: + +```sh +npx -p @openprose/reactor-devtools reactor-devtools --example masked-relay --describe +``` + +A global install is an alternative, but `-g` can collide with other tools' binaries and is `EACCES`-prone on Linux/WSL. If you go that route, install the SDK first and add the peers: `npm i -g @openprose/reactor @openprose/reactor-cli @openprose/reactor-devtools @openai/agents zod`. + + + Requires **Node >=20** (the SDK's `engines` floor). `reactor --version` prints the **CLI** version (`0.2.0`), not the SDK version (`0.3.0`) -- expected, not a mismatch. + + +## Scaffold a project + +`reactor init` writes a minimal, compilable project: a gateway, a responsibility that subscribes to it, a `reactor.yml`, a `.gitignore`, and a short README. + +```sh +npx reactor init my-project +cd my-project +``` + +The scaffold is the smallest end-to-end shape with a real edge. The `inbox` gateway accepts external arrivals and materializes them as a set. The `digest` responsibility subscribes to that set, so when the inbox moves the digest re-renders, and only then. + +Your contracts live as `*.prose.md` files under the scaffold's `src/`. That is where you author the standing truths the reactor maintains -- see [Contracts](/openprose/contracts) for the authored surface. + +`init` refuses to overwrite existing files by default. Pass `--force` to clobber a directory that already contains scaffold files. + +## Check your environment + +`reactor doctor` runs fully offline and reports node version, SDK resolvability and version, live-key presence (it never prints the key), live-dep presence, the SKILL bundle, the sandbox mode, whether the state directory is writable, and the compiled-IR freshness. + +```sh +npx reactor doctor +``` + +```text +reactor doctor + + node v22.3.0 (ok) + sdk @openprose/reactor@0.3.0 (resolved) + offline mode not forced + live key present (OPENROUTER_API_KEY) + live dep @openai/agents: ok + live dep zod: ok + skill bundle present (/abs/my-project/node_modules/@openprose/...) + sandbox mode none + state dir /abs/my-project/.reactor (writable) + compiled IR not compiled -- run `reactor compile` + + status: healthy-for-offline + live: READY -- key + model peers + SKILL present; `reactor compile`/`run` can render +``` + +Add `--live` to probe one live smoke render against the real provider. The keyless surface (everything but `compile`/`run`/`serve`/`trigger`) works even when the live key, the peers, and the SKILL bundle are absent. + +## Compile + +`compile` runs the intelligent compile sessions (Forme topology, per-node canonicalizer, postconditions) and freezes them into the content-addressed IR cache. It needs a live key (`OPENROUTER_API_KEY`) plus the `@openai/agents` and `zod` peers. + +```sh +export OPENROUTER_API_KEY=... # doctor confirms it's present, never echoes it +npx reactor compile +``` + +The cache key is `(contract-set fingerprint, SDK version, model id)` -- cost is never part of cache identity, so an unchanged contract set recompiles at **zero session cost** (a cache hit). To check freshness without compiling (handy in CI), use `--check`, which exits non-zero when the cache is stale. + +```sh +npx reactor compile --check # exits 1 right after init, before the first compile +``` + + + Reading the exit code in CI? Check `$?` from the bare command -- **do not pipe** if you need the status. A pipe reports the *last* command's exit, so a STALE failure silently looks like a pass. + + +## Inspect the compiled DAG + +Once compiled, the offline observability commands work with no key. `topology` prints the resolved DAG. + +```sh +npx reactor topology # the compiled DAG (inbox -> digest) +``` + +## Drive the static gateway + +The scaffold's `inbox` gateway uses a `static` connector: its seeded items are ingested by the **serve** continuity loop, not by a bare boot-and-drain. Use `reactor serve` here, not `reactor run` -- `serve` polls the gateway and stages the seeded arrivals, which moves the inbox fingerprint and wakes `digest`. (`reactor run` is the one-shot drain for graphs whose connectors emit on their own; on a static scaffold it would boot, find nothing newly arrived, and exit without rendering.) + +`serve` boots the durable host, runs the continuity loop, and binds the built-in HTTP surface. It stays up until you stop it with Ctrl-C. + +```sh +npx reactor serve --http 8080 +``` + + + `serve --http` binds **`127.0.0.1` by default** and ships **no auth** in v1. `POST /trigger/` is unauthenticated, so anything that can reach the port can wake a node and cause model spend. Only expose it externally (`--host 0.0.0.0`) behind a reverse proxy or network policy that adds auth and rate-limiting. + + +On the first tick the `static` connector ingests the seeded items, so the gateway and digest both render. In another shell you can read the trail and the standing cost: + +```sh +npx reactor status # standing compile cost beside the run cost +npx reactor receipts list # the gateway + digest receipts +npx reactor receipts cost # cost rolled up by surprise_cause +``` + +Then replay your own run's receipt ledger -- keyless, no model call: + +```sh +npx reactor-devtools .reactor --describe +``` + +Every receipt carries a `surprise_cause`. A node that re-wakes but whose inputs did not move memo-skips at zero render cost, so a cost spike is always a real change propagating. Benchmarks are openly pending -- the proof is the receipts and the keyless replay, not a number in our marketing. + +## Next steps + + + + + + + + diff --git a/content/docs/index.mdx b/content/docs/index.mdx index ce85d3a..3308d7a 100644 --- a/content/docs/index.mdx +++ b/content/docs/index.mdx @@ -1,84 +1,129 @@ --- -title: OpenProse Documentation -description: A programming language for AI sessions. +title: OpenProse +description: One system, two layers. OpenProse is the paradigm you author in -- declare the outcomes you want kept true, in Markdown. Reactor is the harness that keeps them true, so cost scales with surprise, not the clock. --- -# OpenProse is a programming language for AI sessions +# OpenProse -Write a Markdown contract. Your agent reads it, wires the services, runs the subagents, passes artifacts through the filesystem, and leaves a trace on disk. +**OpenProse is a programming paradigm: you _declare the outcomes you want kept true_ -- an ideal world-model -- instead of issuing instructions.** You write that intent as familiar structured **Markdown contracts** (`*.prose.md`), with optional imperative **ProseScript** fulfillment plans for when order, loops, or exact choreography matter. The render function is not deterministic byte code -- it is declarative Markdown fulfilled by a bounded agent session. OpenProse shipped first as a Skill and runs on **any Prose-Complete agent harness**. -Plain prompts are great for one-off work. They get messy when the same process needs roles, handoffs, retries, memory, or a receipt. OpenProse gives that process a contract and lets an agent session execute it as a program. +**Reactor (`@openprose/reactor`) is the harness built to run OpenProse** -- and the recommended **fast path**. It keeps a composed **world-model** up to date against a changing world, re-rendering only the declared facets whose upstream inputs actually moved (memoized agent sessions wired into a DAG), and leaves a content-addressed **receipt** behind every decision. + +These are not two systems. They are **one system, two layers that fit together**: OpenProse is the language you author in; Reactor is the deterministic host that serves it efficiently. The thesis Reactor works toward: + +> **Inference cost that scales with surprise, not wall-clock time.** + +In plain terms: you declare what should stay true, the system watches the world, and it does expensive model work **only when something material actually moved**. + +## Start here + -## The shape +## The shape of a contract -An OpenProse program is Markdown with a contract: +You author **Responsibilities** -- standing goals, written as Markdown contracts. A responsibility declares what it subscribes to (`### Requires`) and the truth it keeps current (`### Maintains`): ```markdown --- -name: docs-review -kind: program +name: renewal-risk +kind: responsibility --- -### Requires +### Goal -- `repo`: repository to inspect +Keep a current risk read on every account whose signals have moved. -### Ensures +### Requires -- `report`: concise review report with risks and next steps +- `usage`: the per-account usage facet from the telemetry responsibility -### Services +### Maintains -- `reader`: maps the relevant files -- `critic`: checks for correctness and missing tests -- `writer`: turns the findings into a report +A current risk score per account. Material: the score band and the top +driver per account; `fetched_at` and request ids are immaterial. Each +account carries a `valid_until`. Postcondition: every flagged account +cites a corroborating signal. ``` -The program format is just Markdown. The execution model is real: contracts, services, wiring, workspaces, bindings, and traces. +`### Maintains` is the world-model **schema** doing four jobs at once: it is a **type**, a **canonicalization spec** (what is material vs. immaterial, how things normalize), an optional set of **facets** (named sub-truths a consumer can subscribe to one at a time), and a set of **postconditions** that compile to commit-gate validators. _Structure is subscription:_ Forme matches `Requires.` against `Maintains.` and wires the graph from the contracts themselves. + +Reactor compiles that set of contracts once (intelligently -- Forme topology, a per-node canonicalizer, postcondition validators, all frozen), then runs them forever (dumbly -- compare fingerprints, then skip, render, or propagate). The reconciler that decides _whether to wake_ is deliberately deterministic: **there is no judge step.** The memo key has no clock in it. A re-poll that returns the same truth costs nothing. See [the foundation](/openprose) for the full authored surface, and [the harness](/reactor) for how it runs. -## When to reach for it +## React-flavored, not React-gated -Use OpenProse when a prompt has started turning into a process: +**You do not need React to use this.** The contracts are Markdown; the CLI, the receipts, and the keyless replay are entirely React-free. The whole product in two sentences: _you declare what should stay true, the system watches the world, and it does expensive model work only when something material actually moved._ The table below is an optional mental model for the people who already carry one -- skip it freely. -- the work has roles, phases, or handoffs -- several agents can work in parallel -- the result needs a durable trace -- the same process will run again -- constraints should survive across runs +
+Optional: the React metaphor (skippable) -If it fits in one good prompt, use one good prompt. OpenProse is ceremony until the workflow needs a contract. +If you know React, you already know the shape -- substitute three nouns: -## What runs it +| React | Reactor | +| --- | --- | +| Component | **Responsibility** -- a declared standing goal | +| DOM | **World-model** -- the maintained truth, on disk, passed by pointer | +| `render()` | **A bounded LLM session** that computes the next world-model | +| props | **Subscriptions** to other responsibilities' outputs | +| `React.memo` (skip if props unchanged) | **Skip the render if subscribed inputs haven't moved** | +| Manual dependency wiring | **Forme** -- the graph wires itself from declared contracts | -A modern agent session is already a kind of computer: a model, a harness, tools, filesystem state, and sometimes subagents. OpenProse gives that computer a program format. +The intelligence is frozen ahead of time, at compile, into a per-node canonicalizer and the Forme wiring. The reconciler at run time is dumb on purpose. There is no `.prose` parser and no interpreter -- a compile step is itself an agent session; the session embodies the VM. -Forme reads the contracts, resolves which services satisfy which requirements, writes a manifest, and then the model plus harness executes the run. Services work in private workspaces. Declared outputs become bindings. Other services receive paths to artifacts instead of giant pasted blobs. +
-## Source of truth +## See the thesis -- keyless, no model call -These docs explain the shape of OpenProse. They are not the execution spec. +The fastest way to understand the system is to replay a real saved run and read the per-node `rendered`/`skipped` dispositions, the receipt counts by `surprise_cause`, the token cost rollup, and per-node chain-verify -- with no key and no spend. No install required: + +```bash +npx -p @openprose/reactor-devtools reactor-devtools --example masked-relay --describe +``` + +```text +reactor-devtools --describe + (synthetic sample ledger -- token counts are illustrative, not a bill) +dispositions rendered=46 · skipped=31 · failed=0 +surprise-cause external=8 · input=69 (a.k.a. wake-cause) ← receipt COUNTS, 77 total + +COST ROLLUP (tokens) + total fresh=27180 tokens · reused=12840 tokens · reuse=32% + external receipts= 8 fresh= 1080 tokens reused=840 tokens + input receipts= 69 fresh= 26100 tokens reused=12000 tokens +CHAIN-VERIFY ok +``` + +The binary prints that synthetic-sample banner first, and it is the honest frame: `masked-relay` is a **saved sample ledger**, so the token magnitudes are illustrative, not a measured bill. What is genuinely checkable here is the **structure** -- the per-node `rendered`/`skipped` dispositions, the `surprise_cause` receipt counts, and `CHAIN-VERIFY ok` (every receipt links its `prev`). That structural shape is "cost scales with surprise," and you can verify it with no key and no spend. (When you run your own contract, the cost rollup becomes your real spend; see the [honest-status notes](/sdk#whats-built-and-what-isnt) on why there are no benchmark numbers yet.) From there, the [setup path](/openprose/setup) walks the full ordered route: the keyless proof first, then `init` -> `doctor` -> `compile` offline, then go live and `serve`. Or jump to the [CLI quickstart](/cli/quickstart). + +## Where the truth lives + +The canonical execution behavior lives in the open-source `open-prose` skill in the [`openprose/prose`](https://github.com/openprose/prose) repo. These docs are orientation; if the docs and the skill disagree, trust the skill. New here? Start with [OpenProse the paradigm](/openprose); coming from a chat-first workflow, jump to [Reactor](/reactor), the dependency-across-runs layer your prompts kept asking for. + +--- -If you are an agent running an OpenProse program, start with [For AI agents](/agents/for-ai-agents). The `open-prose` skill and its linked specs are the source of truth for execution behavior. +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/meta.json b/content/docs/meta.json index e8b22c9..9cbc469 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -1,4 +1,4 @@ { "title": "OpenProse", - "pages": ["index", "start", "think", "use", "agents", "reference"] + "pages": ["index", "openprose", "reactor", "sdk", "cli", "reactor-devtools"] } diff --git a/content/docs/openprose/contracts.mdx b/content/docs/openprose/contracts.mdx new file mode 100644 index 0000000..fbbf95a --- /dev/null +++ b/content/docs/openprose/contracts.mdx @@ -0,0 +1,195 @@ +--- +title: Contracts +description: The authored surface of OpenProse -- Markdown contracts, the five kinds, and the load-bearing sections that turn a declaration into a typed, subscribable node. +--- + +# Contracts + +A contract is the thing you author. It is a Markdown file -- `*.prose.md` -- +with a small YAML frontmatter and a handful of `###` sections. The Markdown, +with its durable trail, is the public artifact: it can leave for any +Prose-Complete harness with no lost semantics. The deployment's secrets and +data stay private. + +Authors are co-equal. A human and an agent write contracts the same way, read +the same sections, and fork or compose each other's work without translation. +Every section below is designed for both readers from the start. + +This page is the authored surface only -- what you write. The runtime that +*serves* these contracts (fingerprints, memoization, the continuity clock, +receipts) is the Reactor harness's concern; see +[the Reactor section](/reactor) for that. Here, intent lives only in the +contract. + +## Frontmatter and identity + +A contract opens with YAML frontmatter. Two fields carry weight: + +- `kind:` -- which of the five contract kinds this is (below). This is the one + field that changes how the file is executed. +- `name:` -- the human-facing slug. + +A third field, `id:`, is a durable identity (a base32 token) minted once by +tooling and preserved across `name:` and filename renames. You do not +hand-write `id:`; tooling manages it. `name:` is what you read; `id:` is what +keys the node's world-model and receipt-ledger state on disk. + +## The five kinds + +`kind:` selects one of five authored kinds. Each has a different run model. + +| Kind | Run model | Purpose | +| --- | --- | --- | +| `responsibility` | Served (continuously reconciled) | The headline kind: a mounted DAG node maintaining a standing truth over time. | +| `function` | Called (one-shot) | A stateless, ephemeral helper. Bind `### Parameters`, run one render, return `### Returns`. No Forme phase, no world-model. | +| `gateway` | Mounted as external-driven | Sugar for an external-driven responsibility. Compiles into a trigger registration for `reactor serve`; refuses a direct `run`. | +| `test` | Via the test path | Fixtures plus natural-language assertions against a subject responsibility or function. | +| `pattern` | Instantiated at compile time | A reusable coordination algorithm, expanded into nodes at compile time. Never directly run. | + +There is **no `kind: service`** (it was renamed to `function`) and **no +`kind: system`** (deleted). Cross-node composition is a Forme-wired +subscription between responsibilities; intra-node composition is an imperative +`call` inside one render. There is never an internally-autowired graph kind. + + +The headline kind is `responsibility`. The rest of this page teaches its +load-bearing sections, because those are what make a contract a *typed, +subscribable node* rather than a bare prompt. + + +## The load-bearing sections + +Most `###` sections mean exactly what their name says. `### Description` is a +human summary preserved for readers, not a contract. `### Goal` is the render's +one-sentence standing intent. These defer to the obvious. + +Three sections carry the semantic load of a `responsibility`, and the contract +must *state* them rather than defer: `### Requires`, `### Maintains`, and +`### Continuity`. + +### `### Maintains` -- the world-model schema (four jobs at once) + +`### Maintains` is the schema for the truth this node keeps current. It does +four jobs in one section: + +1. It **types** the maintained truth -- the shape of the world-model the node + owns. +2. It carries the **canonicalization spec** -- which fields are material and + how they normalize. This compiles into the node's fingerprint, the cheap + content identity the run phase compares. +3. It declares **facets** (below) -- the named parts of the truth. +4. It states **postconditions** -- the obligations a render must satisfy + before it may commit. + +There is no separate judge and no `### Criteria` section. Satisfaction folds +into `### Maintains`: checked deterministically where it can be written as a +validator, and self-attested by the render where it is semantic. A render that +cannot satisfy its postconditions commits nothing -- the prior truth stands, +and a `failed` receipt records why. + +### Facets make subscription structural + +A `####` sub-heading inside `### Maintains` declares a **facet** -- a named +part of the truth. The facet's name is, at once, three things: + +- its **fingerprint unit** (the canonicalizer emits one token per facet, plus + an always-on atomic token over the whole truth); +- its **subscription symbol** (a consumer names it in `### Requires`, and the + reconciler wakes that consumer only when *that* facet's token moves -- + `Requires.` ↔ `Maintains.`); +- and its **region** of the world-model. + +Declaring no parts is the atomic default -- one truth, one token -- and costs +nothing. This adds no new grammar: it reuses the heading hierarchy and the +Requires↔Maintains join. **Structure is subscription.** The way you shape the +`### Maintains` section *is* the way downstream nodes subscribe to it. + +### `### Requires` -- what the node subscribes to + +`### Requires` names the upstream facets this node consumes. Forme matches each +entry to a producer's `### Maintains` facet and draws the subscription edge. +`### Requires` is the input side of the memo key: the run phase fingerprints +the node's contract together with its subscribed inputs, and re-renders only +when one of them moves. + +### `### Continuity` -- the wake source + +`### Continuity` declares when, beyond a subscribed input changing, a node +should re-render. It is **not a schedule**. A node is *input-driven* by default +(it wakes when a subscribed facet moves). `### Continuity` adds one of: + +- a *self-driven* cadence -- the truth goes stale on its own. A `valid_until` + freshness state lives in the world-model as data; `### Continuity` carries + only the *policy* that reads it on a forecast cadence. When a `valid_until` + lapses, the harness mechanically moves the facet's fingerprint and wakes the + node -- no model call to decide that time has passed. +- *external-driven* -- marks a `kind: gateway` so an ingress event wakes it. + +The author writes these sections. The harness owns the fingerprinting, the +forecast cadence, the receipts, and the subscription wiring. You declare the +truth; the runtime decides, deterministically, when it is worth recomputing. + +## Functions: the call interface + +A `kind: function` does not maintain a world-model and is not part of the DAG. +It declares a plain call interface instead of the four-jobs schema: + +- `### Parameters` -- the inputs bound at call time. +- `### Returns` -- the return value of its single render. + +A lone function has no Forme phase: bind parameters, spawn one render, return +`### Returns`. It is the stateless, ephemeral helper. + +## A contract in full + +Here is a real, migrated `responsibility`. Read it top to bottom: the `> ` +description states intent for a human, `### Requires` names the single facet it +subscribes to, `### Maintains` types the `CountSummary` truth and declares one +`#### structured` facet with an explicit postcondition, and `### Continuity` +declares it `input-driven` -- so it wakes only when its `counts` input moves. + + + +And here is the `gateway` that feeds it. A gateway has no `### Requires` (its +input arrives from outside the graph). Notice how `### Maintains` splits the +ledger into two `####` facets -- `counts` and `raw_events` -- so a +metadata-only event moves `raw_events` (waking the auditor) without moving +`counts` (the summary above stays dark). That split *is* the subscription wiring. + + + +A bare prompt is `any`: it tells an agent what to do once and forgets. A +contract is a typed function over the world -- a declared truth with a fingerprint, +named facets others can subscribe to, and postconditions it must satisfy before +it commits. That is the whole difference, and it is authored entirely in +Markdown. + +## Where to go next + + + + + + + + +For the full language semantics, the authoritative source is the OpenProse +spec ([01-Language.md](https://github.com/openprose/prose/blob/main/spec/01-Language.md)) +and the loaded `open-prose` SKILL. If the docs and the skill ever disagree, +trust the skill. diff --git a/content/docs/openprose/declare-outcomes.mdx b/content/docs/openprose/declare-outcomes.mdx new file mode 100644 index 0000000..8bbd994 --- /dev/null +++ b/content/docs/openprose/declare-outcomes.mdx @@ -0,0 +1,186 @@ +--- +title: Declare outcomes, not instructions +description: The central teaching of OpenProse -- author an ideal world-model to keep true, not a sequence of steps to run. Intent lives only in the contract. +--- + +# Declare outcomes, not instructions + +OpenProse asks one thing of you that most agent frameworks do not: **declare the +truth you want kept current, and stop there.** Not the steps. Not the order. Not +the prompt that coaxes a model through them. You write down the world as it +should be -- an ideal world-model the system is responsible for making real and +keeping real -- and the host figures out the work. + +This is the inversion at the heart of the paradigm, and everything else (the +five contract kinds, fingerprints, the DAG, receipts) is downstream of it. + +## Instructions decay; outcomes endure + +A prompt is a sequence of instructions aimed at one moment. It runs, it produces +something, and then the moment passes. When the world moves -- a new filing +lands, a competitor ships, a number drifts -- the instructions have nothing to +say, because they were never about the world. They were about a turn. + +An **outcome** is different. "Every tracked competitor has a current, +corroborated funding view" is true or it is not, today and tomorrow and after +the source you scraped changes its layout. It is a standing claim about the +world, not a recipe for one visit to it. Declare the outcome and you have +declared something that can be *re-checked* and *re-established* without you +re-typing the steps. + +That is the whole shift: + +| You stop writing | You start writing | +| --- | --- | +| "First fetch X, then summarize, then compare to Y..." | "This truth, with these fields, kept current and corroborated." | +| a sequence pinned to one run | a standing goal that survives many runs | +| intent spread across a prompt, a config, and a judge | intent in exactly one place | + + +This is the language layer. It says **what** is true and **what** counts as a +material change. It deliberately says nothing about *how often* the host checks, +*how* it decides something moved, or *what* it records along the way. Those are +runtime mechanics, owned by a harness like +[Reactor](/reactor) -- and deferred to that section on purpose. + + +## Intent lives only in the contract + +The first OpenProse tenet states it plainly: **intent lives only in the +contract.** The `*.prose.md` you author carries 100% of the semantic weight. +Everything else -- the compiled artifacts, the projections, the operational +policy a host applies -- is *derived*, and when a derived thing disagrees with +your contract, the contract is right. + +There is no second authored surface for intent. Not a prompt you tune on the +side. Not a YAML config that quietly overrides the Markdown. Not a hidden judge +prompt deciding what you really meant. If you find yourself reaching for one of +those, the meaning has leaked out of the contract, and the system can no longer +guarantee the outcome you declared -- because you declared it in two places that +can drift apart. + +One source of meaning is not a stylistic preference. It is what lets the same +contract run identically on any compliant host, what lets an agent fork and +compose your work without archaeology, and what makes the audit trail honest: +there is exactly one thing the trail can be checked against. + +## The world-model: a maintained truth, like the DOM + +When you declare an outcome, the thing the system keeps real on your behalf is +the **world-model** -- a node's maintained truth, persisted on disk, standing +between one unit of work and the next. + +If you know React, you already know the shape. The world-model is the Reactor's +**DOM**: a current, structured representation that survives between renders, is +read by the next render as its prior state, and is subscribed to by whatever +depends on it. You do not rebuild it from scratch each turn. You declare its +*schema* -- its shape, and what about it actually matters -- and the system keeps +that truth current against a changing world. + +A render, then, is one bounded step from prior truth to next truth: + +```text +(contract, evidence, prior world-model) -> (new world-model, receipt) +``` + +Read it as a sentence. Given **what you declared** (the contract), **what the +world now shows** (evidence), and **what was true last time** (the prior +world-model), one bounded session computes **the truth as it should now be** (the +new world-model) and leaves **a record of the decision** (the receipt). The +outcome you declared is the contract on the left and the world-model on the +right. The instructions you did *not* write are what the session figures out in +the middle. + + +You declare the world-model's schema -- its fields, what counts as a material +change, and how its parts divide for subscription. The host owns *how* it decides +a change occurred (fingerprints), *when* it re-checks (the reconciler), and *what* +it records (receipts). See +[World-model and fingerprints](/reactor/world-model-and-fingerprints) for those +mechanics; this page is only about the act of declaring the truth. + + +## A type system for agent workflows + +Here is the mental model that makes the discipline pay for itself. + +Think of a bare prompt as `any`. It runs, and nothing is checked: no declared +inputs, no declared output, no way for a caller to reason about it or for a +violation to fail loudly. A contract is a **typed function**. Its inputs and +outputs are declared, its callers can reason about composition, and an unmet +obligation fails where you can see it rather than rotting silently downstream. + +You would not write a two-thousand-line TypeScript system in `any`. Multi-step +agent work is the same. Declaring outcomes is how you give an agent workflow a +type: the contract states what must be true, so the system can check that it is. + +## What you actually write + +Declaring an outcome is concrete. A standing goal becomes a +`kind: responsibility` -- the headline kind, mounted as a node whose truth is +maintained over time. You name the truth it keeps current and what counts as a +material change to it; the host does the rest. + +```markdown +--- +name: competitor-funding +kind: responsibility +--- + +### Goal + +Keep a current, corroborated funding view for every tracked competitor. + +### Maintains + +A funding view per competitor: each carries a stable `name`, a set of funding +events (round, amount, date), and a `last_corroborated` field. Material: the +event set (unordered) and each event's round, amount, and date. Immaterial +everywhere: `fetched_at` and source request ids. Postcondition: every funding +event cites a source. +``` + +There is no `### Execution` here, and that absence is the point. You did not +write the steps. You declared the outcome -- the truth, its shape, what matters +about it, and what must hold before it may be committed -- and a render figures +out the work each time the world moves. `### Maintains` is doing the load-bearing +job: it is the world-model schema, and learning to author it well is the next +page. + + +Declarative is the default, not a cage. When a step genuinely must happen in a +specific way -- a tool that must run, an order that must hold -- OpenProse has an +optional imperative layer (ProseScript) for pinning exactly that, and nothing +more. The rule is "declarative by default, explicit when needed," and the +explicit part stays subordinate to the declared outcome. See +[ProseScript](/openprose/prosescript). + + +## Where to go next + + + + + + + +These docs are orientation. The canonical execution behavior lives in the +open-source `open-prose` skill in the +[`openprose/prose`](https://github.com/openprose/prose) repo; if the docs and the +skill disagree, trust the skill. + +--- + +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/openprose/harness-agnostic.mdx b/content/docs/openprose/harness-agnostic.mdx new file mode 100644 index 0000000..0ada937 --- /dev/null +++ b/content/docs/openprose/harness-agnostic.mdx @@ -0,0 +1,136 @@ +--- +title: Harness-agnostic +description: OpenProse contracts describe an abstract VM. Any Prose-Complete host can run them, and the SKILL-loaded session embodies that VM -- there is no parser. Reactor is one host, the recommended fast path. +--- + +# Harness-agnostic + +A `.prose.md` contract does not name a host. It declares an ideal world-model +and, optionally, a fulfillment plan, in terms of an abstract VM. Anything that +can map that VM's primitives onto real capabilities can run the same contract. +This is the deliberate seam at the center of OpenProse: **the language is one +layer, the host is another**, and you author against the language. + +This is why "harness-agnostic" is a property of the contract, not a promise from +any one runtime. The wisdom of the ancients applies: you write to an interface, +and the interface outlives any single implementation. + +## The VM primitives a host must provide + +A host is **Prose-Complete** when it can map the abstract VM operations onto its +own tools. There are five, and they are the entire contract between the language +and any host: + +| Primitive | What the host must do | +| --- | --- | +| `spawn_session` | Run a render -- a responsibility or a called function -- in an isolated agent/session with a prompt, an optional model, and access to the declared input/output paths | +| `ask_user` | Pause for missing required caller input, and resume with the answer | +| `read_state` / `write_state` | Read and write run state through the selected backend (the OpenProse root's run artifacts and durable records) | +| `copy_binding` | Publish a declared output through the active backend; never publish undeclared scratch | +| `check_env` | Confirm an environment variable exists without exposing its value | + +A host that can do these five things can execute any OpenProse contract. The +contract never reaches past this table. It does not know whether `spawn_session` +becomes a subagent call in Claude Code, a `codex-sdk` activation, or a bounded +session inside Reactor -- it only knows that a render runs in isolation with the +paths it declared. + + +The mapping is the host's job, not yours. Codex-style and Claude Code-style +environments are the primary documented targets. A Prose-Complete host with a +real subagent primitive runs multi-agent contracts; a host without one may +execute trivial single-render runs inline and must report the limitation rather +than pretend. + + +## The same Markdown runs on any host + +Because the contract speaks only in VM primitives, the **same file** runs +unchanged across hosts. You do not maintain a Claude variant and a Codex variant +and a Reactor variant. You maintain one `.prose.md`. The host changes; the +declared outcome does not. + +Concretely, a shell line like: + +```bash +prose run src/hello.prose.md +``` + +means "ask the selected agent harness to embody the OpenProse VM and execute +this contract." Swapping the host -- `codex-sdk`, `claude-sdk`, a local `mock`, +or Reactor -- changes who provides the five primitives, not what the contract +asks for. + +## The session embodies the VM -- there is no parser + +This is the load-bearing idea, and it is easy to miss because it inverts the +usual assumption. OpenProse is **never parsed or interpreted**. There is no +`.prose` parser, no bytecode, no interpreter loop that walks the Markdown. + +Instead, a SKILL-loaded agent session **is** the VM. When a Prose-Complete host +loads the `open-prose` skill and reads a contract, the session itself carries +the execution semantics: it resolves the contract, spawns renders, maintains the +world-model, and signs receipts. The intelligence is the runtime. Even a compile +step is an agent session -- the topology, the canonicalizer, and the validators +are all session output, not parser output. + + +A `prose` CLI is not a replacement VM. It forwards a run to the selected harness +and never parses `.prose` semantics itself. The CLI is plumbing; the +SKILL-loaded session is the machine. + + +This is what makes the language portable without a shared runtime binary. There +is nothing to port. Any host that can host a capable agent session, and can map +the five primitives, already has everything the language needs. + +## The language/harness seam + +Keeping the two layers distinct keeps both honest. The boundary is sharp: + +- **The language has one source-derived compile.** `prose compile` lowers the + `*.prose.md` source set into compile-phase IR. That IR is a pure function of + the source set and nothing else -- no clock, no run history, no host state. +- **Runtime mechanics are sibling state, owned by the host.** A host's + token-truth receipts, forecasts, freshness tracking, and reconciler decisions + are runtime state owned by `@openprose/reactor`. They are **not** IR fields and + **not** new `*.prose.md` syntax. A host may keep none of this, some of it, or + all of it; the language does not require any of it. + +So when you read about receipts, fingerprints, memoization, and the reconciler +in the [Reactor section](/reactor), read them as **one host's runtime**, not as +language features. The contract you author is the same whether or not a host +chooses to memoize it. + +## Reactor is one host -- the recommended fast path + +OpenProse contracts run on any Prose-Complete host. **Reactor** (the +`@openprose/reactor` SDK, plus `reactor-cli` and `reactor-devtools`) is the host +built specifically to run them well: it memoizes the agent-session DAG, +re-renders only what moved, and leaves a content-addressed receipt behind every +decision, so cost scales with surprise rather than the clock. + +That makes Reactor the recommended fast path -- not the only path. You can run +the same contracts on a Claude Code or Codex session today. You reach for +Reactor when you want the deterministic reconciler, the durable world-model, and +the inspectable receipt trail. The choice is a host choice; your contracts do +not change. + +## Where to go next + + + + + + +The canonical execution behavior lives in the open-source `open-prose` skill in +the [`openprose/prose`](https://github.com/openprose/prose) repo. These docs are +orientation; if the docs and the skill disagree, trust the skill. diff --git a/content/docs/openprose/index.mdx b/content/docs/openprose/index.mdx new file mode 100644 index 0000000..977a739 --- /dev/null +++ b/content/docs/openprose/index.mdx @@ -0,0 +1,147 @@ +--- +title: OpenProse +description: The foundation -- a programming language for AI sessions. You declare an ideal world-model in Markdown contracts, and any Prose-Complete harness keeps it true. +--- + +# OpenProse + +OpenProse is a programming language for AI sessions. You write a `*.prose.md` +contract that declares an ideal world-model -- the truths you want kept current +-- and an agent host reads that contract, wires the work, and keeps the truths +true. The program is not a Python graph or a hosted workflow. The program is the +Markdown, and it runs inside the agent session itself. + +This is the foundation. [Reactor](/reactor) is the deterministic harness built +to run it. They are not two systems; they are one system, two layers. OpenProse +is the language you author in. Reactor is the recommended host that serves it +efficiently. The contract is harness-agnostic; the same Markdown runs on any +compliant host. + +## Declare outcomes, not steps + +A prompt says *do this*. A contract says *these things must be true when you are +done, and they must stay true as the world changes*. Intent lives only in the +contract (Tenet 1): the `*.prose.md` carries 100% of the semantic weight. There +is no second authored surface for meaning -- not a prompt, not a config, not a +tuned judge. Compiled artifacts and projections are derived views; when one +disagrees with the Markdown, the Markdown is right. + +So you do not script a sequence of model calls. You declare a standing goal and +the shape of the truth that satisfies it, and you let the host figure out the +rest -- which is exactly what makes the work reusable, inspectable, and portable. + +## A type system for agent workflows + +The cleanest way to hold OpenProse in your head is as a type system for agent +work: + +> A bare prompt is `any` -- it runs, but nothing is checked. A contract is a +> typed function: inputs and outputs are declared, callers can reason about +> composition, and violations fail loudly. + +You would not write a 2,000-line TypeScript system in `any`. Multi-step agent +workflows are the same. A contract gives the model a typed shape to satisfy +instead of a pile of instructions, and gives you something better than vibes to +inspect afterward. + +## The render atom + +Underneath every kind of contract is one operation that both the compile and run +phases agree on -- the render atom: + +```text +(contract, evidence, prior world-model) -> (new world-model, receipt) +``` + +A bounded agent session reads the contract and the new evidence, queries the +truth it maintained last time, computes the next world-model, and signs a +receipt. The receipt is the durable commit; it is the audit record, the +composition edge, and the exit ticket all at once (Tenet 5). Continuity lives in +that trail, not in a session that runs forever (Tenet 3) -- a one-shot run is +just the degenerate case of a standing one. + +## The authored surface is small and complete + +The whole language is five kinds and a fixed set of `###` sections. + +- **`responsibility`** -- the headline kind. A standing goal mounted as a node + whose maintained truth is kept current over time. Node-ness comes from + *mounting*, never from statefulness. +- **`function`** -- a called, ephemeral helper that binds `### Parameters` and + returns `### Returns`. Never a node. +- **`gateway`** -- an external source (webhook, cron, manual ingress) that + maintains the latest incoming truth. +- **`pattern`** -- reusable coordination, expanded into nodes at compile time. +- **`test`** -- assertions over a subject's world-model or receipts. + +The load-bearing section is `### Maintains`: the world-model schema, doing four +jobs at once. It *types* the maintained truth, carries the *canonicalization +spec* (which fields are material and how they normalize), declares optional +*facets* (named sub-truths a consumer can subscribe to one at a time), and +states *postconditions* (the obligations a render must satisfy before it may +commit). A render that cannot satisfy its postconditions commits nothing -- the +prior truth stands and a `failed` receipt records why. + + +New capability in OpenProse is new *semantics* in the skill docs, never new +syntax or a YAML overlay. The authored surface stays small, stable, and +complete on purpose. + + +## Hands off to Reactor + +OpenProse defines the contract and what it means. It deliberately does **not** +define the runtime that serves it -- the two-phase compile/run split, +memoization, the continuity clock, receipts, and composition are the harness's +concern. [Reactor](/reactor) (`@openprose/reactor` + `reactor-cli` + +`reactor-devtools`) is that harness: it compiles your contracts into a +content-addressed DAG and runs them so that expensive model work happens only +when something material actually moved. Cost scales with surprise, not the clock. + +You can author OpenProse against any Prose-Complete host. Reactor is the +recommended fast path because it makes the maintained-truth model real, +deterministic, and observable. + + + + + + + + + + +## Source of truth + +The canonical execution behavior lives in the open-source +[`open-prose` skill](https://github.com/openprose/prose) (currently `0.15.0`). +These docs are orientation. If the docs and the skill ever disagree, trust the +skill. + +--- + +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/openprose/meta.json b/content/docs/openprose/meta.json new file mode 100644 index 0000000..122813f --- /dev/null +++ b/content/docs/openprose/meta.json @@ -0,0 +1,4 @@ +{ + "title": "OpenProse", + "pages": ["index", "declare-outcomes", "contracts", "prosescript", "harness-agnostic", "setup"] +} diff --git a/content/docs/openprose/prosescript.mdx b/content/docs/openprose/prosescript.mdx new file mode 100644 index 0000000..3d77df4 --- /dev/null +++ b/content/docs/openprose/prosescript.mdx @@ -0,0 +1,376 @@ +--- +title: ProseScript +description: The optional imperative pinning layer inside ### Execution. Declarative by default, explicit when needed. Contract Markdown owns the interface; ProseScript only choreographs one render. +--- + +# ProseScript + +Contract Markdown is declarative. You write down the world you want to keep +true, and the harness decides the graph. ProseScript is the **pinning layer**: +the optional imperative script you reach for when the *order* of work inside a +single render matters and you do not want a host to choose it for you. + +The mantra is **declarative by default, explicit when needed**. ProseScript is +always secondary to the contract. It never declares what a node depends on or +what it produces -- that is the contract's job. It only choreographs the steps +*inside* one render. + + +ProseScript is the **intra-node** layer. `call` invokes a `function`, and +`session` / `agent` / `resume` spawn one-off subagents -- all ephemeral and +internal to producing this node's world-model. Cross-node connections are never +made in ProseScript; they are subscriptions that Forme wires from a +responsibility's `### Requires` to a producer's `### Maintains`. + + +## When to pin, and when not to + +Use Contract Markdown when the *end state* matters and the host can pick the +graph. Use ProseScript when **order, loops, branching, retries, parallelism, or +exact call choreography** matter and you want them written down, not inferred. + +The Prose VM follows pinned choreography exactly. It does not infer new +parallelism, reorder calls, or add missing calls. That precision is the whole +point: a pinned `### Execution` block is a promise that the steps run as +written. + +| You want | Reach for | +| --- | --- | +| A standing truth kept current; the host may choose the graph | Contract Markdown (`### Maintains`) | +| An exact sequence of calls inside one render | ProseScript in `### Execution` | +| Parallel fan-out with a join policy | `parallel` in ProseScript | +| Retry / backoff on a flaky call | `retry` / `backoff` call modifiers | +| A one-off subagent internal to this render | `session` / `agent` / `resume` | + +## Where ProseScript lives + +ProseScript appears on exactly two surfaces. Both are owned by Contract +Markdown, so an embedded script must **not** redeclare caller inputs or public +outputs. + +| Surface | Scope | Primary call style | Interface source | +| --- | --- | --- | --- | +| `### Execution` in a `*.prose.md` | Pinned intra-node choreography | `call function-name` | `### Requires` / `### Maintains` (responsibility) or `### Parameters` / `### Returns` (function) | +| Pattern `### Delegation` | Slot interaction rules inside a pattern instance | `call slot-name` | `### Slots`, `### Config`, and the pattern instance bindings | + +A responsibility declares its interface with `### Requires` / `### Maintains`; a +function declares it with `### Parameters` / `### Returns`. Inside the script, +those declared names are simply in scope. The reactive interface always belongs +to the contract sections -- embedded ProseScript should never restate them. + +## A first pinned block + +Inside Contract Markdown, a fenced `prose` block under `### Execution` pins the +choreography for one render: + +````markdown +### Execution + +```markdown +let findings = call researcher + topic: topic + +let report = call writer + findings: findings + +return report +``` +```` + +`topic` comes from the contract's `### Requires` (responsibility) or +`### Parameters` (function). `return report` hands the result to the enclosing +`### Maintains` or `### Returns`. The VM runs `researcher`, then `writer`, in +that order, every time -- nothing is reordered or inferred. + +## The constructs + +ProseScript is a small, complete language. The constructs below cover everything +the grammar exposes. + +### Bindings and values + +`let` creates a mutable binding; `const` creates an immutable one. Object +results can be destructured. + +```markdown +let draft = call writer + brief: brief + +const threshold = "high confidence" + +let { findings, sources } = call researcher + topic: topic +``` + +Values are strings (`"short"`, `"""multi\nline"""`), numbers, booleans, `null`, +arrays, objects, and references. Object shorthand keeps variable names: +`{ findings, sources }` means `{ findings: findings, sources: sources }`. String +interpolation uses `{name}` or `{object.property}`. Scope is lexical, and loop +variables, block parameters, and catch variables are immutable within their +body. + +### `call` + +`call` invokes a Contract Markdown `function`, a pattern instance or slot, or a +`use`d dependency function. Input bindings are indented `name: expression` +lines; the result is the target's declared outputs. + +```markdown +let response = call external-api + request: request + retry: 3 + backoff: exponential +``` + +`retry` and `backoff` are call modifiers (`backoff` may be `none`, `linear`, or +`exponential`). Each `call` target must resolve to a real function or pattern +slot, or Forme rejects the block. + +### `parallel` + +Branch-local statements run concurrently and join according to modifiers. + +```markdown +parallel: + let security = call security-reviewer + code: code + let performance = call performance-reviewer + code: code + +let report = call synthesizer + context: { security, performance } +``` + +Modifiers control the join: `"all"` (default), `"first"`, `"any"` with +`count: N`, and `on-fail:` policies (`"fail-fast"` default, `"continue"`, +`"ignore"`). Defaults are `("all", on-fail: "fail-fast")`. + +### `for`, `repeat`, and `loop` + +Fixed repetition, collection iteration, and open or model-sized loops: + +```markdown +repeat 3 as attempt: + call generator + attempt: attempt + +parallel for item in items: + call processor + item: item + +loop until all tests pass (max: 5): + let results = call tester + if results include failures: + call fixer + test-results: results +``` + +Open `loop` blocks should carry a `(max: N)` bound -- it is a warning in source +and an error in generated canonical docs. `parallel for` preserves input order +unless a parallel modifier requests race semantics. + +### `if` / `elif` / `else` and `choice` + +`if` runs the first true branch in order. `choice` lets the VM select exactly +one labeled peer branch by criteria. + +```markdown +if review has critical concerns: + call reviser + review: review +elif review has minor concerns: + call polisher + review: review +else: + call approver + +choice best recovery path: + option "retry": + call retryer + option "abort": + throw "No safe recovery path" +``` + +Conditions are **discretion text** -- natural-language conditions the VM +evaluates. They may be bare, `**wrapped**`, or `***multi-line***`. Prefer +concrete, observable conditions over vague ones. + +### `try` / `catch` / `finally` and `throw` + +```markdown +try: + let response = call external-api + request: request + retry: 3 + backoff: exponential +catch as err: + call fallback + error: err +finally: + call cleanup +``` + +`try` runs its body; `catch` handles an unhandled failure; `finally` always +runs. `throw` (bare) re-raises the active error inside `catch`; +`throw expression` raises a new one. A `try` needs at least a `catch` or a +`finally`. + +### `session`, `agent`, and `resume` + +Pinned blocks can spawn direct subagents when the work is intentionally a +one-off internal to this render -- not a reusable `function`. + +```markdown +agent researcher: + model: sonnet + persist: project + prompt: "Research thoroughly and keep a compact project memory." + skills: ["web-search"] + shape: + self: ["research", "source evaluation"] + prohibited: ["writing source files", "running shell commands"] + +let findings = session: researcher + prompt: "Research {topic}" + context: topic + +let review = resume: researcher + prompt: "Review the new draft" + context: findings +``` + +`session "prompt"` spawns a one-off subagent; `session: agent` uses an `agent` +definition; `resume: agent` continues a persistent agent with memory. `shape` is +the ProseScript equivalent of Contract Markdown `### Shape` -- it expresses +behavioral boundaries, not raw secret or permission values. Host sandbox +permissions remain a host adapter concern. + + +Inside `### Execution`, prefer `call function` over a direct `session` when an +equivalent `function` exists -- the VM warns when you skip a real function for an +ad-hoc subagent. Reach for `session` / `agent` / `resume` only when the work is +genuinely an intentional one-off. + + +### Blocks, `do`, and pipelines + +Anonymous `do:` groups sequential statements; named `block` definitions are +reusable local choreography invoked with `do name(...)`. Pipelines transform +collections left to right with `map`, `pmap`, `filter`, and `reduce`. + +```markdown +block review-and-fix(artifact, max_rounds): + let review = call critic + artifact: artifact + if review has critical issues: + return call fixer + artifact: artifact + review: review + return artifact + +let result = do review-and-fix(draft, 3) + +let summaries = articles + | filter: + call relevance-checker + article: item + | map: + call summarizer + article: item +``` + +Block definitions are collected before execution, so a block may be invoked +before its definition. Each invocation gets its own scope, and parameters are +immutable within the call. + +## `use`: declaring dependencies + +`use` declares an external dependency for pinned choreography. It is processed +before agents, blocks, and statements, and follows the same disk-only resolution +rules as `prose run`. + +```markdown +use "github.com/openprose/prose/packages/std/evals/inspector" as inspector + +let inspection = call inspector + run-path: run_path +``` + +`use` is valid in a standalone ProseScript block, but **not** inside embedded +Contract Markdown ProseScript -- declare the dependency in the contract, not in +the render body. + +## The boundary: ProseScript does not own the interface + +This is the single most important rule, restated. ProseScript does not own +public interfaces in current OpenProse source: + +- `### Requires` (responsibility) or `### Parameters` (function) declares the + variables available to `### Execution`. +- `### Maintains` (responsibility) declares the world-model truth the render + must keep current; `### Returns` (function) declares the value it must + produce. +- `return` chooses the execution block's result for the enclosing contract. + +Legacy standalone `.prose` files used `input` and `output` declarations. Treat +those as upgrade inputs, not current syntax -- writing `input` or `output` +inside current ProseScript is an error. Run `prose upgrade --dry-run` to migrate. + +## How a pinned block runs + +Embedded `### Execution` runs in three phases: + +1. **Parse** the lexical structure, blocks, declarations, and statements. +2. **Validate** names, scopes, call targets, inputs and outputs, loop bounds, and + surface-specific restrictions. Forme checks that every `call` target resolves + to a real `function` (local, `use`d, or a `std/` library function) or a + delegated helper the render declares, and that the returned value satisfies + the enclosing `### Maintains` / `### Returns`. +3. **Execute** in source order, using the Prose VM for calls, sessions, state, + bindings, retries, and final result publication. + +Validation errors block execution; warnings do not, but generated canonical docs +should resolve warnings where possible. + +## Pattern delegation + +Pattern `### Delegation` describes how bound slots interact. When written as +fenced `prose`, it is validated as ProseScript with pattern-specific scope: slot +names, `config` keys, and parent-provided inputs are in scope, and `call` may +target slots or nested pattern instances. + +```markdown +loop until critic accepts output (max: config.max_rounds): + let output = call worker + task: task + feedback: feedback + let verdict = call critic + output: output + quality-bar: config.quality-bar + if verdict is not accepted: + let feedback = verdict.feedback + +return output +``` + +Pattern files are not directly runnable. A responsibility instantiates the +pattern and binds its slots before the delegation runs. + +## Where to go next + + + + + + +For the complete grammar, validation tables, and execution model, see the +ProseScript reference in the OpenProse spec. When the docs and the skill +disagree, trust the skill. diff --git a/content/docs/openprose/setup.mdx b/content/docs/openprose/setup.mdx new file mode 100644 index 0000000..aeb2e74 --- /dev/null +++ b/content/docs/openprose/setup.mdx @@ -0,0 +1,153 @@ +--- +title: Setup +description: One ordered path for agents onboarding on behalf of a user -- keyless proof first, then init, doctor, compile, and serve. Where contracts live and what each step needs. +--- + +# Setup + +This is the one ordered path. If you are an agent onboarding on behalf of a user, follow it top to bottom: prove the idea with no key and no spend, scaffold and inspect entirely offline, then go live only when the user is ready to reach a model. + +The binary is `reactor`. The keyless replay viewer is `reactor-devtools`. Both ship from npm and run locally. + + +**Trust before spend.** Nothing below reaches a model provider or costs a token until the final "go live" step. Steps 1 through 3 are fully offline -- you can earn the user's trust by showing the whole shape first, with no key in the environment. + + +## 1. Prove the thesis -- keyless, no model call + +Run a saved sample run and read the per-node dispositions, the receipt counts by surprise cause, the token cost rollup, and the chain verify. No install, no key, no spend: + +```bash +npx -p @openprose/reactor-devtools reactor-devtools --example masked-relay --describe +``` + +```text +reactor-devtools --describe + (synthetic sample ledger -- token counts are illustrative, not a bill) +dispositions rendered=46 · skipped=31 · failed=0 +surprise-cause external=8 · input=69 (a.k.a. wake-cause) ← receipt COUNTS, 77 total + +COST ROLLUP (tokens) + total fresh=27180 tokens · reused=12840 tokens · reuse=32% + external receipts= 8 fresh= 1080 tokens reused=840 tokens + input receipts= 69 fresh= 26100 tokens reused=12000 tokens +CHAIN-VERIFY ok +``` + +The first line the binary prints is the honest frame: `masked-relay` is a **synthetic sample ledger**, so the token magnitudes are illustrative, not a measured bill. The genuinely checkable part is the **structure** -- the `surprise-cause` line counts receipts by what woke them, and the cost rollup splits `fresh` (what each surprise cost) from `reused` (what memoization saved), all chained under `CHAIN-VERIFY ok`. That structural shape is "cost scales with surprise" -- checkable, with no key and no spend. When you run your own contract (step 4), the rollup becomes your real spend. + +Prefer the browser? Drop `--describe` and `reactor-devtools --example masked-relay` boots an animated DAG viewer at a localhost URL: nodes flash on render, dim-pulse on memo-skip, with a live cost meter. + +## 2. Install -- local-first + +The keyless step above needs no install. For the full CLI, prefer a project-local install (no root, no global collisions): + +```bash +npm install @openprose/reactor @openprose/reactor-cli @openprose/reactor-devtools +# then call the binaries with `npx reactor …` / `npx reactor-devtools …` +``` + +`@openprose/reactor` is the SDK engine the CLI drives; it is a real dependency of `@openprose/reactor-cli`, so the install above just makes the SDK and the replay viewer explicit. None of these pull a model provider or a key. + + +**Local install means `npx`.** The bare `reactor …` / `reactor-devtools …` commands below assume the binaries are on your `PATH` (a global install). After a project-local install, prepend `npx` -- for example `npx reactor init my-project`. The keyless `npx -p @openprose/reactor-devtools …` line already does this. + + +A global install (`npm i -g …`) is an alternative, but on Linux/WSL it can fail with `EACCES` and may collide with other tools' binaries. Prefer the local install above. + +## 3. Scaffold and inspect -- keyless + +Everything here runs offline. + +```bash +reactor init my-project && cd my-project +reactor doctor # what's present + the exact fix for anything missing +reactor compile --check; echo "exit=$?" # offline; exits 1 if the contract set is STALE (CI-wireable) +``` + +`reactor init` writes a minimal, compilable project: a gateway, a responsibility that subscribes to it, a `reactor.yml`, a `.gitignore`, and a short README. It refuses to overwrite existing files by default; pass `--force` to clobber a directory that already holds scaffold files. + +`reactor doctor` runs fully offline and reports the node version, SDK resolvability, live-key presence (it never prints the key), live-dep presence, the sandbox mode, whether the state directory is writable, and the compiled-IR freshness. + +`reactor compile --check` is the offline freshness gate. It exits non-zero when the IR cache is stale -- which it is right after `init`, before the first real compile -- so it wires cleanly into CI without ever touching a model. + +### Where contracts live + +Author your OpenProse contracts as `*.prose.md` files under the scaffold's `src/` directory: + +- one `kind: responsibility` per standing goal, carrying its `### Maintains` (the world-model schema it keeps current), `### Requires` (the upstream facets it subscribes to), and `### Continuity` (its wake source); +- optional `kind: gateway` contracts for ingress; +- optional `kind: function` contracts for stateless helpers. + +`src/` is authored intent; the compiler writes its frozen output into `dist/`, and run state (world-models and the signed receipt ledger) lands under the state directory. You write `src/`; Reactor owns the rest. + + +**Structure is subscription.** You never wire the graph by hand. Forme -- the compile-time wiring layer -- matches each `Requires.` to the `Maintains.` that produces it and draws the subscription edge. The DAG assembles itself from the contracts. See [contracts](/openprose/contracts). + + +## 4. Go live -- needs a model key + +These steps reach the model surface. A keyless reader can stop at step 3. To go live, set `OPENROUTER_API_KEY` and add the two optional live peers: + +```bash +npm i @openai/agents zod # the two optional live peers +reactor compile # Forme wires the DAG; freezes per-node canonicalizers +reactor serve --http 8080 # drive the scaffold's static gateway to a real receipt +reactor-devtools .reactor --describe # replay YOUR live run's ledger +``` + +`reactor compile` runs the intelligent compile sessions once and freezes them into the content-addressed IR cache. An unchanged contract set recompiles at zero session cost because the cache key is content-addressed. + + +**Use `serve`, not `run`, for the scaffold.** The scaffold's `inbox` gateway uses a `static` connector: its seeded items are ingested by the `serve` continuity loop, which polls the gateway and stages the seeded arrivals -- moving the inbox fingerprint and waking `digest`. `reactor run` is the one-shot drain for graphs whose connectors emit on their own; on a static scaffold it would boot, find nothing newly arrived, and exit without rendering. + + +`serve` boots the durable host, runs the continuity loop, and binds the built-in HTTP surface; it stays up until you stop it with Ctrl-C. In another shell you can read the trail and the standing cost: + +```bash +reactor status # standing compile cost beside the run cost +reactor receipts list # the gateway + digest receipts +reactor receipts cost # cost rolled up by surprise_cause +``` + +## Honest status + +In the spirit of the receipts, a few things are openly pending. They do not block the path above, but you should onboard the user knowing them: + +- **Benchmarks are pending on purpose.** We publish the harness before the numbers and will not imply a measured speedup we have not run. The proof you can check today is the keyless replay in step 1. +- **Signer caveat.** In v1, *signed* means tamper-evident at the meaning layer and chain-consistent -- not yet a cryptographic byte hash. `reactor receipts verify` proves the receipt chain is consistent but does not yet bind the world-model artifacts. +- **No timestamp, no actor yet.** A v1 receipt records *what* changed and *why*, but not *when* it was committed or *who* committed it -- so the ledger is a verifiable record of decisions, not yet a substitute for an external audit log. +- **The fixpoint (topology-as-responsibility) is specified and deferred.** Facet inference and ledger compaction are named roadmap. + +This honesty is the point. The harness is young and should be used with caution. There is nothing new here -- we are applying classical engineering paradigms to our brave new world, and finding that the wisdom of the ancients still applies. + +## Where to go next + + + + + + + + +To author your first real contract, copy a shape from [`skills/open-prose/examples/`](https://github.com/openprose/prose/tree/main/skills/open-prose/examples) -- each ships a committed, chain-verifiable `replay/` you can read keyless. When the docs and the [SKILL](https://github.com/openprose/prose/tree/main/skills/open-prose) disagree, trust the skill: it is the source of truth for the language. + +--- + +*The conversation always ends. The responsibility shouldn't have to.* diff --git a/content/docs/reactor-devtools/describe.mdx b/content/docs/reactor-devtools/describe.mdx new file mode 100644 index 0000000..ead945b --- /dev/null +++ b/content/docs/reactor-devtools/describe.mdx @@ -0,0 +1,92 @@ +--- +title: --describe +description: The headless, browser-free text summary of a run -- the surface an agent or a CI gate reads to verify a reactor without rendering pixels. +--- + +# `--describe` + +The viewer is for a human at a browser. `--describe` is for everyone else -- a terminal, an agent, a CI gate. It prints a full text summary of the same replayed run and exits. No server, no browser, no key. + +```sh +reactor-devtools --describe +``` + +Because the primary consumer of the Reactor is often an agent, this is a first-class surface, not an afterthought: it is the text an agent reads to sanity-check a run -- or to assert on it -- without watching a video. + +## The output + +This is the canonical `masked-relay` sample (the documented default `--example`). Its first line is the honest frame: a **synthetic sample ledger**, so the token magnitudes are illustrative, not a measured bill -- the checkable part is the structural shape (dispositions, surprise counts, and chain-verify). + +```text +reactor-devtools --describe + (synthetic sample ledger -- token counts are illustrative, not a bill) + state-dir ./fixtures/masked-relay + topology yes · 12 nodes · 23 edges · acyclic=true + receipts 77 frames + dispositions rendered=46 · skipped=31 · failed=0 + surprise-cause external=8 · input=69 (a.k.a. wake-cause) + +COST ROLLUP (tokens) + total fresh=27180 tokens · reused=12840 tokens · reuse=32% + input receipts=69 fresh=26100 tokens reused=12000 tokens + external receipts=8 fresh=1080 tokens reused=840 tokens + peak fresh 1620 tokens at frame 58 (viewport-masker) + +PER-NODE + signal-inbox r=3 s=1 f=0 fresh=1080 tokens chain✓ + viewport-masker r=3 s=6 f=0 fresh=3240 tokens chain✓ + insight-synthesizer r=6 s=15 f=0 fresh=6840 tokens chain✓ + diversity-auditor r=6 s=3 f=0 fresh=1080 tokens chain✓ + ... + +CHAIN-VERIFY ok -- meaning-layer chain-consistency + (each receipt's content_hash matches its canonical payload and links its + prev -- NOT a cryptographic signature. v1 has a null signer, so this is + tamper-EVIDENT against accidental / independent edits, NOT against a forge.) + +FRAMES (frame node status moved[output facets that changed] fresh tokens woke[…]) + 0 signal-inbox rendered moved[@atomic,inbox] fresh 0 tokens woke[signal-inbox] + 6 viewport-masker rendered moved[@atomic,view_e1,view_e2] fresh 540 tokens woke[expander-1,expander-2,...] + 8 viewport-masker skipped moved[--] fresh 0 tokens woke[--] + ... +``` + +## What each block tells you + +| Block | What it answers | +| --- | --- | +| **header** | How big is the run? Is the topology present and acyclic? How many receipts? | +| **dispositions** | How much actually re-rendered vs memo-skipped vs failed. A high `skipped` count is a healthy, quiet system. | +| **wake-cause** | Why nodes woke -- `external` (a signal arrived), `input` (an upstream moved), `self` (the audit floor). | +| **COST ROLLUP** | Fresh vs reused tokens, the **reuse %**, and the same split by cause. `peak fresh` names the single most expensive frame -- the biggest surprise. | +| **PER-NODE** | Per node: `r`/`s`/`f` counts, total fresh tokens, and a `chain✓` badge (its receipt chain is `prev`-linked and consistent). | +| **CHAIN-VERIFY** | The global tamper check -- every node's chain verified against the content-addressed `prev` links. | +| **FRAMES** | One line per receipt: `frame · node · status · moved[...] · fresh · woke[...]`. The whole timeline, greppable. | + +## Reading memoization off the trace + +The `FRAMES` block is where "cost scales with surprise" stops being a slogan. In the example above: + +```text + 6 viewport-masker rendered moved[@atomic,view_e1,view_e2] fresh 540 tokens woke[expander-1,expander-2,...] + 8 viewport-masker skipped moved[--] fresh 0 tokens woke[--] + 10 viewport-masker skipped moved[--] fresh 0 tokens woke[--] +``` + +Frame 6 renders and wakes its subscribers; frames 8 and 10 are byte-identical re-wakes that **memo-skip at zero fresh cost** and wake nothing. The skip is not a missing log line -- it is a recorded receipt proving the reactor was asked to re-check and correctly decided nothing moved. + +## Use it in CI + +`--describe` is deterministic, so a state-dir checked into a repo is a fixture you can assert against without a browser or a key. Grep the output to gate behavior -- for example, that a quiet stretch stays free, that exactly one node failed, or that a selective wake stayed selective: + +```sh +reactor-devtools fixtures/contract-redline --describe > out.txt + +# a single-section edit must not re-render sibling sections: +grep -E "Summarize §[0-9]+ +r=1 " out.txt # untouched summarizers rendered once (cold boot) only + +# the chain must verify: +grep -q "CHAIN-VERIFY ok" out.txt || exit 1 +``` + +This is how the demo-suite fixtures lock their invariants (see [Recording](/reactor-devtools/recording)) -- the same trace a reviewer reads is the trace CI asserts on. diff --git a/content/docs/reactor-devtools/index.mdx b/content/docs/reactor-devtools/index.mdx new file mode 100644 index 0000000..c714343 --- /dev/null +++ b/content/docs/reactor-devtools/index.mdx @@ -0,0 +1,104 @@ +--- +title: Reactor DevTools +description: A replay-first visualizer for the Reactor -- it reads the append-only receipt ledger and animates the DAG the way React DevTools animates a component tree. +--- + +# Reactor DevTools + +`@openprose/reactor-devtools` is the visualizer for the [`@openprose/reactor`](/reactor) harness. It reads the SDK's **append-only, content-addressed receipt ledger** and animates the DAG the way React DevTools' "highlight updates" animates a component tree: nodes **flash** on render, **dim-pulse** on memo-skip, go **red** on fail, per-facet edges light on propagation, and a **fresh-vs-reused token meter** tracks the thesis -- _cost scales with surprise, not the clock._ + +The standalone command is `reactor-devtools`. It ships deliberately decoupled from the CLI (no `@openprose/reactor-cli` dependency); folding it in behind a `reactor dev` verb is the intended end-state, with the standalone bin as the interim surface. + + +The visualization **is** the audit trail, animated. It reads the same receipts you would audit -- there is no separate telemetry channel, no instrumentation to add, and nothing the reactor does to feed it. A run that already happened is a run you can already replay. + + +## Replay-first + +DevTools is replay-first: you point it at a saved **state directory** and it re-derives the whole run from the durable trail. Replay needs **zero** running reactor and **zero** model key -- the receipts and world-models on disk are the complete record. + +```sh +reactor-devtools +# reactor-devtools: replaying 92 receipt(s) across 14 node(s) +# open http://127.0.0.1:4555/ +``` + +A state directory is what [`reactor run`](/cli/compile-run-serve) and `reactor serve` already persist: the receipt ledger, the world-models, and the compiled topology. Open it and the run plays back, beat for beat. + +## React DevTools for the Reactor + +The Reactor borrows its shape from React, and so does its DevTools. If you have used React DevTools' "highlight updates," you understand this in one screen: + +| React DevTools | Reactor DevTools | +| --- | --- | +| Component tree | The topology DAG (responsibilities + subscriptions) | +| A component re-renders → highlight flash | A node `rendered` + a moved fingerprint → node flash | +| A component bailed out of re-render | A node `skipped` (memo hit) → dim grey pulse | +| Render threw | A node `failed` → red flare; prior truth stands | +| Props that changed | The moved facets whose edge lanes light | +| (no equivalent) | The fresh-vs-reused cost meter -- surprise, priced | + +The shot React DevTools _can't_ take is the one that matters most here: a node that **correctly did nothing**. A quiet Reactor is a screen full of dim grey pulses and a flat cost line, and that is the point. + +## Two ways to read a run + +DevTools meets two different readers -- a human at a browser and an agent at a terminal: + +- **The viewer** -- `reactor-devtools ` boots a local server and a no-build SPA: the layered DAG, a live cost meter, the ordered receipt timeline, and a scrubber that steps the cascade. See [The viewer](/reactor-devtools/the-viewer). +- **`--describe`** -- a headless, browser-free text dump of the same run: per-node dispositions, the cost rollup, chain-verification, and a one-line-per-frame trace. The surface an agent (or a CI gate) reads to check a run without rendering pixels. See [`--describe`](/reactor-devtools/describe). + +## Where it sits in the stack + +DevTools is a pure, near-zero-dependency reader. Its only runtime dependency is `@openprose/reactor` itself; every read goes through the SDK's [`createReplaySession`](/reactor-devtools/state-dirs-and-replay) shaping helper -- imported from the package's curated `.` front door -- so the viewer never re-implements the reconciler's moved-facet diff or its propagation. The SDK stays headless and zero-dep; every opinionated UI choice is quarantined in this package. + +```text +@openprose/reactor the harness -- compiles + runs, writes receipts + └─ "@openprose/reactor" : createReplaySession shapes the ledger into a replay (ordering, moved facets, cost) +@openprose/reactor-cli the CLI -- `reactor run` / `serve` persist the state-dir +@openprose/reactor-devtools this package -- reads the state-dir, animates it +``` + +`createReplaySession` is re-exported from the curated `.` front door, so the import is just `import { createReplaySession } from "@openprose/reactor"` -- no deep subpath. See the [SDK front door](/sdk/front-door) for the full reference. + +## Install + +```sh +npm install -g @openprose/reactor-devtools +``` + +The package ships the `reactor-devtools` bin and an importable library. Its one runtime dependency is `@openprose/reactor`; the server is built on Node's `node:http` and the front-end is a hand-rolled, no-build SVG SPA. No web framework, no graph library, no bundler. + +## Where to go next + + + + + + + + + diff --git a/content/docs/reactor-devtools/meta.json b/content/docs/reactor-devtools/meta.json new file mode 100644 index 0000000..7a246af --- /dev/null +++ b/content/docs/reactor-devtools/meta.json @@ -0,0 +1,12 @@ +{ + "title": "Reactor DevTools", + "pages": [ + "index", + "quickstart", + "state-dirs-and-replay", + "the-viewer", + "describe", + "recording", + "reference" + ] +} diff --git a/content/docs/reactor-devtools/quickstart.mdx b/content/docs/reactor-devtools/quickstart.mdx new file mode 100644 index 0000000..fd669f5 --- /dev/null +++ b/content/docs/reactor-devtools/quickstart.mdx @@ -0,0 +1,91 @@ +--- +title: Quickstart +description: Get a state directory, open the viewer in a browser, and read the same run headlessly with --describe. +--- + +# Quickstart + +DevTools replays a saved **state directory**. So the quickstart is two steps: get a state-dir, then look at it -- in a browser, or as text. + +## 1. Get a state directory + +Any [`reactor run`](/cli/compile-run-serve) or `reactor serve` writes one. Point the reactor at a state-dir and run it to quiescence: + +```sh +reactor run --state-dir ./run +``` + +That persists `./run/receipts.json`, `./run/world-models/`, and `./run/compile/topology.json` -- the complete, replayable record of the run. (See [State dirs and replay](/reactor-devtools/state-dirs-and-replay) for the full anatomy.) + +You do **not** need a key or a running reactor to replay -- only the saved directory. + +## 2. Open the viewer + +```sh +reactor-devtools ./run +# reactor-devtools: replaying 92 receipt(s) across 14 node(s) +# open http://127.0.0.1:4555/ +``` + +Open the printed URL. You get three coordinated regions: + +- the **layered DAG** of the topology on the left, +- a live **fresh-vs-reused cost meter** and the **ordered receipt timeline** on the right, +- a **scrubber** along the bottom. + +Press **space** to play. Each step animates one receipt: a node flashes when it rendered and something moved, dim-pulses when it memo-skipped, flares red on a failure; the moved facet's edges light and the cost meter ticks. A quiet stretch is dim and flat; a real change is a bright cascade and a cost spike. + +| Key | Action | +| --- | --- | +| `space` | play / pause | +| `←` / `→` | step one receipt back / forward | +| `Home` / `End` | jump to the first / last receipt | +| click a receipt | jump to it | + +Pick the port with `--port` (default `4555`) and the bind host with `--host` (default `127.0.0.1`): + +```sh +reactor-devtools ./run --port 4591 +``` + +## 3. Read it headlessly with `--describe` + +For a terminal -- or an agent, or a CI gate -- `--describe` prints the same run as text and exits, no browser: + +```sh +reactor-devtools ./run --describe +``` + +```text +reactor-devtools --describe + state-dir ./run + topology yes · 14 nodes · 22 edges · acyclic=true + receipts 92 frames + dispositions rendered=63 · skipped=28 · failed=1 + wake-cause external=36 · input=54 · self=2 + +COST ROLLUP + total fresh=33660 · reused=13160 · reuse=28% + ... +PER-NODE + Session Ledger r=5 s=7 f=0 fresh=2700 chain✓ + ... +CHAIN-VERIFY ok -- every node chain is prev-linked & consistent + +FRAMES (frame node status moved[...] fresh woke[...]) + 8 Session Ledger rendered moved[@atomic,session:claudeA,...] fresh 540 woke[Session Summary ...] + 9 Session Ledger skipped moved[--] fresh 0 woke[--] + ... +``` + +That last block is the whole story in one place: frame 8 renders and wakes its subscribers, frames 9+ are byte-identical re-wakes that memo-skip at zero cost. See [`--describe`](/reactor-devtools/describe) for the full output and how to assert on it. + +## Try it on a demo corpus + +The repository ships seven committed demo state-dirs under `packages/reactor-devtools/fixtures/` -- replayable with no key, no model, no reactor. If you have the repo checked out: + +```sh +reactor-devtools packages/reactor-devtools/fixtures/agent-observatory +``` + +`agent-observatory`, `monorepo-ci`, `news-desk`, `inbox-triage`, `contract-redline`, `research-tree`, and `masked-relay` each demonstrate a different Reactor behavior -- selective wake, dedup, failure isolation, incremental rollup, faceted relay. See [Recording](/reactor-devtools/recording) for what each one shows. diff --git a/content/docs/reactor-devtools/recording.mdx b/content/docs/reactor-devtools/recording.mdx new file mode 100644 index 0000000..b73e4cb --- /dev/null +++ b/content/docs/reactor-devtools/recording.mdx @@ -0,0 +1,73 @@ +--- +title: Recording +description: The headless Playwright recorder behind the launch videos, the beat map that drives it, and the demo corpus of worked scenarios. +--- + +# Recording + +The viewer animates a run in a browser. To turn that into a shareable clip -- a launch video, a bug repro, a teaching GIF -- the package source ships a **headless recorder** that drives the viewer through a scripted set of beats and renders the result to video. + + +The recorder and the demo fixtures are **repository tooling**, not part of the published npm package (`@openprose/reactor-devtools` ships only the viewer, `--describe`, and the library). Recording uses Playwright, a dev dependency. Clone the repo to use it. + + +## The recorder + +`scripts/record-demo.mjs` is end-to-end and fully automatic -- no manual steps: + +1. frees the target port, then **spawns the devtools server** as a managed child against a fixture (waits for `/api/state`, kills it on exit, aborts on a stale server); +2. launches **headless Chromium** (1280x720, 2x device scale, `recordVideo`); +3. drives the **beats** by stepping the cascade with paced holds, so every flash, skip, edge light, and cost spike actually plays on camera; +4. parks a **keyframe PNG** per beat via the `#frame=N` deep-link; +5. finalizes the `.webm`, then transcodes to `.mp4` with `ffmpeg`. + +```sh +# from the repo, with the package built: +FIXTURE=monorepo-ci node packages/reactor-devtools/scripts/record-demo.mjs +``` + +The `FIXTURE` environment variable (or the first argument) selects which committed fixture to record. Unset -- or `observatory` -- records the Agent State Observatory demo. `ffmpeg` must be on `PATH` for the mp4 step; a fresh machine installs the Chromium build once via Playwright. + +## The beat map + +The recorder is data-driven. It reads `/beats.json` -- a director's script over the run -- for which frames to hold on, how long, and what each one says: + +```json +{ + "scenario": "monorepo-ci", + "title": "Your CI re-ran 200 checks. Reactor re-ran 3.", + "beats": [ + { "name": "cold-boot", "park": 21, "from": 0, "to": 21, "holdMs": 2600, "caption": "the graph builds once" }, + { "name": "hero-leaf", "park": 39, "from": 33, "to": 39, "holdMs": 3800, "caption": "a 4-line diff wakes only the ui lane · 5 packages stay dark" } + ] +} +``` + +| Field | Meaning | +| --- | --- | +| `park` | the frame to screenshot for this beat's still | +| `from` / `to` | the inclusive step range the recorder drives, so the moving pulses fire on camera | +| `holdMs` | how long to hold the parked frame | +| `caption` | the self-narrating caption shown for the beat (also surfaced live in the viewer) | + +`beats.json` is optional and purely presentational for **replay** -- it changes nothing about the run, and a state-dir without it still replays in the viewer (it just falls back to computed captions). **Recording** is the exception: the `FIXTURE=` recorder requires authored beats (either a `beats.json` in the state-dir, or, for `agent-observatory`, the recorder's built-in beat map), so a bare state-dir replays but does not record until you author a `beats.json` for it. + +## The demo corpus + +The repository ships seven committed, replayable demo state-dirs under `packages/reactor-devtools/fixtures/` -- each a deterministic, zero-key corpus chosen to make one Reactor behavior unmistakable: + +| Fixture | What it shows | +| --- | --- | +| `agent-observatory` | Selective wake across six agent runtimes -- touch one Claude session, only that path lights, five runtimes stay dark. Plus a self-tick floor, a diamond single-wake, a failure, and a batch cost spike. | +| `monorepo-ci` | Memoization across a build/test/review DAG -- a 4-line diff wakes only its package's lane; a hub change fans out wider; a failing test blocks the merge gate. | +| `news-desk` | Cost scales with surprise -- a long flat-cost stretch of noisy no-op re-wakes, one real story spikes the briefing, a duplicate of it dedups away. | +| `inbox-triage` | Diamond dedup + failure isolation -- five identical newsletters collapse to one render; one malformed email fails red while the digest still ships. | +| `contract-redline` | Incremental re-summarization -- a single-clause edit re-runs only that section plus the rollup chain; a cosmetic edit memo-skips. | +| `research-tree` | Structural recursion -- revise one finding three levels deep and only its ancestor path re-synthesizes; sibling branches stay dark. | +| `masked-relay` | Per-consumer facets + a convergent diamond -- mask lanes light independently per consumer, and scouts/expanders converge into the masker and critics without double-firing. The original standalone fixture; it ships no authored `beats.json`, so it replays with computed captions and is the one fixture you cannot drive through the `FIXTURE=` recorder. | + +Open any of them in the viewer (`reactor-devtools packages/reactor-devtools/fixtures/`) or inspect them with `--describe`. Five carry an authored `beats.json` and `agent-observatory` carries the recorder's built-in beat map, so those six record with `FIXTURE=`; `masked-relay` (no beats) replays but does not record. Each fixture is paired with an invariant test that locks the behavior its video claims, so the demo and the test assert the same thing. + +## Recording your own run + +The recorder works against any state-dir with a `beats.json`. To film one of your own reactors: run it to a state-dir, author a `beats.json` against the frame indices you see in [`--describe`](/reactor-devtools/describe), drop it in the state-dir, and point the recorder at it. diff --git a/content/docs/reactor-devtools/reference.mdx b/content/docs/reactor-devtools/reference.mdx new file mode 100644 index 0000000..e1dd2ff --- /dev/null +++ b/content/docs/reactor-devtools/reference.mdx @@ -0,0 +1,107 @@ +--- +title: Reference +description: The reactor-devtools bin, the read-only HTTP API, the frame shape the SPA consumes, and the importable library surface. +--- + +# Reference + +## The `reactor-devtools` bin + +```text +reactor-devtools [--port ] [--host ] [--describe] [--json] +reactor-devtools --example [--describe] [--json] # replay a bundled fixture +reactor-devtools --example --copy-to [--force] # seed a sample ledger +``` + +| Argument / option | Default | Effect | +| --- | --- | --- | +| `` | _(required unless `--example`)_ | A saved Reactor state directory (receipts + `compile/topology.json`). | +| `--example ` | -- | Replay a fixture **shipped in the package** by name -- no path needed, works after a global install from any cwd. Six fixtures ship: `masked-relay`, `surprise-cost`, `agent-observatory`, `inbox-triage`, `monorepo-ci`, `research-tree`. The keyless proof from the README is `--example masked-relay`; the "cost scales with surprise" thesis is `--example surprise-cost`. | +| `--copy-to ` | -- | Copy the bundled `--example` fixture into `` so you have a real state-dir on disk to inspect or replay. Refuses a non-empty / existing dir unless `--force`. | +| `--force` | -- | Overwrite a non-empty / existing state-dir on `--copy-to`. | +| `-p`, `--port ` | `4555` | Port to listen on. Must be an integer. | +| `--host ` | `127.0.0.1` | Host to bind. | +| `--describe` | -- | Print the headless run summary (per-node + per-frame dispositions, moved-facet diff, cost rollup, chain-verify) and exit. No server, no browser. | +| `--json` | -- | With `--describe`, emit that summary as machine-readable JSON (the CI/agent surface). Only meaningful with `--describe`; refused otherwise. | +| `-V`, `--version` | -- | Print the package version and exit. | +| `-h`, `--help` | -- | Print usage. | + +Exit codes: `0` on `--help`, `--version`, a successful `--describe`, or a successful `--copy-to`; `1` when `` is missing (and no `--example`), when `--port` is not an integer, when `--json` is passed without `--describe`, when `--copy-to` hits a non-empty dir without `--force`, or on any startup error. The server runs until `SIGINT` / `SIGTERM`, then closes and exits `0`. + +If `compile/topology.json` is absent, the viewer falls back to a node-only set derived from the receipts' distinct `node` values (boxes, no edges). + +## HTTP API + +The server serves the SPA plus a tiny read-only API: + +| Route | Returns | +| --- | --- | +| `GET /api/state` | The full `ReplaySnapshot` (topology + frames + cost rollup). `GET /api/snapshot` is a kept alias. **This is the only endpoint the SPA fetches** -- the whole view is a pure function of this snapshot. | +| `GET /api/node/:id?version=` | A node's world-model at a version, via `readVersion` (`version` is a frame's `atomicVersion`). `400` if missing, `404` if no such node/version. This is a **server/library endpoint for tools and integrators** -- the shipped SPA does not call it (there is no node-click UI in v1); fetch it yourself, or use the `readNodeWorldModel` / `versionForFrame` library helpers. | +| `GET /events` | An SSE seam reserved for future live-attach -- idle (held open) in replay. | + +## The frame shape + +`GET /api/state` returns a `ReplaySnapshot` carrying `frames: ReceiptFrame[]` in append order (the scrubber index = `frame.index`). Each frame is a pure projection of one receipt: + +```ts +interface ReceiptFrame { + index: number; // scrubber position (append order) + node: string; // which node to flash / dim / red + status: "rendered" | "skipped" | "failed"; + wakeSource: "input" | "self" | "external"; // flash hue + movedFacets: string[]; // facets that moved vs this node's prior receipt + edgesToLight: { producer: string; subscriber: string; facet: string }[]; + // per-facet lanes to light -- only on rendered+moved + // (skipped/failed light none); strict facet match + wokenSubscribers: string[]; // DISTINCT downstreams woken -- diamond single-wake + cost: { fresh: number; reused: number; surpriseCause: "input" | "self" | "external" }; + contentHash: string; // this receipt's address (inspector chain key) + atomicVersion: string; // = fingerprints["@atomic"]; pass to /api/node?version= +} +``` + +`edgesToLight` and `wokenSubscribers` are derived server-side in `buildSnapshot` from the saved topology and the receipt's moved facets, reusing the SDK's own `propagationTargets`. So the **diamond single-wake** (a subscriber reached by >=2 moved facets of one producer fires exactly once) matches the live reconciler. + +## Library + +The package is importable directly, so a benchmark front-end or a docs site can embed the renderer without pulling the CLI. + +```ts +import { openStateDir, buildSnapshot, startDevToolsServer } from "@openprose/reactor-devtools"; + +// 1. Open a saved dir and build the SPA payload (a pure read of the SDK). +const opened = openStateDir("/path/to/state-dir"); +const snapshot = buildSnapshot(opened); + +// 2. Or just serve it. +const server = await startDevToolsServer({ stateDir: "/path/to/state-dir", port: 4555 }); +console.log(server.url); // -> http://127.0.0.1:4555/ +// server.snapshot // the ReplaySnapshot it is serving +await server.close(); // stop the server +``` + +Exported surface: + +| Export | What it is | +| --- | --- | +| `openStateDir(dir, opts?)` | Open a state-dir → `OpenedStateDir` (ledger session + topology + world-models + labels + beats). | +| `buildSnapshot(opened)` | Build the `ReplaySnapshot` the SPA consumes (topology + frames + cost rollup). | +| `startDevToolsServer(opts)` | Boot the `node:http` server → `DevToolsServer` (`url`, `snapshot`, `close()`). | +| `readTopology`, `openWorldModels`, `readNodeWorldModel`, `versionForFrame`, `verifyReceiptChain` | Lower-level read helpers over the state-dir. | +| Types | `OpenStateDirOptions`, `OpenedStateDir`, `ReplaySnapshot`, `ReceiptFrame`, `EdgeLight`, `NodeView`, `EdgeView`, `CostRollupView`, `NodeWorldModelView`, `WorldModelFileView`, `DevToolsServer`, `DevToolsServerOptions`. | + +The ledger-shaping primitive itself -- `createReplaySession` -- lives in the SDK, re-exported from the curated `.` front door (`@openprose/reactor`), not here, so other tools can shape a trail without this package. See [Front door](/sdk/front-door) and [State dirs and replay](/reactor-devtools/state-dirs-and-replay). + +## Stack and dependencies + +| | | +| --- | --- | +| Version | `0.2.0` | +| Bin | `reactor-devtools` → `dist/cli.js` | +| Runtime dependency | `@openprose/reactor` only | +| Server | Node built-in `node:http` (no web framework) | +| Front-end | Vanilla, no-build SVG SPA (no bundler, no graph library) | +| Recording (dev) | Playwright (a dev dependency; repo tooling -- see [Recording](/reactor-devtools/recording)) | + +The package boundary is the point: the SDK stays zero-dep and headless; every opinionated UI choice is quarantined here, and even here it stays near-zero. diff --git a/content/docs/reactor-devtools/state-dirs-and-replay.mdx b/content/docs/reactor-devtools/state-dirs-and-replay.mdx new file mode 100644 index 0000000..6aa8164 --- /dev/null +++ b/content/docs/reactor-devtools/state-dirs-and-replay.mdx @@ -0,0 +1,92 @@ +--- +title: State dirs and replay +description: What a replayable state directory is, how DevTools re-derives a run from it, and the ReplaySession SDK surface that shapes the ledger. +--- + +# State dirs and replay + +DevTools never instruments a run. It replays one -- from the durable artifacts the Reactor already writes. This page is the data contract: what a state directory contains, and the SDK surface that turns it into a replay. + +## A replayable state directory + +A state directory is what [`reactor run`](/cli/compile-run-serve) and `reactor serve` persist. The minimum replayable set is the receipt trail plus the compiled topology: + +```text +/ + receipts.json the append-only, content-addressed receipt ledger (the run) + world-models/ per-node maintained truth, content-addressed by version + /published.json current published pointer ( = the node id, hex-encoded, so any id is a safe dir name) + /versions/sha256_.bin each truth version, filename = its @atomic content-address + compile/ + topology.json the DAG -- nodes, edges, per-facet subscriptions + labels.json (optional) friendly node-id → label map + beats.json (optional) an authored beat map for recording / captions +``` + +- **`receipts.json`** is the run. Each receipt records one node wake: its disposition (`rendered` / `skipped` / `failed`), its `wake.source`, its per-facet `fingerprints`, its `cost.tokens` (`fresh` / `reused`) and `surprise_cause`, and the `prev` link that chains a node's receipts together. +- **`world-models/`** holds the maintained truth. A receipt's `@atomic` fingerprint _is_ the content-address of the truth version it produced, so the viewer can fetch the exact world-model a node held at any frame. +- **`compile/topology.json`** is the graph DevTools draws. If it is absent, the viewer falls back to a node-only set derived from the receipts' distinct `node` values (boxes, no edges). +- **`labels.json`** and **`beats.json`** are optional presentation data. Without them the viewer is fully generic -- raw node ids, computed captions. With them it shows friendly names and an authored beat narration (see [The viewer](/reactor-devtools/the-viewer) and [Recording](/reactor-devtools/recording)). + + +Replay needs **zero** running reactor and **zero** model key. The state directory is the complete record; opening it re-derives the run deterministically. This is the same content-addressed trail you would audit -- see [Reconciler and receipts](/reactor/reconciler-and-receipts). + + +## How DevTools reads it + +Every read goes through one module (`src/data`), the only place the package touches the SDK. It opens the durable trail with the SDK's filesystem storage adapter, re-derives the ledger, and shapes it with `createReplaySession`: + +| Need | SDK surface | +| --- | --- | +| Open the durable trail | `createFileSystemStorageAdapter({ directory })` -- `@openprose/reactor` | +| Re-derive the ledger (= replay) | `new FileSystemReceiptLedger({ storage })` -- `@openprose/reactor/adapters` | +| Ordering + per-node chain index + moved-facet diff + cost rollup | `createReplaySession({ ledger })` -- `@openprose/reactor` | +| Topology graph | `/compile/topology.json` (a `TopologyWorldModel`) | +| Chain / tamper badge | `verifyReceiptChain` -- `@openprose/reactor` | +| Click-through world-model | `createFileSystemWorldModelStore({ directory }).readVersion(node, version)` where `version === receipt.fingerprints["@atomic"]` | + +The viewer re-implements none of this. The moved-facet diff and the diamond single-wake are computed by the SDK's own helpers, so what you see matches what the live reconciler did. Almost everything here lives on the curated `@openprose/reactor` front door (see the [SDK front door](/sdk/front-door)); the one exception is the `FileSystemReceiptLedger` class, which the data module imports from `@openprose/reactor/adapters` (the front door ships the `createFileSystemReceiptLedger` builder for the common case). + +## The ReplaySession surface + +`createReplaySession` is the SDK half of the data contract -- a tiny, pure-data shaping helper that any tool (the viewer, a benchmark front-end, your own script) can use to read a trail without re-deriving the math. It is exported from the curated `@openprose/reactor` front door, does no I/O, and pulls no new dependency. + +```ts +import { createReplaySession } from "@openprose/reactor"; + +// Prefer handing in an already-opened ledger (stays filesystem-agnostic): +const session = createReplaySession({ ledger }); + +// Or pass a receipt array directly (scenario / benchmark runs that hold the trail): +const session = createReplaySession({ receipts }); +``` + +The returned `ReplaySession` exposes the run as shaped, pure data: + +| Field | What it is | +| --- | --- | +| `receipts` | The ordered trail -- **append order is the replay timeline**. | +| `chainByNode` | Each node's `prev`-linked receipts, in append order (the inspector chain). | +| `movedFacetsFor(receipt)` | The facets that moved vs the **same node's previous** receipt (a null prior = cold start = every facet moved). Computed by the exported `movedFacetsBetween` -- not reinvented. | +| `movedFacetsByIndex` | The same diff, precomputed per receipt and index-aligned with `receipts`. | +| `costRollup` | The cumulative fresh / reused / `$` rollup, bucketed by `surprise_cause` (`input` / `self` / `external`) plus a grand total. | +| `verifyNodeChain(node)` | Verifies one node's `prev`-linked chain via `verifyReceiptChain` -- the tamper / consistency badge. | + +### The cost rollup + +`costRollup` is the data behind the fresh-vs-reused meter. Each bucket carries `{ receipts, fresh, reused, dollars }`; `skipped` and `failed` receipts contribute zero fresh, so a quiet world keeps `total.fresh` flat and a surprise spikes it. + +```ts +const session = createReplaySession({ ledger }, { + cost: { freshRate: 0.000002 }, // optional coarse $/token; defaults to 0 +}); + +session.costRollup.total; // { receipts, fresh, reused, dollars } +session.costRollup.byCause.input; // the same shape, for input-driven wakes +``` + +Pricing is opt-in and coarse: `freshRate` and `reusedRate` default to `0`, so the rollup is deterministic and dependency-free unless you supply real rates. `fresh` is the meaningful line -- it is the spend that surprise actually drove. + +## Building a snapshot yourself + +The DevTools package wraps all of this in `openStateDir` + `buildSnapshot`, which produce the exact `ReplaySnapshot` the SPA consumes (topology + per-receipt frames + the cost rollup). You can call them directly to embed the renderer or to drive your own analysis -- see the [reference](/reactor-devtools/reference). diff --git a/content/docs/reactor-devtools/the-viewer.mdx b/content/docs/reactor-devtools/the-viewer.mdx new file mode 100644 index 0000000..89c45c1 --- /dev/null +++ b/content/docs/reactor-devtools/the-viewer.mdx @@ -0,0 +1,77 @@ +--- +title: The viewer +description: The no-build SPA -- its three regions, the animation language that maps receipts to pulses, the keyboard controls, and the deep-links. +--- + +# The viewer + +`reactor-devtools ` boots a local server and prints a URL. Open it for the viewer: a single, no-build SPA (hand-rolled SVG + CSS animation, no bundler) that renders three coordinated regions, all driven by one `GET /api/state` payload. + +## Three regions + +- **The layered DAG** (left) -- a longest-path layered layout of the topology, drawn as SVG. Every node referenced by the topology _or_ by an edge endpoint gets a box, so a producer-only ingress still appears (drawn dashed). Entry-point gateways are gold-bordered. Per-facet edges curve with arrowheads -- named-facet lanes dashed, `@atomic` solid. The whole DAG fits the viewport. +- **The sidebar** (right) -- a live **fresh-vs-reused token meter**, cumulative up to the scrub head and split by `surprise_cause`, with the replay grand total; below it, the **ordered receipt timeline** -- each receipt's index, disposition tick, node, and wake cause, current row highlighted, future rows dimmed, click to jump. +- **The scrubber** (bottom) -- a transport (jump-to-start, step back, play/pause, step forward, jump-to-end), a speed selector, a seek range, and a readout: `frame i/N · node · status · cause · moved [...]`. + +The scrub head marks which node each receipt hit on the graph -- cyan for rendered, grey for skipped, red for failed -- and dims nodes not yet touched in the replay. + +## The animation language + +Stepping forward fires, per receipt, a **transient, fire-and-forget** pulse -- the cascade. These pulses are layered onto the same DOM as idempotent state, so a backward scrub or a long jump never replays a cascade; only a real forward step or play tick animates. That is what lets a `#frame=N` screenshot be exact and a rewind be silent. + +| Receipt | Visual | +| --- | --- | +| `rendered` + a moved fingerprint | **Node flash** -- a bright decaying halo + box glow, hued by `wake.source`. The React-DevTools "highlight update." | +| moved facet _f_ on producer _p_ | **Per-facet edge light** -- the `p → subscriber` lanes for _f_ light to the facet color and a token bead rides the path. _Only the moved facet's lanes light_ -- a subscriber on a different facet stays dark. The selector boundary, made visible. | +| a move that wakes a downstream | **Woken ring** -- each distinct subscriber the move wakes pulses a ring, staggered just after the producer flash so propagation reads as a cascade. A subscriber reached by >=2 moved facets fires **once** (the diamond single-wake). | +| `skipped` | **Dim grey ripple** -- a faint grey halo breathes once, no glow, no edges. The "correctly did nothing" shot. (A `rendered` self-tick that moved nothing gets this same dim pulse.) | +| `failed` | **Red flare** -- a red halo + box flare; prior truth stands, no edges light. | +| `cost.tokens.fresh` / `reused` + `surprise_cause` | **Cost sparkline tick** -- fresh tokens per receipt, colored by cause, over a faint reused underlay. Flat near zero on a quiet stretch, a tall spike on a surprise. | + +### Wake-source hues + +The flash color tells you _why_ a node woke, straight from `wake.source`: + +| `wake.source` | Hue | Meaning | +| --- | --- | --- | +| `input` | cyan | An upstream subscription moved. | +| `self` | violet | A self-wake (the audit floor -- a re-check that found nothing, or freshness lapse). | +| `external` | gold | An external signal arrived at a gateway. | + +A gold flash at a gateway rippling into cyan flashes downstream is, at a glance, "the outside world changed and the change propagated." + +## The hero shot + +The frame worth screenshotting is a **selective wake**: one external signal lands, exactly one path lights through the graph, the cost meter ticks once off a flat line, and everything off that path stays dark. It is the whole thesis in a single still -- _the system spent tokens only where something actually changed._ Its inverse is just as legible: a long run of dim grey pulses and a flat cost line is a correct, idle system. + +## Controls + +| Input | Action | +| --- | --- | +| `space` | play / pause | +| `←` / `→` | step one receipt | +| `Home` / `End` | jump to first / last | +| click a receipt row | jump to it | +| drag the seek bar | scrub | +| speed selector | `0.5x`-`8x` -- scales the step cadence (~600 ms/receipt at 1x) and the pulse duration | + +At fast speeds the pulse duration is capped near the step interval, so play stays crisp instead of smearing. + +## Deep-links + +The URL carries view state, so you can link a specific moment (handy for screenshots, bug reports, or a launch thread): + +| Param | Effect | +| --- | --- | +| `#frame=` | park the viewer on receipt _n_ (idempotent -- no cascade replays) | +| `?autoplay=1` | start playing on load | +| `?speed=` | set the playback speed | + +```text +http://127.0.0.1:4555/#frame=39 a specific still +http://127.0.0.1:4555/?autoplay=1&speed=2 the arc, at 2x +``` + +## Labels and captions + +If the state-dir carries a `compile/labels.json`, the viewer renders friendly node names ("Claude Adapter") instead of raw ids and hides the structural kind prefix -- the renderer itself stays generic. If it carries a `beats.json`, the authored beat captions narrate the run as it plays. Both are optional presentation data; a bare state-dir replays fine without them. See [Recording](/reactor-devtools/recording) for the beat map. diff --git a/content/docs/reactor/continuity-and-ingestion.mdx b/content/docs/reactor/continuity-and-ingestion.mdx new file mode 100644 index 0000000..1b6312d --- /dev/null +++ b/content/docs/reactor/continuity-and-ingestion.mdx @@ -0,0 +1,97 @@ +--- +title: Continuity and ingestion +description: The self-recheck cadence, the freshness bridge, gateways as external evidence, and durable idempotency cursors. +--- + +# Continuity and ingestion + +A Reactor node has to wake somehow. This page covers what can wake a node, how a +node rechecks itself when the world stays silent, and how outside evidence enters +the graph. + +## Every wake is a receipt + +The reconciler only ever observes one kind of event: a receipt arrived. The only +open question is who emitted it. That gives three wake sources, declared by a +node's `### Continuity`: + +- **input-driven** (the default): an upstream node's receipt. This falls out of + `### Requires`, so it needs no declaration. +- **self-driven**: the node's own continuity clock emits a synthetic + self-receipt, a tick. The tick is a deterministic, zero-token bridge, not a + model render: when a facet's `valid_until` has lapsed it mechanically moves that + facet's fingerprint (surprise propagates), and when nothing has lapsed it sleeps + until the soonest deadline (the tick stops there, costing nothing downstream). +- **external-driven**: a gateway turns a webhook, cron, or manual trigger into a + receipt at the system's edge. This is the gateway case. + +One event type, three sources. The reconciler stays dumb; `### Continuity` is +what declares which sources may wake a node. The author declares the wake-source; +[Forme](/reactor/the-dag-and-compile) infers the wiring. + +## Freshness: state versus policy + +Two senses of "stale" live in two different places, and keeping them apart is +what makes the freshness story clean. + +- **Freshness state lives in the world-model**: `valid_until`, + `last_corroborated`, `confidence`. Freshness is content. A fact corroborated + yesterday is genuinely a different truth than the same string corroborated six + months ago. +- **Freshness policy lives in `### Continuity`**: the self-driven recheck cadence, + the node's own clock for the case where the world will not announce the change. + +The bridge between them is elegant: **a `valid_until` lapsing flips a fact's +status, which moves that facet's fingerprint, so "time becoming material" is just +another change that propagates as surprise.** There is no special clock path in +the reconciler. `### Continuity` may read the world-model's soonest `valid_until` +to drive its recheck cadence, but the cadence rule stays in `### Continuity` and +the expiry data stays in the world-model. + +```text +self-driven tick (a synthetic self-receipt; deterministic, no model render) + -> the clock checks the fact's valid_until + -> valid_until has lapsed -> the fact's status flips -> the facet fingerprint moves + -> propagation fires exactly as if an upstream had changed +``` + +One honest caveat: a lapsed `valid_until` only moves the fingerprint once +something re-examines the fact. For the silent-staleness case, that something is +the self-driven tick. So freshness propagation is event-driven where the world +speaks and forecast-driven where it stays silent. This forecast cadence is the +deliberately-declared floor under "cost scales with surprise," not a hidden +clock. + +## Gateways: outside evidence enters the graph + +A gateway is the system's ingress. It is sugar for a `responsibility` whose +wake-source is external-driven, so it has no `### Requires` and maintains the +latest incoming truth. Forme registers the external-driven nodes as the entry +points: the ways the system gets kicked off from outside. + +A freshly-mounted gateway has a defined initial empty world-model with an initial +fingerprint, so downstreams see a valid "no data yet" state until the first +external event arrives. + +## Idempotency cursors + +A connector that polls or receives external events must not let the same arrival +count twice. Each gateway connector keeps a durable **idempotency cursor**: a +persisted high-water mark over the source so a re-poll, a retry, or a replay +after a crash does not re-ingest an event the graph already saw. + +This is why a duplicate trigger is deduped rather than re-rendered, and why a +crash-window restart replays cleanly: the cursor and the receipt ledger together +are the durable record of what has already been ingested. Staging an external +arrival writes it into the gateway's world-model and emits an external receipt, +which is the honest way evidence enters a graph whose source nodes would +otherwise memo-skip a bare re-wake. + +## Putting it together + +A self-driven node wakes itself on its clock to catch silent drift. An +input-driven node wakes when an upstream facet it subscribes to moves. A gateway +wakes the graph when the outside world delivers an event. All three are the same +mechanism, a receipt arriving, and all three ride the same propagation path +through the [reconciler](/reactor/reconciler-and-receipts) -- the same path a +forecast-paced recheck and an input-driven surprise both ride. diff --git a/content/docs/reactor/index.mdx b/content/docs/reactor/index.mdx new file mode 100644 index 0000000..453e8ce --- /dev/null +++ b/content/docs/reactor/index.mdx @@ -0,0 +1,172 @@ +--- +title: Reactor +description: The deterministic harness built to run OpenProse contracts -- the recommended fast path. Compile a contract set once, then run it forever, doing expensive model work only when something material moved. +--- + +# Reactor: the harness that runs OpenProse + +[OpenProse](/openprose) is the foundation: a paradigm where you declare the +outcomes you want kept true, written as Markdown contracts that run on any +Prose-Complete agent harness. **Reactor (`@openprose/reactor`) is the harness +built to run those contracts** -- the deterministic host that compiles, runs, +and inspects standing responsibilities, keeps the world-model up to date, and +leaves a receipt behind every decision. + +It is one harness among the possible Prose-Complete hosts, and it is the one we +recommend: the **fast path**. The thesis it works toward, stated plainly: + +> **Inference cost that scales with surprise, not wall-clock time.** + +You declare what should stay true, Reactor watches the world, and it does +expensive model work **only when something material actually moved**. + + + Reactor does not redefine the contracts. The five kinds, the `### Maintains` + schema, facets, and subscriptions are OpenProse -- taught once in + [the foundation](/openprose/contracts). This section is about the runtime: how + Reactor compiles that contract set and runs it efficiently. + + +## If you know React, you already know the shape + +Reactor borrows its shape from React. React keeps a DOM consistent with declared +component state and re-renders only what moved. Reactor keeps a set of maintained +truths consistent with the changing world and re-runs only the nodes whose inputs +moved. Substitute three nouns and the whole design follows. + +| React | Reactor | +| --- | --- | +| Component | **Responsibility** -- a declared standing goal | +| DOM | **World-model** -- the maintained truth, on disk, passed by pointer | +| `render()` | **A bounded agent session** that computes the next world-model | +| props | **Subscriptions** to other responsibilities' outputs | +| `React.memo` (skip if props unchanged) | **Skip the render if subscribed inputs haven't moved** | +| Manual dependency wiring | **Forme** -- the graph wires itself from the contracts | + +The intelligence is frozen ahead of time, at compile, into a per-node +canonicalizer and the Forme wiring. The reconciler that decides _whether to wake_ +at run time is deliberately **dumb and deterministic**: there is **no judge +step**, and the memo key has no clock in it. + + + You do not need React to use Reactor. It is React-_flavored_, not React-gated: + the contracts are Markdown, and the CLI, the receipts, and the keyless replay + are entirely React-free. The table above is an optional mental model for the + people who already carry one -- skip it freely. + + +## Compile once, run forever + +The single most important fact about Reactor is that it has two phases, and +intelligence lives in only one of them. + +```text +.prose.md contracts + -> COMPILE (intelligent sessions, fires on contract change) + Forme draws the DAG from Requires <-> Maintains + each node's ### Maintains is frozen into a canonicalizer + validators + -> a content-addressed topology DAG + per-node deterministic artifacts + -> RUN (a dumb reconciler, fires on every wake) + fingerprint inputs, skip the unchanged, render, commit, propagate +``` + +Compile is where intelligence acts. Sessions read the contracts, resolve which +node depends on which, and freeze each declaration into deterministic code. Run +is deliberately dumb: the reconciler compares fingerprints and never asks a model +"did this change." + +This is the binding model of OpenProse, stated for Reactor: **compile freezes +intelligent sessions into deterministic artifacts; run is a dumb reconciler that +executes them.** There is no `.prose` parser and no interpreter -- a compile step +is itself an agent session, and the session embodies the VM. See +[the DAG and compile](/reactor/the-dag-and-compile). + +## Cost scales with surprise + +Most automation runs on a clock. A job wakes every hour, re-reads the world, +re-does its work, and sleeps, whether or not anything changed. Cost scales with +time. + +Reactor inverts that. Before a render runs, the reconciler fingerprints the +node's subscribed inputs and its own contract. If nothing moved, the render does +not run: the reconciler writes a cheap `skipped` receipt and spawns no session. A +thousand-node system costs almost nothing on a quiet day and exactly what it +should on a loud one. + +The honest version of the claim is not "zero cost on a static world." It is +**cost scales with surprise, plus a forecast-amortized floor** for the +self-rechecks that catch silent staleness. See +[world-model and fingerprints](/reactor/world-model-and-fingerprints). + +## The one-paragraph mental model + +A node declares a standing goal, the shape of the truth it maintains, and what it +needs from upstream. When the reconciler decides a node should run, it spawns one +bounded agent session -- the render -- which reads new evidence, queries the prior +world-model, writes the updated world-model, and signs a receipt. The receipt is +the commit; downstream subscribers wake on it. A render that cannot satisfy its +postconditions commits nothing: the prior truth stands and a `failed` receipt +records why. Quiet nodes stay quiet and free. + +## How Reactor reaches your code + +Reactor is a real SDK you plug into your own stack, not a closed product. One +call takes a directory of `.prose.md` contracts all the way to a booted, +reconciling reactor and hands back one typed handle: + +```ts +import { reactor } from "@openprose/reactor"; + +// Compile ./my-project, assemble a durable reactor over ./state, boot to a +// fixpoint (cold nodes render once; warm nodes memo-skip), return a live handle. +const { reactor: r } = await reactor("./my-project", { directory: "./state" }); + +console.log(r.ledger.all().length); // the receipt trail +await r.ingest("source", { wake: { source: "external", refs: [] } }); +``` + +That is the curated front door. The deeper surface lives behind six reasoned +subpaths -- the facade, the full `@openai/agents` escape hatch, the substrate and +record/replay seams, the offline boundary, and the engine room. The +[SDK API reference](/sdk) documents all of it. To drive Reactor from the shell +instead, see the [CLI overview](/cli/overview) and the +[quickstart](/cli/quickstart). + +## Where to go next + + + + + + + + + + +--- + +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/reactor/meta.json b/content/docs/reactor/meta.json new file mode 100644 index 0000000..c14c369 --- /dev/null +++ b/content/docs/reactor/meta.json @@ -0,0 +1,10 @@ +{ + "title": "Reactor", + "pages": [ + "index", + "the-dag-and-compile", + "world-model-and-fingerprints", + "reconciler-and-receipts", + "continuity-and-ingestion" + ] +} diff --git a/content/docs/reactor/reconciler-and-receipts.mdx b/content/docs/reactor/reconciler-and-receipts.mdx new file mode 100644 index 0000000..435be9e --- /dev/null +++ b/content/docs/reactor/reconciler-and-receipts.mdx @@ -0,0 +1,119 @@ +--- +title: The reconciler and receipts +description: The dumb reconciler, the memo key, the signed receipt chain, and render-writes-files with harvest-and-promote. +--- + +# The reconciler and receipts + +The reconciler is the run phase of the Reactor, and it is deliberately dumb. It +holds no intelligence. It compares fingerprints, skips the unchanged, schedules +renders, commits results, and propagates along the edges Forme drew. Everything +intelligent was frozen at [compile](/reactor/the-dag-and-compile) time; the +reconciler only executes that frozen output. + +## The memo key + +Before a render runs, the reconciler checks one key: + +```text +memo key = (contract fingerprint, input fingerprints) +``` + +If neither the node's own contract nor any subscribed input has moved since the +node's last receipt, the reconciler writes a cheap `skipped` receipt and spawns +nothing. No session, no cost. This is `React.memo` applied to expensive agent +work, and it is the mechanism behind "cost scales with surprise." + +Two facts the wiring must honor or propagation silently fails: + +- A facet-emitting producer must mount with the canonicalizer its + canonicalizer-session emitted, not a bare atomic canonicalizer. Otherwise the + moved facet never appears in the propagation set and the edge never fires. +- A pure source node memo-skips on a bare self or external re-wake, because its + key `(contract fingerprint, [])` never moves. Evidence enters honestly only via + a contract-fingerprint bump, a freshness lapse, a staged external arrival, or + the first cold-miss render at boot. + +## Single-flight, coalescing, failure + +These fall out of the primitives; none needs a new subsystem. + +- **Single-flight**: a node renders one at a time. A render reads its own prior + truth and appends to its own ledger, so two concurrent renders would race. +- **Coalescing**: wakes that arrive while a render is in flight do not stack into + N more renders. They mark the node dirty; when the in-flight render commits, if + the node is still dirty it renders once more against the freshly-moved inputs. + This is React batching. Five inputs moving mid-render cost one follow-up render, + not five. +- **Failure**: a render that errors or leaves a postcondition unsatisfied commits + nothing. The last-good published truth stands. It still writes a `failed` + receipt: failures are cheap audit signal, not silence. Downstreams do not wake, + because the fingerprint did not move. + +So a node's receipts have three statuses, and only one propagates: + +```text +rendered the truth moved -> wake the subscribed downstreams +skipped nothing changed -> propagate nothing +failed nothing committed -> prior truth stands, propagate nothing +``` + +## The receipt is the commit + +The receipt is the single commit object and the unit of the ledger: the wake +event, the memo-key record, the audit entry, and the trust artifact all in one. + +| Field | Meaning | +| --- | --- | +| `node` | the node identity (the ledger is node-scoped) | +| `contract_fingerprint` | which contract version produced this | +| `wake` | the wake's source (input, self, external) and the waking refs | +| `input_fingerprints` | the consumed tuple, one per subscribed facet | +| `fingerprints` | a `facet -> token` map of the published truth (atomic is the reserved whole-truth facet) | +| `semantic_diff` | render-input context, never a wake signal | +| `prev` | pointer to the prior receipt, chaining the ledger | +| `status` | `rendered`, `skipped`, or `failed` | +| `cost` | token attribution that makes "cost scales with surprise" observable | +| `sig` | the signature | + +A node's receipts accumulate into its **ledger**: the append-only trail that is +the node's durable memory. The system can be killed and resumed because the +ledger is the memory. At boot the reactor re-derives each node's last receipt +from the durable trail, so a restart memo-skips the unchanged nodes instead of +re-rendering them. + +## What "signed" means in v1 + +The receipt chain is attributable and tamper-evident at the **meaning layer**. +Each receipt commits to its fingerprints and to its predecessor, so the trail is +verifiable as a coherent sequence produced by a known node. In v1 this does not +yet mean a cryptographic byte-hash of canonical bytes; trust rests on the +fingerprint chain. The cryptographic digest and a real signer are deferred to +the integrity and composition-pinning milestone. + +## Render writes files; the harness promotes + +The division of labor at commit time is exact and is part of the binding model. + +The **render writes files** into its own workspace. The truth's contents never +ride the session's final output text. When the render signals `rendered`, the +**harness harvests** the declared workspace files, **promotes** them into the +node's published world-model, and **fingerprints** the canonical result with the +node's compiled canonicalizer. + +This is why the render and the reactor share one world-model store: the render's +workspace write must be visible to the harness harvest in the same render. The +published artifact is fingerprinted; the workspace is not, and it reaches the +published truth only through this explicit promote-and-fingerprint commit. + +## Propagation + +On a `rendered` receipt whose fingerprint moved, the reconciler wakes the +downstreams subscribed to the moved facets, resolved by reading the topology's +edges. A `skipped` or `failed` receipt propagates nothing. + +The wake event and the fingerprint move are the same mechanism seen two ways: a +receipt arrived, and a facet fingerprint differs from the one the downstream last +consumed. That unification is what lets self-rechecks and external triggers ride +the same propagation path as ordinary upstream changes. See +[continuity and ingestion](/reactor/continuity-and-ingestion). diff --git a/content/docs/reactor/the-dag-and-compile.mdx b/content/docs/reactor/the-dag-and-compile.mdx new file mode 100644 index 0000000..f1bde3c --- /dev/null +++ b/content/docs/reactor/the-dag-and-compile.mdx @@ -0,0 +1,102 @@ +--- +title: The DAG and compile +description: How a .prose project compiles, as sessions, into a content-addressed topology DAG plus per-node canonicalizers and validators. +--- + +# The DAG and compile + +Compile is the only intelligent phase of the Reactor. It runs ahead of run +time, it fires when the contract set changes, and it freezes intelligent +sessions into deterministic artifacts the run phase executes. + +There is no `.prose` parser. A compile step is an agent session that reads the +contracts and emits structured output, which a small deterministic lowering +turns into run-time artifacts. The session embodies the VM; the only +deterministic structures are the compile-frozen outputs, and even those are +produced by sessions. + +## What compile produces + +A full compile runs Forme once, then the per-node materiality and gate sessions +for each node Forme found. + +```text +loadContractSet(dir) load the contract text (trivial, deterministic) + -> compileForme the topology session -> the DAG + -> per node: + compileCanonicalizer the materiality session -> a canonicalizer + compilePostcondition the gate session -> validators + => a mountable ReconcilerTopology + per-node compiled artifacts +``` + +The output is a content-addressed topology plus, for every node, a canonicalizer +(what counts as a change, frozen) and a set of validators (the commit gate). +These are what the run phase mounts. The compile run reports its own token cost +with `surprise_cause: self`, because a compile is a `self`-driven build. + +## Forme draws the edges + +Forme is the wiring session. It reads the full set of declared contracts and +resolves each node's needs to the producer that satisfies them, **semantically**. +A node says in `### Requires` that it needs "a current view of competitor +funding"; Forme matches that need to the `funding` part of some producer's +`### Maintains`. This is meaning matching, not string matching. + +The edges of the DAG are Forme's output, not human-authored config. Intent +stays with the human (the need, in the contract); the wiring is Forme's. Two +rules make this safe: + +- A need with no producer, or two equally plausible producers, is a surfaced + **wiring diagnostic**, never a silent guess. +- Acyclicity is a postcondition on Forme's own output. A topology that would + close a loop is rejected and surfaced. + +Genuine feedback (a node's output shaping its own next input) is not a back-edge. +It is self-driven [continuity](/reactor/continuity-and-ingestion): loops live in +time, not in edges. + +## What a node is + +Every node in the DAG is the same shape: a declaration plus a render. The `kind` +in the contract frontmatter is sugar over that one atom. + +| `kind` | Role | Interface | World-model | +| --- | --- | --- | --- | +| `responsibility` | The mounted node, the headline | `### Requires` to `### Maintains` | persisted | +| `function` | A called helper, the library tier | `### Parameters` to `### Returns` | none | +| `gateway` | Source or ingress | no `### Requires`; `### Maintains` the incoming truth | persisted | +| `pattern` | Reusable coordination, expanded at compile | n/a | n/a | +| `test` | Assertions over a subject's truth or receipts | n/a | n/a | + +A `responsibility` is a mounted render with a standing, persisted world-model +that other nodes can subscribe to. A `function` is a called render: stateless, +ephemeral, returns a value, carries no world-model. A `gateway` is sugar for a +`responsibility` whose wake-source is external (a webhook, cron, or manual +trigger), so it has no `### Requires` and maintains the latest incoming truth. + +Being subscribed-to is what makes something a node. Inside a render you can +`call` functions and spawn sub-agents, but none of that is a node. The only +cross-node connection is a subscription. + +## Mounting makes a node, not statefulness + +A render becomes a DAG node when it is **mounted**. Mounting is a harness act +that adds identity, a persisted world-model, and the resolved subscriptions. +Node-ness is conferred by mounting, never by holding memory. A pure transform +with no internal memory is still a node if it is mounted as a producer; a render +that reads its own prior truth is not a node merely because it is stateful. + +## Content-addressed, and re-runnable + +Forme's resolved topology is itself a maintained truth: it records the nodes, +the resolved edges, the external entry points, and `acyclic: true` as a +postcondition. Because the topology is committed and versioned, the wiring is +inspectable and the compiled artifacts are cached. Compile re-fires only when +the contract set changes, the rarest event in the system. Editing a `### Maintains` +schema moves that node's contract fingerprint, which is a memo miss, so the +node simply re-renders into the new shape on the next wake. There is no separate +migration machinery. + +Once compile has produced the topology and the per-node artifacts, the +[reconciler](/reactor/reconciler-and-receipts) takes over and the intelligent +phase is done until a contract changes again. diff --git a/content/docs/reactor/world-model-and-fingerprints.mdx b/content/docs/reactor/world-model-and-fingerprints.mdx new file mode 100644 index 0000000..719bb7d --- /dev/null +++ b/content/docs/reactor/world-model-and-fingerprints.mdx @@ -0,0 +1,158 @@ +--- +title: World-model and fingerprints +description: The content-addressed maintained truth, the fingerprints of meaning, and facet-granular propagation along edges. +--- + +# World-model and fingerprints + +The world-model is the maintained truth a node keeps current. It is the +Reactor's DOM: standing between renders, read by the next render as its prior +state, and subscribed to by downstream nodes. One node, one world-model. + +This page covers what the truth is, how "changed" is decided, and how a change +in one node reaches exactly the right downstreams. + +## The world-model is a content-addressed artifact + +The canonical world-model is a single content-addressable artifact, by default a +small directory of files, with a single file as the degenerate case. It is +addressed by the fingerprint of its canonical form. + +Three forces pin that shape: + +- A render reads the prior truth agentically, the way a coding agent pulls a + file on demand, so the truth must be legible and navigable, not flattened into + a prompt blob. +- The receipt trail is signed and append-only, so the truth's evolution must be + content-addressable and auditable. +- Memoization needs a cheap "did it move?" check, so the truth must be + deterministically serializable. + +Anything else (a SQL index for query, a vector index for retrieval, a rendered +dashboard) is a **derived projection** of that canonical truth, never the truth +itself. + +One discipline matters: the **published** world-model is the fingerprinted +artifact. The render's private scratch is workspace, never fingerprinted and +never subscribed to. A node updates its published truth only when something +semantically material actually changed. + +## The schema lives in `### Maintains` + +`### Maintains` declares the shape of the world-model. It does four jobs in one +block: + +- A **type**: the fields, including any freshness fields (`valid_until`, + `last_corroborated`, `confidence`). +- A **canonicalization spec**: what equality means for the fingerprint, written + as unambiguous natural language. +- A **subscription surface**: the named facets a downstream may depend on. +- **Postconditions**: the validators the render must leave the truth satisfying + (the folded-in `### Criteria`, not a separate judge beat). + +## A fingerprint is a fingerprint of meaning + +A fingerprint is a cheaply computed token that changes if and only if the +semantically-relevant content changed. That invariant is the whole definition. +How it is computed (a content digest, a high-water timestamp, a revision +counter) is a swappable detail, the same family as HTTP ETags or file mtimes. + +The apparent paradox, a semantic "if and only if" decided deterministically, +dissolves once you split it across time, exactly as React does: + +1. **Compile time** (intelligent, once per contract). The natural-language + canonicalization spec is compiled into a deterministic canonicalizer: what is + material, what is dropped, how text, sets, and numbers normalize. This is + where intelligence decides what counts as a change. +2. **Render time** (intelligent, per render). The session writes the truth and + self-polices its postconditions. It never decides "did this change." +3. **Wake time** (dumb, every time). The reconciler runs the compiled + canonicalizer plus a digest and compares. + +So the fingerprint changes if and only if the canonical material content +changes, and "material" was frozen by intelligence at compile time, not judged +at wake time. An agent never judges "did this change" at wake time. + +The single highest-leverage control here is **material versus immaterial fields**. +A feed re-polled every three minutes carries a fresh `fetched_at` and new request +ids every time. Excluding those from the fingerprint is what keeps "cost scales +with surprise" from silently degrading back into "cost scales with the clock." + +## The structured-backing rule + +Anything subscribed must have a structured, canonicalizable backing. Free-form +rendered prose is a derived projection excluded from the fingerprint. Otherwise +a session re-rendering the same paragraph hashes differently every time and +falsely re-triggers downstreams. The rule: **fingerprint the structured truth; +render prose from it.** + +## The identity vocabulary + +Three fingerprints of meaning are the load-bearing reactive primitives. + +| Name | Of what | Answers | +| --- | --- | --- | +| contract fingerprint | the node's own contract | which version of the responsibility produced this | +| input fingerprint | each upstream facet a node subscribes to | did the watched thing change | +| world-model fingerprint | the node's own published truth (plus one per facet) | the identity downstreams subscribe to | + +These chain. A node publishes its world-model fingerprint in its receipt; a +downstream sees that as one of its input fingerprints. The memoization key is +`(contract fingerprint, input fingerprints)` and nothing else. No judge, no +policy artifact. + +A cryptographic byte-hash is deliberately deferred for v1. Memoization and +propagation run entirely on the fingerprints of meaning above. A byte digest's +only jobs are integrity and cross-party composition-pinning, neither of which the +reactive core needs to work. + +## Facets: atomic always, named parts when they help + +Fingerprinting has two layers. + +- **Atomic** (mandatory). One token over the whole canonical truth. This is the + reconciler's primitive and what a diamond reconverges on: a node reachable by + several paths renders once per distinct input-fingerprint tuple, not once per + inbound edge. A leaf truth needs only this. +- **Facets** (optional, schema-declared). Per-part tokens that make propagation + finer-grained. A downstream that subscribes to facet *X* does not wake when + facet *Y* moves. This is React's selector boundary made authorable. + +You declare a facet simply by naming a part: a `####` sub-heading inside +`### Maintains` is a facet, and its body says which fields are material. The +name is the fingerprint unit, the subscription symbol, and the world-model +subtree all at once. Structure is subscription. + +```markdown +### Maintains + +A current, corroborated view of each tracked competitor. Each competitor carries +a stable `name` and a `last_corroborated` field; `fetched_at` and source request +ids are immaterial everywhere. Postcondition: every competitor cites a source. + +#### funding +Funding events per competitor: round, amount, date. Material: the event set +(unordered) and each event's round, amount, and date. + +#### hiring +Open-role activity: the department set and the open-role count (exact). + +#### product-launches +Announced or shipped products: the launch set; a ship-date slipping past today +flips `shipped`, which is material. +``` + +A downstream that requires *funding* wakes only when `#### funding`'s fingerprint +moves, not when hiring or launches move. The shared `name` and `last_corroborated` +sit outside any part, so they move only the atomic token. + +Atomic is the always-on correctness primitive and the free default. Facets are +the efficiency primitive that keeps fan-out from burning the surprise budget. A +leaf truth declares none and pays nothing. + +A semantic diff ("3 controls went stale, 1 newly accepted") is valuable, but it +is carried as **render input** in the receipt, never as a wake signal. The wake +decision is fingerprint-only. + +Next: how the [reconciler](/reactor/reconciler-and-receipts) uses these +fingerprints to skip, commit, and propagate. diff --git a/content/docs/reference/commands.mdx b/content/docs/reference/commands.mdx deleted file mode 100644 index 36a7812..0000000 --- a/content/docs/reference/commands.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Command surface -description: The OpenProse command names and what they are for. ---- - -# Command surface - -OpenProse commands are first an agent-session command language. - -When you type `prose run file.md` inside an agent session, the agent should load the `open-prose` skill and execute the program in-session. - -When you use the CLI from a shell, the CLI packages that same instruction and sends it into a supported harness. The CLI does not replace the VM. - -## Commands - -| Command | Purpose | -| --- | --- | -| `prose run file.md` | run a Contract Markdown or ProseScript program | -| `prose lint file.md` | check Contract Markdown structure before a run | -| `prose preflight file.md` | check dependencies and environment without executing | -| `prose test path` | run OpenProse tests | -| `prose inspect run-id` | inspect a completed run | -| `prose status [--graph]` | summarize recent runs, optionally as a graph | -| `prose install [--update]` | install or update OpenProse dependencies | -| `prose examples [name]` | list or run bundled examples | -| `prose help` | show OpenProse help | -| `prose migrate file.prose` | convert ProseScript toward Contract Markdown | - -## Shell CLI - -The shell CLI is useful when you are outside an agent session. - -```bash -prose run programs/reviewer.md -prose run programs/reviewer.md --harness claude-sdk -prose doctor -``` - -The current CLI supports harnesses such as `codex-sdk`, `claude-sdk`, `codex`, `claude`, and `mock`. It can check whether the `open-prose` skill is installed for the selected provider and can run `prose doctor` to inspect setup. - -For exact install commands, flags, harness defaults, and exit behavior, read the canonical [CLI README](https://github.com/openprose/prose/blob/main/cli/README.md). - -## Agent rule - -If you are an agent and the user gives you `prose ...` in chat, do not shell out to the CLI by default. - -Load the skill and treat the command as an instruction to the current session. - -Next: [canonical specs](/reference/specs). diff --git a/content/docs/reference/meta.json b/content/docs/reference/meta.json deleted file mode 100644 index be461db..0000000 --- a/content/docs/reference/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Reference", - "pages": ["overview", "commands", "specs"] -} diff --git a/content/docs/reference/overview.mdx b/content/docs/reference/overview.mdx deleted file mode 100644 index 71820f6..0000000 --- a/content/docs/reference/overview.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Reference overview -description: Where the canonical OpenProse specs live. ---- - -# Reference overview - -These docs are intentionally small. They explain how to think about OpenProse and how to get started. - -The canonical behavior lives in the open source repo, especially the `open-prose` skill and its reference specs. - -That split is deliberate. - -The docs site should help a human get oriented. The skill should tell an agent how to run the thing. - -## What belongs here - -This section gives you: - -- a shallow map of the command surface -- links to the skill and canonical specs -- enough vocabulary to know which file to read next - -It does not try to mirror the full language reference. Mirrored specs drift, and drift is dangerous when agents read the docs as if they are executable instructions. - -## Where to go - -| Need | Read | -| --- | --- | -| Run, lint, test, or explain a program as an agent | [For AI agents](/agents/for-ai-agents) | -| Understand the shell command names | [Command surface](/reference/commands) | -| Find the authoritative execution files | [Canonical specs](/reference/specs) | -| Decide whether OpenProse is a fit | [When to use OpenProse](/think/when-to-use-it) | - -If this site and the skill disagree, trust the skill. - -Next: [commands](/reference/commands). diff --git a/content/docs/reference/specs.mdx b/content/docs/reference/specs.mdx deleted file mode 100644 index bf37737..0000000 --- a/content/docs/reference/specs.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Canonical specs -description: Links to the skill and source-of-truth reference files. ---- - -# Canonical specs - -If this site and the skill disagree, trust the skill. - -The docs site is orientation. These files define how OpenProse agents should behave. - -## Skill - -- [`open-prose` skill](https://github.com/openprose/prose/blob/main/skills/open-prose/SKILL.md) - -Agents should start here. The skill contains the activation rules, path table, command routing, host primitive adapter, and fit check. - -## Core specs - -| File | What it defines | -| --- | --- | -| [Contract Markdown](https://github.com/openprose/prose/blob/main/skills/open-prose/contract-markdown.md) | the `.md` program and service format | -| [Forme](https://github.com/openprose/prose/blob/main/skills/open-prose/forme.md) | semantic wiring and manifest generation | -| [Prose VM](https://github.com/openprose/prose/blob/main/skills/open-prose/prose.md) | execution behavior for manifests and services | -| [ProseScript](https://github.com/openprose/prose/blob/main/skills/open-prose/prosescript.md) | the imperative scripting layer for `.prose` and `### Execution` | -| [Filesystem state](https://github.com/openprose/prose/blob/main/skills/open-prose/state/filesystem.md) | the default run-state layout | -| [Session primitive](https://github.com/openprose/prose/blob/main/skills/open-prose/primitives/session.md) | how isolated service sessions should behave | - -## Authoring guidance - -| File | What it helps with | -| --- | --- | -| [Tenets](https://github.com/openprose/prose/blob/main/skills/open-prose/guidance/tenets.md) | design principles behind the language | -| [Patterns](https://github.com/openprose/prose/blob/main/skills/open-prose/guidance/patterns.md) | reusable workflow shapes | -| [Antipatterns](https://github.com/openprose/prose/blob/main/skills/open-prose/guidance/antipatterns.md) | common ways OpenProse programs go wrong | - -## CLI and packages - -- [OpenProse README](https://github.com/openprose/prose/blob/main/README.md) -- [CLI README](https://github.com/openprose/prose/blob/main/cli/README.md) -- [Standard library](https://github.com/openprose/prose/tree/main/packages/std) - -Reference pages here should stay shallow. If a detail is normative, link to source instead of copying it. diff --git a/content/docs/sdk/adapters.mdx b/content/docs/sdk/adapters.mdx new file mode 100644 index 0000000..7f43a08 --- /dev/null +++ b/content/docs/sdk/adapters.mdx @@ -0,0 +1,266 @@ +--- +title: Adapters +description: 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. +--- + +# Adapters + +`@openprose/reactor/adapters` is the **injection boundary**: the seam where a +reactor's I/O lives. Everything above it -- the reconciler, the canonicalizer, +the receipt chain -- is a pure function over the ports this subpath defines. A +deployment swaps the backends here without touching the engine, and the +[front door](/sdk/front-door) facade reaches in for you on the common paths. + +This page is the reference for what `/adapters` exports and, more importantly, +the contracts a custom backend must honor to stay correct. The headline is one +record: + +```ts +import { fileSystemSubstrate } from "@openprose/reactor/adapters"; +``` + +## The one Substrate primitive + +A reactor keeps two durable things: its **truth** (the world-model) and its +**memory of what it already decided** (the receipt trail). `Substrate` is the +single record that answers "where does a reactor keep both" -- one shape, four +fields: + +```ts +interface Substrate { + readonly clock: ClockAdapter; // the only time source + readonly storage: StorageAdapter; // the receipt ledger's append-only trail + readonly worldModel: WorldModelStore; // the truth the render commits through + readonly ledger: MutableReceiptLedger; // derived from `storage` (see below) +} +``` + +Two named factories build it correctly so you do not hand-wire the four fields: + +- **`fileSystemSubstrate({ directory })`** -- the durable substrate. A system + clock, a filesystem storage adapter (the receipt trail at + `/receipts.json`), a durable ledger re-derived from that storage, + and a filesystem world-model store under `/world-models/`. This is + the canonical layout the CLI and DevTools fixtures share, so a substrate built + here and a state-dir the CLI populated re-open the **same** durable trail and + truth. +- **`inMemorySubstrate()`** -- the ephemeral substrate for tests and replay. The + same clock, an in-memory storage adapter, a ledger re-derived from that + in-memory storage (identical re-derivation semantics, just no disk), and an + in-memory world-model store. Nothing persists; a fresh `inMemorySubstrate()` + is empty. + +The constructors (`mountDag` / `createReactor` / `runProject`) accept a +`{ substrate }`, and the substrate is a strict superset of the older à-la-carte +fields -- no backend was removed. The facade builds one for you from +`{ directory }`. + +## The restart-survival invariant + +This is the load-bearing reason the durable factory exists, and the one thing a +custom backend must not get wrong. + + +For a durable substrate, the **ledger MUST be derived from the same storage +adapter** -- `createFileSystemReceiptLedger({ storage })` over the exact storage +the substrate also exposes. The durable ledger re-derives every node's last +receipt from that storage's append-only trail at construction. So re-opening the +same `directory` re-opens the full prior memory, and the boot sweep memo-skips +the unchanged nodes instead of re-rendering them. + + +`fileSystemSubstrate` bakes this in -- it builds the storage adapter and then +builds the ledger over that very adapter, so a consumer never has to remember to +wire it. "The ledger is the source of truth": a restart that re-opens the same +directory resumes the prior memory, which is what makes "cost scales with +surprise, not wall-clock time" survive a process restart. + +### The storage-only spread idiom + +You may need to swap one field -- say a custom storage adapter -- while keeping +the rest correct. The blessed override is a spread over the factory, never a +hand-built record: + +```ts +import { createReactor } from "@openprose/reactor"; +import { fileSystemSubstrate } from "@openprose/reactor/adapters"; + +const r = createReactor({ + substrate: { ...fileSystemSubstrate({ directory }), storage: myStorage }, + topology, + mounts, +}); +``` + +Spreading the factory keeps the other three fields built-right. Supplying a +divergent in-memory ledger alongside durable storage is the explicit opt-out, +not an accident -- if you spread a different `ledger` over durable storage, you +have deliberately broken restart-survival. The factory makes the correct path +the short path; the override makes the unusual path visible. + +## Gateway ingress and the idempotency cursor + +`/adapters` ships the toolkit for turning an external arrival into a receipt at +the system's edge, without ever letting a re-delivered arrival manufacture a +second one. + +The connector is the one impure seam -- the actual I/O against a source: + +```ts +import { createPollConnectorAdapter } from "@openprose/reactor/adapters"; + +// `fetch` performs the real read (HTTP GET, queue drain, file read). A test +// supplies a deterministic stub. Either way the adapter stays a pure function +// over injected I/O. +const connector = createPollConnectorAdapter((request) => fetchSource(request)); +``` + +`pollGateway` (and its async sibling `pollGatewayAsync`) drives the edge. For +each new arrival it stages the item into the gateway node's upstream truth, +marks the cursor, then wakes the node: + +```ts +import { + pollGateway, + createIdempotencyCursor, +} from "@openprose/reactor/adapters"; + +const cursor = createIdempotencyCursor(); + +const result = pollGateway(dag, { + connector, + source_id: "inbox", + node: "gateway", + extract: (payload) => toArrivals(payload), // -> readonly GatewayArrival[] + cursor, + stage: (arrival) => appendToInbox(arrival.item), +}); + +result.ingested_ids; // arrivals past the cursor that drove a wake this poll +result.skipped_ids; // arrivals already in the cursor -- no wake, no receipt +``` + +The cursor is what dedups **before** the edge. Each `GatewayArrival` carries a +stable `id` (a message id, an event id, a content hash) -- the same logical +arrival yields the same `id` across polls and redeliveries. An already-seen +`(source, id)` pair is dropped, so it never produces a second receipt. The order +of arrivals is the delivery order, so the cursor advances monotonically. + +The cursor is durable. It round-trips through the storage registry as a plain +JSON-able snapshot, so a restart resumes without re-ingesting the backlog: + +```ts +import { + loadIdempotencyCursor, + cursorRegistryPatch, +} from "@openprose/reactor/adapters"; + +// At boot: rehydrate from the registry the storage adapter persisted. +const cursor = loadIdempotencyCursor(storage.readRegistry()); + +// After a poll: project the cursor's snapshot into a registry patch to persist. +storage.writeRegistry({ ...storage.readRegistry(), ...cursorRegistryPatch(cursor) }); +``` + +The staging order is deliberate -- **stage, mark, then wake**. Marking before +the wake means a throw mid-render does not re-stage on the next poll: the arrival +is durably consumed once it is staged, and the gateway's render re-runs against +the staged truth on a later wake if it failed. A single payload that lists the +same idempotency key twice is a source bug the cursor cannot disambiguate, so +`pollGateway` fails loudly rather than silently ingesting one and dropping the +other. + +## Record and replay + +Two recording adapters make a run reproducible without a live provider -- the +mechanism the keyless [DevTools](/reactor-devtools) replay path rests on. + +- **`createRecordReplayModelGatewayAdapter({ records })`** replays a captured + sequence of model calls. Each `invoke` is matched against the next record's + request (canonical-JSON equality); a mismatch throws with the record id, so a + drifted run is caught at the exact diverging call rather than silently + producing a different truth. The adapter exposes `calls()` and `remaining()` + for inspection. +- **`createPassthroughAgentSdkAdapter({ launch?, sandbox? })`** is the agent-SDK + passthrough that records every launch and sandbox run. Without handlers it + echoes the request payload back; `createNullAgentSdkAdapter(payload)` is the + inert variant that returns a fixed payload. Both fold the sandbox path in (the + architecture folds sandbox execution into the agent-SDK port). + +Every leaf adapter clones its request in and payload out through canonical JSON, +so an adapter cannot be mutated by a caller and a payload is always a defensive +copy. + +## The port contracts a backend implements + +A custom backend implements one of the trimmed v1 ports. These are the contracts +the harness is a pure function over -- the short names are the headline +vocabulary `Substrate` uses; the `Reactor*`-prefixed originals are the +deprecated aliases, kept reachable from both `/adapters` and +[`/internals`](/sdk/internals) (nothing was removed). + +| Port (short name) | `Reactor*` original | Responsibility | +| --- | --- | --- | +| `ClockAdapter` | `ReactorClockAdapter` | the only time source -- `now(): string` | +| `StorageAdapter` | `ReactorStorageAdapter` | the receipt ledger's append-only trail plus the shrunk registry | +| `WorldModelStore` | `ReactorWorldModelStore` | read-by-reference, commit-and-fingerprint, content-addressed versioning | +| -- | `ReactorModelGatewayAdapter` | render / compile-step invocation -- the model call seam | +| -- | `ReactorConnectorAdapter` | external evidence sources (gateways) | + +A few contracts are worth stating plainly, because they are where a custom +backend tends to drift: + +- **`StorageAdapter`** is append-and-list over receipts plus read/write of a + shrunk registry. The registry is a dumb canonical-JSON key/value -- it carries + the topology world-model and self-driven schedule as opaque blobs, nothing + more. Keep it byte-stable across equal states (sorted, canonical) so the + durable snapshot is deterministic. +- **`WorldModelStore`** hands the render a queryable **reference** to a node's + prior truth (never pre-stuffed into context), and `commit` produces the + deterministic canonical serialization, content-addresses it, and returns the + new version plus the canonicalizer-computed fingerprints. Only the `published` + workspace is fingerprinted; the render's private `workspace` is not. +- **`ReactorModelGatewayAdapter`** reports usage as a `{ provider, model, tokens }` + triple -- the cost-bearing half of the receipt. It does **not** know the wake + source, so `surprise_cause` is supplied by the reconciler, not the gateway. + +The reference backends for these ports also live on `/adapters`: +`createSystemClockAdapter` / `createFixedClockAdapter`, +`createFileSystemStorageAdapter` / `createMemoryStorageAdapter`, +`FileSystemWorldModelStore` / `InMemoryWorldModelStore` (and their `create*` +helpers), `FileSystemReceiptLedger` / `InMemoryReceiptLedger`, and the +`createStaticConnectorAdapter` for an inert in-memory source. + + +Honest status: the v1 port surface is deliberately trimmed. The `signer` port is +deferred to the crypto byte-hash milestone -- the only honest v1 signature is the +null signature, so the receipt chain is tamper-evident and chain-consistent, not +a cryptographic byte hash. `eventSink` is dropped on purpose: telemetry is read +off the ledger, not a separate sink. `sandbox` is folded into the agent-SDK port. +None of this is a gap to paper over; it is the surface as shipped. + + +## Where to go next + + + + + + + diff --git a/content/docs/sdk/agents.mdx b/content/docs/sdk/agents.mdx new file mode 100644 index 0000000..374fdf1 --- /dev/null +++ b/content/docs/sdk/agents.mdx @@ -0,0 +1,407 @@ +--- +title: The /agents escape hatch +description: The peer-dep-isolated @openai/agents passthrough -- the layered RenderOptions (Tier A sugar, Tier B verbatim passthrough, Tier C backstop), the RenderBackend injection seam, and the compile-session surface. Every render knob is reachable, with zero capability loss. +--- + +# The `/agents` escape hatch + +`@openprose/reactor/agents` is the one subpath where the harness gets out of your +way. Every render and every compile step in Reactor is **one bounded +`@openai/agents` session**, and this subpath is the named priority of the +`0.3.0` surface: **every knob that SDK anticipates is reachable**, layered so an +agent's auto-import sees intent rather than a wall of peers, with **zero +capability loss** versus driving `@openai/agents` by hand. + +It is its own subpath for one honest reason: importing it pulls the optional +peers (`@openai/agents`, `zod`). The keyless inspection and replay surface +installs neither, so the escape hatch lives behind a door you only open when you +mean to. Everything here is side-effect-free at import: the `Agent` and `Runner` +are constructed lazily inside the render closure, never at module load. + + +**TypeScript needs `nodenext` or `bundler` module resolution** to reach this +subpath. The escape-hatch subpaths (`/agents`, `/adapters`, `/run`, +`/run/types`, `/internals`) are declared through the package's `"exports"` map, +which the legacy `"moduleResolution": "node"` resolver does not read. The root +`@openprose/reactor` import resolves under legacy `node` too; the cliff bites +only the explicit subpaths, where you are already in a real `tsconfig`. + + +## The promise, stated honestly + +A wrapper earns trust by being lossless. The old `createAgentRender` exposed only +a handful of knobs and built the `Agent`/`Runner` internally with hardcoded +settings, so a consumer who needed anything more had to throw away the **whole** +render -- losing the harness's instruction composition, the `wm_*` tools, the +harvest, and the cost capture. That is the lossy wrapper this subpath was +designed to retire. + +The fix is a single `RenderOptions` with three tiers. The harness reserves only +the four fields it cannot let you touch without breaking the render contract; +**everything else passes through verbatim.** When the bottom tier is not enough, +you build the `Agent` and `Runner` yourself -- so there is no ceiling. + +## The layered seam + +```ts +import type { RenderOptions } from "@openprose/reactor/agents"; +``` + +`RenderOptions` is the shared escape hatch. The render config (`AgentRenderConfig`) +extends it; the facade's `render` option forwards it verbatim to every node; the +sub-agent primitive and the compile session thread the same fields. Learn it once. + +### Tier A -- harness-specific sugar + +These are **not** plain `@openai/agents` fields. They are harness knobs that map +onto the SDK config, and the two decoding sugars (`temperature`, `seed`) **fill +only the fields you left unset** (precedence, below). + +| Field | Type | Notes | +| --- | --- | --- | +| `provider` | `ModelProvider` | Keep first-class: the scoped-not-global invariant. Defaults to the scoped OpenRouter provider, resolved lazily on first render. | +| `model` | `string \| Model` | Widened from `string` so you may pass a constructed `Model` instance, not just a provider-resolved id. | +| `maxTurns` | `number \| null` | The session's turn cap. `null` is the **deliberate unbounded opt-in** -- it bypasses the turn guard. Unset means the high default cap (`200` for a render). | +| `signal` | `AbortSignal` | Per-run cancellation. Operational, not config, so it earns a top-level home rather than hiding in `runOptions` (where it is reserved). | +| `temperature` | `number` | Sugar for `agent.modelSettings.temperature`. Defaults `0`. | +| `seed` | `number` | Sugar for `agent.modelSettings.providerData.seed`. | + +### Tier B -- the verbatim `@openai/agents` passthrough + +This is the layer that closes the gap. Each field is deep-merged **over** the +harness's defaults. + +| Field | Type | What it carries | +| --- | --- | --- | +| `agent` | `AgentPassthrough` | The consumer's `Agent` config, reserved fields removed (below). Home of `modelSettings.*` (reasoning, maxTokens, toolChoice, topP, providerData, ...), `handoffs`, `inputGuardrails` / `outputGuardrails`, `mcpServers` / `mcpConfig`, `toolUseBehavior`, `prompt`, `handoffDescription`, `model`. | +| `runConfig` | `Partial` | Runner-**construction** config: `tracingDisabled`, `workflowName`, `traceId`, `groupId`, `traceMetadata`, `modelProvider`, the SDK `sandbox`, `sessionInputCallback`. | +| `runOptions` | `RunOptionsPassthrough` | The **per-run** options bag -- the ONLY home for `previousResponseId`, `conversationId`, `session`, `sessionInputCallback`, `errorHandlers`. | +| `extraTools` | `(defaults) => Tool[]` | Receives the built-in `wm_*` / cwd / spawn set and returns the full set. It **concatenates** -- the built-ins are always present, never replaced. | +| `instructionsSuffix` | `string` | Appended to the composed system prompt (after the SKILL + contract layers). Extend the prompt without dropping to a factory. | +| `tracing` | `boolean \| TracingConfig` | Re-enable tracing with your own api key, or toggle the per-run `tracingDisabled`. The default backend keeps tracing **disabled per run** (safe egress) -- never via a process-global mutation. | + +### Tier C -- the full backstop + +When even verbatim passthrough is not enough -- you want a non-`@openai/agents` +model, or instance-level lifecycle hooks -- you build the instances yourself. + +| Field | Type | What it gives you | +| --- | --- | --- | +| `agentFactory` | `(spec: RenderAgentSpec) => Agent` | Build the `Agent` from the harness-required pieces. The ONLY place to attach `AgentHooks` (`agent.on(event, ...)` -- hooks are emitters on the instance, not config fields). | +| `runnerFactory` | `(provider: ModelProvider) => Runner` | Build the `Runner` yourself from the scoped provider. The ONLY place to attach `RunHooks`. | + +## Reserved fields are a compile error + +The harness owns four `Agent` fields, because they carry the render contract: drop +them and the harvest, cost capture, or routing silently breaks. Rather than let +you set one and stomp the render, the type system **removes** them from the +passthrough. + +```ts +type ReservedAgentFields = "instructions" | "tools" | "outputType" | "name"; + +// AgentPassthrough = Omit, ReservedAgentFields> +const render: RenderOptions = { + agent: { + modelSettings: { reasoning: { effort: "high" }, maxTokens: 8000 }, + // instructions: "..." // <- COMPILE ERROR: reserved. Use instructionsSuffix. + // tools: [...] // <- COMPILE ERROR: reserved. Use extraTools. + }, +}; +``` + +`instructions` is the composed SKILL + contract prompt -- extend it with +`instructionsSuffix`. `tools` is the `wm_*` / cwd / spawn set -- extend it with +`extraTools`. `outputType` is the render's done/failed signal schema. `name` is +the node id. The `runOptions` passthrough reserves four more for the same reason +-- `context`, `maxTurns`, `signal`, and `stream` are harness- or Tier-A-owned, so +they are `Omit`-ed from `RunOptionsPassthrough` (use the Tier-A knobs). + + +This is the trust mechanism made mechanical. You cannot accidentally break the +render contract, because the contract-bearing fields are not in the type. The +compiler points you at the supported extension (`instructionsSuffix` / +`extraTools`) instead of letting a silent stomp ship. + + +## Precedence + +The rule is locked and it is simple: + +- **Consumer `agent.*` wins wholesale.** Whatever you set on `agent` is your + base; the harness merges its four reserved fields **over** it so the contract + can never be broken, and the type system forbids you setting those four at all. +- **Tier-A sugar fills only what you left unset.** `temperature` and `seed` are + folded in only where your `agent.modelSettings` did not already specify them. + Set `agent.modelSettings.temperature` and the sugar steps aside. +- **`extraTools` appends.** The built-in `wm_*` / cwd / spawn set is always + present; your tools are added to it. + +Concretely, `mergeModelSettings` keeps your `modelSettings.temperature` if you set +it and otherwise drops in the Tier-A `temperature`; `providerData` is +shallow-merged so the `seed` sugar coexists with your own `providerData` keys +(yours win). The harness-owned `name` / `instructions` / `tools` / `outputType` +always merge last. + +## The knob-routing table + +Every `@openai/agents` knob has exactly one home. This table is verified against +the shipped `RenderOptions` type, not a proposal. + +| Knob | Reach it via | Tier | +| --- | --- | --- | +| `modelSettings.*` (toolChoice, parallelToolCalls, maxTokens, reasoning, topP, penalties, truncation, store, promptCacheRetention, contextManagement, text, providerData, retry) | `agent.modelSettings` | B | +| `handoffs`, `inputGuardrails`, `outputGuardrails`, `mcpServers`, `mcpConfig`, `toolUseBehavior`, `resetToolChoice`, `prompt`, `handoffDescription`, `model` | `agent` | B | +| `tracingDisabled`, `workflowName`, `traceId`, `groupId`, `traceMetadata`, `modelProvider`, `sandbox` (SDK), `sessionInputCallback` | `runConfig` | B | +| `previousResponseId`, `conversationId`, `session`, `errorHandlers` | `runOptions` | B | +| `tracing` re-enable (`{ apiKey }`) | `tracing` | B | +| `temperature`, `seed` (sugar) | `temperature` / `seed` | A | +| `provider`, `model`, `maxTurns` (incl. `null`), `signal` | first-class | A | +| Instance lifecycle hooks (`AgentHooks.on` / `RunHooks.on`) | `agentFactory` / `runnerFactory` | C | +| A fully custom `Agent` / `Runner`, or a non-`@openai/agents` backend | `agentFactory` / `runnerFactory`, or `RenderBackend` (below) | C | + + +The passthrough is forward-compatible for **additive** `@openai/agents` field +additions only. The peer is pinned to a verified `@openai/agents` version, and +`SharedRunOptions` is a moving surface (it recently grew `errorHandlers` / +`sessionInputCallback`); `runOptions` tracks it. If a future SDK renames or +replaces a reserved field, growing the `Omit` set is a breaking change. "New +versions flow through automatically" is true for additive fields, not +unconditionally version-proof. + + +## Escape-hatch in practice + +The facade forwards a `RenderOptions` to every node via its `render` option. This +is the common path -- one config, applied uniformly. + +```ts +import { reactor } from "@openprose/reactor"; +import type { RenderOptions } from "@openprose/reactor/agents"; + +const render: RenderOptions = { + model: "anthropic/claude-sonnet-4", + temperature: 0.2, // Tier-A sugar -- fills modelSettings if unset + maxTurns: 24, // null is the deliberate unbounded opt-in + agent: { modelSettings: { providerData: { top_p: 0.9 } } }, // Tier-B, wins wholesale + runConfig: { workflowName: "nightly-digest" }, // runner-construction config + instructionsSuffix: "Prefer terse, sourced claims.", +}; + +const { reactor: r } = await reactor("./my-project", { directory: "./state", render }); +await r.ingest("source", { wake: { source: "external", refs: [] } }); +``` + +A fuller config showing each tier carrying its weight: + +```ts +const render: RenderOptions = { + provider: myScopedOpenRouterProvider, // scoped, never the global default client + agent: { // Tier B -- verbatim, wins wholesale + modelSettings: { + reasoning: { effort: "high" }, + maxTokens: 8000, + toolChoice: "required", + providerData: { transforms: ["middle-out"] }, + }, + inputGuardrails: [piiGuardrail], + handoffs: [escalationAgent], + // instructions / tools / outputType / name are Omit-ed -> COMPILE ERROR. + }, + extraTools: (defaults) => [...defaults, mySearchTool], // append, never replace + instructionsSuffix: "\nAlways cite sources inline.", + runConfig: { traceMetadata: { env: "prod" } }, + runOptions: { conversationId: "thread-42", errorHandlers: myErrorHandlers }, + signal: abortController.signal, + tracing: { apiKey: process.env.MY_TRACE_KEY! }, + maxTurns: null, // deliberate unbounded opt-in, preserved end-to-end +}; +``` + +## The `RenderBackend` injection seam + +The Tier-C factories let you rebuild the `Agent`/`Runner` while staying inside the +`@openai/agents` shape. `RenderBackend` goes one level deeper: it lets you replace +the **entire model session** -- record/replay, a proxy, or a non-`@openai/agents` +model (Claude, a local model) -- while **reusing** the harness's instruction +composition, working-dir prep, harvest, and cost mapping. + +The port is `@openai/agents`-free: it traffics only in the harness-composed +request and the structured session output, so a non-SDK backend implements it +without the peer dep. + +```ts +import type { + RenderBackend, + RenderSessionRequest, + RenderSessionOutput, +} from "@openprose/reactor/agents"; + +// One bounded session. The harness hands you the resolved request (composed +// instructions, resolved model + decoding settings, the built tools, the output +// schema, the pointer input, the per-render context, the turn cap, the signal) +// and maps your returned signal + usage into a receipt Cost. +const recordingBackend: RenderBackend = { + async runSession(req: RenderSessionRequest): Promise { + // ... run your model / replay a fixture using req.instructions, req.tools, ... + return { + signal: undefined, // undefined => the harness treats the session as failed + usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, + }; + }, +}; + +const { reactor: r } = await reactor("./my-project", { + directory: "./state", + adapters: { renderBackend: recordingBackend }, +}); +void r; +``` + +`RenderSessionRequest` is exactly what the harness resolved before the model runs +-- `node`, `instructions`, `model`, `modelSettings`, `tools`, `outputType`, +`input` (the short pointer run-input), `context`, `maxTurns`, and an optional +`signal`. Your backend runs one session and returns a `RenderSessionOutput`: the +structured `signal` (a done/failed signal, or `undefined` -> treated as failed, +nothing commits) and the token `usage` that becomes the receipt `Cost`. + +### The default backend, and what it stopped doing + +`createDefaultRenderBackend(config)` is the `@openai/agents` session, lifted +verbatim out of the historic inline render body. It resolves its provider and +runner **lazily and once** (a keyless build that never renders never constructs +them), threads the full Tier-A/B/C escape hatch, and owns the one +`@openai/agents`-specific addition the port abstracts away -- the +`spawn_subagent` tool, whose sub-agents inherit the same per-run escape hatch as +the parent render. + +The sub-agent primitive is itself a named export off this subpath: +`createSpawnSubagentTool(deps: SpawnSubagentDeps): Tool` +builds that `spawn_subagent` tool. Recursion is a first-class seam -- `deps.subTools` +is read at spawn time, so the factory may push the tool onto its own `subTools` +after building it, letting a sub-agent spawn its own helper, with the same +`maxTurns`/`Usage` backstop bounding every level. You import it when you assemble a +render backend by hand instead of taking `createDefaultRenderBackend`. + +One behavior changed, and it is a correctness fix worth naming. The default +backend **no longer calls the process-global `setTracingDisabled(true)`**. That +mutation stomped a consumer's `runConfig.tracingDisabled = false` and leaked +across every other `@openai/agents` user in the same process. Tracing is now +decided **per run** -- still disabled by default (safe egress), but overridable +through `RenderOptions.tracing` or `runConfig.tracingDisabled`, and scoped to this +render alone. + +### `RenderAgentSpec` -- what a Tier-C `agentFactory` receives + +When you supply `agentFactory`, you still must honour the render contract. The +spec hands you every harness-required piece pre-assembled, so you do not +reconstruct them: + +| Field | Meaning | +| --- | --- | +| `name` | the node id (the `Agent.name`) | +| `instructions` | the composed SKILL + contract prompt (+ any `instructionsSuffix`) | +| `model` | the model id/instance the render resolved | +| `modelSettings` | the merged decoding settings (temperature/seed + any `agent.modelSettings`) | +| `tools` | the built-in render tools + any `extraTools` -- the full tool surface | +| `outputType` | the render done/failed signal schema | +| `agent?` | your `agent.*` passthrough, for the factory to fold in itself | + +Add anything you like -- guardrails, handoffs, instance hooks via `agent.on(...)` +-- but dropping a spec field breaks the harvest/cost/commit contract, and at this +tier you own that risk. + +## The compile-session surface + +`createAgentRender` is the **run-phase** render. The same `@openai/agents` +machinery powers the **compile** phase, and it lives on this subpath too. Each +compile step is itself a SKILL-loaded session over the loaded contract set that +emits a structured artifact, which the harness then lowers deterministically. + +```ts +import { + compileForme, // -> the topology DAG (ReconcilerTopology) + compileCanonicalizer, // -> a node's run-time canonicalizer + compilePostcondition, // -> a node's commit-gate validators + loadContractSet, + runCompileSession, +} from "@openprose/reactor/agents"; +``` + +The full flow that mounts a project **without hand-authoring**: + +1. `loadContractSet(dir)` enumerates and slices the `*.prose.md` set (a dumb file + load -- nothing parses `.prose` semantics). +2. `compileForme(contracts, fingerprints)` runs the Forme session and lowers its + decisions into a mountable topology. +3. Per node, `compileCanonicalizer(node, contracts)` and + `compilePostcondition(node, contracts)` freeze the run-time canonicalizers and + the commit-gate validators. +4. Mount the topology + canonicalizers via `mountDag` / `createReactor` and run + dumbly. + +Each step takes a `CompileStepOptions` -- the same `provider` / `model` / +`temperature` / `seed` / `maxTurns` knobs **and the same escape hatch** (`agent` / +`runConfig` / `runOptions` / `signal` / `tracing`) as the render, because a +compile step is just another bounded session. `runCompileSession` is the runner +underneath all three; `renderContractSet(contracts)` is the contract-set evidence +it folds into the session's run input. + + +The Determinism boundary holds throughout. The **session** makes the one judgment +only it can -- semantic match (Forme), materiality (canonicalizer), or +postcondition mode. The deterministic scaffolding does the rest and produces an +artifact the dumb run phase executes. A compile that cannot emit its artifact +throws, and the prior compiled artifact stands. + + +## What's here, and what isn't + +The honesty that earns trust, on this surface specifically: + +- **Lossless is a claim with a backstop.** Tiers A and B cover every knob the SDK + anticipates; Tier C and `RenderBackend` guarantee no ceiling. If you find a knob + with no home, the `agentFactory` / `runnerFactory` / `RenderBackend` path + reaches it. +- **The working dir is scoped, not sandboxed.** The render's per-node `workspaceRoot` + has path-escape guards, but a shell command can still escape `cwd`. This is for + trusted, self-authored `.prose` projects; an OS sandbox is deferred. +- **`maxTurns` is a high explicit cap, not a budget.** The real spend signal is the + token `usage` mapped to each receipt's `Cost`. `maxTurns: null` opts out of the + cap deliberately. +- **Forward-compat is additive-only.** The version caveat above is the contract: + new SDK fields flow through, reserved-field renames do not. + +## Where to go next + + + + + + + + +These docs are orientation. The canonical execution behavior lives in the +open-source `open-prose` skill in the +[`openprose/prose`](https://github.com/openprose/prose) repo; if the docs and the +skill disagree, trust the skill. + +--- + +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/sdk/front-door.mdx b/content/docs/sdk/front-door.mdx new file mode 100644 index 0000000..fbab5d2 --- /dev/null +++ b/content/docs/sdk/front-door.mdx @@ -0,0 +1,371 @@ +--- +title: The front door (`.`) +description: The curated `@openprose/reactor` entry point -- the reactor() facade, the one typed Reactor handle, the assemblers, the substrate, observe(), and the driver vocabulary. +--- + +# The front door (`.`) + +`import { ... } from "@openprose/reactor"` is the one obvious entry point, for +engineers and for coding agents alike. It is a **deliberate curation** -- roughly +45 headline names -- not a 767-name firehose. The deep domain shapes, the +reconciler-construction spine, and the nine ex-doc-only domains all re-home under +[`/internals`](/sdk/internals); nothing was removed. The escape hatches live at +[`/agents`](/sdk/agents), [`/adapters`](/sdk/adapters), and +[`/run`](/sdk/run). + +Read this page top to bottom and you have the whole 90% path: one call takes a +directory of `.prose.md` contracts all the way to a booted, reconciling reactor, +and hands you a typed handle you drive and observe without a single cast. + + +The front door is **keyless at load**. Importing `reactor` never pulls a model +provider -- the model-bearing run phase is reached through a dynamic `import("../run")` +inside the facade body (the [offline boundary](/sdk/run)). You can `import` and +inspect before you ever set a key. + + +## Tier 1 -- the `reactor()` facade + +The one batteries-included top rung. `reactor(projectPath, options?)` compiles the +`.prose` project, assembles a durable reactor over its substrate, optionally boots +it to a fixpoint, and returns the typed [`Reactor`](#the-one-typed-reactor-handle) +handle. It is pure sugar: everything it does is reachable one rung down +(`compileProject` + `createReactor` + `boot()`), and **its return value is the +rung-1 handle**, so there is never a second parallel API. + +The facade returns a **record**, not the handle directly -- destructure it: + +```ts +import { reactor, textFile } from "@openprose/reactor"; + +const { reactor: r } = await reactor("./my-project", { directory: "./state" }); +// compile the .prose project, assemble a durable reactor over ./state, boot to a +// fixpoint (cold nodes render once; warm nodes memo-skip), hand back a live handle. + +console.log(r.view.cost); // { fresh, reused, byCause, byNode, total } -- the hero metric +console.log(r.view.dispositions); // { rendered, skipped, failed, coalesced } + +await r.ingest("source", { data: { "in.txt": textFile("hello") } }); +// deliver input, reconcile to a fixpoint, re-render only what the new input moved +``` + +This mirrors the facade's documented usage in `sdk/facade.ts` (the docstring +there shows the same `{ reactor: r } = await reactor(...)` destructure, then +`r.ingest(...)`). The `{ reactor: r }` destructure is the idiom -- the result +also carries `bootResults` and `pollConnectors`. + +### `ReactorFacadeResult` + +```ts +export interface ReactorFacadeResult { + readonly reactor: Reactor; // the typed handle (drive / observe / schedule) + readonly bootResults: readonly ReconcileResult[]; // the boot sweep's per-node results ([] when boot: false) + readonly pollConnectors: PollConnectors; // drive one poll of every armed connector (no-op when none) +} +``` + +### `ReactorOptions` + +Every field is a documented desugaring of a rung below. + +| Field | Type | What it does | +|---|---|---| +| `directory` | `string` | Durable truth + receipts. Omit for in-memory (ephemeral; tests/replay). | +| `mode` | `"run" \| "inspect"` | `"inspect"` is the **keyless posture** -- compiles, assembles, and boots without loading a render provider. | +| `boot` | `boolean` | Run the boot cold-miss sweep after assembly. Default `true`. | +| `render` | `RenderOptions & { buildRender?, ... }` | THE `@openai/agents` escape hatch, forwarded verbatim to every render. See [`/agents`](/sdk/agents). | +| `adapters` | `ReactorAdapters` | The backends to swap in (a `Partial` substrate + the model/ingress seams). | +| `schedule` | `ScheduleOptions` | Arm the self-driven continuity cadence off the handle's topology. | +| `compile` | `Omit` | Compile-phase knobs forwarded to `compileProject` (provider/model/skill, per-step overrides). | + +### `ReactorAdapters` + +The backends the facade swaps in. Every substrate field is **optional** -- the +facade defaults the rest (filesystem when `directory` is set, in-memory +otherwise): + +```ts +export interface ReactorAdapters { + readonly clock?: ClockAdapter; // defaults to the system clock + readonly storage?: StorageAdapter; // the ledger's append-only trail + readonly worldModel?: WorldModelStore; // defaults to a store over `directory` + readonly ledger?: MutableReceiptLedger; // defaults to the storage-derived durable ledger + readonly connectors?: readonly ConnectorAdapter[]; // arm ingress sources (§ Ingress, /adapters) + readonly renderBackend?: RenderBackend; // the model-injection seam (/agents) +} +``` + +The `renderBackend` and `connectors` seams are the live, documented injection +points; the substrate fields below are the persistence answer. + +## The one typed `Reactor` handle + +The return of `reactor()`, `createReactor()`, and `runProject()` is **one** +interface -- one object graph at multiple altitudes, never two parallel APIs. +Before `0.3.0` the assembler returned a nested `.dag`, so a driver cast to reach +`store`/`ledger`; the typed handle makes those first-class and the casts vanish. + +```ts +export interface Reactor { + // ── drive -- async-by-default (the live path) ── + ingest(node: string, input?: IngestInput): Promise; + tick(node: string): Promise; + drain(seeds: readonly WakeEvent[]): Promise; + boot(): Promise; + + // ── observe -- first-class read accessors (no casts) ── + readonly view: ReactorView; // the one read-and-rollup surface + onReceipt(cb: (receipt: LedgerReceipt) => void): () => void; // the ledger-is-telemetry tap + readonly ledger: MutableReceiptLedger; + readonly store: WorldModelStore; + readonly clock: ClockAdapter; + readonly topology: ReconcilerTopology; + + // ── self-driven cadence -- wired off the handle ── + scheduler(readFreshness: NodeFreshnessReader, nodes: readonly string[]): AsyncContinuityScheduler; + + // ── sync drive -- the deterministic test path, behind an explicit door ── + readonly sync: SyncDriveSurface; +} +``` + +A few facts that earn the handle its keep: + +- **Async-by-default.** A live render *is* one bounded LLM session = one `await`. + A synchronous render is trivially an already-resolved promise, so the async + verbs subsume the sync ones losslessly. The synchronous verbs (the deterministic + fake-render / test path) are preserved verbatim behind `handle.sync` -- never + amputated, just demoted from co-equal to a named door. +- **`view` re-derives on each read** off the live ledger, so a fresh read always + reflects the current trail. See [observe](#observe-one-read-and-rollup-surface). +- **`onReceipt` is the telemetry tap.** It fires for every committed receipt -- + `rendered` (real spend), `skipped` (memo hit), and `failed` -- across every + drive verb, and returns an unsubscribe function. +- **`topology` is load-bearing.** It threads the `scheduler`, and it reserves the + `createEpochDriver` seam (see [honest status](#honest-status)). +- **The full reconciler primitive is NOT on the handle.** Re-hosting the loop by + hand is engine-room altitude; reach it via [`/internals`](/sdk/internals). + +### `IngestInput` -- the `{ wake }` vs `{ data }` rule + +```ts +export interface IngestInput { + readonly wake?: Wake; // deliver a raw wake (the advanced path) + readonly data?: WorldModelFiles; // STAGE a payload, then fire a memo-MISS wake (requires an armed stager) +} +``` + +The bare `{ wake }` form delivers a raw wake. The `{ data }` form folds the +phantom-ingress stage-and-move dance into one call: the payload is staged into the +node's `::ingress` truth -- moving its `input_fingerprints` -- and then a +memo-MISS external wake fires so the node re-renders reading the staged input. + +The `{ data }` form **requires an armed ingress stager**. The `reactor()` facade +wires one (augmenting the topology with each node's phantom-ingress edge). A handle +assembled without a stager throws a legible error on `{ data }` rather than +silently dropping the payload -- deliver a raw `{ wake }` instead. + +## Tier 2 -- the assemblers (the rungs below the facade) + +When you need to mount a topology by hand -- a custom driver, a re-host of the +loop, an offline harness -- the assemblers are the rungs the facade desugars onto. +All return (or feed) the same `Reactor` handle. + +- **`createReactor(input)`** -- the durable keystone. Wires a `Substrate` + (clock + storage + world-model + ledger) and the per-node render bodies into the + run-phase surface and returns the typed handle. Take a `Substrate` (or a + `Partial`; missing pieces default), a compiled `topology`, and the + per-node `mounts` / `asyncMounts`. Restart-survival is real: re-`createReactor` + over the same storage + directory and the durable ledger re-derives every node's + last receipt, so `boot()` memo-skips the unchanged nodes. +- **`mountDag(input)`** -- the lower assembler. Wires the dumb reconciler over a + world-model store + a receipt ledger and exposes the drive verbs. `createReactor` + is the durable wrapper around it. +- **`renderAtom(input)` / `renderAtomAsync(input)`** -- one render of one node: + `(contract, evidence, prior world-model) -> (RenderProduct | RenderFailure)`. The + render atom is the smallest unit -- the same `(contract, evidence, prior) -> (new + world-model, receipt)` step the [OpenProse foundation](/openprose) declares, + realized as a function. + +```ts +export { + createReactor, type CreateReactorInput, + mountDag, type MountDagInput, type MountedDag, type NodeMount, + renderAtom, renderAtomAsync, + type RenderAtomInput, type RenderContext, type RenderProduct, type RenderFailure, +} from "@openprose/reactor"; +``` + +## Tier 2 -- the durable substrate + +Persistence has **one** answer: the `Substrate` record. `fileSystemSubstrate` +bakes in the storage to ledger restart-survival derivation; `inMemorySubstrate` is +the ephemeral test/replay form. + +```ts +import { fileSystemSubstrate, inMemorySubstrate, type Substrate } from "@openprose/reactor"; + +export interface Substrate { + readonly clock: ClockAdapter; + readonly storage: StorageAdapter; + readonly worldModel: WorldModelStore; + readonly ledger: MutableReceiptLedger; +} + +const durable = fileSystemSubstrate({ directory: "./state" }); // ledger derived over the same storage +const ephemeral = inMemorySubstrate(); // tests, replay +``` + +The blessed à-la-carte leaf factories stay on the front door for custom wiring and +the spread-override idiom: + +```ts +// one storage swap, the rest of the durable substrate intact +const substrate = { ...fileSystemSubstrate({ directory }), storage: myStorage }; +``` + +Available leaf builders: `createFileSystemStorageAdapter`, +`createMemoryStorageAdapter`, `createFixedClockAdapter`, +`createSystemClockAdapter`, `createFileSystemReceiptLedger`, +`createFileSystemWorldModelStore`, `createInMemoryWorldModelStore`. The full set of +backend ports and their custom-backend contracts lives at +[`/adapters`](/sdk/adapters). + +## `observe()` -- one read-and-rollup surface + +`observe(source)` is the SDK's **single** read-and-rollup entry point. The +"fresh-vs-reused" hero metric -- _cost scales with surprise, not the clock_ -- is +computed **here, once**, and every consumer (the `serve` line, the HTTP `/cost` +endpoint, the observability commands, the DevTools meter) reads off this one shape +rather than re-implementing the rollup. + +```ts +import { observe, type ReactorView, type CostRollup } from "@openprose/reactor"; + +// FOUR source forms share ONE rollup: +observe(r); // a live Reactor handle +observe({ ledger }); // a replayed trail +observe({ receipts }); // a trail array +observe({ results }); // a synchronous drive return +``` + +```ts +export interface ReactorView { + readonly receipts: readonly LedgerReceipt[]; + readonly byNode: ReadonlyMap; + readonly dispositions: Record; // rendered/skipped/failed/coalesced, zero-filled + readonly cost: CostRollup; + verifyChain(): { ok: boolean; errors: readonly string[] }; +} + +export interface CostRollup { + readonly fresh: number; // tokens surprise actually drove (a moved fingerprint = a real render) + readonly reused: number; // memo-hit / skipped-render tokens + readonly byCause: Readonly>; // per surprise_cause + readonly byNode: Readonly>; // per node + readonly total: CostBucket; +} +``` + +The single `CostRollup` carries **both** bucketings -- `byCause` (which wake source +drove the spend) and `byNode` (which node spiked) -- so nothing is lost to a parallel +rollup. `verifyChain()` is the tamper / chain-consistency check (see the honest +note on [what "signed" means](/reactor/reconciler-and-receipts)). + +For the DevTools replay viewer, `createReplaySession` shapes a saved trail (per-receipt +moved-facet diff + cumulative rollup) and is re-exported from this front door (and +from [`/run`](/sdk/run)). + +## The driver vocabulary + +The names a driver actually reaches for, all on the front door. + +### Branded identity + +`NodeId`, `Facet`, and `Fingerprint` are branded strings -- the surface is +self-documenting and agent-correct (the `"*"`-never-propagates and +`surprise_cause !== wake.source` footguns become compile errors). The ergonomic +boundary keeps author literals working: every **input** position accepts a plain +`string` (via `NodeIdInput` / `FacetInput`), so `r.ingest("source")` still +compiles, while everything the SDK **returns** is branded and tracked. `Fingerprint` +is branded hard -- consumers never author it. + +```ts +import { asNodeId, asFacet, ATOMIC_FACET } from "@openprose/reactor"; +import type { + NodeId, NodeIdInput, Facet, FacetInput, Fingerprint, FingerprintMap, +} from "@openprose/reactor"; + +asNodeId("scout-desire"); // string -> NodeId +asFacet("status"); // string -> Facet +ATOMIC_FACET; // the reserved whole-truth token every FingerprintMap carries +``` + +### Wake constructors + +One event type, three sources. The constructors build the `{ source, refs }` wake +a driver hands to `ingest` / the reconciler, so the literal is never re-derived by +hand at every ingress / continuity-fire site. + +```ts +import { inputWake, selfWake, externalWake } from "@openprose/reactor"; + +externalWake(); // { source: "external", refs: [] } -- a fresh external arrival +inputWake(...refs); // an upstream input moved +selfWake(...refs); // the continuity cadence fired +``` + +### Files, receipts, ingress + +```ts +import { + files, textFile, jsonFile, // build the WorldModelFiles a node reads/writes + verifyReceipt, verifyReceiptChain, // the chain-consistency check (the v1 "signed" meaning) + ingressSourceFor, augmentTopologyWithIngress, buildIngressStager, armConnectors, +} from "@openprose/reactor"; +``` + +`textFile` / `jsonFile` are the `{ data }` payload builders you saw in the facade +snippet. `verifyReceipt` / `verifyReceiptChain` verify the per-node `prev`-linked +receipt chain -- chain-consistency, not a cryptographic byte hash (see [honest +status](#honest-status)). The ingress building blocks are what +`reactor({ adapters: { connectors } })` wires; reach for them directly only when +hand-rolling a poll loop over the lower `pollGateway` / cursor primitives at +[`/adapters`](/sdk/adapters). + +### Reconcile vocabulary + types + +```ts +import type { + ReconcileResult, ReconcileDisposition, RenderOutcome, WakeEvent, + Receipt, LedgerReceipt, Cost, Wake, WakeSource, +} from "@openprose/reactor"; +``` + +## Honest status + +Honesty is the trust mechanism -- what is built, and what is not, stated plainly. + + +- **`verifyReceipt` / `verifyChain()` check chain-consistency, not crypto.** The + v1 "signed" meaning is tamper-evident `prev`-linked chaining over a content-addressed + trail -- not a cryptographic byte-hash signature. The `SignerPort` is declared so the + crypto milestone is a pure backend swap, but no crypto signer ships in `0.3.0`. +- **No benchmark numbers are asserted** anywhere in these docs. The "cost scales + with surprise" rollup is the mechanism; published numbers are pending. +- **The epoch driver is reserved, not built.** The handle exposes everything a + rollover loop needs (`topology` + `drain` + `onReceipt`), so `createEpochDriver` + lands additively later. The `Reserved*` epoch-driver shapes are type-only -- no + value ships in `0.3.0`. +- **The fixpoint is specified and deferred.** It attaches additively to this + surface (a relocated `input_fingerprints` memo key, no `Receipt`-field change). + + +## Where to go next + + + + + + + diff --git a/content/docs/sdk/index.mdx b/content/docs/sdk/index.mdx new file mode 100644 index 0000000..ce02cfe --- /dev/null +++ b/content/docs/sdk/index.mdx @@ -0,0 +1,185 @@ +--- +title: SDK API Reference +description: The @openprose/reactor 0.3.0 public surface -- six reasoned entrypoints, one curated front door, and an honest map of what's built and what isn't. +--- + +# SDK API reference + +`@openprose/reactor` is the [Reactor](/reactor) layer of one system, made +programmable. [OpenProse](/openprose) is the foundation -- the paradigm where you +declare the outcomes you want kept true, as Markdown contracts. The +[CLI](/cli/overview) is one driver of this SDK; so is the +[DevTools replay viewer](/reactor-devtools). This reference documents the surface +both of them sit on, so you can host a reactor from your own code. + +One call takes a directory of `.prose.md` contracts all the way to a booted, +reconciling reactor and hands back one typed handle: + +```ts +import { reactor } from "@openprose/reactor"; + +// Compile ./my-project, assemble a durable reactor over ./state, boot to a +// fixpoint (cold nodes render once; warm nodes memo-skip), return a live handle. +const { reactor: r } = await reactor("./my-project", { directory: "./state" }); + +console.log(r.view.cost); // { fresh, reused, byCause, byNode } -- the hero metric +await r.ingest("source", { data: { "in.txt": textFile("hello") } }); +``` + +That is the [front door](/sdk/front-door). Everything deeper is reachable, but it +is deliberately one rung down. This page is the map. + + + Source of truth. Every name, path, and version on these pages is grounded in the + shipped `@openprose/reactor@0.3.0` package -- its `exports` map and its + entrypoint barrels -- not a proposal. If a code example and prose ever disagree, + trust the code; if the docs and [the skill](/openprose) disagree, trust the + skill. + + +## The six reasoned entrypoints + +The package ships **six** entrypoints (plus `./package.json`). This is not +arbitrary surface area: each split exists for a reason an agent can reason about +before importing. The curated front door is `.`; the rest are isolation seams you +reach for only when you need what they isolate. + +| Entrypoint | What it is | Why it's split out | +| --- | --- | --- | +| `@openprose/reactor` | **The front door** -- the `reactor()` facade, the one typed `Reactor` handle, the assemblers, the substrate factories, `observe`, and the vocabulary a driver needs. | This is the one obvious door. A deliberate ~45-name curation, not a firehose. Start here. | +| `@openprose/reactor/agents` | The full `@openai/agents` escape hatch -- the layered render config and the compile-session surface. | **Peer-dep isolation.** Importing it pulls the optional `@openai/agents` + `zod` peers; the keyless core installs neither. It is the full passthrough, not a lossy wrapper. | +| `@openprose/reactor/adapters` | The injection boundary -- substrate backends, the gateway-ingress + cursor toolkit, record/replay, passthrough adapters, and the port contracts a custom backend implements. | The seam you wire custom backends against. Kept distinct so swapping persistence or a gateway is a one-import change. | +| `@openprose/reactor/run` | The **offline boundary** -- `compileProject` / `runProject` (model-bearing). | These deep-import the live agent adapters. Kept **off** the front door so a keyless inspection/replay build never loads a provider. The facade reaches them by dynamic import. | +| `@openprose/reactor/run/types` | The type-only mirror of the run-phase shapes (incl. the `Reactor` handle type). | Carries **no** `@openai/agents` value import -- so a consumer (the CLI) can type the handle it drives without crossing the offline boundary. | +| `@openprose/reactor/internals` | The engine room -- the reconciler-construction spine and every deep domain shape (receipt / cost / forme / memo / composition / forecast / evidence-plan / projection / canonicalizer), plus the deprecated `Reactor*`-prefixed port aliases. | The honest deep door for power users re-hosting the reconciler loop by hand. Stable but deep; nothing here was removed from the package, it is simply not on the headline surface. | + + + Folding `.` to a curation removes the `nodenext` resolution cliff for the 90% + path: the root import resolves under legacy `node` resolution, so the cliff now + bites only the explicit escape-hatch subpaths -- where a power user is already in + a real `tsconfig`. + + +## Read these pages in order + + + + + + + + + +## Install and prerequisites + +The SDK is a published npm package. Install it per-project alongside its optional +peers (you only need the peers when you actually render against a model): + +```sh +npm install @openprose/reactor +# When you render against @openai/agents (the live run path): +npm install @openai/agents zod +``` + +- **Version:** `@openprose/reactor@0.3.0`. (The CLI is `@openprose/reactor-cli@0.2.0` + and DevTools is `@openprose/reactor-devtools@0.2.0`; they version independently. + The OpenProse language and skill version separately again -- do not attach + `0.3.0` to the language.) +- **Node:** `>=20.0.0`. +- **Peers (optional):** `@openai/agents@^0.11.6` and `zod@^4.0.0`. They are marked + optional in `peerDependenciesMeta`: the keyless inspection/replay surface never + loads them. You pull them by importing `/agents`, `/run`, or the live render path. + + + Prefer a local-first install over a global one, and prove the system keyless + before you spend a token. For the full agent-first setup path -- keyless proof, + then `init` to `doctor` to `compile`, then going live, then where contracts live + -- see [the OpenProse setup guide](/openprose/setup). + + +## What's built, and what isn't + +In the spirit of the receipts, the honest status. For coding agents onboarding on +behalf of a user, this block is the trust mechanism: the SDK does not pretend. + +**Built and runnable.** The render atom, the content-addressed world-model store +(with the published-truth / private-workspace split), the compiled canonicalizer +with facets, Forme's wiring with diagnostics and acyclicity, postcondition-gated +commits with **no judge step**, the receipt ledger with chain verification, and +composition pins are implemented and exercised by a test suite that runs +offline -- no model calls in the commit gate. The reconciler's surprise property +is enforced as a **tested invariant**: when an input fingerprint doesn't move, the +render body provably never runs. + +**Deliberately not yet here.** + +- **No benchmark or dollar numbers.** We are not going to pretend a structural + invariant is a measured speedup. Designing honest long-horizon benchmarks is the + help we most want -- these pages assert no performance figure. +- **The signer is an explicit null state.** In v1, *signed* means tamper-evident + at the meaning layer and chain-consistent -- **not** yet a cryptographic byte + hash. The signature is an honest null + (`{ scheme: "none", null_reason: "no-signer-adapter-configured" }`). The reserved + `SignerPort` seam means the crypto milestone is a backend swap, not a reshape. +- **No timestamp or actor on receipts yet.** The chain is content-addressed and + ordered; wall-clock attribution is named, not shipped. +- **The fixpoint is specified and deferred.** The topology as a responsibility (an + epoch driver layered over the fixed-topology handle) is designed -- the handle + already exposes `topology` + `drain` + `onReceipt` for exactly that loop -- but + the driver itself is a reserved, forward-only seam, declared and not built. +- **Facet inference and ledger compaction are named roadmap**, not shipped. + +These caveats are surfaced inline where they bite, too: +[the reconciler and receipts](/reactor/reconciler-and-receipts) for what "signed" +means in v1, [world-model and fingerprints](/reactor/world-model-and-fingerprints) +for the signer caveat, and [CLI configuration](/cli/configuration) for the +deferred knobs. + +## Where to go next + + + + + + + + +--- + +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/sdk/internals.mdx b/content/docs/sdk/internals.mdx new file mode 100644 index 0000000..cc7889b --- /dev/null +++ b/content/docs/sdk/internals.mdx @@ -0,0 +1,215 @@ +--- +title: /internals +description: 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. +--- + +# `/internals`: the engine room + +`@openprose/reactor/internals` is the **honest deep door**. Every public name +that is not on the curated front door (`.`), the `@openai/agents` escape hatch +([`/agents`](/sdk/agents)), the substrate and ingress backends +([`/adapters`](/sdk/adapters)), or the offline run-phase boundary +([`/run`](/sdk/run)) re-homes here. + +Nothing was removed from the package. These names are simply not on the headline +surface. If you are wiring a project from the [front door](/sdk/front-door) you +will never import this subpath -- the facade, the assemblers, and the substrate +factories already cover the 90% path. You reach for `/internals` only when you +are re-hosting the reconciler loop by hand against your own ports, or reading the +deep domain shapes the front door wraps. + + + Think of this as **stable-but-deep**, not unstable. The names here back the + same engine the front door drives; they are de-emphasized, not experimental. + But it is a wide surface, and the headline vocabulary on the + [front door](/sdk/front-door) is the one we curate for agents. Start there. + + +## What lives here + +`/internals` re-exports the whole engine, grouped by the design doc each module +implements (`src/internals/index.ts`): + +| Group | Module | What it is | +| --- | --- | --- | +| The coordination spine | `../shapes` | The shared discriminated unions every other module reads (`SHAPES.md`). | +| Cycle + predicate keep-home | `../cycle` | The cycle/predicate shapes (`SHAPES.md` §8). | +| The world-model store | `../world-model` | The content-addressed store object (`architecture.md` §5.2, §10). | +| The compiled canonicalizer | `../canonicalizer` | The per-node canonicalizer Forme froze (`architecture.md` §3.2). | +| The compiled postcondition validators | `../postcondition` | The commit-gate validators (`architecture.md` §3.3). | +| Forme | `../forme` | The compile-phase wiring (`architecture.md` §3.1, §6.3). | +| The receipt ledger object | `../receipt` | The receipt builder and the ledger trail (`SHAPES.md` §4). | +| The memo re-key + skip decision | `../memo` | The memo key and the skip disposition (`SHAPES.md` §3). | +| Composition | `../composition` | Subscriptions-as-props, pins-as-read-isolation (§7). | +| Forecast | `../forecast` | The continuity clock + self-recheck (`architecture.md` §3.5). | +| Evidence resolution | `../evidence-plan` | Evidence-by-reference resolution (`delta.md` §A3.1). | +| Surprise-cost + projection | `../cost`, `../projection` | The observable cost shapes + receipt projection (`delta.md` §A4). | +| The run-phase reconciler spine | `../reactor` | The dumb reconciler loop (`architecture.md` §4.1). | +| The injection boundary | `../adapters` | The adapter port contracts, including the deprecated `Reactor*`-prefixed aliases (`architecture.md` §5.3). | +| The SDK assembler spine | `../sdk` | `createReactor` / `mountDag` internals. | + +These were nine separate doc-only subpaths in the pre-`0.3.0` surface +(`/receipt`, `/cost`, `/forme`, `/evidence-plan`, `/memo`, `/forecast`, +`/composition`, `/projection`, `/canonicalizer`). They held zero consumer usage +as standalone import paths, so `0.3.0` folded them into one engine-room door. The +load-bearing few names they carried -- `ATOMIC_FACET`, `verifyReceipt`, +`verifyReceiptChain`, `Receipt` -- were **pulled forward** onto the +[front door](/sdk/front-door); the rest stay reachable here. Only the import path +changed. No name became unavailable. + + + `createSkippedReceipt` ships from both `../receipt` and `../memo`. `/internals` + pins the canonical `../receipt` builder over the `../memo` star-export, so the + name resolves to one implementation -- not a star-merge collision. + + +## The reconciler-construction spine + +This is the reason to be here: re-hosting the run-phase loop by hand against +custom ports, instead of going through the [facade](/sdk/front-door) or +[`runProject`](/sdk/run). The constructor takes injected ports and a **fixed** +compiled topology and returns a handle: + +```ts +import { + createReconciler, + type ReconcilerPorts, + type ReconcilerTopology, + type ReconcilerHandle, + inboundEdges, + COLD_START_ATOMIC_FINGERPRINT, +} from "@openprose/reactor/internals"; + +// Construct the dumb reconciler over injected ports + a fixed compiled topology. +// No judge, no policy, no backstop -- the entire decision is fingerprint +// comparison. +const handle: ReconcilerHandle = createReconciler(ports, topology); +``` + +The topology is a **constructor input** (`createReconciler(ports, topology)`, +`src/reactor/index.ts`), not mutable state -- which is exactly why the deferred +fixpoint (topology-as-responsibility) attaches additively later: "an epoch +*names* that fact" without reshaping the handle. + +The handle exposes the four drive verbs, sync and async: + +| Member | What it does | +| --- | --- | +| `reconcile(event)` | Handle one wake for one node: memo/skip, single-flight schedule, commit, propagate. Returns the disposition, the receipt written, and the downstream wakes to enqueue. | +| `drain(initial)` | Drain a seeded queue of wakes to a fixpoint, honoring single-flight + coalescing + propagation. Returns the ordered per-node results. | +| `reconcileAsync(event)` | The async sibling: awaits the one bounded LLM session. A wake delivered while a render is in flight marks the node dirty and collapses into exactly one coalesced follow-up -- never a second concurrent render, never a lost wake. | +| `drainAsync(initial)` | The async fixpoint loop: `await`s each `reconcileAsync` fully before shifting the next event, preserving the sync path's exact ordering and one-render-in-flight guarantee. | + +The supporting names complete the loop you would hand-roll: `inboundEdges` +resolves a node's subscribed edges off the topology; `memoKeyMoved`, +`movedFacetsBetween`, and `propagationTargets` are the comparison + propagation +helpers; and `COLD_START_ATOMIC_FINGERPRINT` (`asFingerprint("cold-start:empty")`) +is the reserved cold-start atomic fingerprint a node carries before its first +render. The full ledger-port (`ReceiptLedgerPort`) and world-model-port +(`WorldModelStorePort`) contracts the handle reads through are here too. + + + Most callers should **not** hand-host the reconciler. The + [facade](/sdk/front-door) wires `createReactor` + the + [run-phase compile](/sdk/run) for you and returns one typed handle. Reach for + `createReconciler` only when you genuinely own the ports -- a custom evidence + plan, an alternate persistence story, an embedding host that drives the loop + on its own cadence. See [the reconciler and receipts](/reactor/reconciler-and-receipts) + for the behavior you are taking responsibility for. + + +## The deprecated `Reactor*`-prefixed port aliases + +In the pre-`0.3.0` surface the adapter port types carried a `Reactor*` prefix. +`0.3.0` made the short names the headline vocabulary -- `StorageAdapter`, +`WorldModelStore`, `ClockAdapter` -- which is what [`/adapters`](/sdk/adapters) +and the [`Substrate`](/sdk/adapters) record use. The original `Reactor*`-prefixed +names are kept reachable from `/internals` as deprecated aliases +(`src/adapters/types.ts`): + +```ts +// The short name is the headline vocabulary used by Substrate; +// the Reactor*-prefixed original is the deprecated alias, kept reachable here. +export type StorageAdapter = ReactorStorageAdapter; +// likewise: WorldModelStore = ReactorWorldModelStore, +// ClockAdapter = ReactorClockAdapter +``` + +These are type aliases, not separate implementations -- a downstream still +pinned to `ReactorStorageAdapter` keeps compiling. Prefer the short names in new +code. Nothing was removed; the prefixed names are simply no longer the door you +are pointed at. + +## The receipt and projection helpers + +Receipt **verification** lives on the [front door](/sdk/front-door) +(`verifyReceipt` / `verifyReceiptChain`). The **proof-projection** helpers -- +deriving a sharable proof summary that avoids private payload fields -- live here, +alongside the deep receipt shapes they operate on (`src/receipt/index.ts`, +`src/projection/index.ts`): + +```ts +import { verifyReceipt } from "@openprose/reactor"; +import { + inspectReceiptProof, + projectReceiptProof, + type LedgerReceipt, + type ReceiptProofInspection, +} from "@openprose/reactor/internals"; + +export function inspectStoredReceipt(receipt: LedgerReceipt) { + const verification = verifyReceipt(receipt); + if (!verification.ok) { + throw new Error(verification.errors.join("; ")); + } + return inspectReceiptProof(receipt); +} + +export function publicReceiptEvidence(proof: ReceiptProofInspection) { + const result = projectReceiptProof({ tier: "public", proof }); + if (!result.ok) { + throw new Error(result.errors.join("; ")); + } + return result.projection; +} +``` + +`inspectReceiptProof` reads a stored receipt into a `ReceiptProofInspection`; +`projectReceiptProof({ tier, proof })` projects that inspection down to a tier +(for example `"public"`) that drops private payload fields. The verify split is +deliberate: the everyday `verifyReceipt` is on the front door, and the deeper +projection machinery -- bound to the receipt domain shapes -- stays in the engine +room. + + + Honest scope, unchanged from the [receipt model](/reactor/reconciler-and-receipts): + in v1 *verified* means tamper-evident at the meaning layer and chain-consistent + -- not yet a cryptographic byte hash, and a receipt records *what* changed and + *why*, not *when* or by *whom*. `projectReceiptProof` projects what the chain + attests; it does not manufacture an audit fact the receipt does not yet carry. + + +## Where to go next + + + + + + + diff --git a/content/docs/sdk/meta.json b/content/docs/sdk/meta.json new file mode 100644 index 0000000..ec713af --- /dev/null +++ b/content/docs/sdk/meta.json @@ -0,0 +1,4 @@ +{ + "title": "SDK API Reference", + "pages": ["index", "front-door", "agents", "adapters", "run", "internals"] +} diff --git a/content/docs/sdk/run.mdx b/content/docs/sdk/run.mdx new file mode 100644 index 0000000..969892b --- /dev/null +++ b/content/docs/sdk/run.mdx @@ -0,0 +1,284 @@ +--- +title: The offline boundary +description: 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](/sdk) 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](/sdk/front-door) 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](/reactor-devtools) + 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. + +```ts +// @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](/reactor/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. + +```ts +import { compileProject } from "@openprose/reactor/run"; + +const compiled = await compileProject({ + contractsDir: "./my-project/src", +}); +``` + +```ts +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: + +```ts +export interface CompiledProject { + readonly reconcilerTopology: ReconcilerTopology; // Forme's output -- the DAG + readonly perNode: Readonly>; // compiled canonicalizers + validators + readonly contracts: ContractSet; // the source the sessions read + readonly contractFingerprints: Readonly>; // 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: + +```ts +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](/sdk/adapters) 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. + +```ts +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: + +```ts +export interface RunProjectInput { + readonly compiled: CompiledProject; // the output of compileProject + readonly substrate?: Partial; // 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](/sdk/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](/sdk/agents) +for the full `RenderOptions` tiers. + +```ts +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: + +```ts +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](/sdk/front-door) -- +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. + +```ts +// @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._ diff --git a/content/docs/start/first-program.mdx b/content/docs/start/first-program.mdx deleted file mode 100644 index bab913a..0000000 --- a/content/docs/start/first-program.mdx +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: Your first program -description: Write and run a tiny OpenProse program. ---- - -# Your first program - -Hello world proves the loop. This program takes one input, asks the agent to -explain it, and leaves a run trace you can inspect afterward. - -Create `hello-openprose.md`: - -```markdown ---- -name: hello-openprose -kind: program ---- - -### Requires - -- `topic`: thing to explain - -### Ensures - -- `summary`: a short explanation of the topic - -### Execution - -Tell the user what `topic` means in plain English. -Write the result to `summary.md`. -``` - -Run it from an agent session: - -```text -prose run hello-openprose.md --topic "why traces matter" -``` - -Or, if you installed the CLI, run the same command from a shell: - -```bash -prose run hello-openprose.md --topic "why traces matter" -``` - -In both cases, the command is asking an agent harness to run the program. The -CLI does not execute the program by itself. - -## What happened - -The agent reads `hello-openprose.md` as a Contract Markdown program. - -`Requires` says the program needs a `topic`. The command supplies it with -`--topic "why traces matter"`. - -`Ensures` says the run must produce a `summary`. This is the promise you can -check when the run is done. - -`Execution` pins the tiny bit of choreography for this first program. Most -OpenProse programs start declarative and only add execution steps when order -matters. - -Because this program has one component and no `Services` section, there is no -service graph to wire and no subagent fan-out required. The agent can run it -directly. - -## Inspect the trace - -After the run, look under the newest directory in `.prose/runs/`. - -You should find the durable record of the run: the inputs, the execution log, -and the output artifacts produced by the program. Exact filenames can vary as -the tooling evolves, but the point is stable: a run leaves a receipt on disk -instead of disappearing into chat history. - -That receipt is why OpenProse is more than a long prompt. The contract, the -artifacts, and the trace can be reviewed, versioned, and improved. - -Next: [try a useful workflow](/start/first-useful-workflow). diff --git a/content/docs/start/first-useful-workflow.mdx b/content/docs/start/first-useful-workflow.mdx deleted file mode 100644 index 5164c4b..0000000 --- a/content/docs/start/first-useful-workflow.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Your first useful workflow -description: Run a small multi-service workflow where OpenProse starts to matter. ---- - -# Your first useful workflow - -Hello world proves that the loop works. A useful OpenProse workflow proves why you would write a program at all. - -Use this example when the work wants more than one point of view. A code review is a good first shape because the roles are obvious: one reviewer can think about security, another can think about performance, another can think about style, and a final service can turn the separate notes into one report. - - - -## What this program says - -The program has one input: - -- `code`: the code to review - -It promises one output: - -- `report`: a unified review report that covers security, performance, and style - -The `Services` section names the roles that can satisfy that promise. The roles are intentionally plain: - -- `security-reviewer` looks for risks and unsafe assumptions -- `perf-reviewer` looks for slow paths and waste -- `style-reviewer` looks for readability and local conventions -- `synthesizer` turns the separate reviews into one report - -That is the first useful OpenProse move: split the work where splitting helps, then make the handoff explicit. - -## Run it small - -Start with a small target. A single file, a short diff, or a narrow directory is better than a whole repo for a first run. - -From an agent session with the `open-prose` skill installed: - -```text -prose run vendor/prose-examples/16-parallel-reviews/index.md -``` - -The program requires `code`. If the runner does not already have a value for that input, it should ask you for one. Give it a path, a diff, or a compact snippet. - -When the run is finished, read the output as a contract check: - -- Did the report cover security, performance, and style? -- Did the separate points of view stay separate until synthesis? -- Did the final report prioritize what matters instead of pasting every note? -- Did the run leave enough trace to understand what happened? - -If the answer is no, the program needs a tighter contract. That is normal. OpenProse programs are meant to be revised like code. - -## Make one change - -Try one small edit after the first run. - -If the report is too broad, make the input narrower. If the reviewers overlap too much, give one role a sharper name. If the final report is noisy, add an `Ensures` obligation for prioritization. - -For example, a stricter output contract might say: - -```markdown -### Ensures - -- `report`: unified review report covering security, performance, and style -- each issue has: severity, evidence, and a concrete next step -- issues are sorted by risk before polish -``` - -That edit changes the shape of the run without turning the program into a long prompt. The contract still says what must be true when the work is done. - -## Why this is different from one prompt - -You could ask an agent to "review this code for security, performance, and style." For a small snippet, that is fine. - -The program becomes useful when you want the shape to survive: - -- the same review should run again next week -- different roles should work independently -- the synthesis should be inspectable -- the output contract should be reviewable in git -- future changes should improve the workflow instead of disappearing into chat history - -That is the OpenProse taste test. Reach for it when the prompt has become a process. - -Next: [learn contracts](/think/contracts-not-prompts). diff --git a/content/docs/start/install.mdx b/content/docs/start/install.mdx deleted file mode 100644 index 4292796..0000000 --- a/content/docs/start/install.mdx +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: Install -description: Install the OpenProse skill and run through the shell entrypoint. ---- - -# Install - -OpenProse runs in an agent session. The important install is the -`open-prose` skill, because the skill tells the agent how to read an -OpenProse program, wire services, run the work, and write a trace. - -The `prose` CLI is optional. It is a shell entrypoint into an agent harness, -not a separate OpenProse VM. It packages commands like `prose run file.md` -into an agent-session instruction, then streams the harness output back to -your terminal. - -## Requirements - -You need: - -- An agent harness that can read and write files in the working directory. -- A subagent or session-spawning primitive for multi-service programs. -- Node.js 18 or newer if you use the CLI. -- Credentials for the harness you choose. - -Single-component programs can run without subagents. Multi-service programs -need a harness that can start isolated worker sessions. - -## Install the skill - -Install the `open-prose` skill: - -```bash -npx skills add openprose/prose -``` - -After the skill is available, an OpenProse command inside an agent session is -an instruction to that agent: - -```text -prose run hello-openprose.md -``` - -The agent should not look for a `prose` binary just because the instruction -starts with `prose`. It should load the skill, read the Markdown contract, and -run the program in-session. - -## Install the CLI - -If you want to run OpenProse from a shell, install the CLI: - -```bash -npm install --global @openprose/prose-cli -prose --help -``` - -For project-local or one-off use, you can also invoke the package through -`npx`: - -```bash -npx @openprose/prose-cli doctor -``` - -The installed binary is named `prose`. - -## Check your setup - -Run `doctor` before the first real program: - -```bash -prose doctor -``` - -The CLI checks whether the `open-prose` skill is available for the selected -harness. For a specific harness, pass `--harness`: - -```bash -prose doctor --harness claude-sdk --install -``` - -The default harness is `codex-sdk`. Other supported harnesses include -`claude-sdk`, `codex`, `claude`, and `mock`. - -## Run path - -There are two common ways to run the same program: - -```text -prose run hello-openprose.md -``` - -Use that form inside an agent session. - -```bash -prose run hello-openprose.md -``` - -Use that form from a shell after installing the CLI. The shell command still -hands execution to the selected agent harness. The harness and skill are what -run the OpenProse program. - -When a run completes, OpenProse writes durable state under `.prose/runs/`. -That trace is where you inspect inputs, outputs, service workspaces, and the -execution log. - -Next: [write your first program](/start/first-program). diff --git a/content/docs/start/meta.json b/content/docs/start/meta.json deleted file mode 100644 index 688183f..0000000 --- a/content/docs/start/meta.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "Start", - "pages": [ - "what-is-openprose", - "install", - "first-program", - "first-useful-workflow" - ] -} diff --git a/content/docs/start/what-is-openprose.mdx b/content/docs/start/what-is-openprose.mdx deleted file mode 100644 index 2356944..0000000 --- a/content/docs/start/what-is-openprose.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: What is OpenProse? -description: The short mental model for OpenProse. ---- - -# What is OpenProse? - -OpenProse is a programming language for AI sessions. - -A modern agent session already has a lot of the pieces of a computer: a model, a tool loop, a filesystem, subprocesses or subagents, and persistent state. OpenProse gives that system a program format. - -The program is a Markdown contract. An agent reads the contract, wires the services, runs the work, passes artifacts through the filesystem, and leaves a durable trace. - -```text -Markdown contract - -> Forme wiring - -> manifest - -> VM run - -> workspaces + bindings + trace -``` - -## The AI session is the computer - -OpenProse does not try to make the model deterministic. It gives the model a contract, a workspace, and a trace so complex work has a shape you can inspect and improve. - -The VM is the model plus its harness: tools, files, shell access, browser access, subagents, and whatever state the run is allowed to use. In a Prose Complete harness, the agent and the skill execute the program inside that session. - -## The program is Markdown - -Markdown is not a gimmick here. The executor is an agent, so the program should be readable by agents. The user is a human builder, so the program should be readable in review. Git should be able to show a useful diff. - -The important parts of a small program are usually easy to spot: - -- `Requires` names what the service needs. -- `Ensures` names what the service must produce. -- `Services` names the work that can satisfy those contracts. -- `Execution` pins order when choreography matters. - -You do not need all of that for every program. Start declarative. Add wiring when relationships matter. Add execution when order matters. - -## Contracts, services, Forme, and traces - -A prompt says, "do this." A contract says, "given these inputs, these things must be true when you are done." - -Services are the pieces of work in the program. Forme is the container that reads their contracts and decides how they fit together. The manifest is the materialized wiring for a run. The VM is the agent session that executes it. - -The trace is the record. Each run has places for private work, declared outputs, and bindings that downstream services can receive as pointers. This keeps handoffs inspectable without turning every step into a giant paste. - -## How it differs from other shapes - -OpenProse is not just a longer prompt. Prompts are good when the work fits in one exchange. OpenProse is for recurring or fragile workflows where roles, handoffs, constraints, and receipts matter. - -OpenProse is also not an external workflow engine that controls the model from the outside. Most orchestration frameworks wrap the model. OpenProse gives the agent a program to execute from inside the session. - -That is the practical difference: the point is not to replace judgment with plumbing. The point is to put recurring agent work on rails. - -OpenProse is early. The program format is useful today, but the tools and conventions are still settling. Expect sharp edges, and prefer small workflows before betting a large process on it. - -Next: [install OpenProse](/start/install). diff --git a/content/docs/think/contracts-not-prompts.mdx b/content/docs/think/contracts-not-prompts.mdx deleted file mode 100644 index fbb04ff..0000000 --- a/content/docs/think/contracts-not-prompts.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Contracts, not prompts -description: Learn the basic shape of OpenProse programs. ---- - -# Contracts, not prompts - -A prompt asks an agent to do something. A contract says what the work needs, what it must produce, what can go wrong, what must stay true, and how to handle judgment calls. - -That difference is small on the page and large in practice. - -Prompts are fine for one-off work. They start to blur when the same process needs handoffs, retries, memory, or review. OpenProse keeps the work in Markdown, but changes the shape from "please do this" to "given these inputs, these obligations must hold when you are done." - -For the exact Contract Markdown grammar, accepted section forms, and extraction rules, see the [canonical specs](/reference/specs). This page is the human mental model. - -## Requires and Ensures - -The center of an OpenProse service is the pair `Requires` and `Ensures`. - -`Requires` names what the service needs from its caller, another service, the environment, or a completed run. `Ensures` names what the service commits to produce or make true. - -```markdown -### Requires - -- `draft`: article draft to improve -- `audience`: who the article is for - -### Ensures - -- `final`: edited article ready for review -- `notes`: short explanation of substantial edits -``` - -This is not decoration. It is the public interface of the work. When services have clear requirements and clear postconditions, Forme can wire them by meaning instead of relying only on file order or a long procedural prompt. - -The useful habit is to write obligations, not intentions. "Make it better" is a prompt. "`final`: edited article that preserves the author's claims and removes unclear structure" is closer to a contract. - -## Errors - -Some failures are part of the shape of the work. `Errors` names the failures a service is allowed to report instead of hiding them in prose after the fact. - -For example, a research service might declare that it can fail with insufficient sources, conflicting evidence, or missing access. A conversion service might declare unsupported input format. A reviewer might declare that the draft is too incomplete to evaluate. - -Named errors help callers decide what to do next. They also make a run easier to inspect because the failure has a place in the contract, not just in the transcript. - -## Invariants - -`Invariants` are promises that must hold regardless of the path the agent takes. - -Use them for boundaries that should survive success, partial success, retries, and failure handling: - -- do not invent citations -- do not edit source files outside the workspace -- preserve user-authored examples unless the contract explicitly asks to change them -- keep private scratch work out of declared outputs - -Invariants are where you put the rules that should not be traded away during a clever workaround. - -## Strategies - -`Strategies` are guidance for judgment calls. - -They are not a script. They tell the service how to choose when the contract leaves room for interpretation: - -- when evidence is thin, broaden the search before concluding -- when two outputs conflict, prefer the one backed by a traceable source -- when the task is too large, produce a smaller useful slice and say what remains -- when a requested change would violate an invariant, stop and report the conflict - -Good strategies preserve judgment without hiding the criteria. The agent still has to reason. The contract makes the criteria visible. - -## What Goes Where - -Put inputs in `Requires`. - -Put promised outputs and postconditions in `Ensures`. - -Put known failure modes in `Errors`. - -Put non-negotiable boundaries in `Invariants`. - -Put judgment guidance in `Strategies`. - -If you find yourself writing a long sequence of steps, ask whether the order truly matters. If the answer is no, tighten the contract. If the answer is yes, that choreography belongs in `Execution`, which is covered by the canonical specs and by the runtime model. - -## Why This Matters - -Contracts make OpenProse programs inspectable. A human can see what each service owes. An agent can extract the same shape. Forme can resolve services that satisfy each other's requirements. The run can leave a trace that says what was promised and what was produced. - -OpenProse does not make the model deterministic. It gives the model a contract, a workspace, and a record of the work. That is enough to make larger sessions easier to inspect and improve. - -Next: [services, Forme, and the VM](/think/services-forme-vm). diff --git a/content/docs/think/meta.json b/content/docs/think/meta.json deleted file mode 100644 index 31bfb79..0000000 --- a/content/docs/think/meta.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "Think", - "pages": [ - "contracts-not-prompts", - "services-forme-vm", - "workspaces-bindings-traces", - "when-to-use-it" - ] -} diff --git a/content/docs/think/services-forme-vm.mdx b/content/docs/think/services-forme-vm.mdx deleted file mode 100644 index 80d990a..0000000 --- a/content/docs/think/services-forme-vm.mdx +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: Services, Forme, and the VM -description: How OpenProse wires and runs programs. ---- - -# Services, Forme, and the VM - -OpenProse programs are made out of services. - -A service is a unit of agent work with a contract. It names what it needs in -`Requires`, what it is obligated to produce in `Ensures`, and any boundaries -that matter for the work. A program is the entry point that gathers services -into one run. - -That split is small, but it changes the shape of a session. Instead of asking -one agent to remember every role, handoff, and output, you give the system a set -of components it can wire and run. - -## Services are boundaries - -A service should be clear about its job. - -```markdown -### Requires - -- `question`: the question to investigate - -### Ensures - -- `findings`: sourced claims that answer the question -- `sources`: the sources consulted during the work -``` - -The names matter, but the prose matters too. OpenProse is read by a model, so a -contract is not just a schema. It is a set of obligations the executing agent can -understand. - -A good service boundary answers three questions: - -- What input does this part of the run need? -- What public output does it promise? -- What work should stay inside this service? - -That last question is easy to miss. Services are not just named steps. They are -isolation boundaries. Each service gets its own workspace, writes its own -artifacts, and publishes only the outputs declared by its contract. - -## Forme wires the graph - -Forme is the container for OpenProse programs. It reads the program and service -contracts, resolves which outputs satisfy which inputs, and writes a manifest -for the runtime. - -Traditional dependency injection containers wire software components by types -and annotations. Forme wires agent services by meaning. If one service ensures -`findings` and another requires `evidence`, Forme can read the descriptions and -decide whether those are the same thing in this program. - -That judgment is deliberate. OpenProse does not try to replace model judgment -with a brittle type system. If wiring is ambiguous, the right fix is usually to -make the contracts clearer. When order or exact data flow matters, the author -can pin it. - -The practical ladder is: - -1. Write contracts and let Forme auto-wire the graph. -2. Add explicit wiring when a relationship matters. -3. Add an `Execution` block when order matters. - -Start declarative. Add control when the work asks for it. - -## The manifest is the handoff - -The manifest is the materialized wiring for a run. It is what Forme produces and -what the VM executes. - -It records: - -- the caller inputs the program needs -- each service in the graph -- where each service reads its inputs -- where each service writes its outputs -- which services can run in parallel -- warnings Forme found while wiring - -The manifest is useful because it makes an intelligent decision inspectable. -Forme may use semantic judgment to connect services, but the result is not -hidden in the chat. It becomes a file the runtime can follow and a human can -review. - -## The VM is the agent session - -The OpenProse VM is the model plus its harness, tools, subagent primitive, and -filesystem access. - -That can sound stranger than it is. A modern coding agent can read files, write -files, call tools, spawn isolated sessions, inspect state, and continue from -what is left on disk. OpenProse gives that machine a program format. - -During execution, the VM reads the manifest and runs each service. A service -does not need to know the whole graph. It receives: - -- its own service definition -- file paths for its inputs -- a private workspace path -- the output files it must write - -When a service finishes, it returns a short confirmation. The VM copies the -declared outputs into public bindings, appends to the run trace, and moves on. - -This is why OpenProse is not a hosted workflow builder wrapped around a model. -The workflow runs inside the agent session. The filesystem is the durable state. -The trace is the receipt. - -## The analogy - -The project uses this analogy because it is precise enough to be useful: - -```text -Prose : Forme : agent harness :: Java : Spring : JVM -``` - -Prose is the language. Forme is the intelligent container. The agent harness is -the VM. - -Do not push the analogy too far. OpenProse does not make model behavior -deterministic, and it does not remove judgment from the run. It gives that -judgment a contract, a graph, and a trace. - -## Agent note - -These docs explain the shape of OpenProse. They are not the execution spec. - -If you are an agent running or editing an OpenProse program, read -[For AI agents](/agents/for-ai-agents) and follow the skill and canonical specs -listed in [Canonical specs](/reference/specs). - -Next: [workspaces, bindings, and traces](/think/workspaces-bindings-traces). diff --git a/content/docs/think/when-to-use-it.mdx b/content/docs/think/when-to-use-it.mdx deleted file mode 100644 index 1e79498..0000000 --- a/content/docs/think/when-to-use-it.mdx +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: When to use OpenProse -description: Good fits, bad fits, and the quick taste test. ---- - -# When to use OpenProse - -Use OpenProse when a prompt has become a process. - -It is not a better wrapper around chat. It is a way to write a reusable contract for agent work: inputs, obligations, roles, artifacts, failure cases, and the trace the run leaves behind. - -If one good prompt gets the job done, use one good prompt. OpenProse adds structure. That structure is only worth it when it buys something real. - -## Good fits - -### Repeated work - -Use OpenProse when you keep rebuilding the same workflow in chat: - -- weekly research digests -- repeated code review passes -- release note generation -- intake, triage, and routing -- report drafting with the same inputs and quality bar - -The question is not "could an agent do this?" The question is "will I want the same agent process again?" - -### Work with handoffs - -OpenProse starts to matter when the work has roles, stages, or artifacts that should not blur together. - -A prompt can say "review this code." A program can say: - -- one service checks security -- one service checks performance -- one service checks documentation impact -- one service synthesizes the findings -- the final output must include file paths, severity, and evidence - -That is a contract, not a chat request. For the mental model, read [Contracts, not prompts](/think/contracts-not-prompts). - -### Parallel work - -Use OpenProse when independent branches can run at the same time and join later. - -Good examples: - -- review the same change from security, performance, and product angles -- collect independent research on different competitors -- classify a batch of files before a synthesis pass - -Bad example: split one bug diagnosis into "code," "logs," and "context" agents when each one needs the whole picture. That usually creates coordination overhead without better thinking. - -### Work that needs a receipt - -OpenProse leaves work on disk. That matters when you need to inspect what happened, rerun part of the work, or hand artifacts to another service by path instead of pasting giant blobs into the next prompt. - -Use it when the trace and workspace are part of the value. See [Workspaces, bindings, and traces](/think/workspaces-bindings-traces). - -### Work with real acceptance criteria - -OpenProse is a good fit when you can name what must be true at the end: - -- every item has a summary and score -- every finding has evidence -- every generated file exists at a declared path -- every failed branch is reported with a reason - -If the only success criterion is "make it better," stay in chat until you know what "better" means. - -## Bad fits - -### One-shot answers - -Do not write an OpenProse program for a single question, a quick explanation, or a small decision. Ask the agent directly. - -OpenProse is overkill when the output is just a paragraph and you do not need a trace. - -### Tiny edits - -Do not use OpenProse to fix one typo, rename one file, or make a simple local change. The ceremony will cost more than the work. - -### Deterministic work - -If shell, SQL, Python, or a normal queue can do the job exactly, use that. - -OpenProse is for judgment-heavy work inside agent sessions. It is a bad core for exact accounting, payment flow, policy enforcement, or anything where a model should not be deciding the next state. - -### Vague work - -A contract will not rescue an unclear goal. - -If you cannot name the inputs, the output, and the definition of done, writing Markdown around the request only makes the vagueness look official. - -### Work that only looks parallel - -Parallel agents are not free. Use them when the work is genuinely independent or when different viewpoints should stay separate until synthesis. - -Do not fan out tiny tasks. Do not split a single line of reasoning across agents just to make the graph look busy. - -## The taste test - -Before writing a program, answer these: - -1. What does the workflow require? -2. What must it ensure? -3. What artifacts should exist on disk? -4. Which parts can run independently? -5. What can fail, retry, or degrade? -6. Will this workflow run again? - -If most answers are blank, OpenProse is too much. Start with a prompt. - -If the answers are clear, OpenProse gives the process a contract, a workspace, and a trace. - -Next: [try a useful workflow](/start/first-useful-workflow) or [browse examples](/use/examples). diff --git a/content/docs/think/workspaces-bindings-traces.mdx b/content/docs/think/workspaces-bindings-traces.mdx deleted file mode 100644 index eb478ff..0000000 --- a/content/docs/think/workspaces-bindings-traces.mdx +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Workspaces, bindings, and traces -description: How OpenProse keeps agent work durable and context-efficient. ---- - -# Workspaces, bindings, and traces - -OpenProse uses the filesystem as the durable part of the session. - -That choice is not incidental. Agent context is scarce and easy to muddy. Files -are inspectable, resumable, and cheap to pass by path. OpenProse keeps service -work on disk, publishes only declared outputs, and records what happened in a -trace. - -The main pieces are: - -- `workspace/`: private working files for each service -- `bindings/`: public outputs that other services can read -- `state.md`: the append-only trace for the run - -## Workspaces are private - -Each service gets its own workspace inside the run directory. - -```text -.prose/runs/20260317-143052-a7b3c9/ -|-- workspace/ -| |-- researcher/ -| | |-- notes.md -| | |-- findings.md -| | `-- sources.md -| `-- synthesizer/ -| |-- outline.md -| `-- report.md -``` - -The service can write whatever it needs there: notes, drafts, scratch files, -source extracts, failed attempts, and final outputs. Those files are preserved so -you can inspect the run later. - -The workspace is private in the service sense. Other services do not consume it -directly. A downstream service should not have to understand another service's -drafts or internal reasoning. It should receive the output promised by the -contract. - -## Bindings are public - -Bindings are the public interface between services. - -When a service completes, the VM looks at the service's `Ensures` contract and -copies the declared output files from its workspace into `bindings/`. - -```text -workspace/researcher/findings.md -bindings/researcher/findings.md -``` - -That copy is the return step. - -The service writes to its workspace. The VM publishes the declared outputs. -Downstream services read from the binding paths named in the manifest. - -This matters because it keeps the contract honest. If `researcher` ensures -`findings` and `sources`, then those are the public artifacts other services can -depend on. Its scratch notes may still exist, but they are not part of the -interface. - -## Pointers beat pasted context - -OpenProse does not need to paste every intermediate artifact into the main -agent context. - -A service can return: - -```text -Service complete: researcher -Outputs written: - - findings: workspace/researcher/findings.md - - sources: workspace/researcher/sources.md -Summary: Found relevant sources and extracted claims. -``` - -The VM copies the files and carries the paths forward. The next service receives -file paths, not a giant blob of text. It can read what it needs from disk. - -That is why workspaces and bindings are not just implementation details. They -are part of the language's context strategy. Big workflows stay legible because -artifacts remain artifacts. - -## Traces are the receipt - -Every run has a trace. In the canonical runtime, the trace is `state.md`. - -It records the important events of the run: - -```text -# run:20260317-143052-a7b3c9 deep-research - -program: deep-research - -1-> [input] question ok -2-> researcher ok -3-> critic ok -4-> synthesizer ok ----end 2026-03-17T14:35:22Z -``` - -The trace is intentionally small. It is not a transcript of every thought or -every token. It is the run ledger: inputs bound, services completed, errors -signaled, delegates called, tests passed or failed, and completion status. - -This gives you a way to answer practical questions after the run: - -- Which service produced this file? -- Did the critic actually run? -- Where did the run stop? -- Can the VM resume from the next service? -- Which prior run did this run depend on? - -Without a trace, an agent workflow is just a long conversation. With a trace, it -has a record. - -## What this buys you - -The workspace, binding, and trace split gives OpenProse three useful properties. - -First, isolation. A service can think and write freely without leaking every -draft into downstream work. - -Second, context control. The runtime passes pointers to artifacts instead of -stuffing the whole run into one prompt. - -Third, inspectability. After the run, you can open the directory and see what -was produced, what was published, and what happened. - -This is the practical reason OpenProse treats the filesystem as part of the VM. -The agent session is still doing the intelligent work, but the important state -does not vanish when the chat scrolls away. - -Next: [when to use OpenProse](/think/when-to-use-it). diff --git a/content/docs/use/examples.mdx b/content/docs/use/examples.mdx deleted file mode 100644 index 3fc5a55..0000000 --- a/content/docs/use/examples.mdx +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: Examples -description: A curated path through the OpenProse examples. ---- - -# Examples - -Read these examples in order. They move from the smallest possible service to a program with explicit phases, review, and testing. - -The point is not to memorize syntax. Look for the contract shape: - -- what the program requires -- what it ensures -- where judgment belongs -- when a workflow stays declarative -- when order is important enough for `Execution` - -## Hello world - -Start here to see the minimum surface area. It is a service, not a program, and it has no required inputs. - - - -Notice how little structure is needed. The service only promises `greeting`. That is enough for the VM to know what successful completion means. - -Use this shape when you are testing whether the OpenProse loop is installed and the agent knows how to read a contract. - -## Research and summarize - -This example adds an input, a more specific output, and strategies for judgment calls. - - - -The useful detail is the constraint on the bullets. The service does not just promise a summary. It promises five bullets, practical implications, and grounding in recent sources. - -The `Strategies` section gives the agent room to reason without leaving the workflow vague. If sources are thin, broaden the search. If the findings are too technical, translate them for developers. - -## Code review - -This is still a single service, but the contract is closer to real work. - - - -The output has shape: - -- a unified report -- coverage across security, performance, and maintainability -- severity on each issue -- actionable recommendations -- priority by severity - -That is the difference between "review this" and a contract. The service has room to use judgment, but the result has obligations. - -## Parallel reviews - -This is the first example where OpenProse starts to pay for itself. - - - -The services are separate points of view: - -- `security-reviewer` -- `perf-reviewer` -- `style-reviewer` -- `synthesizer` - -The program does not need a long script. The roles and the final report are clear enough for a first pass. If you need sharper boundaries later, give the services their own contracts or add explicit execution. - -For a guided run, see [your first useful workflow](/start/first-useful-workflow). - -## Captain's chair - -This example is larger because order matters. It plans, researches, reviews the plan, implements, reviews the implementation, tests, and then summarizes. - - - -The important change is the `Execution` block. Declarative contracts are still present, but the program also pins the phases: - -- plan first -- run research sweeps -- synthesize an implementation plan -- let a critic review the plan -- implement -- review the implementation -- test -- produce the final result - -Use this shape when the workflow has real order, branching, or retry points. Do not start here for a small task. Start declarative, add wiring when relationships matter, and use `Execution` when order matters. - -## Choosing the right example - -| Example | Use it to study | -| --- | --- | -| Hello world | the minimum service contract | -| Research and summarize | inputs, output constraints, and strategies | -| Code review | a practical single-service contract | -| Parallel reviews | fan out, then synthesize | -| Captain's chair | explicit order with `Execution` | - -Plain prompts are still the right tool for one-off work. These examples are useful when you want a workflow to be inspectable, reusable, and worth improving. - -Next: [study common patterns](/use/patterns). diff --git a/content/docs/use/meta.json b/content/docs/use/meta.json deleted file mode 100644 index 6ba3526..0000000 --- a/content/docs/use/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Use", - "pages": ["examples", "patterns", "troubleshooting"] -} diff --git a/content/docs/use/patterns.mdx b/content/docs/use/patterns.mdx deleted file mode 100644 index fef4408..0000000 --- a/content/docs/use/patterns.mdx +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Patterns -description: Common workflow shapes in OpenProse. ---- - -# Patterns - -Patterns are small workflow shapes. They are not a reason to use OpenProse by themselves. First decide the job deserves a program. Then pick the shape. - -Start declarative. Add control only when the workflow needs it. - -1. Contract only: say what the run requires and ensures. -2. Services and wiring: name the roles when handoffs matter. -3. Execution: pin order, loops, and failure policy when choreography matters. - -For exact syntax, use [Specs](/reference/specs). This page is about shape. - -## Fan out, then synthesize - -Use this when branches are independent and the final answer benefits from distinct views. - -Good fits: - -- security, performance, and documentation review of the same change -- separate research on different companies -- multiple translation drafts before one editor pass - -Bad fit: one diagnosis where every branch needs the full context. In that case, a single focused investigator is usually cheaper and better. - -```text -parallel: - security = session "Review for security issues" - performance = session "Review for performance issues" - docs = session "Review for documentation impact" - -session "Synthesize the review into prioritized findings" - context: { security, performance, docs } -``` - -In Contract Markdown, this usually means separate services with clear `Requires` and `Ensures`, then one synthesizer service that requires their outputs. - -## Pipeline - -Use a pipeline when each stage narrows or reshapes the previous artifact. - -```text -let sources = session "Collect relevant source material" - -let claims = session "Extract claims with evidence" - context: sources - -let recommendations = session "Turn claims into recommendations" - context: claims - -session "Format recommendations as a report" - context: recommendations -``` - -Keep the context narrow. Pass the artifact the next stage needs, not everything the run has seen. - -## Worker plus critic - -Use this when review pressure is part of the work. - -```text -let draft = session "Draft the migration plan" - -let critique = session "Find concrete risks, missing steps, and unsupported claims" - context: draft - -session "Revise the plan and address each valid critique" - context: { draft, critique } -``` - -Do not add a critic just because it feels rigorous. For ordinary self-checks, put the verification requirement into the worker's own contract. Use a separate critic when you want an adversarial pass or a different specialty. - -## Retry with learning - -Use a bounded loop when each attempt can learn from the last one. - -```text -let draft = session "Create the first implementation" - -loop until **tests pass and lint passes** (max: 5): - let failures = session "Run checks and summarize failures" - context: draft - - draft = session "Fix the implementation using the failure summary" - context: { draft, failures } -``` - -Every loop needs a concrete exit condition and a maximum. "Improve until good" is not a condition. - -## Controlled-burn exploration - -Use controlled exploration when several approaches might work and you want useful candidates without spending forever. - -```text -parallel ("any", count: 3, on-fail: "ignore"): - session "Try a minimal implementation" - session "Try a library-based implementation" - session "Try a data-model-first implementation" - session "Try a test-first implementation" - -session "Compare the first three successful approaches" -``` - -Set the budget before the run. Decide how many candidates are enough. Stop when you have the signal you came for. - -## Cheap pass first, expensive pass later - -Use a cheap first pass when every item needs some signal, but only a few deserve deeper work. - -```text -let screened = session "Score every item with fast local evidence" - -let shortlist = session "Select the top items that need deep review" - context: screened - -session "Perform deep review only on the shortlist" - context: shortlist -``` - -This pattern keeps the deferred items useful. A shallow score for every item is better than rich detail for the top slice and nothing for the rest. - -## Batch similar work - -Do not spin up one session per trivial item. - -```text -session "Classify each item and return structured results" - context: items -``` - -Use per-item fan-out when the items are heavy, independent, or likely to fail separately. Batch when the task is simple and the output schema is crisp. - -## Pattern checks - -A pattern is probably wrong if: - -- one focused session would have better context than several branches -- branches depend on each other but are marked parallel -- a loop has no maximum -- a critic gives generic taste notes instead of evidence -- every stage receives the whole run context -- the program uses the deepest model for every task -- the workflow would be easier as a normal script - -OpenProse should make the work more inspectable, not just more elaborate. - -Next: [troubleshooting](/use/troubleshooting). diff --git a/content/docs/use/troubleshooting.mdx b/content/docs/use/troubleshooting.mdx deleted file mode 100644 index 6a17c59..0000000 --- a/content/docs/use/troubleshooting.mdx +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: Troubleshooting -description: Common OpenProse confusions and how to correct them. ---- - -# Troubleshooting - -Most OpenProse problems are not mysterious. They come from treating it like a shell tool, a prompt template, or a normal workflow framework. - -The quick rule: - -> If the docs and the `open-prose` skill disagree, trust the skill. - -## The agent tried to run `prose` as a shell command - -Inside an agent session, `prose run file.md` is an instruction to the agent. It is not usually a request to find a `prose` executable on `PATH`. - -The right behavior is: - -1. Load the `open-prose` skill. -2. Read the program. -3. Wire services if the program declares them. -4. Execute through the current harness primitives. -5. Write run state under `.prose/runs/`. - -From a normal shell, the CLI can pass the same instruction into a supported harness. That CLI is a bridge into the agent session, not the VM itself. - -See [for AI agents](/agents/for-ai-agents) for the stricter version. - -## The program is too vague - -A vague OpenProse program usually has vague `Ensures`. - -Weak: - -```markdown -### Ensures - -- `report`: a good report -``` - -Better: - -```markdown -### Ensures - -- `report`: concise Markdown report with findings, evidence, risks, and next steps -- `evidence`: links or file paths supporting each major claim -``` - -The agent still has room to use judgment. The contract should make the obligation clear enough that another agent can tell whether the run fulfilled it. - -## Forme picked the wrong service - -Forme wires by contract meaning, not just by string equality. Exact names help, but they are not the whole system. - -If wiring is ambiguous: - -- make `Requires` and `Ensures` more specific -- rename outputs so their intent is visible -- add `Shape` constraints when a service must not take over another role -- add explicit `Wiring` only when the relationship really needs to be pinned - -Do not jump straight to `Execution` just because the first draft was ambiguous. Tighten the contracts first. - -## The run produced a pasted blob - -OpenProse is meant to pass artifacts by path whenever possible. - -Services should do real work in their private workspace. Declared outputs should be copied into bindings. Downstream services should receive pointers to those artifacts, not a giant paste of every intermediate result. - -If the run is bloating context: - -- ask the service to write files instead of returning long text -- make each `Ensures` output an artifact with a clear path -- have the synthesizer read only the artifacts it needs -- keep scratch work out of bindings - -See [workspaces, bindings, and traces](/think/workspaces-bindings-traces). - -## A subagent did the coordinator's job - -This is usually a shape problem. - -The coordinator should wire, delegate, and synthesize. A worker should do its own bounded service. If a worker starts running the whole program, the contract needs a clearer boundary. - -Useful fixes: - -- add `Shape` language that names what the service may and may not do -- split a large service into smaller services -- make upstream inputs and downstream outputs explicit -- tell the coordinator not to hand the full manifest to every subagent - -OpenProse works best when each agent has the smallest useful view of the work. - -## The workflow is slower than a prompt - -It might be the wrong fit. - -OpenProse adds structure. That structure is useful when you need reuse, handoffs, parallel work, retries, or a trace. It is waste when the task fits in one good prompt. - -If a run feels heavy, ask: - -- Would I run this again? -- Do I care what happened after the fact? -- Does another agent need to understand the handoff? -- Would parallel work or review improve the result? - -If the answer is no, use a prompt. - -## The docs and skill disagree - -Trust the skill. - -These docs explain the shape of OpenProse for humans. The `open-prose` skill and its linked reference specs are the execution source of truth for agents. - -If the site is stale, fix the site. Do not teach agents to infer runtime behavior from a friendly docs page. - -Next: [agent instructions](/agents/for-ai-agents). diff --git a/vendor/prose-examples/basic-unit-suite/src/count-summary.prose.md b/vendor/prose-examples/basic-unit-suite/src/count-summary.prose.md new file mode 100644 index 0000000..3bbc628 --- /dev/null +++ b/vendor/prose-examples/basic-unit-suite/src/count-summary.prose.md @@ -0,0 +1,46 @@ +--- +name: count-summary +kind: responsibility +--- + +# Count Summary + +> The single responsibility that turns the gateway's `counts` facet into a +> threshold-aware summary (U02). It reads its prior world-model by reference, +> writes a new `CountSummary`, and signs a receipt naming the upstream receipt it +> consumed. It skips when `counts` has not moved (U03). + +### Requires + +- `counts`: the numeric tallies. *(Maintained by `counter-events.counts`.)* + +This is the only subscribed input. `count-summary` is **input-driven**: it wakes +iff the `counts` facet fingerprint moves. A metadata-only event moves only +`raw_events`, so this node stays dark while `Raw Event Auditor` wakes (U05). + +### Maintains + +The `CountSummary` world-model — the structured summary the alerting chain reads. + +- `total` — the material event count. +- `by_kind` — the per-kind tallies. +- `threshold_crossed` — whether `total` reached the alert threshold. +- `explanation` — a short rationale string. + +#### structured + +The whole summary is material: any change to `total`, `by_kind`, or +`threshold_crossed` moves this node's truth and propagates to `Alert State`. + +**Postcondition:** `total` equals the count of accepted material events; +`threshold_crossed` is true iff `total ≥ threshold`. Self-policed before signing — +no separate judge beat. + +### Execution + +Read the `counts` facet and the prior summary by reference, fold the per-kind +totals, set `threshold_crossed`, and commit the structured summary. + +### Continuity + +input-driven diff --git a/vendor/prose-examples/basic-unit-suite/src/counter-events.prose.md b/vendor/prose-examples/basic-unit-suite/src/counter-events.prose.md new file mode 100644 index 0000000..381d48f --- /dev/null +++ b/vendor/prose-examples/basic-unit-suite/src/counter-events.prose.md @@ -0,0 +1,67 @@ +--- +name: counter-events +kind: gateway +--- + +# Counter Events + +> The gateway — the system's ingress. It has no `### Requires` (its input arrives +> from outside the graph), it `### Maintains` the canonical `CounterEventLedger`, +> and its `### Continuity` is **external-driven**, which is how Forme registers it +> as the single DAG entry point (U11). + +### Continuity + +external-driven + +A webhook, a poll, or a manual kick becomes one external wake at the system's +edge. The gateway folds each accepted counter event into the canonical ledger and +projects two **independent facets** so a downstream subscriber wakes only on the +slice it actually depends on (U05). Replaying the same event id is a no-op — the +ledger dedups by id, so a re-delivery produces a byte-identical world-model and +the gateway memo-skips (U01/U03). + +### Receives + +- A counter event: `{ id, kind, value, material? }`. An event with + `material: false` is **accepted into the audit trail but excluded from the + tallies** — it is the metadata-only event that moves `raw_events` without moving + `counts`. + +### Maintains + +The `CounterEventLedger` — the standing truth every downstream responsibility +subscribes to. Its canonicalization splits the truth into the two facets below, so +a change to one slice never spuriously wakes a subscriber of the other. + +- `high_water_mark` — the running material event total. +- `counts_by_kind` — the per-kind material tallies. +- `accepted_event_ids` — the full accepted id set (material and metadata-only). +- `last_seen_at` — an immaterial monotone marker (it never appears in a facet, so + it cannot wake a subscriber on its own). + +#### counts + +The numeric tallies (`high_water_mark`, `counts_by_kind`) over **material events +only**. Moves when a material event is accepted; does NOT move on a metadata-only +event. `Count Summary` and `Count Trend` subscribe here. + +#### raw_events + +The accepted-event id set plus duplicate / malformed flags. Moves whenever the +accepted set changes — including a metadata-only event. `Raw Event Auditor` +subscribes here. + +### Emits + +- count-summary +- raw-event-auditor +- count-trend + +Forme keys the wake on the producing node; the subscribers above resolve their +edges to this gateway's `counts` / `raw_events` facets. + +### Continuity recheck + +A weekday 09:00 self-kick may re-scan even when no webhook fires; a byte-identical +re-scan memo-skips, so the self-kick costs nothing when nothing changed.