From c7da13c6dacaf190895ad0a8223c9267d4ee7e38 Mon Sep 17 00:00:00 2001 From: Jose Montes de Oca Date: Tue, 28 Jul 2026 11:17:05 -0400 Subject: [PATCH 1/5] docs: focus the site on the language --- content/docs/cli/command-reference.mdx | 178 ------ content/docs/cli/compile-run-serve.mdx | 122 ---- content/docs/cli/configuration.mdx | 187 ------ content/docs/cli/connectors-and-sandbox.mdx | 114 ---- content/docs/cli/meta.json | 13 - content/docs/cli/observability.mdx | 94 ---- content/docs/cli/overview.mdx | 159 ------ content/docs/cli/quickstart.mdx | 163 ------ content/docs/cli/telemetry.mdx | 107 ---- content/docs/index.mdx | 93 +-- content/docs/meta.json | 2 +- content/docs/openprose/contracts.mdx | 16 +- content/docs/openprose/declare-outcomes.mdx | 19 +- content/docs/openprose/harness-agnostic.mdx | 61 +- content/docs/openprose/index.mdx | 32 +- content/docs/openprose/setup.mdx | 143 +---- content/docs/openprose/typed-image.mdx | 14 +- content/docs/reactor-devtools/describe.mdx | 92 --- content/docs/reactor-devtools/index.mdx | 104 ---- content/docs/reactor-devtools/meta.json | 12 - content/docs/reactor-devtools/quickstart.mdx | 91 --- content/docs/reactor-devtools/recording.mdx | 73 --- content/docs/reactor-devtools/reference.mdx | 107 ---- .../state-dirs-and-replay.mdx | 92 --- content/docs/reactor-devtools/the-viewer.mdx | 77 --- .../docs/reactor/continuity-and-ingestion.mdx | 97 ---- content/docs/reactor/index.mdx | 172 ------ content/docs/reactor/meta.json | 10 - .../docs/reactor/reconciler-and-receipts.mdx | 119 ---- content/docs/reactor/the-dag-and-compile.mdx | 102 ---- .../reactor/world-model-and-fingerprints.mdx | 158 ------ content/docs/sdk/adapters.mdx | 266 --------- content/docs/sdk/agents.mdx | 530 ------------------ content/docs/sdk/front-door.mdx | 371 ------------ content/docs/sdk/index.mdx | 185 ------ content/docs/sdk/internals.mdx | 215 ------- content/docs/sdk/meta.json | 4 - content/docs/sdk/run.mdx | 284 ---------- next.config.mjs | 46 ++ 39 files changed, 152 insertions(+), 4572 deletions(-) delete mode 100644 content/docs/cli/command-reference.mdx delete mode 100644 content/docs/cli/compile-run-serve.mdx delete mode 100644 content/docs/cli/configuration.mdx delete mode 100644 content/docs/cli/connectors-and-sandbox.mdx delete mode 100644 content/docs/cli/meta.json delete mode 100644 content/docs/cli/observability.mdx delete mode 100644 content/docs/cli/overview.mdx delete mode 100644 content/docs/cli/quickstart.mdx delete mode 100644 content/docs/cli/telemetry.mdx delete mode 100644 content/docs/reactor-devtools/describe.mdx delete mode 100644 content/docs/reactor-devtools/index.mdx delete mode 100644 content/docs/reactor-devtools/meta.json delete mode 100644 content/docs/reactor-devtools/quickstart.mdx delete mode 100644 content/docs/reactor-devtools/recording.mdx delete mode 100644 content/docs/reactor-devtools/reference.mdx delete mode 100644 content/docs/reactor-devtools/state-dirs-and-replay.mdx delete mode 100644 content/docs/reactor-devtools/the-viewer.mdx delete mode 100644 content/docs/reactor/continuity-and-ingestion.mdx delete mode 100644 content/docs/reactor/index.mdx delete mode 100644 content/docs/reactor/meta.json delete mode 100644 content/docs/reactor/reconciler-and-receipts.mdx delete mode 100644 content/docs/reactor/the-dag-and-compile.mdx delete mode 100644 content/docs/reactor/world-model-and-fingerprints.mdx delete mode 100644 content/docs/sdk/adapters.mdx delete mode 100644 content/docs/sdk/agents.mdx delete mode 100644 content/docs/sdk/front-door.mdx delete mode 100644 content/docs/sdk/index.mdx delete mode 100644 content/docs/sdk/internals.mdx delete mode 100644 content/docs/sdk/meta.json delete mode 100644 content/docs/sdk/run.mdx diff --git a/content/docs/cli/command-reference.mdx b/content/docs/cli/command-reference.mdx deleted file mode 100644 index 020ed64..0000000 --- a/content/docs/cli/command-reference.mdx +++ /dev/null @@ -1,178 +0,0 @@ ---- -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 deleted file mode 100644 index 68acda6..0000000 --- a/content/docs/cli/compile-run-serve.mdx +++ /dev/null @@ -1,122 +0,0 @@ ---- -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 deleted file mode 100644 index 114b357..0000000 --- a/content/docs/cli/configuration.mdx +++ /dev/null @@ -1,187 +0,0 @@ ---- -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 # openrouter (default) | openai | anthropic | google | - render_model: google/gemini-3.5-flash - compile_model: google/gemini-3.5-flash - temperature: 0 - max_turns: 200 - # base_url: ... # optional: override the provider's endpoint - # api_key_env: ... # optional: read the key from a different env var - -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. A built-in (`openrouter`, `openai`, `anthropic`, `google`) supplies its own endpoint + key env; any other name requires `base_url` + `api_key_env`. | -| `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. | -| `model.base_url` | (provider default) | Override the OpenAI-compatible base URL. Optional for a built-in; required (with `api_key_env`) for a custom vendor. | -| `model.api_key_env` | (provider default) | The env var holding the API key (e.g. `ANTHROPIC_API_KEY`). Optional for a built-in; set it to read from a non-default variable. | - -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. - -### Choosing a model provider - -By default Reactor talks to models through OpenRouter, but it is **not** bound to -it. The CLI drives one bounded `@openai/agents` session per render/compile step, -and any vendor with an **OpenAI-compatible Chat Completions** endpoint plugs in by -naming it in `model:`. Point at OpenAI, Anthropic, or Google **directly** -- no -OpenRouter account required: - -```yaml -# Anthropic, directly (set ANTHROPIC_API_KEY in your env or a .env) -model: - provider: anthropic - render_model: claude-haiku-4-5 - compile_model: claude-haiku-4-5 -``` - -Each built-in provider resolves to an endpoint + a key env var: - -| `provider` | Endpoint | Key env var | Example model id | -| --- | --- | --- | --- | -| `openrouter` (default) | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` | `google/gemini-3.5-flash` | -| `openai` | `https://api.openai.com/v1` | `OPENAI_API_KEY` | `gpt-4o-mini` | -| `anthropic` | Anthropic **Messages API** (native, see below) | `ANTHROPIC_API_KEY` | `claude-haiku-4-5` | -| `google` | `https://generativelanguage.googleapis.com/v1beta/openai/` | `GEMINI_API_KEY` | `gemini-2.5-flash` | - -`openrouter`, `openai`, and `google` are OpenAI-compatible Chat Completions -surfaces. `anthropic` is special: the CLI routes it through Anthropic's **native -Messages API** (via the bundled `@openai/agents` AI-SDK adapter over -`@ai-sdk/anthropic`), because Anthropic's OpenAI-compat endpoint ignores -`response_format` and can't drive Reactor's structured compile/render. You don't -install or wire anything -- `provider: anthropic` just works, structured outputs and -all. An optional `base_url` points the adapter at a proxy in front of the Messages -API. - -For any other OpenAI-compatible vendor (or a self-hosted gateway), name it freely -and supply both `base_url` and `api_key_env`: - -```yaml -model: - provider: together - base_url: https://api.together.xyz/v1 - api_key_env: TOGETHER_API_KEY - render_model: meta-llama/Llama-3.3-70B-Instruct-Turbo - compile_model: meta-llama/Llama-3.3-70B-Instruct-Turbo -``` - -`api_key_env` also overrides a built-in's key var -- handy when, say, your -OpenRouter key lives under a different name. Run `reactor doctor` to confirm the -configured provider's key is visible (it reports the exact env var, never printing -the value), and `reactor doctor --live` to drive one real round-trip against it. - - -The key is read from the named env var first, then a `.env` discovered by walking -up from the project directory. A missing key fails `compile`/`run`/`serve` with a -**non-zero** exit and a message naming the exact variable to set -- it never -silently misdirects you to OpenRouter, and never exits 0 on an auth dead-end. - - - -**Claude, two ways.** `provider: anthropic` drives Claude directly through the -native Messages API and fully supports the structured compile/render path -- it is -the recommended route for Anthropic models. If you already aggregate models through -one gateway, `provider: openrouter` with a `render_model`/`compile_model` like -`anthropic/claude-haiku-4-5` also works. Avoid pointing a `base_url` at Anthropic's -OpenAI-compatible endpoint (`https://api.anthropic.com/v1/`): it ignores -`response_format` and rejects Reactor's JSON-schema with -`400 response_format.json_schema.strict`. See the -[SDK provider guide](/sdk/agents#claude-via-the-native-anthropic-messages-api) -for the underlying `@openai/agents` wiring. - - -## 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 **default** provider. Required by `compile`, `run`, `serve`, and `trigger` when `model.provider` is `openrouter`. Read from the process env first, then a `.env` file discovered from the working directory upward. | -| `` | The live key for a **non-default** provider -- `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, or whatever `model.api_key_env` names. Resolved the same way (env first, then a discoverable `.env`). See [Choosing a model provider](#choosing-a-model-provider). | -| `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 deleted file mode 100644 index c7b6674..0000000 --- a/content/docs/cli/connectors-and-sandbox.mdx +++ /dev/null @@ -1,114 +0,0 @@ ---- -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 deleted file mode 100644 index 65c6186..0000000 --- a/content/docs/cli/meta.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "title": "Reactor CLI", - "pages": [ - "overview", - "quickstart", - "configuration", - "compile-run-serve", - "connectors-and-sandbox", - "observability", - "telemetry", - "command-reference" - ] -} diff --git a/content/docs/cli/observability.mdx b/content/docs/cli/observability.mdx deleted file mode 100644 index 0cfff61..0000000 --- a/content/docs/cli/observability.mdx +++ /dev/null @@ -1,94 +0,0 @@ ---- -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 deleted file mode 100644 index c8c1ecc..0000000 --- a/content/docs/cli/overview.mdx +++ /dev/null @@ -1,159 +0,0 @@ ---- -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 deleted file mode 100644 index c24ddbe..0000000 --- a/content/docs/cli/quickstart.mdx +++ /dev/null @@ -1,163 +0,0 @@ ---- -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/cli/telemetry.mdx b/content/docs/cli/telemetry.mdx deleted file mode 100644 index 69574ca..0000000 --- a/content/docs/cli/telemetry.mdx +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: Telemetry -description: Anonymous, opt-out, content-free CLI telemetry -- exactly what is collected, and every way to turn it off. ---- - -# Telemetry - -The Reactor CLI collects anonymous, content-free usage telemetry so we can see how many people use Reactor and for what. It is on by default for interactive runs, off in CI, and one line to turn off for good. This page is the plain-language version; the exact field-by-field schema lives in [`TELEMETRY.md`](https://github.com/openprose/prose/blob/main/packages/reactor-cli/TELEMETRY.md) in the package. - -The posture, in one breath: - -- **Anonymous.** The only identifier is a random per-machine UUID. No account, no user, no email, no IP-derived geo. -- **CLI-only.** Telemetry lives entirely in `@openprose/reactor-cli`. The SDK, `@openprose/reactor`, emits zero network traffic -- a library that phones home from inside your stack is a trust violation, so it never does. -- **Content-free.** We collect the *shape* of usage, never the *content* -- never your prose, file paths, names, prompts, keys, or model input/output. -- **Opt-out, honored permanently.** `DO_NOT_TRACK=1`, `REACTOR_TELEMETRY=0`, or `reactor telemetry disable` each turn it off for good. -- **Fire-and-forget.** A short, bounded request that never blocks, slows, or errors a command -- even when the endpoint is down. - -## What is collected - -Each event carries a coarse, content-free shape of one command: - -- The CLI and SDK versions, Node version, OS family, and CPU arch. -- A `ci` boolean, the command name, and a coarse outcome (`success`, `failure`, or `cache_hit`). -- Bucketed durations and counts -- never raw numbers. A compile or run also reports bucketed node/edge/cost counts, the disposition tally, and the provider **class** (`anthropic`, `openai`, `local`, ...), never a key. -- On a failure, a coarse error **category** (`provider`, `config`, `io`, `chain_verify`, `unknown`) -- never the message or stack. - -Counts collapse to `0` / `1-5` / `6-20` / `21+`; durations to `<1s` / `1-5s` / `5-30s` / `30s+`. The bucketers exist so a raw value can never slip through. - -## What is never collected - -The trust invariant is that we send the shape of usage, never the content of it. Forbidden in every field, no exceptions: - -- World-model content, the markdown, prompt text, or any model input/output -- File paths, project or directory names, exact facet or node names -- API keys, tokens, base URLs, or model ids -- Error messages or stacks -- Raw counts or durations, and precise or IP-derived geo - -## Turning it off - -Telemetry is disabled if **any** one of these holds -- each is permanent on its own: - -| Condition | How | -| --- | --- | -| `DO_NOT_TRACK` is truthy | `export DO_NOT_TRACK=1` (the [consoledonottrack.com](https://consoledonottrack.com) convention). | -| Reactor env opt-out | `export REACTOR_TELEMETRY=0`, or set `REACTOR_TELEMETRY_DISABLED`. | -| Offline | `REACTOR_OFFLINE=1`. | -| CI | `CI` is truthy, or a known CI marker is set. | -| Non-interactive | stdout is not a TTY (piped / redirected / automated runs are never tracked). | -| Project config | `reactor.yml` → `telemetry.enabled: false`. | -| Machine config | `reactor telemetry disable` (writes `~/.reactor/config.json`). | - -So a CI pipeline or a piped invocation is off without you doing anything. The simplest permanent opt-out on a workstation is: - -```sh -reactor telemetry disable -``` - -### First-run notice - -The first time you run `reactor doctor` on a machine, a short notice prints to stdout: what is collected, that it is anonymous, the one-liner to turn it off, and a pointer to the schema. It shows once per machine. There is no banner at CLI entry and nothing on stderr. - -## Inspecting and managing it - -```sh -reactor telemetry # status: enabled?, reason if off, endpoint, install id -reactor telemetry disable # permanent machine-level opt-out -reactor telemetry enable # clear the machine-level opt-out -reactor telemetry --dump # print the exact JSON that would be sent, then exit -``` - -`reactor telemetry --dump` is the transparency surface: it prints the precise endpoint and Segment batch a representative event would send, and it never opens a socket. All three of `status`, `enable`, and `disable` accept `--json`. - -```sh -$ reactor telemetry --dump -{ - "endpoint": "https://api.openprose.ai/analytics", - "batch": [ - { - "type": "track", - "anonymousId": "3f8c1e0a-...", - "event": "reactor.doctor", - "properties": { - "schemaVersion": 1, - "cliVersion": "0.2.0", - "command": "doctor", - "outcome": "success", - "durationBucket": "<1s" - }, - "context": { "library": "@openprose/reactor-cli" }, - "timestamp": "2026-06-02T18:00:00.000Z" - } - ] -} -``` - -## Endpoints - -The published CLI sends to `https://api.openprose.ai/analytics`. Local and dev builds send to `https://api.dev.openprose.ai/analytics`. Self-hosters can redirect telemetry with the `REACTOR_TELEMETRY_ENDPOINT` environment variable, or per project: - -```yaml -# reactor.yml -telemetry: - endpoint: https://analytics.example.com/analytics -``` - -The collection code is open source under `src/telemetry/` in `@openprose/reactor-cli`, and the full field-by-field schema is published in [`TELEMETRY.md`](https://github.com/openprose/prose/blob/main/packages/reactor-cli/TELEMETRY.md). diff --git a/content/docs/index.mdx b/content/docs/index.mdx index 3308d7a..bce8ba8 100644 --- a/content/docs/index.mdx +++ b/content/docs/index.mdx @@ -1,53 +1,53 @@ --- 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. +description: A declarative language for standing AI work. You declare the outcomes you want kept true, in Markdown contracts, and any Prose-Complete agent harness runs them. --- # OpenProse -**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**. +**OpenProse is a declarative language for standing AI work.** You write Markdown contracts (`*.prose.md`) that declare an ideal world-model: the truths you want kept current. You say what must stay true, and the host session works out the model work it takes to keep it that way. When order, loops, or exact choreography genuinely matter, optional imperative **ProseScript** plans drop in. Declarative by default, imperative where you want the control. -**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. +The language ships as a skill. Install it, and any Prose-Complete coding agent (Claude Code, Codex CLI, OpenCode, and friends) can author and run contracts: -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.** +```bash +npx skills add openprose/prose +``` -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**. +From there, point your agent at a contract and say `prose run `. The session itself embodies the VM: there is no parser and no separate binary. ## Start here ## The shape of 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`): +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 --- @@ -71,58 +71,13 @@ account carries a `valid_until`. Postcondition: every flagged account cites a corroborating signal. ``` -`### 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. - -## React-flavored, not React-gated - -**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. - -
-Optional: the React metaphor (skippable) - -If you know React, you already know the shape -- substitute three nouns: - -| 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 | - -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. - -
- -## See the thesis -- keyless, no model call - -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 -``` +`### 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 gate what a render may commit. _Structure is subscription:_ Forme, the compile-phase wiring, matches `Requires.` against `Maintains.` and wires the graph from the contracts themselves. -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). +How a host *serves* that contract set (memoization, receipts, a reconciler) is the host's concern, not the language's. The contract is the public artifact; it runs unchanged on any compliant host. See [Harness-agnostic](/openprose/harness-agnostic) for the seam. ## 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. +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/meta.json b/content/docs/meta.json index 9cbc469..8fe69bb 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -1,4 +1,4 @@ { "title": "OpenProse", - "pages": ["index", "openprose", "reactor", "sdk", "cli", "reactor-devtools"] + "pages": ["index", "openprose"] } diff --git a/content/docs/openprose/contracts.mdx b/content/docs/openprose/contracts.mdx index fbbf95a..97851a1 100644 --- a/content/docs/openprose/contracts.mdx +++ b/content/docs/openprose/contracts.mdx @@ -17,9 +17,9 @@ 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. +receipts) is the harness's concern; see +[Harness-agnostic](/openprose/harness-agnostic) for that seam. Here, intent +lives only in the contract. ## Frontmatter and identity @@ -42,7 +42,7 @@ keys the node's world-model and receipt-ledger state on disk. | --- | --- | --- | | `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`. | +| `gateway` | Mounted as external-driven | Sugar for an external-driven responsibility. Compiles into a trigger registration for the serving host; 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. | @@ -173,9 +173,9 @@ Markdown. description="The optional imperative pinning layer inside ### Execution, for when declarative defaults are not enough." /> diff --git a/content/docs/openprose/declare-outcomes.mdx b/content/docs/openprose/declare-outcomes.mdx index 8bbd994..a3e2293 100644 --- a/content/docs/openprose/declare-outcomes.mdx +++ b/content/docs/openprose/declare-outcomes.mdx @@ -40,8 +40,8 @@ That is the whole shift: 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. +runtime mechanics, owned by the harness that serves the contract -- and left to +it on purpose. ## Intent lives only in the contract @@ -70,8 +70,8 @@ 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 +If you know React, you already know the shape. The world-model plays the role +of the **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 @@ -95,9 +95,8 @@ 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. +it records (receipts). Those mechanics belong to the host; this page is only +about the act of declaring the truth. ## A type system for agent workflows @@ -165,9 +164,9 @@ explicit part stays subordinate to the declared outcome. See description="The authored surface in depth -- the five kinds and the load-bearing sections, with `### Maintains` as the world-model schema doing four jobs at once." /> The mapping is the host's job, not yours. Codex-style and Claude Code-style @@ -46,9 +46,9 @@ 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. +unchanged across hosts. You do not maintain a Claude variant and a Codex +variant. You maintain one `.prose.md`. The host changes; the declared outcome +does not. Concretely, a shell line like: @@ -57,8 +57,8 @@ 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 +this contract." Swapping the host -- `codex-sdk`, `claude-sdk`, or a local +`mock` -- changes who provides the five primitives, not what the contract asks for. ## The session embodies the VM -- there is no parser @@ -92,42 +92,29 @@ Keeping the two layers distinct keeps both honest. The boundary is sharp: `*.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. + receipts, forecasts, freshness tracking, and reconciler decisions are runtime + state owned by that host. 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 a host advertises receipts, fingerprints, memoization, or a +reconciler, read those as **one host's runtime**, not as language features. A +deterministic host can use them to re-render only what moved, so cost scales +with surprise rather than the clock. The contract you author is the same +whether or not a host chooses to memoize it. ## Where to go next - + diff --git a/content/docs/openprose/index.mdx b/content/docs/openprose/index.mdx index 2020cc2..1320cd9 100644 --- a/content/docs/openprose/index.mdx +++ b/content/docs/openprose/index.mdx @@ -11,11 +11,10 @@ contract that declares an ideal world-model -- the truths you want kept current 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. +This is the foundation. The contract is harness-agnostic: the same Markdown +runs on any compliant host, and the language deliberately leaves the runtime +that serves it to the host. You author against the language; the host provides +the machine. ## Declare outcomes, not steps @@ -88,19 +87,19 @@ syntax or a YAML overlay. The authored surface stays small, stable, and complete on purpose. -## Hands off to Reactor +## Hands off to the host 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. +concern. A deterministic host can compile a contract set once and then do +expensive model work only when something material actually moved, so cost +scales with surprise rather than the clock; the language never requires any of +that, and the same contract runs on a plain agent session. -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. +You author OpenProse against any Prose-Complete host. See +[Harness-agnostic](/openprose/harness-agnostic) for the seam between the +language and the machine that runs it. - diff --git a/content/docs/openprose/setup.mdx b/content/docs/openprose/setup.mdx index aeb2e74..e41ed2b 100644 --- a/content/docs/openprose/setup.mdx +++ b/content/docs/openprose/setup.mdx @@ -1,153 +1,48 @@ --- 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. +description: Install the skill, point your agent at a contract, and run it. The session embodies the VM; there is no separate binary to install. --- # 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. +OpenProse runs where your agent already lives. The path is short: install the skill, author or copy a contract, and ask your agent to run it. -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: +## 1. Install the skill ```bash -npx -p @openprose/reactor-devtools reactor-devtools --example masked-relay --describe +npx skills add openprose/prose ``` -```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 -``` +That installs the `open-prose` skill into any Prose-Complete coding agent (Claude Code, Codex CLI, OpenCode, and friends). The skill teaches the session the language: the contract grammar, the compile behavior, and the execution semantics. There are no other dependencies. -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. +## 2. Run a contract -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): +Point your agent at a `*.prose.md` file and say: ```bash -npm install @openprose/reactor @openprose/reactor-cli @openprose/reactor-devtools -# then call the binaries with `npx reactor …` / `npx reactor-devtools …` +prose run ``` -`@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) -``` +This is an instruction to the session, not a shell binary. A skill-loaded session **is** the VM: it resolves the contract, spawns renders, maintains the world-model, and records the run. See [Harness-agnostic](/openprose/harness-agnostic) for why there is no parser and what a host must provide. -`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. +## 3. Author your first contract -`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. +A contract is a Markdown file with `kind:` frontmatter and a handful of `###` sections: -`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; +- one `kind: responsibility` per standing goal, carrying its `### Goal` and `### Maintains` (the truth it keeps current), plus `### Requires` when it subscribes to another contract's facets; +- optional `kind: gateway` contracts for external 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: +Start small: a single responsibility with a `### Goal` and a `### Maintains` is a complete program. [Contracts](/openprose/contracts) teaches the full authored surface, and [Declare outcomes](/openprose/declare-outcomes) teaches the discipline of writing the truth instead of the steps. -```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. +## 4. Learn from the examples - -**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. - +The fastest way to pick up the idiom is to read working contracts. The [`skills/open-prose/examples/`](https://github.com/openprose/prose/tree/main/skills/open-prose/examples) directory in the open-source repo is the tour: each example carries its contract source and a README with its standing goal. Copy a shape that resembles your problem and adapt it. Read the contract before you run it. -`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 -``` +## Where the truth lives -## 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 canonical execution behavior is the open-source [`open-prose` skill](https://github.com/openprose/prose/tree/main/skills/open-prose) itself. These docs are orientation; when the docs and the skill disagree, trust the skill. --- -*The conversation always ends. The responsibility shouldn't have to.* +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/openprose/typed-image.mdx b/content/docs/openprose/typed-image.mdx index 4daea0f..bf1a9af 100644 --- a/content/docs/openprose/typed-image.mdx +++ b/content/docs/openprose/typed-image.mdx @@ -24,8 +24,10 @@ A diagram is a *better* notation than prose for the half of a contract that is tedious to write, and a worse one for the half that is easy. A drawing of the graph conveys structure at a glance -- which node subscribes to which facet, where the fan-out and the diamonds are -- exactly the wiring that is -error-prone to hand-author and that [Forme](/reactor/the-dag-and-compile) has to -resolve. Prose, in turn, owns the intent nuance a picture can only gesture at: +error-prone to hand-author and that +[Forme](https://github.com/openprose/prose/blob/main/skills/open-prose/forme.md), +the compile-phase wiring, has to resolve. Prose, in turn, owns the intent +nuance a picture can only gesture at: the exact postcondition, the freshness window. So a typed image lets the **picture own the structural skeleton** and the @@ -81,7 +83,7 @@ reads is an adapter binding wired at serve time, not something the brief encodes ## Scope: a system, a node, or a function The same type holds at any size. A many-box image resolves to a -[Forme](/reactor/the-dag-and-compile) graph of `responsibility`/`gateway` +Forme-wired graph of `responsibility`/`gateway` contracts; a single box resolves to one `responsibility`; one box drawn as a function resolves to a `function` -- a visual signature and intent. Fewer boxes, same predicate. @@ -102,9 +104,9 @@ contracts. description="The *.prose.md the resolve emits -- the five kinds and the load-bearing sections." /> diff --git a/content/docs/reactor-devtools/describe.mdx b/content/docs/reactor-devtools/describe.mdx deleted file mode 100644 index ead945b..0000000 --- a/content/docs/reactor-devtools/describe.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -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 deleted file mode 100644 index c714343..0000000 --- a/content/docs/reactor-devtools/index.mdx +++ /dev/null @@ -1,104 +0,0 @@ ---- -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 deleted file mode 100644 index 7a246af..0000000 --- a/content/docs/reactor-devtools/meta.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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 deleted file mode 100644 index fd669f5..0000000 --- a/content/docs/reactor-devtools/quickstart.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -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 deleted file mode 100644 index b73e4cb..0000000 --- a/content/docs/reactor-devtools/recording.mdx +++ /dev/null @@ -1,73 +0,0 @@ ---- -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 deleted file mode 100644 index e1dd2ff..0000000 --- a/content/docs/reactor-devtools/reference.mdx +++ /dev/null @@ -1,107 +0,0 @@ ---- -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 deleted file mode 100644 index 6aa8164..0000000 --- a/content/docs/reactor-devtools/state-dirs-and-replay.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -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 deleted file mode 100644 index 89c45c1..0000000 --- a/content/docs/reactor-devtools/the-viewer.mdx +++ /dev/null @@ -1,77 +0,0 @@ ---- -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 deleted file mode 100644 index 1b6312d..0000000 --- a/content/docs/reactor/continuity-and-ingestion.mdx +++ /dev/null @@ -1,97 +0,0 @@ ---- -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 deleted file mode 100644 index 453e8ce..0000000 --- a/content/docs/reactor/index.mdx +++ /dev/null @@ -1,172 +0,0 @@ ---- -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 deleted file mode 100644 index c14c369..0000000 --- a/content/docs/reactor/meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "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 deleted file mode 100644 index 435be9e..0000000 --- a/content/docs/reactor/reconciler-and-receipts.mdx +++ /dev/null @@ -1,119 +0,0 @@ ---- -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 deleted file mode 100644 index f1bde3c..0000000 --- a/content/docs/reactor/the-dag-and-compile.mdx +++ /dev/null @@ -1,102 +0,0 @@ ---- -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 deleted file mode 100644 index 719bb7d..0000000 --- a/content/docs/reactor/world-model-and-fingerprints.mdx +++ /dev/null @@ -1,158 +0,0 @@ ---- -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/sdk/adapters.mdx b/content/docs/sdk/adapters.mdx deleted file mode 100644 index 7f43a08..0000000 --- a/content/docs/sdk/adapters.mdx +++ /dev/null @@ -1,266 +0,0 @@ ---- -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 deleted file mode 100644 index 3bb3e3e..0000000 --- a/content/docs/sdk/agents.mdx +++ /dev/null @@ -1,530 +0,0 @@ ---- -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 -}; -``` - -## Bring your own LLM provider - -Reactor is **not** bound to OpenRouter. The default render points at OpenRouter's -OpenAI-compatible surface only because it is a cheap, broad gateway -- but the -`provider` field is plain `@openai/agents` configuration, so you point it -anywhere the way **any** `@openai/agents` consumer would: build a scoped -`OpenAIProvider` at the base URL of your choice and hand it in. Nothing about the -harness is OpenRouter-specific. - -```ts -import { reactor } from "@openprose/reactor"; -import { OpenAIProvider } from "@openai/agents"; - -// Anthropic, directly -- not through OpenRouter. (OpenAI and Google work the same -// way; only the base URL, key, and model id change -- see the table below.) -const provider = new OpenAIProvider({ - apiKey: process.env.ANTHROPIC_API_KEY!, - baseURL: "https://api.anthropic.com/v1/", - useResponses: false, // Chat Completions -- the surface these vendors share -}); - -const { reactor: r } = await reactor("./my-project", { - directory: "./state", - render: { provider, model: "claude-haiku-4-5" }, // run-phase renders - compile: { options: { provider, model: "claude-haiku-4-5" } }, // compile sessions -}); -await r.ingest("source", { wake: { source: "external", refs: [] } }); -``` - -The same scoped `provider` flows to **both** phases: `render.provider` drives the -run-phase renders, and `compile.options.provider` drives the compile sessions -(they are the same bounded-session machinery). Pass it in only one place and the -other still defaults to OpenRouter, so set both when you mean to switch wholesale. - -Any vendor with an OpenAI-compatible Chat Completions endpoint drops straight in: - -| Vendor | `baseURL` | Key | Example model id | -| --- | --- | --- | --- | -| OpenRouter (default) | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` | `google/gemini-3.5-flash` | -| OpenAI | `https://api.openai.com/v1` | `OPENAI_API_KEY` | `gpt-4o-mini` | -| Anthropic (plain text only -- see below) | `https://api.anthropic.com/v1/` | `ANTHROPIC_API_KEY` | `claude-haiku-4-5` | -| Google Gemini | `https://generativelanguage.googleapis.com/v1beta/openai/` | `GEMINI_API_KEY` | `gemini-2.5-flash` | - - -**`useResponses: false` is the safe default for a non-OpenAI host.** It selects -Chat Completions, the surface every vendor above implements; the newer Responses -API is OpenAI-only and 404s elsewhere. Against OpenAI's own host you may leave it -unset to use Responses. - - -Because `provider` is a **scoped** `ModelProvider`, this never mutates the -`@openai/agents` process-global default client -- two reactors in one process can -target two different vendors. The wiring is exercised live (OpenRouter + OpenAI + -Anthropic) in -`packages/reactor/src/adapters/agent-render/__tests__/provider-byo.live.test.ts`. - - -**Don't use Anthropic's OpenAI-compatible endpoint for Claude -- use the native -adapter below.** Compile sessions and the render's done/failed signal use a -JSON-schema `response_format`, and renders drive tools. Anthropic documents its -[OpenAI SDK compatibility layer](https://platform.claude.com/docs/en/api/openai-sdk) -as "primarily for testing" -- it **ignores `response_format`** and rejects our -structured schema with `400 response_format.json_schema.strict`. So -`baseURL: "https://api.anthropic.com/v1/"` (the row above) is fine for plain-text -probes but **not** the structured compile/render path. For Claude with structured -outputs you have two supported routes: the **native Messages API** via the AI-SDK -adapter (next), or **OpenRouter** (`provider` pointed at -`https://openrouter.ai/api/v1`, model `anthropic/claude-...`), where Anthropic's -own models accept the schema as-is. - - -### Claude via the native Anthropic Messages API - -The supported way to drive Claude directly is the official `@openai/agents` route -for non-OpenAI models: the [AI-SDK adapter](https://openai.github.io/openai-agents-js/extensions/ai-sdk/) -over [`@ai-sdk/anthropic`](https://platform.claude.com/docs/en/build-with-claude/structured-outputs), -which hits Anthropic's native Messages API where structured outputs and tools -work. This is **still plain `@openai/agents` configuration** -- `provider` accepts -any `ModelProvider`, so the harness is untouched; only the model session changes. -No SDK changes are needed. - -```sh -npm add @openai/agents-extensions ai @ai-sdk/anthropic -``` - -```ts -import { reactor } from "@openprose/reactor"; -import type { ModelProvider } from "@openai/agents"; -import { aisdk } from "@openai/agents-extensions/ai-sdk"; -import { createAnthropic } from "@ai-sdk/anthropic"; - -// A scoped ModelProvider backed by Anthropic's NATIVE Messages API. -function anthropicNativeProvider(apiKey: string): ModelProvider { - const anthropic = createAnthropic({ apiKey }); - const cache = new Map>(); - return { - getModel(modelName = "claude-haiku-4-5") { - let model = cache.get(modelName); - if (!model) cache.set(modelName, (model = aisdk(anthropic(modelName)))); - return model; - }, - }; -} - -const provider = anthropicNativeProvider(process.env.ANTHROPIC_API_KEY!); -const { reactor: r } = await reactor("./my-project", { - directory: "./state", - render: { provider, model: "claude-haiku-4-5" }, // run-phase renders - compile: { options: { provider, model: "claude-haiku-4-5" } }, // compile sessions -}); -await r.ingest("source", { wake: { source: "external", refs: [] } }); -``` - -The [Reactor CLI](/cli/configuration#choosing-a-model-provider) builds exactly -this provider for you when you set `provider: anthropic` in `reactor.yml` -- you -don't write the adapter wiring yourself there. - -For an API that is **not** OpenAI-compatible at all (a local model, a bespoke -gateway) and has no AI-SDK provider, drop one level to the -[`RenderBackend` injection seam](#the-renderbackend-injection-seam) below: you -own the whole session and the harness keeps its instruction composition, tools, -harvest, and cost capture. - -## 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 deleted file mode 100644 index fbab5d2..0000000 --- a/content/docs/sdk/front-door.mdx +++ /dev/null @@ -1,371 +0,0 @@ ---- -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 deleted file mode 100644 index ce02cfe..0000000 --- a/content/docs/sdk/index.mdx +++ /dev/null @@ -1,185 +0,0 @@ ---- -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 deleted file mode 100644 index cc7889b..0000000 --- a/content/docs/sdk/internals.mdx +++ /dev/null @@ -1,215 +0,0 @@ ---- -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 deleted file mode 100644 index ec713af..0000000 --- a/content/docs/sdk/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "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 deleted file mode 100644 index 969892b..0000000 --- a/content/docs/sdk/run.mdx +++ /dev/null @@ -1,284 +0,0 @@ ---- -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/next.config.mjs b/next.config.mjs index 03012b8..733b30b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -16,6 +16,52 @@ const config = { destination: "/openprose", permanent: true, }, + // The docs site covers the language. The harness reference lives with + // the packages in the openprose/prose repo; send the old harness routes + // there. Temporary redirects on purpose: these routes may host docs + // again when the harness documentation is reworked. + { + source: "/reactor", + destination: + "https://github.com/openprose/prose#reactor-the-recommended-harness", + permanent: false, + }, + { + source: "/reactor/:path*", + destination: + "https://github.com/openprose/prose#reactor-the-recommended-harness", + permanent: false, + }, + { + source: "/sdk/:path*", + destination: + "https://github.com/openprose/prose/tree/main/packages/reactor", + permanent: false, + }, + { + source: "/sdk", + destination: + "https://github.com/openprose/prose/tree/main/packages/reactor", + permanent: false, + }, + { + source: "/cli/:path*", + destination: + "https://github.com/openprose/prose/tree/main/packages/reactor-cli", + permanent: false, + }, + { + source: "/reactor-devtools/:path*", + destination: + "https://github.com/openprose/prose/tree/main/packages/reactor-devtools", + permanent: false, + }, + { + source: "/reactor-devtools", + destination: + "https://github.com/openprose/prose/tree/main/packages/reactor-devtools", + permanent: false, + }, ]; }, }; From 1600342efa60e961c7c4b8de07a0773b772269d8 Mon Sep 17 00:00:00 2001 From: Jose Montes de Oca Date: Tue, 28 Jul 2026 14:08:22 -0400 Subject: [PATCH 2/5] docs: smooth the first-run path and tighten the door pages Setup now hands the reader a complete contract before asking them to run one, and says where to file an issue when something breaks. The root door stops duplicating the language overview (the section is retitled "The language"), the seam page defines its state terms abstractly and names Reactor once, pointing at the repo for the harness reference. --- content/docs/index.mdx | 6 +-- content/docs/openprose/declare-outcomes.mdx | 14 ++----- content/docs/openprose/harness-agnostic.mdx | 14 ++++--- content/docs/openprose/index.mdx | 4 +- content/docs/openprose/meta.json | 2 +- content/docs/openprose/prosescript.mdx | 4 +- content/docs/openprose/setup.mdx | 41 +++++++++++++++------ 7 files changed, 48 insertions(+), 37 deletions(-) diff --git a/content/docs/index.mdx b/content/docs/index.mdx index bce8ba8..29f4ea4 100644 --- a/content/docs/index.mdx +++ b/content/docs/index.mdx @@ -7,7 +7,7 @@ description: A declarative language for standing AI work. You declare the outcom **OpenProse is a declarative language for standing AI work.** You write Markdown contracts (`*.prose.md`) that declare an ideal world-model: the truths you want kept current. You say what must stay true, and the host session works out the model work it takes to keep it that way. When order, loops, or exact choreography genuinely matter, optional imperative **ProseScript** plans drop in. Declarative by default, imperative where you want the control. -The language ships as a skill. Install it, and any Prose-Complete coding agent (Claude Code, Codex CLI, OpenCode, and friends) can author and run contracts: +The language ships as a skill. Install it, and any Prose-Complete coding agent (any agent host that can spawn sessions, read and write files, and call tools: Claude Code, Codex CLI, OpenCode, and friends) can author and run contracts: ```bash npx skills add openprose/prose @@ -19,7 +19,7 @@ From there, point your agent at a contract and say `prose run `. The sessi @@ -71,7 +71,7 @@ account carries a `valid_until`. Postcondition: every flagged account cites a corroborating signal. ``` -`### 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 gate what a render may commit. _Structure is subscription:_ Forme, the compile-phase wiring, matches `Requires.` against `Maintains.` and wires the graph from the contracts themselves. +`### Maintains` is the load-bearing section: the schema of the truth this contract keeps current. [Contracts](/openprose/contracts) teaches it in depth, along with the five kinds and the rest of the authored surface. How a host *serves* that contract set (memoization, receipts, a reconciler) is the host's concern, not the language's. The contract is the public artifact; it runs unchanged on any compliant host. See [Harness-agnostic](/openprose/harness-agnostic) for the seam. diff --git a/content/docs/openprose/declare-outcomes.mdx b/content/docs/openprose/declare-outcomes.mdx index a3e2293..b4b27a5 100644 --- a/content/docs/openprose/declare-outcomes.mdx +++ b/content/docs/openprose/declare-outcomes.mdx @@ -101,17 +101,9 @@ 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. +The mental model that makes the discipline pay for itself: a bare prompt is +`any`, a contract is a typed function. Declaring outcomes is how you give an +agent workflow a type; [The language](/openprose) develops the analogy in full. ## What you actually write diff --git a/content/docs/openprose/harness-agnostic.mdx b/content/docs/openprose/harness-agnostic.mdx index b2e2b6e..433ce62 100644 --- a/content/docs/openprose/harness-agnostic.mdx +++ b/content/docs/openprose/harness-agnostic.mdx @@ -25,8 +25,8 @@ and any host: | --- | --- | | `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 | +| `read_state` / `write_state` | Read and write run state through whatever durable store the host provides | +| `copy_binding` | Publish a declared output through that same durable store; 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 @@ -57,9 +57,8 @@ 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`, or a local -`mock` -- changes who provides the five primitives, not what the contract -asks for. +this contract." Swapping the host changes who provides the five primitives, +not what the contract asks for. ## The session embodies the VM -- there is no parser @@ -101,7 +100,10 @@ So when a host advertises receipts, fingerprints, memoization, or a reconciler, read those as **one host's runtime**, not as language features. A deterministic host can use them to re-render only what moved, so cost scales with surprise rather than the clock. The contract you author is the same -whether or not a host chooses to memoize it. +whether or not a host chooses to memoize it. One such host today is +**Reactor**, the deterministic harness built alongside the language; its +reference lives with the packages in the +[openprose/prose repo](https://github.com/openprose/prose#reactor-the-recommended-harness). ## Where to go next diff --git a/content/docs/openprose/index.mdx b/content/docs/openprose/index.mdx index 1320cd9..0a21eb0 100644 --- a/content/docs/openprose/index.mdx +++ b/content/docs/openprose/index.mdx @@ -1,9 +1,9 @@ --- -title: OpenProse +title: The language 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 +# The language 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 diff --git a/content/docs/openprose/meta.json b/content/docs/openprose/meta.json index 3ae5ebf..a5e44dc 100644 --- a/content/docs/openprose/meta.json +++ b/content/docs/openprose/meta.json @@ -1,4 +1,4 @@ { - "title": "OpenProse", + "title": "The language", "pages": ["index", "declare-outcomes", "contracts", "prosescript", "typed-image", "harness-agnostic", "setup"] } diff --git a/content/docs/openprose/prosescript.mdx b/content/docs/openprose/prosescript.mdx index 3d77df4..028bf94 100644 --- a/content/docs/openprose/prosescript.mdx +++ b/content/docs/openprose/prosescript.mdx @@ -372,5 +372,5 @@ pattern and binds its slots before the delegation runs. 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. +[ProseScript reference](https://github.com/openprose/prose/blob/main/skills/open-prose/prosescript.md) +in the loaded skill. When the docs and the skill disagree, trust the skill. diff --git a/content/docs/openprose/setup.mdx b/content/docs/openprose/setup.mdx index e41ed2b..70a49c0 100644 --- a/content/docs/openprose/setup.mdx +++ b/content/docs/openprose/setup.mdx @@ -1,6 +1,6 @@ --- title: Setup -description: Install the skill, point your agent at a contract, and run it. The session embodies the VM; there is no separate binary to install. +description: Install the skill, author a contract, and run it where your agent lives. The session embodies the VM; there is no separate binary to install. --- # Setup @@ -15,25 +15,40 @@ npx skills add openprose/prose That installs the `open-prose` skill into any Prose-Complete coding agent (Claude Code, Codex CLI, OpenCode, and friends). The skill teaches the session the language: the contract grammar, the compile behavior, and the execution semantics. There are no other dependencies. -## 2. Run a contract +## 2. Author your first contract -Point your agent at a `*.prose.md` file and say: +A contract is a Markdown file with `kind:` frontmatter and a handful of `###` sections. This one is complete; save it as `competitor-funding.prose.md`: -```bash -prose run +```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. ``` -This is an instruction to the session, not a shell binary. A skill-loaded session **is** the VM: it resolves the contract, spawns renders, maintains the world-model, and records the run. See [Harness-agnostic](/openprose/harness-agnostic) for why there is no parser and what a host must provide. +A single responsibility with a `### Goal` and a `### Maintains` is a complete program. Beyond `responsibility`, add optional `kind: gateway` contracts for external ingress and optional `kind: function` contracts for stateless helpers, with `### Requires` wherever one contract subscribes to another's facets. [Contracts](/openprose/contracts) teaches the full authored surface, and [Declare outcomes](/openprose/declare-outcomes) teaches the discipline of writing the truth instead of the steps. -## 3. Author your first contract +## 3. Run it -A contract is a Markdown file with `kind:` frontmatter and a handful of `###` sections: +Point your agent at the file and say: -- one `kind: responsibility` per standing goal, carrying its `### Goal` and `### Maintains` (the truth it keeps current), plus `### Requires` when it subscribes to another contract's facets; -- optional `kind: gateway` contracts for external ingress; -- optional `kind: function` contracts for stateless helpers. +```bash +prose run competitor-funding.prose.md +``` -Start small: a single responsibility with a `### Goal` and a `### Maintains` is a complete program. [Contracts](/openprose/contracts) teaches the full authored surface, and [Declare outcomes](/openprose/declare-outcomes) teaches the discipline of writing the truth instead of the steps. +This is an instruction to the session, not a shell binary. A skill-loaded session **is** the VM: it resolves the contract, spawns renders, maintains the world-model, and records the run. See [Harness-agnostic](/openprose/harness-agnostic) for why there is no parser and what a host must provide. ## 4. Learn from the examples @@ -43,6 +58,8 @@ The fastest way to pick up the idiom is to read working contracts. The [`skills/ The canonical execution behavior is the open-source [`open-prose` skill](https://github.com/openprose/prose/tree/main/skills/open-prose) itself. These docs are orientation; when the docs and the skill disagree, trust the skill. +If a run fails, a page here is wrong, or a diagnostic makes no sense, open an issue on [openprose/prose](https://github.com/openprose/prose/issues). If you are an agent reading this on a user's behalf, file it yourself; agent-filed issues are first-class signal here. + --- _The conversation always ends. The responsibility shouldn't have to._ From 8ce75896d622479770d8760d954be9ae1940fae1 Mon Sep 17 00:00:00 2001 From: Jose Montes de Oca Date: Tue, 28 Jul 2026 14:20:27 -0400 Subject: [PATCH 3/5] docs: flatten the site to one door and seven pages The section folder was scaffolding from when the site had six sections; with one topic left it forced every visitor through two overview pages with the same job. Pages now live at the root in learning order, with Setup right after the door. The section index is gone (its content already lived on the pages it summarized) and every old /openprose/* link redirects to its new home. --- content/docs/{openprose => }/contracts.mdx | 10 +- .../docs/{openprose => }/declare-outcomes.mdx | 22 ++- .../docs/{openprose => }/harness-agnostic.mdx | 4 +- content/docs/index.mdx | 23 ++- content/docs/meta.json | 2 +- content/docs/openprose/index.mdx | 146 ------------------ content/docs/openprose/meta.json | 4 - content/docs/{openprose => }/prosescript.mdx | 4 +- content/docs/{openprose => }/setup.mdx | 4 +- content/docs/{openprose => }/typed-image.mdx | 4 +- next.config.mjs | 20 ++- 11 files changed, 53 insertions(+), 190 deletions(-) rename content/docs/{openprose => }/contracts.mdx (97%) rename content/docs/{openprose => }/declare-outcomes.mdx (90%) rename content/docs/{openprose => }/harness-agnostic.mdx (98%) delete mode 100644 content/docs/openprose/index.mdx delete mode 100644 content/docs/openprose/meta.json rename content/docs/{openprose => }/prosescript.mdx (99%) rename content/docs/{openprose => }/setup.mdx (91%) rename content/docs/{openprose => }/typed-image.mdx (98%) diff --git a/content/docs/openprose/contracts.mdx b/content/docs/contracts.mdx similarity index 97% rename from content/docs/openprose/contracts.mdx rename to content/docs/contracts.mdx index 97851a1..fbcc8db 100644 --- a/content/docs/openprose/contracts.mdx +++ b/content/docs/contracts.mdx @@ -18,7 +18,7 @@ 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 harness's concern; see -[Harness-agnostic](/openprose/harness-agnostic) for that seam. Here, intent +[Harness-agnostic](/harness-agnostic) for that seam. Here, intent lives only in the contract. ## Frontmatter and identity @@ -169,22 +169,22 @@ Markdown. diff --git a/content/docs/openprose/declare-outcomes.mdx b/content/docs/declare-outcomes.mdx similarity index 90% rename from content/docs/openprose/declare-outcomes.mdx rename to content/docs/declare-outcomes.mdx index b4b27a5..75492a8 100644 --- a/content/docs/openprose/declare-outcomes.mdx +++ b/content/docs/declare-outcomes.mdx @@ -101,9 +101,17 @@ about the act of declaring the truth. ## A type system for agent workflows -The mental model that makes the discipline pay for itself: a bare prompt is -`any`, a contract is a typed function. Declaring outcomes is how you give an -agent workflow a type; [The language](/openprose) develops the analogy in full. +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 @@ -144,7 +152,7 @@ specific way -- a tool that must run, an order that must hold -- OpenProse has a 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). +[ProseScript](/prosescript). ## Where to go next @@ -152,17 +160,17 @@ explicit part stays subordinate to the declared outcome. See diff --git a/content/docs/openprose/harness-agnostic.mdx b/content/docs/harness-agnostic.mdx similarity index 98% rename from content/docs/openprose/harness-agnostic.mdx rename to content/docs/harness-agnostic.mdx index 433ce62..72a5bda 100644 --- a/content/docs/openprose/harness-agnostic.mdx +++ b/content/docs/harness-agnostic.mdx @@ -110,12 +110,12 @@ reference lives with the packages in the diff --git a/content/docs/index.mdx b/content/docs/index.mdx index 29f4ea4..d77f1e9 100644 --- a/content/docs/index.mdx +++ b/content/docs/index.mdx @@ -19,30 +19,25 @@ From there, point your agent at a contract and say `prose run `. The sessi - ## The shape of a contract @@ -71,9 +66,9 @@ account carries a `valid_until`. Postcondition: every flagged account cites a corroborating signal. ``` -`### Maintains` is the load-bearing section: the schema of the truth this contract keeps current. [Contracts](/openprose/contracts) teaches it in depth, along with the five kinds and the rest of the authored surface. +`### Maintains` is the load-bearing section: the schema of the truth this contract keeps current. [Contracts](/contracts) teaches it in depth, along with the five kinds and the rest of the authored surface. -How a host *serves* that contract set (memoization, receipts, a reconciler) is the host's concern, not the language's. The contract is the public artifact; it runs unchanged on any compliant host. See [Harness-agnostic](/openprose/harness-agnostic) for the seam. +How a host *serves* that contract set (memoization, receipts, a reconciler) is the host's concern, not the language's. The contract is the public artifact; it runs unchanged on any compliant host. See [Harness-agnostic](/harness-agnostic) for the seam. ## Where the truth lives diff --git a/content/docs/meta.json b/content/docs/meta.json index 8fe69bb..d2ff519 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -1,4 +1,4 @@ { "title": "OpenProse", - "pages": ["index", "openprose"] + "pages": ["index", "setup", "declare-outcomes", "contracts", "prosescript", "typed-image", "harness-agnostic"] } diff --git a/content/docs/openprose/index.mdx b/content/docs/openprose/index.mdx deleted file mode 100644 index 0a21eb0..0000000 --- a/content/docs/openprose/index.mdx +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: The language -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. ---- - -# The language - -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. The contract is harness-agnostic: the same Markdown -runs on any compliant host, and the language deliberately leaves the runtime -that serves it to the host. You author against the language; the host provides -the machine. - -## 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 the host - -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. A deterministic host can compile a contract set once and then do -expensive model work only when something material actually moved, so cost -scales with surprise rather than the clock; the language never requires any of -that, and the same contract runs on a plain agent session. - -You author OpenProse against any Prose-Complete host. See -[Harness-agnostic](/openprose/harness-agnostic) for the seam between the -language and the machine that runs it. - - - - - - - - - - -## 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 deleted file mode 100644 index a5e44dc..0000000 --- a/content/docs/openprose/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "The language", - "pages": ["index", "declare-outcomes", "contracts", "prosescript", "typed-image", "harness-agnostic", "setup"] -} diff --git a/content/docs/openprose/prosescript.mdx b/content/docs/prosescript.mdx similarity index 99% rename from content/docs/openprose/prosescript.mdx rename to content/docs/prosescript.mdx index 028bf94..b350fba 100644 --- a/content/docs/openprose/prosescript.mdx +++ b/content/docs/prosescript.mdx @@ -361,12 +361,12 @@ pattern and binds its slots before the delegation runs. diff --git a/content/docs/openprose/setup.mdx b/content/docs/setup.mdx similarity index 91% rename from content/docs/openprose/setup.mdx rename to content/docs/setup.mdx index 70a49c0..f3f1aca 100644 --- a/content/docs/openprose/setup.mdx +++ b/content/docs/setup.mdx @@ -38,7 +38,7 @@ everywhere: `fetched_at` and source request ids. Postcondition: every funding event cites a source. ``` -A single responsibility with a `### Goal` and a `### Maintains` is a complete program. Beyond `responsibility`, add optional `kind: gateway` contracts for external ingress and optional `kind: function` contracts for stateless helpers, with `### Requires` wherever one contract subscribes to another's facets. [Contracts](/openprose/contracts) teaches the full authored surface, and [Declare outcomes](/openprose/declare-outcomes) teaches the discipline of writing the truth instead of the steps. +A single responsibility with a `### Goal` and a `### Maintains` is a complete program. Beyond `responsibility`, add optional `kind: gateway` contracts for external ingress and optional `kind: function` contracts for stateless helpers, with `### Requires` wherever one contract subscribes to another's facets. [Contracts](/contracts) teaches the full authored surface, and [Declare outcomes](/declare-outcomes) teaches the discipline of writing the truth instead of the steps. ## 3. Run it @@ -48,7 +48,7 @@ Point your agent at the file and say: prose run competitor-funding.prose.md ``` -This is an instruction to the session, not a shell binary. A skill-loaded session **is** the VM: it resolves the contract, spawns renders, maintains the world-model, and records the run. See [Harness-agnostic](/openprose/harness-agnostic) for why there is no parser and what a host must provide. +This is an instruction to the session, not a shell binary. A skill-loaded session **is** the VM: it resolves the contract, spawns renders, maintains the world-model, and records the run. See [Harness-agnostic](/harness-agnostic) for why there is no parser and what a host must provide. ## 4. Learn from the examples diff --git a/content/docs/openprose/typed-image.mdx b/content/docs/typed-image.mdx similarity index 98% rename from content/docs/openprose/typed-image.mdx rename to content/docs/typed-image.mdx index bf1a9af..a66343e 100644 --- a/content/docs/openprose/typed-image.mdx +++ b/content/docs/typed-image.mdx @@ -100,12 +100,12 @@ contracts. diff --git a/next.config.mjs b/next.config.mjs index 733b30b..f6e3a4f 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -8,13 +8,23 @@ const config = { reactStrictMode: true, async redirects() { return [ - // The early docs lived under /start/*. PR #2 restructured them into - // /openprose/*. Keep the old "What is OpenProse?" link alive: it now - // maps to the OpenProse overview page (content/docs/openprose/index.mdx). + // The early docs lived under /start/*, then under /openprose/*. The + // site now covers one topic, so the pages live at the root; keep every + // old link alive. { source: "/start/what-is-openprose", - destination: "/openprose", - permanent: true, + destination: "/", + permanent: false, + }, + { + source: "/openprose", + destination: "/", + permanent: false, + }, + { + source: "/openprose/:path*", + destination: "/:path*", + permanent: false, }, // The docs site covers the language. The harness reference lives with // the packages in the openprose/prose repo; send the old harness routes From 96ec81c6503b659ee8a1d5a0b3a7d539774e6ab2 Mon Sep 17 00:00:00 2001 From: Jose Montes de Oca Date: Tue, 28 Jul 2026 15:01:35 -0400 Subject: [PATCH 4/5] docs: fix code-panel chips, mobile wrapping, and stray dashes The contracts page's two program panels inherited the inline-code chip styling from the typography layer, painting a cream box behind every line; the panel now opts out. Markdown fences soft-wrap on narrow viewports so their sentences stay readable (the fence language is now exposed as an attribute for styling), the one four-column table drops its overflow column into the paragraph below it, and prose across the site stops rendering literal double hyphens where a dash was meant. The theme toggle is gone: the site is light-only by design and the control did nothing. --- app/brand.css | 17 +++++++++ app/layout.tsx | 6 ++- components/prose-program.tsx | 2 +- content/docs/contracts.mdx | 62 ++++++++++++++++--------------- content/docs/declare-outcomes.mdx | 47 ++++++++++++----------- content/docs/harness-agnostic.mdx | 12 +++--- content/docs/prosescript.mdx | 42 +++++++++++---------- content/docs/typed-image.mdx | 32 ++++++++-------- source.config.ts | 10 +++++ 9 files changed, 135 insertions(+), 95 deletions(-) diff --git a/app/brand.css b/app/brand.css index 1852882..ac23cf1 100644 --- a/app/brand.css +++ b/app/brand.css @@ -249,3 +249,20 @@ a:hover .op-wordmark span:first-child { .prose-program figcaption span { color: var(--code-text-muted); } + +/* On narrow viewports, soft-wrap prose-shaped fences (markdown) so their + sentences stay readable instead of scrolling off-screen. Alignment-sensitive + fences (text diagrams, bash) keep their horizontal scroll. */ +@media (max-width: 767px) { + #nd-page figure[data-lang="markdown"] pre, + #nd-page figure[data-lang="md"] pre { + min-width: 0; + width: auto; + } + + #nd-page figure[data-lang="markdown"] pre code, + #nd-page figure[data-lang="md"] pre code { + white-space: pre-wrap; + overflow-wrap: anywhere; + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 1b530e6..7e0459e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -16,7 +16,11 @@ export default function Layout({ children }: LayoutProps<"/">) { - + {children} diff --git a/components/prose-program.tsx b/components/prose-program.tsx index 2e1583f..143395f 100644 --- a/components/prose-program.tsx +++ b/components/prose-program.tsx @@ -20,7 +20,7 @@ export async function ProseProgram({ const html = await highlightProse(code, { highlightLines }); return ( -
+
{(title || copy) && (
diff --git a/content/docs/contracts.mdx b/content/docs/contracts.mdx index fbcc8db..7b1d169 100644 --- a/content/docs/contracts.mdx +++ b/content/docs/contracts.mdx @@ -1,11 +1,11 @@ --- 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. +description: The authored surface of OpenProse, covering 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` -- +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 @@ -15,7 +15,7 @@ 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 +This page is the authored surface only (what you write). The runtime that *serves* these contracts (fingerprints, memoization, the continuity clock, receipts) is the harness's concern; see [Harness-agnostic](/harness-agnostic) for that seam. Here, intent @@ -25,9 +25,9 @@ lives only in the contract. 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. +- `kind:` names which of the five contract kinds this is (below). This is the + one field that changes how the file is executed. +- `name:` is 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 @@ -67,44 +67,44 @@ 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`: 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 +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 +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. +3. It declares **facets** (below), the named parts of the truth. +4. It states **postconditions** that 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, +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 +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 -- + 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 +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`: 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. @@ -112,18 +112,20 @@ entry to a producer's `### Maintains` facet and draws the subscription edge. the node's contract together with its subscribed inputs, and re-renders only when one of them moves. -### `### Continuity` -- the wake source +### `### 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. +- a *self-driven* cadence, for truth that 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 decides that time has + passed. +- *external-driven*, which 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 @@ -134,8 +136,8 @@ truth; the runtime decides, deterministically, when it is worth recomputing. 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. +- `### 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. @@ -146,20 +148,20 @@ 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. +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 +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, +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. @@ -175,7 +177,7 @@ Markdown. @@ -48,15 +48,15 @@ it on purpose. 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 +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 +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 @@ -67,14 +67,14 @@ 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 +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 plays the role of the **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 +*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: @@ -92,7 +92,7 @@ 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 +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). Those mechanics belong to the host; this page is only @@ -116,7 +116,7 @@ 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 +`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. @@ -140,17 +140,17 @@ 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 +write the steps. You declared the outcome: the truth, its shape, what matters +about it, and what must hold before it may be committed. 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 +Declarative is the default, not a cage. Some steps genuinely must happen in a +specific way: a tool that must run, an order that must hold. For those, +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](/prosescript). @@ -161,7 +161,7 @@ explicit part stays subordinate to the declared outcome. See + These docs are orientation. The canonical execution behavior lives in the diff --git a/content/docs/harness-agnostic.mdx b/content/docs/harness-agnostic.mdx index 72a5bda..843acd0 100644 --- a/content/docs/harness-agnostic.mdx +++ b/content/docs/harness-agnostic.mdx @@ -1,6 +1,6 @@ --- 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. +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. --- # Harness-agnostic @@ -23,7 +23,7 @@ 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 | +| `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 whatever durable store the host provides | | `copy_binding` | Publish a declared output through that same durable store; never publish undeclared scratch | @@ -32,7 +32,7 @@ and any host: 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 some other host -- it only knows that a render runs in isolation +session inside some other host. It only knows that a render runs in isolation with the paths it declared. @@ -60,7 +60,7 @@ means "ask the selected agent harness to embody the OpenProse VM and execute this contract." Swapping the host changes who provides the five primitives, not what the contract asks for. -## The session embodies the VM -- there is no parser +## 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 @@ -70,7 +70,7 @@ 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 +step is an agent session. The topology, the canonicalizer, and the validators are all session output, not parser output. @@ -89,7 +89,7 @@ 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. + 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 receipts, forecasts, freshness tracking, and reconciler decisions are runtime state owned by that host. They are **not** IR fields and **not** new diff --git a/content/docs/prosescript.mdx b/content/docs/prosescript.mdx index b350fba..ae8a4e9 100644 --- a/content/docs/prosescript.mdx +++ b/content/docs/prosescript.mdx @@ -12,12 +12,12 @@ 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 +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 +`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`. @@ -48,15 +48,17 @@ 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 | +| Surface | Scope | Primary call style | +| --- | --- | --- | +| `### Execution` in a `*.prose.md` | Pinned intra-node choreography | `call function-name` | +| Pattern `### Delegation` | Slot interaction rules inside a pattern instance | `call slot-name` | 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. +function declares it with `### Parameters` / `### Returns`; a pattern's +delegation script draws on `### Slots`, `### Config`, and the instance +bindings the same way. 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 @@ -80,7 +82,7 @@ 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. +that order, every time. Nothing is reordered or inferred. ## The constructs @@ -165,9 +167,9 @@ loop until all tests pass (max: 5): 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. +Open `loop` blocks should carry a `(max: N)` bound (a missing bound 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` @@ -191,7 +193,7 @@ choice best recovery path: throw "No safe recovery path" ``` -Conditions are **discretion text** -- natural-language conditions the VM +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. @@ -218,7 +220,7 @@ runs. `throw` (bare) re-raises the active error inside `catch`; ### `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`. +one-off internal to this render, not a reusable `function`. ```markdown agent researcher: @@ -241,13 +243,13 @@ let review = resume: researcher `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 +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 +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. @@ -297,7 +299,7 @@ let inspection = call inspector ``` `use` is valid in a standalone ProseScript block, but **not** inside embedded -Contract Markdown ProseScript -- declare the dependency in the contract, not in +Contract Markdown ProseScript. Declare the dependency in the contract, not in the render body. ## The boundary: ProseScript does not own the interface @@ -313,7 +315,7 @@ public interfaces in current OpenProse source: - `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` +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 @@ -367,7 +369,7 @@ pattern and binds its slots before the delegation runs. diff --git a/content/docs/typed-image.mdx b/content/docs/typed-image.mdx index a66343e..14caa67 100644 --- a/content/docs/typed-image.mdx +++ b/content/docs/typed-image.mdx @@ -1,13 +1,13 @@ --- title: Typed Image -description: A pixel-only diagram is a visual source one rung above Markdown. An intelligent compile resolve reads the picture and emits the contracts -- and whether it compiles is the typecheck. +description: A pixel-only diagram is a visual source one rung above Markdown. An intelligent compile resolve reads the picture and emits the contracts, and whether it compiles is the typecheck. --- # Typed Image A contract is Markdown. But Markdown is not the only thing that can carry intent -into the compiler. A **typed image** is an ordinary picture -- a `.png` or -`.svg` of boxes and labelled edges, *pixels only*, no embedded payload -- that +into the compiler. A **typed image** is an ordinary picture (a `.png` or +`.svg` of boxes and labelled edges, *pixels only*, no embedded payload) that an intelligent compile step reads and turns into `*.prose.md` contracts. The picture is the human-facing surface; the contracts are what runs. The image @@ -22,8 +22,8 @@ image -> (resolve) -> *.prose.md -> (compile) -> DAG -> (run) -> receipts A diagram is a *better* notation than prose for the half of a contract that is tedious to write, and a worse one for the half that is easy. A drawing of the -graph conveys structure at a glance -- which node subscribes to which facet, -where the fan-out and the diamonds are -- exactly the wiring that is +graph conveys structure at a glance: which node subscribes to which facet, +where the fan-out and the diamonds are. That is exactly the wiring that is error-prone to hand-author and that [Forme](https://github.com/openprose/prose/blob/main/skills/open-prose/forme.md), the compile-phase wiring, has to resolve. Prose, in turn, owns the intent @@ -31,17 +31,17 @@ nuance a picture can only gesture at: the exact postcondition, the freshness window. So a typed image lets the **picture own the structural skeleton** and the -**text inside the boxes own the intent**. It is a schematic -- think circuit -diagram or score -- where labels and glyphs are load-bearing, not decoration. +**text inside the boxes own the intent**. It is a schematic (think circuit +diagram or score) where labels and glyphs are load-bearing, not decoration. ## A brief, not a binary Intent still lives only in the contract (Tenet 1). The resolve does not make the -image a second authored surface for meaning -- it **emits** `*.prose.md` that a +image a second authored surface for meaning. It **emits** `*.prose.md` that a human ratifies, and *that* Markdown carries 100% of the semantic weight, exactly as if it had been typed. The picture is a *brief*; the contract is the spec; the compiled DAG is the binary. All of the model's interpretive latitude is -quarantined to authoring time -- the rarest event in the system -- and the +quarantined to authoring time, the rarest event in the system, and the deterministic runtime that follows is unchanged. This is why the typed image is not new syntax. New capability in OpenProse is @@ -53,7 +53,7 @@ kinds and the same `###` sections you already author by hand. The "type" is conceptual and deliberately loose. It is not a byte schema and there is no parser. An image is **well-typed iff it compiles**: the resolve yields a contract set that Forme can wire into an *acyclic* DAG, and the result -*round-trips stably* -- re-rendering the topology and re-resolving it lands on +*round-trips stably*: re-rendering the topology and re-resolving it lands on the same graph. A picture that cannot be resolved into a compiling, acyclic, stable graph is ill-typed, and the resolve must say so rather than invent a graph into existence. @@ -69,12 +69,12 @@ The resolve reads the image in three tiers: (subscriptions), the facet each multi-out edge carries, and the overall fan-out / diamond geometry. These must be visually unambiguous. - **Pin-or-interrupt (safety).** Freshness windows (`### Continuity`) and - load-bearing postconditions are safety-critical -- a wrong freshness window is + load-bearing postconditions are safety-critical. A wrong freshness window is silent staleness or runaway spend. The resolve reads an *explicit* annotation (a clock glyph, a checklist) or **interrupts** with a `failed` receipt naming the gap. It never guesses these. - **Elaborated (intent).** Given the skeleton pinned, the resolve may author the - `### Goal` prose and the body around the facets -- and it comes back as + `### Goal` prose and the body around the facets, and it comes back as ordinary authoring output for you to ratify. Deployment facts stay out of the picture: which URL or queue a gateway actually @@ -85,14 +85,14 @@ reads is an adapter binding wired at serve time, not something the brief encodes The same type holds at any size. A many-box image resolves to a Forme-wired graph of `responsibility`/`gateway` contracts; a single box resolves to one `responsibility`; one box drawn as a -function resolves to a `function` -- a visual signature and intent. Fewer boxes, +function resolves to a `function`: a visual signature and intent. Fewer boxes, same predicate. The typed image is an **authoring-surface** capability of the OpenProse compile, specified in the [`open-prose` skill](https://github.com/openprose/prose/blob/main/skills/open-prose/visual-source.md) (`visual-source.md`). Resolution is an intelligent compile step, so it is -generative, not deterministic -- which is why its output is reviewable Markdown +generative, not deterministic. That is why its output is reviewable Markdown you ratify before anything runs. The deterministic harness still runs ordinary contracts. @@ -101,12 +101,12 @@ contracts. diff --git a/source.config.ts b/source.config.ts index 9e5cc9f..a227098 100644 --- a/source.config.ts +++ b/source.config.ts @@ -23,6 +23,16 @@ export default defineConfig({ light: 'solarized-dark', dark: 'solarized-dark', }, + transformers: [ + { + // Expose the fence language so styles can treat prose-shaped + // fences (markdown) differently from alignment-sensitive ones. + name: 'lang-attribute', + pre(node) { + node.properties['data-lang'] = this.options.lang; + }, + }, + ], }, }, }); From 6b3d17a71c30e4f3a60aa50a5c501fa5ed3ea658 Mon Sep 17 00:00:00 2001 From: Jose Montes de Oca Date: Tue, 28 Jul 2026 15:26:17 -0400 Subject: [PATCH 5/5] chore: lean on framework options instead of hand-rolled mechanisms The fence-language styling hook becomes the built-in addLanguageClass flag; the custom transformer was silently replacing the default code notation transformers. The redirect table collapses to one entry per archived section (a wildcard source also matches its bare prefix), the mobile soft-wrap now covers the program panels too, and the theme switch is disabled where the rest of the layout config lives. --- __tests__/prose-program-srcs.test.ts | 8 ++-- app/brand.css | 15 ++++--- app/global.css | 3 +- app/layout.tsx | 6 +-- lib/layout.shared.tsx | 3 ++ next.config.mjs | 65 +++++++++------------------- source.config.ts | 14 ++---- 7 files changed, 42 insertions(+), 72 deletions(-) diff --git a/__tests__/prose-program-srcs.test.ts b/__tests__/prose-program-srcs.test.ts index 6e60903..3d7885a 100644 --- a/__tests__/prose-program-srcs.test.ts +++ b/__tests__/prose-program-srcs.test.ts @@ -8,10 +8,10 @@ const REPO_ROOT = resolve(__dirname, ".."); describe(" references", () => { const uses = findProseProgramUses(REPO_ROOT); - // 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. + // Docs pages may embed vendor `.prose` programs via , and an + // empty set is also 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); }); diff --git a/app/brand.css b/app/brand.css index ac23cf1..bbd4a56 100644 --- a/app/brand.css +++ b/app/brand.css @@ -250,18 +250,19 @@ a:hover .op-wordmark span:first-child { color: var(--code-text-muted); } -/* On narrow viewports, soft-wrap prose-shaped fences (markdown) so their - sentences stay readable instead of scrolling off-screen. Alignment-sensitive - fences (text diagrams, bash) keep their horizontal scroll. */ +/* On narrow viewports, soft-wrap prose-shaped fences (markdown fences and the + ProseProgram panels, which are markdown by construction) so their sentences + stay readable instead of scrolling off-screen. Alignment-sensitive fences + (text diagrams, bash) keep their horizontal scroll. */ @media (max-width: 767px) { - #nd-page figure[data-lang="markdown"] pre, - #nd-page figure[data-lang="md"] pre { + #nd-page pre:has(> code.language-markdown), + .prose-program pre { min-width: 0; width: auto; } - #nd-page figure[data-lang="markdown"] pre code, - #nd-page figure[data-lang="md"] pre code { + #nd-page code.language-markdown, + .prose-program pre code { white-space: pre-wrap; overflow-wrap: anywhere; } diff --git a/app/global.css b/app/global.css index 26c8432..0738321 100644 --- a/app/global.css +++ b/app/global.css @@ -5,7 +5,8 @@ /* ============================================ OPENPROSE DOCS - DESIGN SYSTEM Mirrors platform/apps/run "warm manuscript" aesthetic. - Light-only: dark mode disabled in app/layout.tsx via RootProvider. + Light-only: the theme provider is disabled in app/layout.tsx and the + theme switch in lib/layout.shared.tsx. ============================================ */ :root { diff --git a/app/layout.tsx b/app/layout.tsx index 7e0459e..1b530e6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -16,11 +16,7 @@ export default function Layout({ children }: LayoutProps<"/">) { - + {children} diff --git a/lib/layout.shared.tsx b/lib/layout.shared.tsx index b6265da..b8794d4 100644 --- a/lib/layout.shared.tsx +++ b/lib/layout.shared.tsx @@ -13,5 +13,8 @@ export function baseOptions(): BaseLayoutProps { ), }, githubUrl: `https://github.com/${gitConfig.user}/${gitConfig.repo}`, + // The site is light-only by design; without this the sidebar still + // renders a theme toggle even though the theme provider is disabled. + themeSwitch: { enabled: false }, }; } diff --git a/next.config.mjs b/next.config.mjs index f6e3a4f..6901f49 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -7,6 +7,20 @@ const config = { output: "standalone", reactStrictMode: true, async redirects() { + const PROSE_REPO = "https://github.com/openprose/prose"; + + // The docs site covers the language. The harness reference lives with + // the packages in the openprose/prose repo; send the old harness routes + // there. Temporary redirects on purpose: these routes may host docs + // again when the harness documentation is reworked. A `/:path*` source + // also matches the bare prefix, so one entry covers a whole section. + const harnessRoutes = { + reactor: `${PROSE_REPO}#reactor-the-recommended-harness`, + sdk: `${PROSE_REPO}/tree/main/packages/reactor`, + cli: `${PROSE_REPO}/tree/main/packages/reactor-cli`, + "reactor-devtools": `${PROSE_REPO}/tree/main/packages/reactor-devtools`, + }; + return [ // The early docs lived under /start/*, then under /openprose/*. The // site now covers one topic, so the pages live at the root; keep every @@ -16,6 +30,8 @@ const config = { destination: "/", permanent: false, }, + // The bare entry is required: `/:path*` cannot produce `/` when the + // wildcard matches zero segments. { source: "/openprose", destination: "/", @@ -26,52 +42,11 @@ const config = { destination: "/:path*", permanent: false, }, - // The docs site covers the language. The harness reference lives with - // the packages in the openprose/prose repo; send the old harness routes - // there. Temporary redirects on purpose: these routes may host docs - // again when the harness documentation is reworked. - { - source: "/reactor", - destination: - "https://github.com/openprose/prose#reactor-the-recommended-harness", - permanent: false, - }, - { - source: "/reactor/:path*", - destination: - "https://github.com/openprose/prose#reactor-the-recommended-harness", - permanent: false, - }, - { - source: "/sdk/:path*", - destination: - "https://github.com/openprose/prose/tree/main/packages/reactor", - permanent: false, - }, - { - source: "/sdk", - destination: - "https://github.com/openprose/prose/tree/main/packages/reactor", - permanent: false, - }, - { - source: "/cli/:path*", - destination: - "https://github.com/openprose/prose/tree/main/packages/reactor-cli", + ...Object.entries(harnessRoutes).map(([prefix, destination]) => ({ + source: `/${prefix}/:path*`, + destination, permanent: false, - }, - { - source: "/reactor-devtools/:path*", - destination: - "https://github.com/openprose/prose/tree/main/packages/reactor-devtools", - permanent: false, - }, - { - source: "/reactor-devtools", - destination: - "https://github.com/openprose/prose/tree/main/packages/reactor-devtools", - permanent: false, - }, + })), ]; }, }; diff --git a/source.config.ts b/source.config.ts index a227098..0baf4c6 100644 --- a/source.config.ts +++ b/source.config.ts @@ -23,16 +23,10 @@ export default defineConfig({ light: 'solarized-dark', dark: 'solarized-dark', }, - transformers: [ - { - // Expose the fence language so styles can treat prose-shaped - // fences (markdown) differently from alignment-sensitive ones. - name: 'lang-attribute', - pre(node) { - node.properties['data-lang'] = this.options.lang; - }, - }, - ], + // Expose the fence language as a `language-*` class so styles can treat + // prose-shaped fences (markdown) differently from alignment-sensitive + // ones, without replacing the default transformers. + addLanguageClass: true, }, }, });