From 0455ba87bd98a055a37eab64475a3db7378297cb Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Wed, 20 May 2026 13:46:35 -0700 Subject: [PATCH 01/14] docs: draft reactor technical report --- reactor-technical-report.md | 342 ++++++++++++++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 reactor-technical-report.md diff --git a/reactor-technical-report.md b/reactor-technical-report.md new file mode 100644 index 0000000..07387ff --- /dev/null +++ b/reactor-technical-report.md @@ -0,0 +1,342 @@ + + +# Reactor-Class Harnesses: A v0.1 Technical Report + +Date: 2026-05-20 + +This report describes the current OpenProse Reactor worktree, not a future +launch claim. The current evidence supports a narrow but useful statement: +under deterministic Cradle scenarios, a Reactor runtime can maintain a +long-lived responsibility with runtime-produced receipts, reusable verdict +proof, forecast-paced rechecks, and explicit surprise attribution. It does not +yet support a public launch claim, hosted production operation, runtime +variable-depth ensemble judging, npm provenance publication, or stranger +quickstart verification. + +## 1. Problem + +AI sessions are good at bounded bursts of work. They are much weaker at +responsibilities that stay alive after the session ends: "keep the incident +briefing current," "watch release readiness," "refresh compliance evidence," +or "notice when a customer risk signal changes." In these cases the useful +unit is not a one-shot answer. It is a maintained state of the world, plus a +record of why the system checked, acted, slept, or asked for help. + +The OpenProse promise is a lived loop: write durable intent, let bounded +activations inspect changing evidence, interrupt only when judgment or missing +input requires a human, and leave an exit trail that another agent can inspect. +That loop has a cost problem. A naive maintenance loop spends because time +passes. A useful maintenance harness should spend because something material +changed, while still paying an honest floor for forecast-paced plan audits. + +The target is therefore not "agents with more memory" in the abstract. The +target is a harness that can maintain declared responsibilities through +durable state, bounded model calls, policy artifacts, receipts, projections, +and replay. Some interruptions are part of the design. If a contract has no +observable referent, or if a judgment has no anchor, the system should stop +and say so rather than hide uncertainty inside fluent prose. + +## 2. Category + +A Reactor-class harness is an evented reconciliation system for declared +responsibilities. A gateway or local trigger provides an event. A bounded +judge or fulfillment activation reads typed evidence and prior state. The +runtime records a receipt. A Reactor decision projects status and chooses one +of the next moves: act, retry, escalate, schedule another check, or go quiet. + +The category differs from a normal agent loop in the question it asks. The +question is not "what should I do next?" It is "which reconciliation action is +justified by this responsibility, this event, these observations, and this +receipt trail?" That shift lets policy, cost, and audit become durable parts +of the system rather than session-local instincts. + +OpenProse makes the category concrete with a model-authored policy artifact, +compiled to deterministic runtime state; content-addressed receipts; stable +memo keys; forecast-gated scheduling; projections for owner, subscriber, and +public readers; and exit bundles. Two recursions sit on top of the base loop: +policy changes over time, and responsibilities can compose over a graph by +consuming upstream receipts. In v0.1, the single-responsibility runtime spine +and local evidence are the demonstrated center. Broader graph behavior and +runtime ensemble depth remain designed or partially built. + +## 3. Prior Patterns + +The closest analogy is React reconciliation: a stable declaration is compared +with observed inputs, partial work is memoized, and a commit boundary controls +when the outside world changes. The analogy is structural, not literal. A +Reactor's "render" includes model judgment over ambiguous evidence, and its +output is not a DOM tree. Its output is a receipt-backed decision about a +responsibility. + +Control systems contribute the time dimension: forecast, drift, hysteresis, +backstops, and revalidation floors. Kubernetes controllers, event sourcing, +dataflow systems, CQRS/read models, actor systems, and workflow engines are +also part of the lineage. The difference is that workflow engines usually +execute known steps, while a Reactor must decide which reconciliation step is +justified now when the world is partly ambiguous. + +Cron remains the right tool for many deterministic jobs. Batch transforms and +one-shot writing tasks often do not need receipts or durable policy. The +Reactor shape matters when a changing world, uncertain evidence, cost, and +human interruption policy all need to be explicit. + +## 4. OpenProse Design + +OpenProse treats the responsibility as the program. The source contract names +the goal, evidence, cadence, criteria, and fulfillment boundary. Source +compile lowers `*.prose.md` into repository IR when authored intent changes. +Policy compile lowers the contract plus receipt history into a token-free +policy artifact when runtime evidence shows the current policy needs revision. +Those are separate compiles: source-derived language state is not the same +thing as Reactor runtime state. + +Quiescence has three meanings. First, do not act when the maintained state is +already up to date. Second, do not check now when the forecast says the next +audit is not due. Third, do not check deeply when shallow evidence and +calibration justify a cheaper path. v0.1 demonstrates the first two local +paths and the single shallow judge path. Variable-depth runtime judging is a +designed part of the architecture, but not a v0.1 runtime claim. + +Memoization depends on stable identity. The runtime can reuse verdict work +when the contract revision, evidence receipts, dependency receipts, and policy +namespace still justify reuse. The cost claim is therefore not "zero cost on a +static world." The honest claim is: cost scales with surprise plus a +forecast-amortized plan-audit floor. + +Receipts are the trust unit. They carry `as_of`, contract and policy identity, +evidence references, verdict, token accounting, surprise cause, forecast +handoff, dependency pins, and projection-safe data. The same receipt trail +supports audit, replay, composition, privacy projection, and exit. + +## 5. Implementation + +The current implementation has three cooperating surfaces: + +- `@openprose/reactor`: the runtime spine, receipt schema, policy artifact + validation, memoization, forecast, evidence plans, composition, + projection, adapters, shallow judge, and exit/import support. +- `@openprose/reactor-cradle`: deterministic worlds, doubles, replay, + baselines, K1/K2 spikes, release-parity fixtures, and measurement harnesses. +- The migrated OpenProse CLI path: local `prose compile`, `prose serve`, and + `prose status` evidence for bundled examples through Reactor receipts. + +The built Reactor rows include receipt v0, kernel backstops, policy artifact +v0, forecast rechecks, evidence-plan validation, memo-hit receipts, +composition pins, model-authored transitive freshness under a kernel floor, +owner/subscriber/public projection, filesystem and memory storage adapters, +record-replay model gateway adapters, local connector and event-sink adapters, +runtime export/import, cold-start policy authorship, policy recompile and +rollback substrate, and a shallow judge that requires adapter-owned usage +metadata for token truth. + +The judge scope is intentionally narrow. v0.1 runtime judging is shallow and +uses `calibration_grade: "none"` where appropriate. The K1 work proves that an +offline evaluator can score a live-recorded ensemble cassette with real +provider metadata. It does not mean the runtime is yet running a live ensemble +or selecting variable depth per turn. + +The local CLI evidence is release-quality for a local path, not a production +host. Five bundled examples compile real `.prose.md` source, serve triggers +through Reactor, show per-token surprise attribution in status, and write +forwarded fulfillment artifacts. The path preserves crash-window replay, +duplicate-trigger dedupe, and projection-tier leak tests. It still uses local +deterministic adapters and forwarded fulfillment artifacts rather than hosted +production ingress or real external side effects. + +Production-equivalent ingress would authenticate external events, normalize +event identities, dedupe and replay event claims, enforce source-specific +budgets, and hand only typed events and evidence to Reactor. v0.1 has local +CLI trigger and serve evidence, not a hosted gateway layer. + +Production-equivalent fulfillment would own idempotent dispatch, retries, +budgets, external side effects, durable claims, and operator-visible failure +states. v0.1 writes forwarded fulfillment artifacts through the local CLI +path, not production actuation. + +Production-equivalent oracle support would expose explicit truth and evidence +sockets for evaluation and operations without smuggling ambient knowledge into +the runtime. v0.1 uses deterministic Cradle worlds plus one live K1 cassette, +not a production oracle service. + +Postgres parity is deferred because the storage adapter v0 is synchronous +while real Postgres IO is async. Signer support remains a Phase F binary +decision: ship one concrete signer adapter or make null-signer the only clean +non-throwing v0.1 state. + +## 6. Evaluation Methodology + +Phase C measures two controlled scenarios: +`incident-briefing-static-zero` and +`incident-briefing-periodic-surprise`. Each scenario compares three rows: + +- Reactor runtime-produced receipts. +- A deterministic no-memo Reactor control over the same scenario schedule and + token charges. +- A non-Reactor naive loop control that rechecks on the same schedule without + receipts, memo keys, forecast policy, or reusable verdict proof. + +The core metrics are receipt count, model invocation count, fresh tokens, +reused tokens, and surprise attribution. `tokens.fresh` is fresh model work. +`tokens.reused` is accounted reusable work, not new model spend. The no-memo +control isolates the value of memoization and forecast reuse. The naive loop +control approximates a fixed cron/prompt maintenance loop. + +The event-changing controls are deterministic counterfactual controls over +the same four-turn schedule. They are not shipped runtime flags or live +production ablations. The measured rows use local deterministic Cradle +providers and recorded token accounting, so this report does not dollarize the +results. + +K1 is evaluated separately. One OpenRouter live-recorded ensemble cassette +captures outputs from multiple provider/model families and records request +ids, response ids, latency, finish reason, usage, and spend. It proves the K1 +evaluator and metadata path against a live cassette. It does not prove runtime +ensemble judging or a full provider matrix. + +## 7. Results + +The result table below is copied from the Phase C result table at +`../planning/plans/2026-05-19-reactor-runtime-wave/phases/C-baseline-and-second-adapter/results/cost-thesis-baseline.md`. + +| Scenario | Row | Provenance | Receipts | Model invocations | Fresh tokens | Reused tokens | Source | +| --- | --- | --- | ---: | ---: | ---: | ---: | --- | +| Static `incident-briefing-static-zero` | Reactor | runtime-produced | 4 | 2 | 46 | 46 | Phase C result table | +| Static `incident-briefing-static-zero` | Reactor no memo | deterministic control | 4 | 4 | 72 | 0 | Phase C result table | +| Static `incident-briefing-static-zero` | Naive loop | non-Reactor control | 0 | 4 | 256 | 0 | Phase C result table | +| Event-changing `incident-briefing-periodic-surprise` | Reactor | runtime-produced | 4 | 2 | 74 | 74 | Phase C result table | +| Event-changing `incident-briefing-periodic-surprise` | Reactor no memo | deterministic control | 4 | 4 | 148 | 0 | Phase C result table | +| Event-changing `incident-briefing-periodic-surprise` | Naive loop | non-Reactor control | 0 | 4 | 148 | 0 | Phase C result table | + +On the static scenario, Reactor performs fresh model work at bootstrap and at +the plan-age audit, while evidence-age rechecks hit memoized verdicts. The +per-turn audit in the Phase C table records fresh/reused work as `41/0`, +`0/41`, `5/0`, and `0/5`. + +On the event-changing scenario, the material surprise is an `incident-opened` +event at `2026-05-18T12:30:00.000Z`, which changes the evidence hash to +`sha256:a890c5e45abb3e746857db1ca9f79905c17f4f769fbe273446fe186e5787aeb8` +in the Phase C result table. The Reactor row spends fresh work on bootstrap +and on that material change, then reuses verdict work on evidence-age +rechecks. The controls re-spend on the same schedule. + +The B5 live K1 cassette is +`../../prose-reactor-build/packages/reactor-cradle/src/spikes/fixtures/k1-live-recorded.json`. +Its SHA-256 digest is +`f64484990635a61a3dcac973a96e97d6433a576ccc297c23742d4a515e2c1868`. +The cassette records an OpenRouter run started at +`2026-05-20T17:55:02.467Z` and completed at +`2026-05-20T17:55:10.106Z`, with a `2.00 USD` cap and `0.00022823 USD` +actual spend. It includes Google/Gemini +`google/gemini-3.1-flash-lite-preview`, Mistral +`mistralai/mistral-small-3.2-24b-instruct`, and Qwen +`qwen/qwen-2.5-72b-instruct` outputs, with real request ids, response ids, +latencies, finish reasons, usage, and spend metadata. +The K1 evaluator accepts that fixture with `output_count = 3`, diversity +floor met across those families/providers, `issue_count = 0`, and +`calibrated_confidence = 1`. + +Recorded provider parity is also part of the accepted Phase C evidence. Two +recorded provider/model paths produce byte-identical policy artifact bytes +from the same contract and history, and provider drift fails closed. This is +recorded policy-artifact parity, not live provider support or model quality +parity. + +The Phase E CLI evidence is local but important. Five bundled examples compile +from source, start spawned `prose serve`, ingest triggers through Reactor, +write forwarded fulfillment artifacts, and render `prose status` with provider +and model labels plus `surprise_cause=real-input`, fresh token counts, reused +token counts, and receipt-derived pressure. Crash-window restart converges +from durable pressure within one cycle, duplicate triggers in one cycle +produce exactly one dispatch, and public/subscriber projections do not leak +owner-only secret-shaped receipt material. + +## 8. Case Study: Incident Briefing Room + +The Incident Briefing Room is the clearest example because its world changes +over time. The maintained state is not "write a report." It is "the incident +channel has an accurate current briefing." Its continuity rule says to recheck +on incident messages, status-page changes, and while the incident is active. +Its criteria include impact, timeline, owner, next action, and +customer-facing status. Its fulfillment boundary is to summarize new facts, +ask for missing owner input, and update the briefing. + +In the static scenario, the first receipt pays fresh work to inspect evidence +and record the current briefing state. The next evidence-age recheck reuses +the prior verdict because the evidence identity has not changed. A later +plan-age audit pays a small fresh floor to make sure the compiled evidence +plan itself is still adequate. A final evidence-age recheck again reuses +work. That is the quiet-world shape: not free, but much cheaper than reasking +the whole question on every clock tick. + +In the event-changing scenario, the world introduces an `incident-opened` +event. The evidence hash changes, so the Reactor spends fresh work and records +the new state. The following evidence-age recheck can reuse that updated +verdict. The user-visible projection can show the current status, why it +changed, the next forecast recheck, and the receipt trail supporting it. + +The case study stays deliberately local. The CLI can write forwarded +fulfillment artifacts for bundled examples, but this report does not claim a +production incident system updating real customer communications. + +## 9. Limitations + +The main runtime layers still deferred from v0.1 are production-equivalent +gateway, fulfillment, and oracle support. The current worktree proves local +trigger/serve behavior, forwarded fulfillment artifacts, and deterministic +Cradle worlds. It does not prove hosted event ingress, real external +actuation, or a production truth socket. + +Judgment is also deliberately scoped. Runtime judging is shallow in v0.1. +Variable-depth ensemble judging, live provider matrix runs, calibration +cadence, degraded-calibration ladder steps 2-4b, and the accrued-anchor +calibration exit bar remain future work. K1 is live-recorded evaluator +evidence, not a production calibration service. + +Infrastructure launch work remains. The packages have version and provenance +settings, local tarball and parity evidence, and release-floor surface, but +public npm publication is not observed in this report. Stranger quickstart +verification is not observed. Postgres parity is deferred under the async +storage seam. The signer adapter decision remains open for Phase F. + +The domain boundaries matter. Some responsibilities have no cheap complete +hash for "semantically relevant content changed." In those domains the system +can remain safe, but the cost differentiator weakens toward forecast-cadence +cost. Some responsibilities have no correctness anchor. Those stay in bounded +degraded calibration unless and until human labels or observed outcomes can +support stronger calibration. Prompt sensitivity, adversarial or silent drift, +public receipt maturity, and human review quality all need larger evaluation. + +The plan-revalidation tax is not a bug. It is the cost of not letting a stale +plan become quietly wrong. The cost thesis is surprise plus a +forecast-amortized audit floor, not surprise alone. + +## 10. Future Work + +The closest category-completing work is production-equivalent ingress, +fulfillment, and oracle adapters. Those layers would let the same receipt +logic operate against authenticated event sources, idempotent external side +effects, and explicit truth/evidence sockets rather than local deterministic +worlds. + +The next judgment work is runtime variable-depth ensemble judging: shallow +checks by default, deeper or more diverse judging when uncertainty or policy +requires it, and a calibration cadence that records why depth changed. That +work includes degraded-calibration ladder steps 2-4b and an accrued-anchor +exit bar for responsibilities that start without labels. + +The policy loop should continue toward richer recurring recompile and rollback +behavior against recorded artifacts, wider receipt-graph composition case +studies, stronger transitive freshness semantics, and learned forecast +policies. The storage layer should gain async parity rows, including Postgres, +once the adapter seam supports real async IO. + +The release work is concrete: publish `@openprose/reactor` and +`@openprose/reactor-cradle` with provenance, pin the CLI to the public package, +publish this report from measured evidence, and run a stranger quickstart +where the observed token ratio matches the report within tolerance. + +Longer term, the policy author can migrate into a first-class OpenProse +`kind: responsibility` once the Prose VM path is proven. That should not add a +second intent surface. The goal remains one durable responsibility, one +inspectable evidence graph, and one receipt trail that lets future agents know +what happened and why. From b4425f0f8e703a618f008bf3084594bff02a9491 Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Wed, 20 May 2026 13:55:19 -0700 Subject: [PATCH 02/14] docs: fold reactor report review --- reactor-technical-report.md | 145 ++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 81 deletions(-) diff --git a/reactor-technical-report.md b/reactor-technical-report.md index 07387ff..b255acc 100644 --- a/reactor-technical-report.md +++ b/reactor-technical-report.md @@ -83,12 +83,22 @@ human interruption policy all need to be explicit. ## 4. OpenProse Design OpenProse treats the responsibility as the program. The source contract names -the goal, evidence, cadence, criteria, and fulfillment boundary. Source -compile lowers `*.prose.md` into repository IR when authored intent changes. -Policy compile lowers the contract plus receipt history into a token-free -policy artifact when runtime evidence shows the current policy needs revision. -Those are separate compiles: source-derived language state is not the same -thing as Reactor runtime state. +the goal, evidence, freshness referents, memo-breaking conditions, plan-audit +horizon, observable criteria, and fulfillment boundary. Criteria that cannot +be observed are expected to produce blocked receipts routed to the author, not +hidden judgment inside prose. Source compile lowers `*.prose.md` into +repository IR when authored intent changes. Policy compile lowers the contract +plus receipt history into a token-free policy artifact when runtime evidence +shows the current policy needs revision. + +Those are separate compiles. The `*.prose.md` contract remains the only +authored source of meaning. If a policy artifact or projection disagrees with +the contract, the contract wins; artifacts, receipts, forecasts, and +projections are derived sibling runtime state. Deterministic code validates, +schedules, records, and executes artifacts, but it does not author semantic +judgment. v0.1 demonstrates source compile, cold-start policy artifact +authorship, and local recompile/rollback substrate; recurring production +policy recompile remains future work. Quiescence has three meanings. First, do not act when the maintained state is already up to date. Second, do not check now when the forecast says the next @@ -120,43 +130,24 @@ The current implementation has three cooperating surfaces: - The migrated OpenProse CLI path: local `prose compile`, `prose serve`, and `prose status` evidence for bundled examples through Reactor receipts. -The built Reactor rows include receipt v0, kernel backstops, policy artifact -v0, forecast rechecks, evidence-plan validation, memo-hit receipts, -composition pins, model-authored transitive freshness under a kernel floor, -owner/subscriber/public projection, filesystem and memory storage adapters, -record-replay model gateway adapters, local connector and event-sink adapters, -runtime export/import, cold-start policy authorship, policy recompile and -rollback substrate, and a shallow judge that requires adapter-owned usage -metadata for token truth. - -The judge scope is intentionally narrow. v0.1 runtime judging is shallow and -uses `calibration_grade: "none"` where appropriate. The K1 work proves that an -offline evaluator can score a live-recorded ensemble cassette with real -provider metadata. It does not mean the runtime is yet running a live ensemble -or selecting variable depth per turn. - -The local CLI evidence is release-quality for a local path, not a production -host. Five bundled examples compile real `.prose.md` source, serve triggers -through Reactor, show per-token surprise attribution in status, and write -forwarded fulfillment artifacts. The path preserves crash-window replay, -duplicate-trigger dedupe, and projection-tier leak tests. It still uses local -deterministic adapters and forwarded fulfillment artifacts rather than hosted -production ingress or real external side effects. +| Surface | v0.1 demonstrated | Not claimed | +| --- | --- | --- | +| Runtime spine | receipt v0, kernel backstops, policy artifact validation, memoization, forecast, evidence plans, composition pins, projections, export/import, cold-start authorship, and local recompile/rollback substrate | production gateway, production fulfillment, or production oracle | +| Judgment | shallow runtime judge with adapter-owned usage metadata, plus K1 live-cassette evaluator evidence | runtime ensemble depth, live provider matrix, or production calibration service | +| CLI path | local source compile, serve, status, forwarded fulfillment artifacts, crash-window replay, duplicate-trigger dedupe, and projection-tier leak tests | hosted production operation or real external side effects | +| Storage/adapters | memory, filesystem, record-replay model gateway, local connector, and event-sink adapters | Postgres parity or signer support | +Cradle is the measurement rig, not a production dependency. Production-equivalent ingress would authenticate external events, normalize event identities, dedupe and replay event claims, enforce source-specific -budgets, and hand only typed events and evidence to Reactor. v0.1 has local -CLI trigger and serve evidence, not a hosted gateway layer. - +budgets, and hand only typed events and evidence to Reactor. Production-equivalent fulfillment would own idempotent dispatch, retries, budgets, external side effects, durable claims, and operator-visible failure -states. v0.1 writes forwarded fulfillment artifacts through the local CLI -path, not production actuation. - -Production-equivalent oracle support would expose explicit truth and evidence -sockets for evaluation and operations without smuggling ambient knowledge into -the runtime. v0.1 uses deterministic Cradle worlds plus one live K1 cassette, -not a production oracle service. +states. Production-equivalent oracle support would expose explicit truth and +evidence sockets for evaluation and operations without smuggling ambient +knowledge into the runtime. v0.1 has local CLI trigger/serve evidence, +forwarded fulfillment artifacts, deterministic Cradle worlds, and one live K1 +cassette instead of those production layers. Postgres parity is deferred because the storage adapter v0 is synchronous while real Postgres IO is async. Signer support remains a Phase F binary @@ -197,6 +188,9 @@ ensemble judging or a full provider matrix. The result table below is copied from the Phase C result table at `../planning/plans/2026-05-19-reactor-runtime-wave/phases/C-baseline-and-second-adapter/results/cost-thesis-baseline.md`. +During Phase D this report cites local worktree evidence; before publication, +the K1 cassette and result artifacts must be included in, or linked from, a +public release artifact. | Scenario | Row | Provenance | Receipts | Model invocations | Fresh tokens | Reused tokens | Source | | --- | --- | --- | ---: | ---: | ---: | ---: | --- | @@ -207,6 +201,14 @@ The result table below is copied from the Phase C result table at | Event-changing `incident-briefing-periodic-surprise` | Reactor no memo | deterministic control | 4 | 4 | 148 | 0 | Phase C result table | | Event-changing `incident-briefing-periodic-surprise` | Naive loop | non-Reactor control | 0 | 4 | 148 | 0 | Phase C result table | +In the static scenario, Reactor uses 2 model invocations and 46 fresh tokens, +compared with 4 invocations and 72 fresh tokens for the no-memo control, and +4 invocations and 256 fresh tokens for the naive loop. In the event-changing +scenario, Reactor uses 2 model invocations and 74 fresh tokens, compared with +4 invocations and 148 fresh tokens for both controls. The event-changing win is +fresh model work plus reusable-verdict proof, not a total accounted-token +claim over the no-memo control. + On the static scenario, Reactor performs fresh model work at bootstrap and at the plan-age audit, while evidence-age rechecks hit memoized verdicts. The per-turn audit in the Phase C table records fresh/reused work as `41/0`, @@ -219,36 +221,17 @@ in the Phase C result table. The Reactor row spends fresh work on bootstrap and on that material change, then reuses verdict work on evidence-age rechecks. The controls re-spend on the same schedule. -The B5 live K1 cassette is -`../../prose-reactor-build/packages/reactor-cradle/src/spikes/fixtures/k1-live-recorded.json`. -Its SHA-256 digest is -`f64484990635a61a3dcac973a96e97d6433a576ccc297c23742d4a515e2c1868`. -The cassette records an OpenRouter run started at -`2026-05-20T17:55:02.467Z` and completed at -`2026-05-20T17:55:10.106Z`, with a `2.00 USD` cap and `0.00022823 USD` -actual spend. It includes Google/Gemini -`google/gemini-3.1-flash-lite-preview`, Mistral -`mistralai/mistral-small-3.2-24b-instruct`, and Qwen -`qwen/qwen-2.5-72b-instruct` outputs, with real request ids, response ids, -latencies, finish reasons, usage, and spend metadata. -The K1 evaluator accepts that fixture with `output_count = 3`, diversity -floor met across those families/providers, `issue_count = 0`, and -`calibrated_confidence = 1`. - -Recorded provider parity is also part of the accepted Phase C evidence. Two -recorded provider/model paths produce byte-identical policy artifact bytes -from the same contract and history, and provider drift fails closed. This is -recorded policy-artifact parity, not live provider support or model quality -parity. - -The Phase E CLI evidence is local but important. Five bundled examples compile -from source, start spawned `prose serve`, ingest triggers through Reactor, -write forwarded fulfillment artifacts, and render `prose status` with provider -and model labels plus `surprise_cause=real-input`, fresh token counts, reused -token counts, and receipt-derived pressure. Crash-window restart converges -from durable pressure within one cycle, duplicate triggers in one cycle -produce exactly one dispatch, and public/subscriber projections do not leak -owner-only secret-shaped receipt material. +Supporting evidence: + +| Evidence | Observed | Boundary | +| --- | --- | --- | +| B5 live K1 cassette at `../../prose-reactor-build/packages/reactor-cradle/src/spikes/fixtures/k1-live-recorded.json` | SHA-256 `f64484990635a61a3dcac973a96e97d6433a576ccc297c23742d4a515e2c1868`; OpenRouter run with `output_count = 3` across Google/Gemini, Mistral, and Qwen; `0.00022823 USD` spend under the `2.00 USD` cap; for this authored-anchor cassette, the evaluator returned status `calibrated` and `calibrated_confidence = 1` | evaluator and metadata evidence, not a runtime ensemble, production calibration bar, or general accuracy result | +| Recorded provider parity | two recorded provider/model paths produce byte-identical policy artifact bytes from the same contract/history, and provider drift fails closed | recorded policy-artifact parity, not live provider support or model quality parity | +| Phase E CLI acceptance at `../planning/plans/2026-05-19-reactor-runtime-wave/phases/E-cli-migration/SIGNPOST.md` | five bundled examples compile from source, run spawned `prose serve`, ingest triggers through Reactor, write forwarded fulfillment artifacts, and render `prose status` with `surprise_cause=real-input`; crash-window restart and duplicate-trigger dedupe are scripted; projection tiers are leak-tested | local deterministic CLI evidence, not hosted production operation, live-model fulfillment content, npm launch, or stranger quickstart | + +The Phase C result table's `## Reproduce` section records the sharpest local +commands: `pnpm --filter @openprose/reactor-cradle test` and +`pnpm --filter @openprose/reactor test` from `/Users/sl/code/prose-reactor-build`. ## 8. Case Study: Incident Briefing Room @@ -260,19 +243,19 @@ Its criteria include impact, timeline, owner, next action, and customer-facing status. Its fulfillment boundary is to summarize new facts, ask for missing owner input, and update the briefing. -In the static scenario, the first receipt pays fresh work to inspect evidence -and record the current briefing state. The next evidence-age recheck reuses -the prior verdict because the evidence identity has not changed. A later -plan-age audit pays a small fresh floor to make sure the compiled evidence -plan itself is still adequate. A final evidence-age recheck again reuses -work. That is the quiet-world shape: not free, but much cheaper than reasking -the whole question on every clock tick. - -In the event-changing scenario, the world introduces an `incident-opened` -event. The evidence hash changes, so the Reactor spends fresh work and records -the new state. The following evidence-age recheck can reuse that updated -verdict. The user-visible projection can show the current status, why it -changed, the next forecast recheck, and the receipt trail supporting it. +One deterministic Cradle receipt trail shows the shape: + +| Beat | Cause | Evidence note | Fresh/reused | Decision shape | User-visible result | +| --- | --- | --- | ---: | --- | --- | +| `2026-05-18T12:00:00.000Z` | `real-input` | initial incident feed | `37/0` | fresh judge receipt | baseline briefing state recorded | +| `2026-05-18T12:15:00.000Z` | `forecast-recheck` / `evidence-age` | evidence unchanged | `0/37` | memo-hit receipt | no material change; next recheck remains visible | +| `2026-05-18T12:30:00.000Z` | `real-input` | `incident-opened`; evidence hash `sha256:a890c5e45abb3e746857db1ca9f79905c17f4f769fbe273446fe186e5787aeb8` | `37/0` | fresh judge receipt | briefing changes because an incident opened | +| `2026-05-18T12:45:00.000Z` | `forecast-recheck` / `evidence-age` | changed evidence unchanged since prior turn | `0/37` | memo-hit receipt | updated verdict is reused instead of asking again | + +The quiet-world static run has the same proof shape with one extra plan-age +audit floor: bootstrap fresh, evidence-age memo hit, plan-age audit fresh +floor, and final evidence-age memo hit. That is not free; it is lower fresh +model work than reasking on every scheduled turn. The case study stays deliberately local. The CLI can write forwarded fulfillment artifacts for bundled examples, but this report does not claim a From 44e33308740e4c3c3e49bbdcce1654687cab7718 Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Wed, 20 May 2026 14:34:26 -0700 Subject: [PATCH 03/14] docs: fold release surfaces into reactor report --- reactor-technical-report.md | 59 ++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/reactor-technical-report.md b/reactor-technical-report.md index b255acc..263a35d 100644 --- a/reactor-technical-report.md +++ b/reactor-technical-report.md @@ -10,8 +10,14 @@ under deterministic Cradle scenarios, a Reactor runtime can maintain a long-lived responsibility with runtime-produced receipts, reusable verdict proof, forecast-paced rechecks, and explicit surprise attribution. It does not yet support a public launch claim, hosted production operation, runtime -variable-depth ensemble judging, npm provenance publication, or stranger -quickstart verification. +variable-depth ensemble judging, actual npm publication, or stranger +quickstart verification. Since the first report draft, the local release +surface has also become concrete: a packed-tarball flat-tokens example reads +runtime-produced receipts and reports `46:46`; the deterministic +`incident-briefing-room` CLI quickstart shows owner and public status +attribution with `surprise_cause=real-input`; and the `v*` tag gate is wired +to publish smoked Reactor and Cradle tarballs with provenance, though no npm +publication has been observed. ## 1. Problem @@ -120,7 +126,7 @@ supports audit, replay, composition, privacy projection, and exit. ## 5. Implementation -The current implementation has three cooperating surfaces: +The current implementation has four cooperating surfaces: - `@openprose/reactor`: the runtime spine, receipt schema, policy artifact validation, memoization, forecast, evidence plans, composition, @@ -129,6 +135,10 @@ The current implementation has three cooperating surfaces: baselines, K1/K2 spikes, release-parity fixtures, and measurement harnesses. - The migrated OpenProse CLI path: local `prose compile`, `prose serve`, and `prose status` evidence for bundled examples through Reactor receipts. +- The local release surface: a packed-tarball flat-tokens example, a + deterministic CLI quickstart, and a `v*` tag workflow that downloads the + exact smoked Reactor and Cradle tarballs and runs provenance-enabled + `npm publish` for those tarballs. | Surface | v0.1 demonstrated | Not claimed | | --- | --- | --- | @@ -136,6 +146,7 @@ The current implementation has three cooperating surfaces: | Judgment | shallow runtime judge with adapter-owned usage metadata, plus K1 live-cassette evaluator evidence | runtime ensemble depth, live provider matrix, or production calibration service | | CLI path | local source compile, serve, status, forwarded fulfillment artifacts, crash-window replay, duplicate-trigger dedupe, and projection-tier leak tests | hosted production operation or real external side effects | | Storage/adapters | memory, filesystem, record-replay model gateway, local connector, and event-sink adapters | Postgres parity or signer support | +| Release surface | packed-tarball flat-tokens smoke, deterministic `incident-briefing-room` quickstart, and tag-gated provenance publish workflow | public npm availability, stranger run, or production operation | Cradle is the measurement rig, not a production dependency. Production-equivalent ingress would authenticate external events, normalize @@ -150,9 +161,10 @@ forwarded fulfillment artifacts, deterministic Cradle worlds, and one live K1 cassette instead of those production layers. Postgres parity is deferred because the storage adapter v0 is synchronous -while real Postgres IO is async. Signer support remains a Phase F binary -decision: ship one concrete signer adapter or make null-signer the only clean -non-throwing v0.1 state. +while real Postgres IO is async. Signer support is honestly deferred: v0.1 +normalizes omitted signers to +`{scheme:"none", null_reason:"no-signer-adapter-configured"}`, and non-null +receipt signatures fail closed rather than implying cryptographic support. ## 6. Evaluation Methodology @@ -228,10 +240,16 @@ Supporting evidence: | B5 live K1 cassette at `../../prose-reactor-build/packages/reactor-cradle/src/spikes/fixtures/k1-live-recorded.json` | SHA-256 `f64484990635a61a3dcac973a96e97d6433a576ccc297c23742d4a515e2c1868`; OpenRouter run with `output_count = 3` across Google/Gemini, Mistral, and Qwen; `0.00022823 USD` spend under the `2.00 USD` cap; for this authored-anchor cassette, the evaluator returned status `calibrated` and `calibrated_confidence = 1` | evaluator and metadata evidence, not a runtime ensemble, production calibration bar, or general accuracy result | | Recorded provider parity | two recorded provider/model paths produce byte-identical policy artifact bytes from the same contract/history, and provider drift fails closed | recorded policy-artifact parity, not live provider support or model quality parity | | Phase E CLI acceptance at `../planning/plans/2026-05-19-reactor-runtime-wave/phases/E-cli-migration/SIGNPOST.md` | five bundled examples compile from source, run spawned `prose serve`, ingest triggers through Reactor, write forwarded fulfillment artifacts, and render `prose status` with `surprise_cause=real-input`; crash-window restart and duplicate-trigger dedupe are scripted; projection tiers are leak-tested | local deterministic CLI evidence, not hosted production operation, live-model fulfillment content, npm launch, or stranger quickstart | +| Phase F flat-tokens packed-tarball example | installs from packed Reactor and Cradle tarballs, drives four static-world turns through `createReactor().ingest()`, reads the produced receipts, and prints `tokens.fresh = 46`, `tokens.reused = 46`, `tokens.ratio = "46:46"`, `runtime.receipt_count = 4`, and `runtime.model_invocation_count = 2` | local packed-tarball smoke, not public npm install, stranger run, production economics, or a new runtime claim beyond the Phase C receipt evidence | +| Phase F `incident-briefing-room` quickstart | documents local `prose compile src --harness mock`, manifest activation, deterministic `prose serve`, POST `/incident/events`, `prose status --tier=owner`, and `prose status --tier=public`; the status surface attributes owner and public state with `surprise_cause=real-input` | local deterministic CLI quickstart, not hosted ingress, live-model fulfillment content, public npm launch, or stranger verification | +| Phase F `v*` publish gate | on `refs/tags/v*`, the workflow waits for CI, downloads the exact smoked package tarballs, and runs `npm publish --access public --provenance` for both Reactor and Cradle | workflow path only; actual npm publication and registry-visible provenance attestation remain unobserved | The Phase C result table's `## Reproduce` section records the sharpest local commands: `pnpm --filter @openprose/reactor-cradle test` and `pnpm --filter @openprose/reactor test` from `/Users/sl/code/prose-reactor-build`. +The Phase F local release signpost adds the tarball smoke and CLI quickstart +checks that turn the same numbers into a release-surface exercise without +claiming publication. ## 8. Case Study: Incident Briefing Room @@ -257,6 +275,14 @@ audit floor: bootstrap fresh, evidence-age memo hit, plan-age audit fresh floor, and final evidence-age memo hit. That is not free; it is lower fresh model work than reasking on every scheduled turn. +Phase F uses this same responsibility as the local CLI quickstart. The +observed sequence is intentionally plain: compile the bundled source, activate +the manifest, run deterministic `prose serve`, POST an incident event, then +compare `prose status --tier=owner` and `prose status --tier=public`. The +useful observation is owner/public status attribution with +`surprise_cause=real-input` on a local receipt trail, not a hosted incident +system. + The case study stays deliberately local. The CLI can write forwarded fulfillment artifacts for bundled examples, but this report does not claim a production incident system updating real customer communications. @@ -276,10 +302,13 @@ calibration exit bar remain future work. K1 is live-recorded evaluator evidence, not a production calibration service. Infrastructure launch work remains. The packages have version and provenance -settings, local tarball and parity evidence, and release-floor surface, but -public npm publication is not observed in this report. Stranger quickstart -verification is not observed. Postgres parity is deferred under the async -storage seam. The signer adapter decision remains open for Phase F. +settings, local tarball and parity evidence, a flat-tokens packed-tarball +example that reports `46:46`, and a `v*` tag gate that would publish the exact +smoked Reactor and Cradle tarballs with provenance. Public npm publication is +still not observed in this report. Stranger quickstart verification is not +observed. Postgres parity is deferred under the async storage seam. A concrete +signer adapter is deferred; null-signer remains the only honest v0.1 receipt +signature state. The domain boundaries matter. Some responsibilities have no cheap complete hash for "semantically relevant content changed." In those domains the system @@ -313,10 +342,12 @@ studies, stronger transitive freshness semantics, and learned forecast policies. The storage layer should gain async parity rows, including Postgres, once the adapter seam supports real async IO. -The release work is concrete: publish `@openprose/reactor` and -`@openprose/reactor-cradle` with provenance, pin the CLI to the public package, -publish this report from measured evidence, and run a stranger quickstart -where the observed token ratio matches the report within tolerance. +The release work is concrete: run the `v*` tag publish gate under npm auth or +trusted publishing, verify `@openprose/reactor` and +`@openprose/reactor-cradle` in the public registry with provenance, pin the CLI +to the public package, publish this report from measured evidence, and run a +stranger quickstart where the observed token ratio matches the report within +tolerance. Longer term, the policy author can migrate into a first-class OpenProse `kind: responsibility` once the Prose VM path is proven. That should not add a From 0216cec490b1910dca006325a8c79d7187c20562 Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Wed, 20 May 2026 15:34:45 -0700 Subject: [PATCH 04/14] docs: correct no-memo static measurement --- reactor-technical-report.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reactor-technical-report.md b/reactor-technical-report.md index 263a35d..52e8400 100644 --- a/reactor-technical-report.md +++ b/reactor-technical-report.md @@ -207,14 +207,14 @@ public release artifact. | Scenario | Row | Provenance | Receipts | Model invocations | Fresh tokens | Reused tokens | Source | | --- | --- | --- | ---: | ---: | ---: | ---: | --- | | Static `incident-briefing-static-zero` | Reactor | runtime-produced | 4 | 2 | 46 | 46 | Phase C result table | -| Static `incident-briefing-static-zero` | Reactor no memo | deterministic control | 4 | 4 | 72 | 0 | Phase C result table | +| Static `incident-briefing-static-zero` | Reactor no memo | deterministic control | 4 | 4 | 92 | 0 | Phase C result table | | Static `incident-briefing-static-zero` | Naive loop | non-Reactor control | 0 | 4 | 256 | 0 | Phase C result table | | Event-changing `incident-briefing-periodic-surprise` | Reactor | runtime-produced | 4 | 2 | 74 | 74 | Phase C result table | | Event-changing `incident-briefing-periodic-surprise` | Reactor no memo | deterministic control | 4 | 4 | 148 | 0 | Phase C result table | | Event-changing `incident-briefing-periodic-surprise` | Naive loop | non-Reactor control | 0 | 4 | 148 | 0 | Phase C result table | In the static scenario, Reactor uses 2 model invocations and 46 fresh tokens, -compared with 4 invocations and 72 fresh tokens for the no-memo control, and +compared with 4 invocations and 92 fresh tokens for the no-memo control, and 4 invocations and 256 fresh tokens for the naive loop. In the event-changing scenario, Reactor uses 2 model invocations and 74 fresh tokens, compared with 4 invocations and 148 fresh tokens for both controls. The event-changing win is From 1024151f15f9866ea4051fb43d0eb64d4f2747e0 Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Sun, 31 May 2026 20:30:20 -0700 Subject: [PATCH 05/14] docs: add Reactor DAG + Reactor CLI sections as headline items Document the two execution-layer pieces of OpenProse as first-class, headline parts of the site: - New content/docs/reactor/ (6 pages): the Reactor DAG engine -- the two-phase compile/run model, the content-addressed DAG + compile-as- sessions, world-model + fingerprints + facet propagation, the dumb reconciler + signed receipts, continuity + ingestion, and the Incident Briefing Room case study. - New content/docs/cli/ (7 pages): @openprose/reactor-cli (command `reactor`) -- overview, quickstart, configuration (reactor.yml), compile/run/serve, connectors + sandbox, observability, and a full command reference with the documented exit codes. Grounded in the as-built source (12 commands, no invented flags). - Landing page (content/docs/index.mdx): both surfaced as headline Cards plus a prose mention; nav (content/docs/meta.json) lists reactor + cli right after "think". - .cspell.json: allowlist the new technical terms. Gates green under Node 24: typecheck, lint, spell, vitest (37), check:emdash, check:links (72 links resolve). Co-Authored-By: Claude Opus 4.8 --- .cspell.json | 32 +++- content/docs/cli/command-reference.mdx | 56 +++++++ content/docs/cli/compile-run-serve.mdx | 107 ++++++++++++ content/docs/cli/configuration.mdx | 111 ++++++++++++ content/docs/cli/connectors-and-sandbox.mdx | 90 ++++++++++ content/docs/cli/meta.json | 12 ++ content/docs/cli/observability.mdx | 90 ++++++++++ content/docs/cli/overview.mdx | 88 ++++++++++ content/docs/cli/quickstart.mdx | 105 ++++++++++++ content/docs/index.mdx | 12 ++ content/docs/meta.json | 2 +- .../docs/reactor/continuity-and-ingestion.mdx | 97 +++++++++++ .../docs/reactor/incident-briefing-room.mdx | 92 ++++++++++ content/docs/reactor/index.mdx | 113 +++++++++++++ content/docs/reactor/meta.json | 11 ++ .../docs/reactor/reconciler-and-receipts.mdx | 119 +++++++++++++ content/docs/reactor/the-dag-and-compile.mdx | 102 +++++++++++ .../reactor/world-model-and-fingerprints.mdx | 158 ++++++++++++++++++ 18 files changed, 1395 insertions(+), 2 deletions(-) create mode 100644 content/docs/cli/command-reference.mdx create mode 100644 content/docs/cli/compile-run-serve.mdx create mode 100644 content/docs/cli/configuration.mdx create mode 100644 content/docs/cli/connectors-and-sandbox.mdx create mode 100644 content/docs/cli/meta.json create mode 100644 content/docs/cli/observability.mdx create mode 100644 content/docs/cli/overview.mdx create mode 100644 content/docs/cli/quickstart.mdx create mode 100644 content/docs/reactor/continuity-and-ingestion.mdx create mode 100644 content/docs/reactor/incident-briefing-room.mdx create mode 100644 content/docs/reactor/index.mdx create mode 100644 content/docs/reactor/meta.json create mode 100644 content/docs/reactor/reconciler-and-receipts.mdx create mode 100644 content/docs/reactor/the-dag-and-compile.mdx create mode 100644 content/docs/reactor/world-model-and-fingerprints.mdx diff --git a/.cspell.json b/.cspell.json index cf9fe78..89fb539 100644 --- a/.cspell.json +++ b/.cspell.json @@ -45,7 +45,37 @@ "antipatterns", "unwired", "dogfood", - "diffable" + "diffable", + "canonicalizer", + "recompiles", + "dedups", + "reconciler", + "idempotency", + "subagents", + "canonicalizers", + "canonicalization", + "fingerprint", + "fingerprints", + "fingerprinted", + "memoization", + "memoize", + "memoized", + "topology", + "subscribable", + "reconciliation", + "recheck", + "rechecks", + "reconverges", + "deduped", + "dollarized", + "mtimes", + "ETags", + "downstreams", + "Acyclicity", + "statefulness", + "agentically", + "canonicalizable", + "authorable" ], "ignoreRegExpList": ["https?://[^\\s)]+", "`[^`]*`"] } diff --git a/content/docs/cli/command-reference.mdx b/content/docs/cli/command-reference.mdx new file mode 100644 index 0000000..31a8158 --- /dev/null +++ b/content/docs/cli/command-reference.mdx @@ -0,0 +1,56 @@ +--- +title: Command reference +description: All twelve reactor commands, their key flags, and the documented exit codes. +--- + +# Command reference + +The command is `reactor`. Run `reactor --help` for the full options of any command. Run `reactor --version` (or `-v`) for the CLI version. + +## Global flags + +These are honored by every command and override `reactor.yml`. + +| 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. Every other command runs fully offline. + +| Command | Live? | What it does | +| --- | --- | --- | +| `reactor init [dir]` | offline | Scaffold a minimal project: a gateway, a responsibility, `reactor.yml`, and `.gitignore`. Flag: `--force`. | +| `reactor doctor [--live]` | offline (`--live` probes) | Report node, SDK, key/deps, offline mode, sandbox, state-dir, and IR health. `--live` runs one smoke render. | +| `reactor compile [--force] [--check]` | live (cache hit and `--check` are offline) | Run the compile sessions into the IR cache. `--check` exits non-zero when stale. | +| `reactor run` | live | Ensure the IR is fresh, boot, drain to quiescence, report, and exit. | +| `reactor serve [--http ] [--concurrency ] [--poll-interval ]` | live | Boot the durable host, the continuity loop, and the HTTP surface. | +| `reactor trigger [--data \|@file]` | live | Trigger a node with an external wake (one-shot mount, or `POST` to a daemon). | +| `reactor status` | offline | Standing compile cost beside live run cost plus dispositions. | +| `reactor topology` | offline | Print the compiled DAG: nodes with wake source, and resolved edges. | +| `reactor inspect [--strict]` | offline | A node's topology position, fingerprints, last receipt, and chain. | +| `reactor logs [--node ]` | offline | The receipt stream, optionally filtered to one node. | +| `reactor trace []` | offline | Each node's receipt chain: wake to disposition. | +| `reactor receipts [list\|verify\|cost] [--node ]` | offline | Audit the receipt trail. `verify` exits non-zero on a broken chain. | + +## Documented exit codes + +The CLI uses stable, documented exit codes so it composes in CI and scripts. + +| Code | Meaning | +| --- | --- | +| `0` | Success, or healthy. A help or version display also exits `0`. | +| `1` | A reported failure with an actionable message: 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, or a missing argument, 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`. + +## See also + +- [Quickstart](/cli/quickstart) walks the commands end to end. +- [Compile, run, serve](/cli/compile-run-serve) covers the three core verbs in depth. +- [Observability](/cli/observability) covers `status`, `inspect`, `topology`, `logs`, `trace`, and `receipts`. diff --git a/content/docs/cli/compile-run-serve.mdx b/content/docs/cli/compile-run-serve.mdx new file mode 100644 index 0000000..a746289 --- /dev/null +++ b/content/docs/cli/compile-run-serve.mdx @@ -0,0 +1,107 @@ +--- +title: Compile, run, serve +description: The three core verbs in depth, the content-addressed compile cache, and the durable daemon. +--- + +# 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. + +## 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. + +### 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 ] [--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: filesystem receipts under `/receipts` 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. + +### 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 cadence ceiling (default `60000`). The loop sleeps to the soonest armed self-recheck, bounded by this ceiling. + +### The HTTP surface + +`--http ` binds a zero-framework `node:http` server. The endpoints: + +| Method and path | Purpose | +| --- | --- | +| `POST /trigger/` | An external wake of `` (the webhook or manual ingress), serialized behind that reactor's queue. | +| `GET /health` | Liveness: boot done, reactor count. | +| `GET /status` | The cost rollup plus node count and queue depth. | +| `GET /cost` | The cost rollup (the headline observability). | +| `GET /topology` | The node ids. | +| `GET /receipts` | The ledger receipt stream. | +| `GET /nodes/` | A node's published fingerprints plus its last receipt. | + +The HTTP surface is namespaced per reactor under `//...`, with the prefix omitted for a single-reactor host. 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. + +### 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. Independent reactors render in parallel up to N. + +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 world-model with an ephemeral receipt trail, ingests the named node with a full external wake, drains to quiescence, and reports the dispositions. + +`--data` accepts inline JSON or `@path` to a JSON file. The wake itself carries no payload slot, so the parsed data is validated and surfaced in the report. For a running daemon, `POST /trigger/` is the equivalent ingress. + +There is no separate `pull` command. Ingest happens through the serve continuity cadence and through `POST /trigger/`. diff --git a/content/docs/cli/configuration.mdx b/content/docs/cli/configuration.mdx new file mode 100644 index 0000000..48a2da9 --- /dev/null +++ b/content/docs/cli/configuration.mdx @@ -0,0 +1,111 @@ +--- +title: Configuration +description: The reactor.yml schema, environment variables, and the global flags every command honors. +--- + +# Configuration + +`reactor init` writes a fully-commented `reactor.yml` at the project root. Every command reads it from `/reactor.yml`. When the file is absent, the documented defaults apply. + +## The reactor.yml schema + +```yaml +state: + dir: ./.reactor # durable state (receipts, world-models, IR cache) + +model: + provider: openrouter + render_model: google/gemini-3.5-flash + compile_model: google/gemini-3.5-flash + temperature: 0 + max_turns: 200 + +sandbox: + mode: none # none (default) | docker + shell_timeout_ms: 300000 + +gateways: # external-driven entry points + - node: inbox + source_id: inbox + connector: + type: static # static | http | file (or a connectors.{cjs,js} plugin) + id_field: id + items: [{ id: item-1, body: "the first item" }] + +reactors: [] # optional: a multi-reactor host (see below) +``` + +## state + +| Key | Default | Meaning | +| --- | --- | --- | +| `state.dir` | `./.reactor` | The durable state directory: receipts, world-models, and the compiled IR cache. Resolved to an absolute path rooted at the project dir, so every command agrees on one location regardless of cwd. | + +## model + +| Key | Default | Meaning | +| --- | --- | --- | +| `model.provider` | `openrouter` | The model provider. | +| `model.render_model` | `google/gemini-3.5-flash` | The model used for renders at run/serve time. | +| `model.compile_model` | `google/gemini-3.5-flash` | The model used for the compile sessions. It is part of the IR cache key. | +| `model.temperature` | `0` | Sampling temperature. | +| `model.max_turns` | `200` | The per-session turn ceiling. | + +The compile model is a component of the content-addressed cache key `(contract-set fingerprint, SDK version, model id)`. Changing it invalidates the cache and forces a recompile. + +## sandbox + +The `sandbox` block is the render threat-model knob. + +| Key | Default | Meaning | +| --- | --- | --- | +| `sandbox.mode` | `none` | `none` runs renders in the SDK's cwd-scoped, bounded shell. `docker` runs each render command in a throwaway, network-disabled container. | +| `sandbox.shell_timeout_ms` | `300000` | The per-command time bound (300 seconds) for the bounded shell. | +| `sandbox.image` | (none) | The container image, when `mode: docker`. | +| `sandbox.network` | (none) | The container network setting, when `mode: docker`. | + +See [connectors and sandbox](/cli/connectors-and-sandbox) for the full render-isolation behavior, including the Docker-absent fallback. + +## gateways + +Each `gateways` entry is an external-driven entry point bound to a connector. + +| Key | Meaning | +| --- | --- | +| `node` | The gateway node id (must match a `kind: gateway` contract). | +| `source_id` | The connector source id (defaults to the node id). Keys the durable idempotency cursor. | +| `poll` | An optional poll cadence for the gateway. | +| `connector` | The connector definition: its `type` plus type-specific fields such as `id_field` and `items`. | + +See [connectors and sandbox](/cli/connectors-and-sandbox) for the built-in connector types (`static`, `http`, `file`) and the `connectors.{cjs,js}` plugin shape. + +## reactors + +A `reactors` list hosts N isolated reactors in one `serve` process. When the list is empty (the default), the project is single-reactor and the top-level `state` and `gateways` are that one reactor. + +| Key | Default | Meaning | +| --- | --- | --- | +| `name` | `reactor-` | The reactor's name, which becomes its HTTP namespace prefix `//...`. | +| `project` | the project dir | The contracts directory for this reactor. | +| `state_dir` | `/` | This reactor's isolated durable state directory. | +| `gateways` | `[]` | This reactor's gateways. | + +Each entry is isolated: its own contracts directory, state directory, substrate, schedule, and cursors, so one reactor never corrupts another. See [the multi-reactor host](/cli/compile-run-serve) for how `--concurrency` parallelizes across reactors. + +## Environment variables + +| Variable | Meaning | +| --- | --- | +| `OPENROUTER_API_KEY` | The live key for the model surface. Required by `compile`, `run`, `serve`, and `trigger`. Read from the process env first, then a `.env` file discovered from the working directory upward. | +| `REACTOR_OFFLINE` | Set to `1` (or `true`) to force offline mode. Equivalent to the `--offline` flag. | + +## Global flags + +These flags are honored by every command and override the file: + +| Flag | Meaning | +| --- | --- | +| `--state-dir ` | Override `state.dir`. | +| `--project ` | The project directory containing `reactor.yml` (default `.`). | +| `--json` | Machine-readable JSON output. | +| `--offline` | Force offline mode (sets `REACTOR_OFFLINE=1`). | diff --git a/content/docs/cli/connectors-and-sandbox.mdx b/content/docs/cli/connectors-and-sandbox.mdx new file mode 100644 index 0000000..1b9cb33 --- /dev/null +++ b/content/docs/cli/connectors-and-sandbox.mdx @@ -0,0 +1,90 @@ +--- +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. + +## 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`: + +```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` | Watch a `dir` of `.json` files. | + +### 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. + +### 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 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 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. diff --git a/content/docs/cli/meta.json b/content/docs/cli/meta.json new file mode 100644 index 0000000..9a4a032 --- /dev/null +++ b/content/docs/cli/meta.json @@ -0,0 +1,12 @@ +{ + "title": "Reactor CLI", + "pages": [ + "overview", + "quickstart", + "configuration", + "compile-run-serve", + "connectors-and-sandbox", + "observability", + "command-reference" + ] +} diff --git a/content/docs/cli/observability.mdx b/content/docs/cli/observability.mdx new file mode 100644 index 0000000..b093540 --- /dev/null +++ b/content/docs/cli/observability.mdx @@ -0,0 +1,90 @@ +--- +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). diff --git a/content/docs/cli/overview.mdx b/content/docs/cli/overview.mdx new file mode 100644 index 0000000..f27ee7a --- /dev/null +++ b/content/docs/cli/overview.mdx @@ -0,0 +1,88 @@ +--- +title: Reactor CLI +description: The reference command-line client that compiles a .prose project and serves it as a durable, cost-observable daemon. +--- + +# Reactor CLI + +`@openprose/reactor-cli` is the deterministic command-line driver for the [`@openprose/reactor`](/reactor) SDK. The command is `reactor`. + +The CLI does one 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. + +## Install + +```sh +npm install -g @openprose/reactor-cli +``` + +Or per-project, alongside the SDK and its peer dependencies: + +```sh +npm install --save-dev @openprose/reactor-cli @openprose/reactor @openai/agents zod +``` + +## 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 +reactor init my-project # scaffold a gateway + responsibility + reactor.yml +cd my-project +reactor doctor # check node, SDK, key/deps, sandbox, state-dir, IR +reactor compile # run the compile sessions -> IR cache +reactor run # boot, drain to quiescence, print dispositions + cost +reactor serve --http 8080 # boot the durable host + continuity loop + HTTP surface +``` + +## Cost scales with surprise + +Every receipt carries a `surprise_cause`. A node that re-wakes but whose inputs did not move memo-skips at zero render cost. A node renders, and spends tokens, only when its `(contract_fp, input_fps)` memo key actually moves. + +So the standing cost of a quiet system trends to zero, and a cost spike is always a real change propagating. `reactor receipts cost` and `reactor status` roll cost up by `surprise_cause` so you can see exactly what surprised the system. See [observability](/cli/observability) for the details. + +## The offline boundary + +The default import surface and every model-free command are keyless. Requiring the CLI entrypoint loads neither `@openai/agents` nor `zod`. + +`compile`, `run`, `serve`, `trigger`, and the connector and render paths reach the model surface only via a dynamic `import()` inside the handler. They need a live key (`OPENROUTER_API_KEY`) plus the optional peer deps. Every other command, including `doctor`, `init`, and the whole observability suite, runs fully offline, with no key and with the model deps absent. + +You can force offline mode on any command with `--offline` (or `REACTOR_OFFLINE=1`). + +## Where to go next + + + + + + + + + diff --git a/content/docs/cli/quickstart.mdx b/content/docs/cli/quickstart.mdx new file mode 100644 index 0000000..16afd4c --- /dev/null +++ b/content/docs/cli/quickstart.mdx @@ -0,0 +1,105 @@ +--- +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 compile` to freeze it, then `reactor run` for a one-shot drain or `reactor serve` for a durable daemon. + +## Install + +```sh +npm install -g @openprose/reactor-cli +``` + +## 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 +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. + +`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, 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. + +```sh +reactor doctor +``` + +```text +reactor doctor + + node v22.3.0 (ok) + sdk @openprose/reactor@1.0.0 (resolved) + offline mode not forced + live key present (OPENROUTER_API_KEY) + live dep @openai/agents: ok + live dep zod: ok + sandbox mode none + state dir /abs/my-project/.reactor (writable) + compiled IR not compiled -- run `reactor compile` + + status: healthy-for-offline +``` + +Add `--live` to probe one live smoke render against the real provider. + +## Compile + +`compile` runs the intelligent compile sessions and freezes them into the content-addressed IR cache. It needs a live key (`OPENROUTER_API_KEY`) plus the `@openai/agents` and `zod` peer deps. + +```sh +reactor compile +``` + +An unchanged contract set recompiles at zero session cost because the cache key is content-addressed. To check freshness without compiling (handy in CI), use `--check`, which exits non-zero when the cache is stale. + +```sh +reactor compile --check # exits 1 right after init, before the first compile +``` + +## Inspect the compiled DAG + +Once compiled, the offline observability commands work with no key. `topology` prints the resolved DAG. + +```sh +reactor topology # the compiled DAG (inbox -> digest) +``` + +## Run once + +`run` ensures the IR is fresh (compiling if stale), boots the reactor, drains to quiescence, prints per-node dispositions plus cost, and exits. + +```sh +reactor run +reactor status # standing compile cost beside the run cost +``` + +## Or serve as a daemon + +`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 +reactor serve --http 8080 +``` + +The scaffold's `static` connector ingests the seeded items on the first tick, so the gateway and digest both render. In another shell you can read the trail: + +```sh +reactor receipts list # the gateway + digest receipts +reactor receipts cost # cost rolled up by surprise_cause +``` + +## Next steps + +- [Configuration](/cli/configuration) covers every `reactor.yml` key. +- [Compile, run, serve](/cli/compile-run-serve) goes deep on the three core verbs. +- [Connectors and sandbox](/cli/connectors-and-sandbox) explains gateways and the render sandbox. diff --git a/content/docs/index.mdx b/content/docs/index.mdx index ce85d3a..725473e 100644 --- a/content/docs/index.mdx +++ b/content/docs/index.mdx @@ -9,6 +9,8 @@ Write a Markdown contract. Your agent reads it, wires the services, runs the sub Plain prompts are great for one-off work. They get messy when the same process needs roles, handoffs, retries, memory, or a receipt. OpenProse gives that process a contract and lets an agent session execute it as a program. +Two first-class parts make that durable. The Reactor DAG is the engine: it compiles a `.prose` project into a content-addressed graph and runs it so cost scales with surprise. The Reactor CLI is the command-line client that drives that engine and serves it as a durable, cost-observable daemon. + + + the node re-examines the fact + -> 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). See the +[Incident Briefing Room](/reactor/incident-briefing-room) for a worked trail that +mixes input-driven surprise with forecast-paced rechecks. diff --git a/content/docs/reactor/incident-briefing-room.mdx b/content/docs/reactor/incident-briefing-room.mdx new file mode 100644 index 0000000..b5a68d6 --- /dev/null +++ b/content/docs/reactor/incident-briefing-room.mdx @@ -0,0 +1,92 @@ +--- +title: "Case study: Incident Briefing Room" +description: A worked receipt trail showing cost scaling with surprise across a quiet world and a changing one. +--- + +# Case study: Incident Briefing Room + +The Incident Briefing Room is the clearest Reactor example because its world +changes over time. The maintained state is not "write a report." It is "the +incident channel has an accurate current briefing." + +Its continuity rule says to recheck on incident messages, on status-page changes, +and while the incident is active. Its maintained truth covers impact, timeline, +owner, next action, and customer-facing status. Its fulfillment boundary is to +summarize new facts, ask for missing owner input, and update the briefing. + +This case study mirrors the v0.1 technical report. The numbers below are from +deterministic measurement worlds, not a hosted production system, and they are +not dollarized. + +## A changing world + +When the world changes, the Reactor spends fresh model work on the change and +reuses prior verdict work on the rechecks in between. One deterministic receipt +trail shows the shape: + +| Beat | Cause | Evidence | Fresh / reused | Receipt | Result | +| --- | --- | --- | ---: | --- | --- | +| `12:00` | real input | initial incident feed | `37/0` | fresh | baseline briefing recorded | +| `12:15` | forecast recheck / evidence age | unchanged | `0/37` | memo hit | no material change | +| `12:30` | real input | `incident-opened` event | `37/0` | fresh | briefing changes | +| `12:45` | forecast recheck / evidence age | changed evidence, unchanged since | `0/37` | memo hit | updated verdict reused | + +The two fresh beats are the two moments something actually moved: the bootstrap +and the `incident-opened` event. The rechecks in between hit the memoized +verdict because the input fingerprint did not move. A naive loop re-spends on +every scheduled turn whether or not anything changed. + +## A quiet world + +When nothing changes, the cost is not zero, and the honest claim is precise about +why. A quiet run has the same proof shape plus one extra plan-age audit floor: + +```text +bootstrap fresh (real first render at boot) +evidence-age tick memo hit (nothing moved) +plan-age audit fresh floor (the forecast-amortized recheck) +evidence-age tick memo hit (nothing moved) +``` + +That floor is the deliberately-declared forecast cadence: the cost of not letting +a stale plan become quietly wrong. The cost thesis is **surprise plus a +forecast-amortized audit floor**, not surprise alone. It is still less fresh +model work than re-asking on every scheduled turn. + +## What the measured rows show + +The technical report measures two controlled scenarios against two controls: a +no-memo Reactor (same schedule, no memoization) and a non-Reactor naive loop +(rechecks on the same schedule with no receipts or memo keys). + +| Scenario | Row | Model invocations | Fresh tokens | Reused tokens | +| --- | --- | ---: | ---: | ---: | +| Static | Reactor | 2 | 46 | 46 | +| Static | Reactor, no memo | 4 | 92 | 0 | +| Static | Naive loop | 4 | 256 | 0 | +| Changing | Reactor | 2 | 74 | 74 | +| Changing | Reactor, no memo | 4 | 148 | 0 | +| Changing | Naive loop | 4 | 148 | 0 | + +In the static scenario the Reactor does fresh work at bootstrap and at the +plan-age audit, while evidence-age rechecks hit memoized verdicts: 2 invocations +and 46 fresh tokens against 4 and 256 for the naive loop. In the changing +scenario the Reactor spends fresh work on the bootstrap and the material change, +then reuses verdict work on the rechecks: 2 invocations and 74 fresh tokens +against 4 and 148 for both controls. + +The changing-world win is fresh model work plus reusable-verdict proof, not a +total accounted-token claim over the no-memo control. + +## What this does and does not show + +This is a deterministic, local demonstration. The Reactor maintains a long-lived +responsibility with runtime-produced receipts, reusable verdict proof, +forecast-paced rechecks, and explicit surprise attribution. It is not a hosted +production incident system updating real customer communications, and the figures +are measured token accounting, not dollar costs. + +To run a project like this from the shell, see the [CLI quickstart](/cli/quickstart). +For the mechanics behind the trail, see +[the reconciler and receipts](/reactor/reconciler-and-receipts) and +[continuity and ingestion](/reactor/continuity-and-ingestion). diff --git a/content/docs/reactor/index.mdx b/content/docs/reactor/index.mdx new file mode 100644 index 0000000..24cb990 --- /dev/null +++ b/content/docs/reactor/index.mdx @@ -0,0 +1,113 @@ +--- +title: The Reactor +description: The execution engine that turns a .prose project into a content-addressed DAG and runs it as a durable, self-maintaining reactor. +--- + +# The Reactor + +The Reactor is the execution engine for OpenProse. + +It takes a `.prose` project, compiles it into a content-addressed graph of +responsibilities, and runs that graph as a durable reactor that does expensive +work only when something material has actually changed. + +The shape borrows from React. React keeps a DOM consistent with declared +component state and re-renders only what moved. The 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 | The Reactor | +| --- | --- | +| Component | A responsibility (a declared ideal world-state) | +| DOM | The world-model (the maintained truth, materialized) | +| Render function | A bounded agent session that computes the next truth | +| Props | Subscriptions to other responsibilities | +| `setState` / re-render | A new signed receipt on a subscription | +| Reconciler | The reconciler (fingerprint, schedule, commit, propagate) | +| `React.memo` | Skip the render if subscribed inputs have not moved | + +## Two phases: compile, then run + +The single most important fact about the Reactor is that it has two phases, and +intelligence lives in only one of them. + +```text +.prose contracts + -> COMPILE (intelligent sessions, fires on contract change) + Forme draws the DAG + 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, 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 the 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; 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. + +The 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. The render writes +files into its workspace; the harness harvests, promotes, and fingerprints +them. Quiet nodes stay quiet and free. + +## Where to go next + + + + + + + + + +To drive a Reactor from the shell, see the [CLI overview](/cli/overview) and +the [CLI quickstart](/cli/quickstart). diff --git a/content/docs/reactor/meta.json b/content/docs/reactor/meta.json new file mode 100644 index 0000000..ede09cf --- /dev/null +++ b/content/docs/reactor/meta.json @@ -0,0 +1,11 @@ +{ + "title": "Reactor", + "pages": [ + "index", + "the-dag-and-compile", + "world-model-and-fingerprints", + "reconciler-and-receipts", + "continuity-and-ingestion", + "incident-briefing-room" + ] +} diff --git a/content/docs/reactor/reconciler-and-receipts.mdx b/content/docs/reactor/reconciler-and-receipts.mdx new file mode 100644 index 0000000..435be9e --- /dev/null +++ b/content/docs/reactor/reconciler-and-receipts.mdx @@ -0,0 +1,119 @@ +--- +title: The reconciler and receipts +description: The dumb reconciler, the memo key, the signed receipt chain, and render-writes-files with harvest-and-promote. +--- + +# The reconciler and receipts + +The reconciler is the run phase of the Reactor, and it is deliberately dumb. It +holds no intelligence. It compares fingerprints, skips the unchanged, schedules +renders, commits results, and propagates along the edges Forme drew. Everything +intelligent was frozen at [compile](/reactor/the-dag-and-compile) time; the +reconciler only executes that frozen output. + +## The memo key + +Before a render runs, the reconciler checks one key: + +```text +memo key = (contract fingerprint, input fingerprints) +``` + +If neither the node's own contract nor any subscribed input has moved since the +node's last receipt, the reconciler writes a cheap `skipped` receipt and spawns +nothing. No session, no cost. This is `React.memo` applied to expensive agent +work, and it is the mechanism behind "cost scales with surprise." + +Two facts the wiring must honor or propagation silently fails: + +- A facet-emitting producer must mount with the canonicalizer its + canonicalizer-session emitted, not a bare atomic canonicalizer. Otherwise the + moved facet never appears in the propagation set and the edge never fires. +- A pure source node memo-skips on a bare self or external re-wake, because its + key `(contract fingerprint, [])` never moves. Evidence enters honestly only via + a contract-fingerprint bump, a freshness lapse, a staged external arrival, or + the first cold-miss render at boot. + +## Single-flight, coalescing, failure + +These fall out of the primitives; none needs a new subsystem. + +- **Single-flight**: a node renders one at a time. A render reads its own prior + truth and appends to its own ledger, so two concurrent renders would race. +- **Coalescing**: wakes that arrive while a render is in flight do not stack into + N more renders. They mark the node dirty; when the in-flight render commits, if + the node is still dirty it renders once more against the freshly-moved inputs. + This is React batching. Five inputs moving mid-render cost one follow-up render, + not five. +- **Failure**: a render that errors or leaves a postcondition unsatisfied commits + nothing. The last-good published truth stands. It still writes a `failed` + receipt: failures are cheap audit signal, not silence. Downstreams do not wake, + because the fingerprint did not move. + +So a node's receipts have three statuses, and only one propagates: + +```text +rendered the truth moved -> wake the subscribed downstreams +skipped nothing changed -> propagate nothing +failed nothing committed -> prior truth stands, propagate nothing +``` + +## The receipt is the commit + +The receipt is the single commit object and the unit of the ledger: the wake +event, the memo-key record, the audit entry, and the trust artifact all in one. + +| Field | Meaning | +| --- | --- | +| `node` | the node identity (the ledger is node-scoped) | +| `contract_fingerprint` | which contract version produced this | +| `wake` | the wake's source (input, self, external) and the waking refs | +| `input_fingerprints` | the consumed tuple, one per subscribed facet | +| `fingerprints` | a `facet -> token` map of the published truth (atomic is the reserved whole-truth facet) | +| `semantic_diff` | render-input context, never a wake signal | +| `prev` | pointer to the prior receipt, chaining the ledger | +| `status` | `rendered`, `skipped`, or `failed` | +| `cost` | token attribution that makes "cost scales with surprise" observable | +| `sig` | the signature | + +A node's receipts accumulate into its **ledger**: the append-only trail that is +the node's durable memory. The system can be killed and resumed because the +ledger is the memory. At boot the reactor re-derives each node's last receipt +from the durable trail, so a restart memo-skips the unchanged nodes instead of +re-rendering them. + +## What "signed" means in v1 + +The receipt chain is attributable and tamper-evident at the **meaning layer**. +Each receipt commits to its fingerprints and to its predecessor, so the trail is +verifiable as a coherent sequence produced by a known node. In v1 this does not +yet mean a cryptographic byte-hash of canonical bytes; trust rests on the +fingerprint chain. The cryptographic digest and a real signer are deferred to +the integrity and composition-pinning milestone. + +## Render writes files; the harness promotes + +The division of labor at commit time is exact and is part of the binding model. + +The **render writes files** into its own workspace. The truth's contents never +ride the session's final output text. When the render signals `rendered`, the +**harness harvests** the declared workspace files, **promotes** them into the +node's published world-model, and **fingerprints** the canonical result with the +node's compiled canonicalizer. + +This is why the render and the reactor share one world-model store: the render's +workspace write must be visible to the harness harvest in the same render. The +published artifact is fingerprinted; the workspace is not, and it reaches the +published truth only through this explicit promote-and-fingerprint commit. + +## Propagation + +On a `rendered` receipt whose fingerprint moved, the reconciler wakes the +downstreams subscribed to the moved facets, resolved by reading the topology's +edges. A `skipped` or `failed` receipt propagates nothing. + +The wake event and the fingerprint move are the same mechanism seen two ways: a +receipt arrived, and a facet fingerprint differs from the one the downstream last +consumed. That unification is what lets self-rechecks and external triggers ride +the same propagation path as ordinary upstream changes. See +[continuity and ingestion](/reactor/continuity-and-ingestion). diff --git a/content/docs/reactor/the-dag-and-compile.mdx b/content/docs/reactor/the-dag-and-compile.mdx new file mode 100644 index 0000000..f1bde3c --- /dev/null +++ b/content/docs/reactor/the-dag-and-compile.mdx @@ -0,0 +1,102 @@ +--- +title: The DAG and compile +description: How a .prose project compiles, as sessions, into a content-addressed topology DAG plus per-node canonicalizers and validators. +--- + +# The DAG and compile + +Compile is the only intelligent phase of the Reactor. It runs ahead of run +time, it fires when the contract set changes, and it freezes intelligent +sessions into deterministic artifacts the run phase executes. + +There is no `.prose` parser. A compile step is an agent session that reads the +contracts and emits structured output, which a small deterministic lowering +turns into run-time artifacts. The session embodies the VM; the only +deterministic structures are the compile-frozen outputs, and even those are +produced by sessions. + +## What compile produces + +A full compile runs Forme once, then the per-node materiality and gate sessions +for each node Forme found. + +```text +loadContractSet(dir) load the contract text (trivial, deterministic) + -> compileForme the topology session -> the DAG + -> per node: + compileCanonicalizer the materiality session -> a canonicalizer + compilePostcondition the gate session -> validators + => a mountable ReconcilerTopology + per-node compiled artifacts +``` + +The output is a content-addressed topology plus, for every node, a canonicalizer +(what counts as a change, frozen) and a set of validators (the commit gate). +These are what the run phase mounts. The compile run reports its own token cost +with `surprise_cause: self`, because a compile is a `self`-driven build. + +## Forme draws the edges + +Forme is the wiring session. It reads the full set of declared contracts and +resolves each node's needs to the producer that satisfies them, **semantically**. +A node says in `### Requires` that it needs "a current view of competitor +funding"; Forme matches that need to the `funding` part of some producer's +`### Maintains`. This is meaning matching, not string matching. + +The edges of the DAG are Forme's output, not human-authored config. Intent +stays with the human (the need, in the contract); the wiring is Forme's. Two +rules make this safe: + +- A need with no producer, or two equally plausible producers, is a surfaced + **wiring diagnostic**, never a silent guess. +- Acyclicity is a postcondition on Forme's own output. A topology that would + close a loop is rejected and surfaced. + +Genuine feedback (a node's output shaping its own next input) is not a back-edge. +It is self-driven [continuity](/reactor/continuity-and-ingestion): loops live in +time, not in edges. + +## What a node is + +Every node in the DAG is the same shape: a declaration plus a render. The `kind` +in the contract frontmatter is sugar over that one atom. + +| `kind` | Role | Interface | World-model | +| --- | --- | --- | --- | +| `responsibility` | The mounted node, the headline | `### Requires` to `### Maintains` | persisted | +| `function` | A called helper, the library tier | `### Parameters` to `### Returns` | none | +| `gateway` | Source or ingress | no `### Requires`; `### Maintains` the incoming truth | persisted | +| `pattern` | Reusable coordination, expanded at compile | n/a | n/a | +| `test` | Assertions over a subject's truth or receipts | n/a | n/a | + +A `responsibility` is a mounted render with a standing, persisted world-model +that other nodes can subscribe to. A `function` is a called render: stateless, +ephemeral, returns a value, carries no world-model. A `gateway` is sugar for a +`responsibility` whose wake-source is external (a webhook, cron, or manual +trigger), so it has no `### Requires` and maintains the latest incoming truth. + +Being subscribed-to is what makes something a node. Inside a render you can +`call` functions and spawn sub-agents, but none of that is a node. The only +cross-node connection is a subscription. + +## Mounting makes a node, not statefulness + +A render becomes a DAG node when it is **mounted**. Mounting is a harness act +that adds identity, a persisted world-model, and the resolved subscriptions. +Node-ness is conferred by mounting, never by holding memory. A pure transform +with no internal memory is still a node if it is mounted as a producer; a render +that reads its own prior truth is not a node merely because it is stateful. + +## Content-addressed, and re-runnable + +Forme's resolved topology is itself a maintained truth: it records the nodes, +the resolved edges, the external entry points, and `acyclic: true` as a +postcondition. Because the topology is committed and versioned, the wiring is +inspectable and the compiled artifacts are cached. Compile re-fires only when +the contract set changes, the rarest event in the system. Editing a `### Maintains` +schema moves that node's contract fingerprint, which is a memo miss, so the +node simply re-renders into the new shape on the next wake. There is no separate +migration machinery. + +Once compile has produced the topology and the per-node artifacts, the +[reconciler](/reactor/reconciler-and-receipts) takes over and the intelligent +phase is done until a contract changes again. diff --git a/content/docs/reactor/world-model-and-fingerprints.mdx b/content/docs/reactor/world-model-and-fingerprints.mdx new file mode 100644 index 0000000..719bb7d --- /dev/null +++ b/content/docs/reactor/world-model-and-fingerprints.mdx @@ -0,0 +1,158 @@ +--- +title: World-model and fingerprints +description: The content-addressed maintained truth, the fingerprints of meaning, and facet-granular propagation along edges. +--- + +# World-model and fingerprints + +The world-model is the maintained truth a node keeps current. It is the +Reactor's DOM: standing between renders, read by the next render as its prior +state, and subscribed to by downstream nodes. One node, one world-model. + +This page covers what the truth is, how "changed" is decided, and how a change +in one node reaches exactly the right downstreams. + +## The world-model is a content-addressed artifact + +The canonical world-model is a single content-addressable artifact, by default a +small directory of files, with a single file as the degenerate case. It is +addressed by the fingerprint of its canonical form. + +Three forces pin that shape: + +- A render reads the prior truth agentically, the way a coding agent pulls a + file on demand, so the truth must be legible and navigable, not flattened into + a prompt blob. +- The receipt trail is signed and append-only, so the truth's evolution must be + content-addressable and auditable. +- Memoization needs a cheap "did it move?" check, so the truth must be + deterministically serializable. + +Anything else (a SQL index for query, a vector index for retrieval, a rendered +dashboard) is a **derived projection** of that canonical truth, never the truth +itself. + +One discipline matters: the **published** world-model is the fingerprinted +artifact. The render's private scratch is workspace, never fingerprinted and +never subscribed to. A node updates its published truth only when something +semantically material actually changed. + +## The schema lives in `### Maintains` + +`### Maintains` declares the shape of the world-model. It does four jobs in one +block: + +- A **type**: the fields, including any freshness fields (`valid_until`, + `last_corroborated`, `confidence`). +- A **canonicalization spec**: what equality means for the fingerprint, written + as unambiguous natural language. +- A **subscription surface**: the named facets a downstream may depend on. +- **Postconditions**: the validators the render must leave the truth satisfying + (the folded-in `### Criteria`, not a separate judge beat). + +## A fingerprint is a fingerprint of meaning + +A fingerprint is a cheaply computed token that changes if and only if the +semantically-relevant content changed. That invariant is the whole definition. +How it is computed (a content digest, a high-water timestamp, a revision +counter) is a swappable detail, the same family as HTTP ETags or file mtimes. + +The apparent paradox, a semantic "if and only if" decided deterministically, +dissolves once you split it across time, exactly as React does: + +1. **Compile time** (intelligent, once per contract). The natural-language + canonicalization spec is compiled into a deterministic canonicalizer: what is + material, what is dropped, how text, sets, and numbers normalize. This is + where intelligence decides what counts as a change. +2. **Render time** (intelligent, per render). The session writes the truth and + self-polices its postconditions. It never decides "did this change." +3. **Wake time** (dumb, every time). The reconciler runs the compiled + canonicalizer plus a digest and compares. + +So the fingerprint changes if and only if the canonical material content +changes, and "material" was frozen by intelligence at compile time, not judged +at wake time. An agent never judges "did this change" at wake time. + +The single highest-leverage control here is **material versus immaterial fields**. +A feed re-polled every three minutes carries a fresh `fetched_at` and new request +ids every time. Excluding those from the fingerprint is what keeps "cost scales +with surprise" from silently degrading back into "cost scales with the clock." + +## The structured-backing rule + +Anything subscribed must have a structured, canonicalizable backing. Free-form +rendered prose is a derived projection excluded from the fingerprint. Otherwise +a session re-rendering the same paragraph hashes differently every time and +falsely re-triggers downstreams. The rule: **fingerprint the structured truth; +render prose from it.** + +## The identity vocabulary + +Three fingerprints of meaning are the load-bearing reactive primitives. + +| Name | Of what | Answers | +| --- | --- | --- | +| contract fingerprint | the node's own contract | which version of the responsibility produced this | +| input fingerprint | each upstream facet a node subscribes to | did the watched thing change | +| world-model fingerprint | the node's own published truth (plus one per facet) | the identity downstreams subscribe to | + +These chain. A node publishes its world-model fingerprint in its receipt; a +downstream sees that as one of its input fingerprints. The memoization key is +`(contract fingerprint, input fingerprints)` and nothing else. No judge, no +policy artifact. + +A cryptographic byte-hash is deliberately deferred for v1. Memoization and +propagation run entirely on the fingerprints of meaning above. A byte digest's +only jobs are integrity and cross-party composition-pinning, neither of which the +reactive core needs to work. + +## Facets: atomic always, named parts when they help + +Fingerprinting has two layers. + +- **Atomic** (mandatory). One token over the whole canonical truth. This is the + reconciler's primitive and what a diamond reconverges on: a node reachable by + several paths renders once per distinct input-fingerprint tuple, not once per + inbound edge. A leaf truth needs only this. +- **Facets** (optional, schema-declared). Per-part tokens that make propagation + finer-grained. A downstream that subscribes to facet *X* does not wake when + facet *Y* moves. This is React's selector boundary made authorable. + +You declare a facet simply by naming a part: a `####` sub-heading inside +`### Maintains` is a facet, and its body says which fields are material. The +name is the fingerprint unit, the subscription symbol, and the world-model +subtree all at once. Structure is subscription. + +```markdown +### Maintains + +A current, corroborated view of each tracked competitor. Each competitor carries +a stable `name` and a `last_corroborated` field; `fetched_at` and source request +ids are immaterial everywhere. Postcondition: every competitor cites a source. + +#### funding +Funding events per competitor: round, amount, date. Material: the event set +(unordered) and each event's round, amount, and date. + +#### hiring +Open-role activity: the department set and the open-role count (exact). + +#### product-launches +Announced or shipped products: the launch set; a ship-date slipping past today +flips `shipped`, which is material. +``` + +A downstream that requires *funding* wakes only when `#### funding`'s fingerprint +moves, not when hiring or launches move. The shared `name` and `last_corroborated` +sit outside any part, so they move only the atomic token. + +Atomic is the always-on correctness primitive and the free default. Facets are +the efficiency primitive that keeps fan-out from burning the surprise budget. A +leaf truth declares none and pays nothing. + +A semantic diff ("3 controls went stale, 1 newly accepted") is valuable, but it +is carried as **render input** in the receipt, never as a wake signal. The wake +decision is fingerprint-only. + +Next: how the [reconciler](/reactor/reconciler-and-receipts) uses these +fingerprints to skip, commit, and propagate. From 987cb6aab512b2ac647a7c9c6322615784f75a8f Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Sun, 31 May 2026 22:50:49 -0700 Subject: [PATCH 06/14] docs: add Reactor DevTools section (replay viewer, --describe, recording, reference) Full documentation for @openprose/reactor-devtools as a sibling of the Reactor CLI section: overview/index, quickstart, state-dirs-and-replay (incl. the ReplaySession SDK surface), the-viewer, --describe, recording, and reference. Registered in the top nav after cli; cross-linked from cli/observability. Grounded in the package source (bin flags, HTTP API, ReceiptFrame shape, library exports). House-style ASCII (no em-dashes); cspell dictionary extended. --- .cspell.json | 15 ++- content/docs/cli/observability.mdx | 4 + content/docs/meta.json | 2 +- content/docs/reactor-devtools/describe.mdx | 89 +++++++++++++++ content/docs/reactor-devtools/index.mdx | 102 ++++++++++++++++++ content/docs/reactor-devtools/meta.json | 12 +++ content/docs/reactor-devtools/quickstart.mdx | 91 ++++++++++++++++ content/docs/reactor-devtools/recording.mdx | 72 +++++++++++++ content/docs/reactor-devtools/reference.mdx | 100 +++++++++++++++++ .../state-dirs-and-replay.mdx | 92 ++++++++++++++++ content/docs/reactor-devtools/the-viewer.mdx | 77 +++++++++++++ 11 files changed, 654 insertions(+), 2 deletions(-) create mode 100644 content/docs/reactor-devtools/describe.mdx create mode 100644 content/docs/reactor-devtools/index.mdx create mode 100644 content/docs/reactor-devtools/meta.json create mode 100644 content/docs/reactor-devtools/quickstart.mdx create mode 100644 content/docs/reactor-devtools/recording.mdx create mode 100644 content/docs/reactor-devtools/reference.mdx create mode 100644 content/docs/reactor-devtools/state-dirs-and-replay.mdx create mode 100644 content/docs/reactor-devtools/the-viewer.mdx diff --git a/.cspell.json b/.cspell.json index 89fb539..d1aa4b6 100644 --- a/.cspell.json +++ b/.cspell.json @@ -75,7 +75,20 @@ "statefulness", "agentically", "canonicalizable", - "authorable" + "authorable", + "DevTools", + "devtools", + "replayable", + "sparkline", + "scrubber", + "monorepo", + "Chromium", + "Playwright", + "ffmpeg", + "webm", + "dedup", + "prev", + "dep" ], "ignoreRegExpList": ["https?://[^\\s)]+", "`[^`]*`"] } diff --git a/content/docs/cli/observability.mdx b/content/docs/cli/observability.mdx index b093540..0cfff61 100644 --- a/content/docs/cli/observability.mdx +++ b/content/docs/cli/observability.mdx @@ -88,3 +88,7 @@ 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/meta.json b/content/docs/meta.json index e6a9b5e..44fdd7d 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -1,4 +1,4 @@ { "title": "OpenProse", - "pages": ["index", "start", "think", "reactor", "cli", "use", "agents", "reference"] + "pages": ["index", "start", "think", "reactor", "cli", "reactor-devtools", "use", "agents", "reference"] } diff --git a/content/docs/reactor-devtools/describe.mdx b/content/docs/reactor-devtools/describe.mdx new file mode 100644 index 0000000..376d256 --- /dev/null +++ b/content/docs/reactor-devtools/describe.mdx @@ -0,0 +1,89 @@ +--- +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 + +```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% + input receipts=54 fresh=30420 reused=11480 + self receipts=2 fresh=0 reused=0 + external receipts=36 fresh=3240 reused=1680 + peak fresh 14580 at frame 73 (Concept Clusterer) + +PER-NODE + Runtime Watch r=6 s=12 f=0 fresh=3240 chain✓ + Claude Adapter r=4 s=0 f=0 fresh=1440 chain✓ + Codex Adapter r=4 s=0 f=1 fresh=720 chain✓ + Session Ledger r=5 s=7 f=0 fresh=2700 chain✓ + Concept Clusterer r=2 s=2 f=0 fresh=19440 chain✓ + ... + +CHAIN-VERIFY ok -- every node chain is prev-linked & consistent + +FRAMES (frame node status moved[...] fresh woke[...]) + 0 Agent FS rendered moved[@atomic,claude,codex,...] fresh 0 woke[Runtime Watch] + 1 Runtime Watch rendered moved[@atomic,claude,codex,...] fresh 540 woke[Claude Adapter,Codex Adapter,...] + 8 Session Ledger rendered moved[@atomic,session:claudeA,session:claudeB,session:codexA] fresh 540 woke[Session Summary [claudeA],...] + 9 Session Ledger skipped moved[--] fresh 0 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 + 8 Session Ledger rendered moved[@atomic,session:claudeA,...] fresh 540 woke[Session Summary ...] + 9 Session Ledger skipped moved[--] fresh 0 woke[--] + 10 Session Ledger skipped moved[--] fresh 0 woke[--] +``` + +Frame 8 renders and wakes its subscribers; frames 9 and 10 are byte-identical re-wakes that **memo-skip at zero fresh cost** and wake nothing. The skip is not a missing log line -- it is a recorded receipt proving the reactor was asked to re-check and correctly decided nothing moved. + +## Use it in CI + +`--describe` is deterministic, so a state-dir checked into a repo is a fixture you can assert against without a browser or a key. Grep the output to gate behavior -- for example, that a quiet stretch stays free, that exactly one node failed, or that a selective wake stayed selective: + +```sh +reactor-devtools fixtures/contract-redline --describe > out.txt + +# a single-section edit must not re-render sibling sections: +grep -E "Summarize §[0-9]+ +r=1 " out.txt # untouched summarizers rendered once (cold boot) only + +# the chain must verify: +grep -q "CHAIN-VERIFY ok" out.txt || exit 1 +``` + +This is how the demo-suite fixtures lock their invariants (see [Recording](/reactor-devtools/recording)) -- the same trace a reviewer reads is the trace CI asserts on. diff --git a/content/docs/reactor-devtools/index.mdx b/content/docs/reactor-devtools/index.mdx new file mode 100644 index 0000000..8d17206 --- /dev/null +++ b/content/docs/reactor-devtools/index.mdx @@ -0,0 +1,102 @@ +--- +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`. + + +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, 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 + └─ /sdk : 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 +``` + +## Install + +```sh +npm install -g @openprose/reactor-devtools +``` + +The package ships the `reactor-devtools` bin and an importable library. Its one runtime dependency is `@openprose/reactor`; the server is built on Node's `node:http` and the front-end is a hand-rolled, no-build SVG SPA. No web framework, no graph library, no bundler. + +## Where to go next + + + + + + + + + diff --git a/content/docs/reactor-devtools/meta.json b/content/docs/reactor-devtools/meta.json new file mode 100644 index 0000000..7a246af --- /dev/null +++ b/content/docs/reactor-devtools/meta.json @@ -0,0 +1,12 @@ +{ + "title": "Reactor DevTools", + "pages": [ + "index", + "quickstart", + "state-dirs-and-replay", + "the-viewer", + "describe", + "recording", + "reference" + ] +} diff --git a/content/docs/reactor-devtools/quickstart.mdx b/content/docs/reactor-devtools/quickstart.mdx new file mode 100644 index 0000000..aae20e5 --- /dev/null +++ b/content/docs/reactor-devtools/quickstart.mdx @@ -0,0 +1,91 @@ +--- +title: Quickstart +description: Get a state directory, open the viewer in a browser, and read the same run headlessly with --describe. +--- + +# Quickstart + +DevTools replays a saved **state directory**. So the quickstart is two steps: get a state-dir, then look at it -- in a browser, or as text. + +## 1. Get a state directory + +Any [`reactor run`](/cli/compile-run-serve) or `reactor serve` writes one. Point the reactor at a state-dir and run it to quiescence: + +```sh +reactor run --state-dir ./run +``` + +That persists `./run/receipts.json`, `./run/world-models/`, and `./run/compile/topology.json` -- the complete, replayable record of the run. (See [State dirs and replay](/reactor-devtools/state-dirs-and-replay) for the full anatomy.) + +You do **not** need a key or a running reactor to replay -- only the saved directory. + +## 2. Open the viewer + +```sh +reactor-devtools ./run +# reactor-devtools: replaying 92 receipt(s) across 14 node(s) +# open http://127.0.0.1:4555/ +``` + +Open the printed URL. You get three coordinated regions: + +- the **layered DAG** of the topology on the left, +- a live **fresh-vs-reused cost meter** and the **ordered receipt timeline** on the right, +- a **scrubber** along the bottom. + +Press **space** to play. Each step animates one receipt: a node flashes when it rendered and something moved, dim-pulses when it memo-skipped, flares red on a failure; the moved facet's edges light and the cost meter ticks. A quiet stretch is dim and flat; a real change is a bright cascade and a cost spike. + +| Key | Action | +| --- | --- | +| `space` | play / pause | +| `←` / `→` | step one receipt back / forward | +| `Home` / `End` | jump to the first / last receipt | +| click a receipt | jump to it | + +Pick the port with `--port` (default `4555`) and the bind host with `--host` (default `127.0.0.1`): + +```sh +reactor-devtools ./run --port 4591 +``` + +## 3. Read it headlessly with `--describe` + +For a terminal -- or an agent, or a CI gate -- `--describe` prints the same run as text and exits, no browser: + +```sh +reactor-devtools ./run --describe +``` + +```text +reactor-devtools --describe + state-dir ./run + topology yes · 14 nodes · 22 edges · acyclic=true + receipts 92 frames + dispositions rendered=63 · skipped=28 · failed=1 + wake-cause external=36 · input=54 · self=2 + +COST ROLLUP + total fresh=33660 · reused=13160 · reuse=28% + ... +PER-NODE + Session Ledger r=5 s=7 f=0 fresh=2700 chain✓ + ... +CHAIN-VERIFY ok -- every node chain is prev-linked & consistent + +FRAMES (frame node status moved[...] fresh woke[...]) + 8 Session Ledger rendered moved[@atomic,session:claudeA,...] fresh 540 woke[Session Summary ...] + 9 Session Ledger skipped moved[--] fresh 0 woke[--] + ... +``` + +That last block is the whole story in one place: frame 8 renders and wakes its subscribers, frames 9+ are byte-identical re-wakes that memo-skip at zero cost. See [`--describe`](/reactor-devtools/describe) for the full output and how to assert on it. + +## Try it on a demo corpus + +The repository ships six 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`, and `research-tree` each demonstrate a different Reactor behavior -- selective wake, dedup, failure isolation, incremental rollup. See [Recording](/reactor-devtools/recording) for what each one shows. diff --git a/content/docs/reactor-devtools/recording.mdx b/content/docs/reactor-devtools/recording.mdx new file mode 100644 index 0000000..31260bb --- /dev/null +++ b/content/docs/reactor-devtools/recording.mdx @@ -0,0 +1,72 @@ +--- +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 -- it changes nothing about the run. A state-dir without it still replays and records; it just falls back to computed captions. + +## The demo corpus + +The repository ships six 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. | + +Open any of them in the viewer (`reactor-devtools packages/reactor-devtools/fixtures/`), inspect them with `--describe`, or record them with `FIXTURE=`. Each fixture is paired with an invariant test that locks the behavior its video claims, so the demo and the test assert the same thing. + +## Recording your own run + +The recorder works against any state-dir with a `beats.json`. To film one of your own reactors: run it to a state-dir, author a `beats.json` against the frame indices you see in [`--describe`](/reactor-devtools/describe), drop it in the state-dir, and point the recorder at it. diff --git a/content/docs/reactor-devtools/reference.mdx b/content/docs/reactor-devtools/reference.mdx new file mode 100644 index 0000000..c60266c --- /dev/null +++ b/content/docs/reactor-devtools/reference.mdx @@ -0,0 +1,100 @@ +--- +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] +``` + +| Argument / option | Default | Effect | +| --- | --- | --- | +| `` | _(required)_ | A saved Reactor state directory (receipts + `compile/topology.json`). | +| `-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. | +| `-h`, `--help` | -- | Print usage. | + +Exit codes: `0` on `--help` or a successful `--describe`; `1` when `` is missing (usage is printed), when `--port` is not an integer, 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. | +| `GET /api/node/:id?version=` | The node's world-model at a version (the click-through inspector) via `readVersion`. `version` is a frame's `atomicVersion`. `400` if missing, `404` if no such node/version. | +| `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 (`@openprose/reactor/sdk`), not here, so other tools can shape a trail without this package. See [State dirs and replay](/reactor-devtools/state-dirs-and-replay). + +## Stack and dependencies + +| | | +| --- | --- | +| Version | `0.1.0` | +| Bin | `reactor-devtools` → `dist/cli.js` | +| Runtime dependency | `@openprose/reactor` only | +| Server | Node built-in `node:http` (no web framework) | +| Front-end | Vanilla, no-build SVG SPA (no bundler, no graph library) | +| Recording (dev) | Playwright (a dev dependency; repo tooling -- see [Recording](/reactor-devtools/recording)) | + +The package boundary is the point: the SDK stays zero-dep and headless; every opinionated UI choice is quarantined here, and even here it stays near-zero. diff --git a/content/docs/reactor-devtools/state-dirs-and-replay.mdx b/content/docs/reactor-devtools/state-dirs-and-replay.mdx new file mode 100644 index 0000000..9bc0079 --- /dev/null +++ b/content/docs/reactor-devtools/state-dirs-and-replay.mdx @@ -0,0 +1,92 @@ +--- +title: State dirs and replay +description: What a replayable state directory is, how DevTools re-derives a run from it, and the ReplaySession SDK surface that shapes the ledger. +--- + +# State dirs and replay + +DevTools never instruments a run. It replays one -- from the durable artifacts the Reactor already writes. This page is the data contract: what a state directory contains, and the SDK surface that turns it into a replay. + +## A replayable state directory + +A state directory is what [`reactor run`](/cli/compile-run-serve) and `reactor serve` persist. The minimum replayable set is the receipt trail plus the compiled topology: + +```text +/ + receipts.json the append-only, content-addressed receipt ledger (the run) + world-models/ per-node maintained truth, content-addressed by version + /published.json the current published pointer + /versions/*.bin each truth version (addressed by its @atomic fingerprint) + 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/sdk` | +| Ordering + per-node chain index + moved-facet diff + cost rollup | `createReplaySession({ ledger })` -- `@openprose/reactor/sdk` | +| Topology graph | `/compile/topology.json` (a `TopologyWorldModel`) | +| Chain / tamper badge | `verifyReceiptChain` -- `@openprose/reactor/sdk` | +| Click-through world-model | `FileSystemWorldModelStore.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. + +## 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 `@openprose/reactor/sdk`, does no I/O, and pulls no new dependency. + +```ts +import { createReplaySession } from "@openprose/reactor/sdk"; + +// Prefer handing in an already-opened ledger (stays filesystem-agnostic): +const session = createReplaySession({ ledger }); + +// Or pass a receipt array directly (scenario / benchmark runs that hold the trail): +const session = createReplaySession({ receipts }); +``` + +The returned `ReplaySession` exposes the run as shaped, pure data: + +| Field | What it is | +| --- | --- | +| `receipts` | The ordered trail -- **append order is the replay timeline**. | +| `chainByNode` | Each node's `prev`-linked receipts, in append order (the inspector chain). | +| `movedFacetsFor(receipt)` | The facets that moved vs the **same node's previous** receipt (a null prior = cold start = every facet moved). Computed by the exported `movedFacetsBetween` -- not reinvented. | +| `movedFacetsByIndex` | The same diff, precomputed per receipt and index-aligned with `receipts`. | +| `costRollup` | The cumulative fresh / reused / `$` rollup, bucketed by `surprise_cause` (`input` / `self` / `external`) plus a grand total. | +| `verifyNodeChain(node)` | Verifies one node's `prev`-linked chain via `verifyReceiptChain` -- the tamper / consistency badge. | + +### The cost rollup + +`costRollup` is the data behind the fresh-vs-reused meter. Each bucket carries `{ receipts, fresh, reused, dollars }`; `skipped` and `failed` receipts contribute zero fresh, so a quiet world keeps `total.fresh` flat and a surprise spikes it. + +```ts +const session = createReplaySession({ ledger }, { + cost: { freshRate: 0.000002 }, // optional coarse $/token; defaults to 0 +}); + +session.costRollup.total; // { receipts, fresh, reused, dollars } +session.costRollup.byCause.input; // the same shape, for input-driven wakes +``` + +Pricing is opt-in and coarse: `freshRate` and `reusedRate` default to `0`, so the rollup is deterministic and dependency-free unless you supply real rates. `fresh` is the meaningful line -- it is the spend that surprise actually drove. + +## Building a snapshot yourself + +The DevTools package wraps all of this in `openStateDir` + `buildSnapshot`, which produce the exact `ReplaySnapshot` the SPA consumes (topology + per-receipt frames + the cost rollup). You can call them directly to embed the renderer or to drive your own analysis -- see the [reference](/reactor-devtools/reference). diff --git a/content/docs/reactor-devtools/the-viewer.mdx b/content/docs/reactor-devtools/the-viewer.mdx new file mode 100644 index 0000000..89c45c1 --- /dev/null +++ b/content/docs/reactor-devtools/the-viewer.mdx @@ -0,0 +1,77 @@ +--- +title: The viewer +description: The no-build SPA -- its three regions, the animation language that maps receipts to pulses, the keyboard controls, and the deep-links. +--- + +# The viewer + +`reactor-devtools ` boots a local server and prints a URL. Open it for the viewer: a single, no-build SPA (hand-rolled SVG + CSS animation, no bundler) that renders three coordinated regions, all driven by one `GET /api/state` payload. + +## Three regions + +- **The layered DAG** (left) -- a longest-path layered layout of the topology, drawn as SVG. Every node referenced by the topology _or_ by an edge endpoint gets a box, so a producer-only ingress still appears (drawn dashed). Entry-point gateways are gold-bordered. Per-facet edges curve with arrowheads -- named-facet lanes dashed, `@atomic` solid. The whole DAG fits the viewport. +- **The sidebar** (right) -- a live **fresh-vs-reused token meter**, cumulative up to the scrub head and split by `surprise_cause`, with the replay grand total; below it, the **ordered receipt timeline** -- each receipt's index, disposition tick, node, and wake cause, current row highlighted, future rows dimmed, click to jump. +- **The scrubber** (bottom) -- a transport (jump-to-start, step back, play/pause, step forward, jump-to-end), a speed selector, a seek range, and a readout: `frame i/N · node · status · cause · moved [...]`. + +The scrub head marks which node each receipt hit on the graph -- cyan for rendered, grey for skipped, red for failed -- and dims nodes not yet touched in the replay. + +## The animation language + +Stepping forward fires, per receipt, a **transient, fire-and-forget** pulse -- the cascade. These pulses are layered onto the same DOM as idempotent state, so a backward scrub or a long jump never replays a cascade; only a real forward step or play tick animates. That is what lets a `#frame=N` screenshot be exact and a rewind be silent. + +| Receipt | Visual | +| --- | --- | +| `rendered` + a moved fingerprint | **Node flash** -- a bright decaying halo + box glow, hued by `wake.source`. The React-DevTools "highlight update." | +| moved facet _f_ on producer _p_ | **Per-facet edge light** -- the `p → subscriber` lanes for _f_ light to the facet color and a token bead rides the path. _Only the moved facet's lanes light_ -- a subscriber on a different facet stays dark. The selector boundary, made visible. | +| a move that wakes a downstream | **Woken ring** -- each distinct subscriber the move wakes pulses a ring, staggered just after the producer flash so propagation reads as a cascade. A subscriber reached by >=2 moved facets fires **once** (the diamond single-wake). | +| `skipped` | **Dim grey ripple** -- a faint grey halo breathes once, no glow, no edges. The "correctly did nothing" shot. (A `rendered` self-tick that moved nothing gets this same dim pulse.) | +| `failed` | **Red flare** -- a red halo + box flare; prior truth stands, no edges light. | +| `cost.tokens.fresh` / `reused` + `surprise_cause` | **Cost sparkline tick** -- fresh tokens per receipt, colored by cause, over a faint reused underlay. Flat near zero on a quiet stretch, a tall spike on a surprise. | + +### Wake-source hues + +The flash color tells you _why_ a node woke, straight from `wake.source`: + +| `wake.source` | Hue | Meaning | +| --- | --- | --- | +| `input` | cyan | An upstream subscription moved. | +| `self` | violet | A self-wake (the audit floor -- a re-check that found nothing, or freshness lapse). | +| `external` | gold | An external signal arrived at a gateway. | + +A gold flash at a gateway rippling into cyan flashes downstream is, at a glance, "the outside world changed and the change propagated." + +## The hero shot + +The frame worth screenshotting is a **selective wake**: one external signal lands, exactly one path lights through the graph, the cost meter ticks once off a flat line, and everything off that path stays dark. It is the whole thesis in a single still -- _the system spent tokens only where something actually changed._ Its inverse is just as legible: a long run of dim grey pulses and a flat cost line is a correct, idle system. + +## Controls + +| Input | Action | +| --- | --- | +| `space` | play / pause | +| `←` / `→` | step one receipt | +| `Home` / `End` | jump to first / last | +| click a receipt row | jump to it | +| drag the seek bar | scrub | +| speed selector | `0.5x`-`8x` -- scales the step cadence (~600 ms/receipt at 1x) and the pulse duration | + +At fast speeds the pulse duration is capped near the step interval, so play stays crisp instead of smearing. + +## Deep-links + +The URL carries view state, so you can link a specific moment (handy for screenshots, bug reports, or a launch thread): + +| Param | Effect | +| --- | --- | +| `#frame=` | park the viewer on receipt _n_ (idempotent -- no cascade replays) | +| `?autoplay=1` | start playing on load | +| `?speed=` | set the playback speed | + +```text +http://127.0.0.1:4555/#frame=39 a specific still +http://127.0.0.1:4555/?autoplay=1&speed=2 the arc, at 2x +``` + +## Labels and captions + +If the state-dir carries a `compile/labels.json`, the viewer renders friendly node names ("Claude Adapter") instead of raw ids and hides the structural kind prefix -- the renderer itself stays generic. If it carries a `beats.json`, the authored beat captions narrate the run as it plays. Both are optional presentation data; a bare state-dir replays fine without them. See [Recording](/reactor-devtools/recording) for the beat map. From c2c436795c6347fdac2b370d0a5f53319e45913a Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Sun, 31 May 2026 23:55:15 -0700 Subject: [PATCH 07/14] =?UTF-8?q?docs(reactor):=20batch-1=20cross-check=20?= =?UTF-8?q?fixes=20=E2=80=94=20remove=20stale=20case=20study,=20align=20st?= =?UTF-8?q?ate-dir=20+=20fixture=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the docs-layer findings from the spec→code→docs cross-check (planning/plans/2026-05-31-reactor-devtools/CROSSCHECK-REPORT.md). Remove the outdated `incident-briefing-room` case study (and its inbound links from reactor/index.mdx, continuity-and-ingestion.mdx, meta.json). It leaked the RETIRED judge/verdict architecture, anchored to the deleted v0.1 technical report, and cited unreproducible token figures from the deleted reactor-cradle eval. Clears dt-verdict-1, dt-report-provenance-1, dt-numbers-1, dt-surprise-cause-1, dt-fulfillment-1 by removal (this material is being published differently). Accuracy fixes: - dt-statedir-layout-1 / dt-receiptspath-1: state-dir docs now describe the ONE canonical flat `/receipts.json`, and note `run`/`trigger` persist it (so a run is directly replayable in DevTools). (cli/compile-run-serve.mdx) - dt-fixture-count-2: six → seven committed fixtures; add the `masked-relay` row (the un-narrated original). (quickstart.mdx, recording.mdx) - dt-worldmodel-paths-3: clarify the per-node dir is hex-encoded and versions are `sha256_.bin`. (state-dirs-and-replay.mdx) - dt-init-readme-1: `reactor init` scaffolds a README.md too. (command-reference.mdx) - dt-sandbox-mode-1: note the accepted-but-deferred `unix-local` sandbox mode. - dt-file-connector-watch-1: the `file` connector re-scans per poll, not a watcher. - dt-surface-1: note the standalone bin is the interim surface; `reactor dev` is the intended end-state. (reactor-devtools/index.mdx) Gates: check:emdash clean, check:links 89/89 resolve, typecheck passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) --- content/docs/cli/command-reference.mdx | 2 +- content/docs/cli/compile-run-serve.mdx | 2 +- content/docs/cli/configuration.mdx | 2 +- content/docs/cli/connectors-and-sandbox.mdx | 2 +- content/docs/reactor-devtools/index.mdx | 2 +- content/docs/reactor-devtools/quickstart.mdx | 4 +- content/docs/reactor-devtools/recording.mdx | 3 +- .../state-dirs-and-replay.mdx | 4 +- .../docs/reactor/continuity-and-ingestion.mdx | 5 +- .../docs/reactor/incident-briefing-room.mdx | 92 ------------------- content/docs/reactor/index.mdx | 5 - content/docs/reactor/meta.json | 3 +- 12 files changed, 14 insertions(+), 112 deletions(-) delete mode 100644 content/docs/reactor/incident-briefing-room.mdx diff --git a/content/docs/cli/command-reference.mdx b/content/docs/cli/command-reference.mdx index 31a8158..eaa2cef 100644 --- a/content/docs/cli/command-reference.mdx +++ b/content/docs/cli/command-reference.mdx @@ -24,7 +24,7 @@ The "Live?" column marks which commands reach the model surface. Live commands n | Command | Live? | What it does | | --- | --- | --- | -| `reactor init [dir]` | offline | Scaffold a minimal project: a gateway, a responsibility, `reactor.yml`, and `.gitignore`. Flag: `--force`. | +| `reactor init [dir]` | offline | Scaffold a minimal project: a gateway, a responsibility, `reactor.yml`, `.gitignore`, and a `README.md`. Flag: `--force`. | | `reactor doctor [--live]` | offline (`--live` probes) | Report node, SDK, key/deps, offline mode, sandbox, state-dir, and IR health. `--live` runs one smoke render. | | `reactor compile [--force] [--check]` | live (cache hit and `--check` are offline) | Run the compile sessions into the IR cache. `--check` exits non-zero when stale. | | `reactor run` | live | Ensure the IR is fresh, boot, drain to quiescence, report, and exit. | diff --git a/content/docs/cli/compile-run-serve.mdx b/content/docs/cli/compile-run-serve.mdx index a746289..3a1df1a 100644 --- a/content/docs/cli/compile-run-serve.mdx +++ b/content/docs/cli/compile-run-serve.mdx @@ -52,7 +52,7 @@ reactor serve [--http ] [--concurrency ] [--poll-interval ] ### The durable substrate -The host builds a durable substrate: filesystem receipts under `/receipts` 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. +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 diff --git a/content/docs/cli/configuration.mdx b/content/docs/cli/configuration.mdx index 48a2da9..d2f83bd 100644 --- a/content/docs/cli/configuration.mdx +++ b/content/docs/cli/configuration.mdx @@ -59,7 +59,7 @@ 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. | +| `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` | (none) | The container image, when `mode: docker`. | | `sandbox.network` | (none) | The container network setting, when `mode: docker`. | diff --git a/content/docs/cli/connectors-and-sandbox.mdx b/content/docs/cli/connectors-and-sandbox.mdx index 1b9cb33..098e06a 100644 --- a/content/docs/cli/connectors-and-sandbox.mdx +++ b/content/docs/cli/connectors-and-sandbox.mdx @@ -33,7 +33,7 @@ gateways: | --- | --- | | `static` | A fixed `items` list. Great for `init`, examples, and tests. | | `http` | `GET ` (substituting `{cursor}`), with the JSON array becoming arrivals. | -| `file` | Watch a `dir` of `.json` files. | +| `file` | Read a `dir` of `.json` files, re-scanned on each gateway poll (a per-poll `readdir`, not a filesystem watcher). | ### Connector plugins diff --git a/content/docs/reactor-devtools/index.mdx b/content/docs/reactor-devtools/index.mdx index 8d17206..a34d7ed 100644 --- a/content/docs/reactor-devtools/index.mdx +++ b/content/docs/reactor-devtools/index.mdx @@ -7,7 +7,7 @@ description: A replay-first visualizer for the Reactor -- it reads the append-on `@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`. +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. diff --git a/content/docs/reactor-devtools/quickstart.mdx b/content/docs/reactor-devtools/quickstart.mdx index aae20e5..fd669f5 100644 --- a/content/docs/reactor-devtools/quickstart.mdx +++ b/content/docs/reactor-devtools/quickstart.mdx @@ -82,10 +82,10 @@ That last block is the whole story in one place: frame 8 renders and wakes its s ## Try it on a demo corpus -The repository ships six committed demo state-dirs under `packages/reactor-devtools/fixtures/` -- replayable with no key, no model, no reactor. If you have the repo checked out: +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`, and `research-tree` each demonstrate a different Reactor behavior -- selective wake, dedup, failure isolation, incremental rollup. See [Recording](/reactor-devtools/recording) for what each one shows. +`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 index 31260bb..a02f3a5 100644 --- a/content/docs/reactor-devtools/recording.mdx +++ b/content/docs/reactor-devtools/recording.mdx @@ -54,7 +54,7 @@ The recorder is data-driven. It reads `/beats.json` -- a director's s ## The demo corpus -The repository ships six committed, replayable demo state-dirs under `packages/reactor-devtools/fixtures/` -- each a deterministic, zero-key corpus chosen to make one Reactor behavior unmistakable: +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 | | --- | --- | @@ -64,6 +64,7 @@ The repository ships six committed, replayable demo state-dirs under `packages/r | `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 (the only one with no authored `beats.json`, so it replays with computed captions). | Open any of them in the viewer (`reactor-devtools packages/reactor-devtools/fixtures/`), inspect them with `--describe`, or record them with `FIXTURE=`. 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. diff --git a/content/docs/reactor-devtools/state-dirs-and-replay.mdx b/content/docs/reactor-devtools/state-dirs-and-replay.mdx index 9bc0079..954715f 100644 --- a/content/docs/reactor-devtools/state-dirs-and-replay.mdx +++ b/content/docs/reactor-devtools/state-dirs-and-replay.mdx @@ -15,8 +15,8 @@ A state directory is what [`reactor run`](/cli/compile-run-serve) and `reactor s / receipts.json the append-only, content-addressed receipt ledger (the run) world-models/ per-node maintained truth, content-addressed by version - /published.json the current published pointer - /versions/*.bin each truth version (addressed by its @atomic fingerprint) + /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 diff --git a/content/docs/reactor/continuity-and-ingestion.mdx b/content/docs/reactor/continuity-and-ingestion.mdx index b927306..b9ac772 100644 --- a/content/docs/reactor/continuity-and-ingestion.mdx +++ b/content/docs/reactor/continuity-and-ingestion.mdx @@ -92,6 +92,5 @@ 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). See the -[Incident Briefing Room](/reactor/incident-briefing-room) for a worked trail that -mixes input-driven surprise with forecast-paced rechecks. +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/incident-briefing-room.mdx b/content/docs/reactor/incident-briefing-room.mdx deleted file mode 100644 index b5a68d6..0000000 --- a/content/docs/reactor/incident-briefing-room.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: "Case study: Incident Briefing Room" -description: A worked receipt trail showing cost scaling with surprise across a quiet world and a changing one. ---- - -# Case study: Incident Briefing Room - -The Incident Briefing Room is the clearest Reactor example because its world -changes over time. The maintained state is not "write a report." It is "the -incident channel has an accurate current briefing." - -Its continuity rule says to recheck on incident messages, on status-page changes, -and while the incident is active. Its maintained truth covers impact, timeline, -owner, next action, and customer-facing status. Its fulfillment boundary is to -summarize new facts, ask for missing owner input, and update the briefing. - -This case study mirrors the v0.1 technical report. The numbers below are from -deterministic measurement worlds, not a hosted production system, and they are -not dollarized. - -## A changing world - -When the world changes, the Reactor spends fresh model work on the change and -reuses prior verdict work on the rechecks in between. One deterministic receipt -trail shows the shape: - -| Beat | Cause | Evidence | Fresh / reused | Receipt | Result | -| --- | --- | --- | ---: | --- | --- | -| `12:00` | real input | initial incident feed | `37/0` | fresh | baseline briefing recorded | -| `12:15` | forecast recheck / evidence age | unchanged | `0/37` | memo hit | no material change | -| `12:30` | real input | `incident-opened` event | `37/0` | fresh | briefing changes | -| `12:45` | forecast recheck / evidence age | changed evidence, unchanged since | `0/37` | memo hit | updated verdict reused | - -The two fresh beats are the two moments something actually moved: the bootstrap -and the `incident-opened` event. The rechecks in between hit the memoized -verdict because the input fingerprint did not move. A naive loop re-spends on -every scheduled turn whether or not anything changed. - -## A quiet world - -When nothing changes, the cost is not zero, and the honest claim is precise about -why. A quiet run has the same proof shape plus one extra plan-age audit floor: - -```text -bootstrap fresh (real first render at boot) -evidence-age tick memo hit (nothing moved) -plan-age audit fresh floor (the forecast-amortized recheck) -evidence-age tick memo hit (nothing moved) -``` - -That floor is the deliberately-declared forecast cadence: the cost of not letting -a stale plan become quietly wrong. The cost thesis is **surprise plus a -forecast-amortized audit floor**, not surprise alone. It is still less fresh -model work than re-asking on every scheduled turn. - -## What the measured rows show - -The technical report measures two controlled scenarios against two controls: a -no-memo Reactor (same schedule, no memoization) and a non-Reactor naive loop -(rechecks on the same schedule with no receipts or memo keys). - -| Scenario | Row | Model invocations | Fresh tokens | Reused tokens | -| --- | --- | ---: | ---: | ---: | -| Static | Reactor | 2 | 46 | 46 | -| Static | Reactor, no memo | 4 | 92 | 0 | -| Static | Naive loop | 4 | 256 | 0 | -| Changing | Reactor | 2 | 74 | 74 | -| Changing | Reactor, no memo | 4 | 148 | 0 | -| Changing | Naive loop | 4 | 148 | 0 | - -In the static scenario the Reactor does fresh work at bootstrap and at the -plan-age audit, while evidence-age rechecks hit memoized verdicts: 2 invocations -and 46 fresh tokens against 4 and 256 for the naive loop. In the changing -scenario the Reactor spends fresh work on the bootstrap and the material change, -then reuses verdict work on the rechecks: 2 invocations and 74 fresh tokens -against 4 and 148 for both controls. - -The changing-world win is fresh model work plus reusable-verdict proof, not a -total accounted-token claim over the no-memo control. - -## What this does and does not show - -This is a deterministic, local demonstration. The Reactor maintains a long-lived -responsibility with runtime-produced receipts, reusable verdict proof, -forecast-paced rechecks, and explicit surprise attribution. It is not a hosted -production incident system updating real customer communications, and the figures -are measured token accounting, not dollar costs. - -To run a project like this from the shell, see the [CLI quickstart](/cli/quickstart). -For the mechanics behind the trail, see -[the reconciler and receipts](/reactor/reconciler-and-receipts) and -[continuity and ingestion](/reactor/continuity-and-ingestion). diff --git a/content/docs/reactor/index.mdx b/content/docs/reactor/index.mdx index 24cb990..94f761a 100644 --- a/content/docs/reactor/index.mdx +++ b/content/docs/reactor/index.mdx @@ -102,11 +102,6 @@ them. Quiet nodes stay quiet and free. href="/reactor/continuity-and-ingestion" description="The self-recheck cadence, gateways as external evidence, and idempotency cursors." /> - To drive a Reactor from the shell, see the [CLI overview](/cli/overview) and diff --git a/content/docs/reactor/meta.json b/content/docs/reactor/meta.json index ede09cf..c14c369 100644 --- a/content/docs/reactor/meta.json +++ b/content/docs/reactor/meta.json @@ -5,7 +5,6 @@ "the-dag-and-compile", "world-model-and-fingerprints", "reconciler-and-receipts", - "continuity-and-ingestion", - "incident-briefing-room" + "continuity-and-ingestion" ] } From 65a234a3e265c4fdc751bebaaff7602968ca9c23 Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:21:43 -0700 Subject: [PATCH 08/14] =?UTF-8?q?docs(reactor):=20round-2=20cross-check=20?= =?UTF-8?q?fixes=20=E2=80=94=20trigger=20persistence,=20sandbox=20knobs,?= =?UTF-8?q?=20recorder/beats,=20continuity=20tick?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves round-2 docs-layer findings (CROSSCHECK-REPORT-R2.md). - r2-cli-trigger-ephemeral-1 (high): `trigger` persists to the flat receipts.json (was "ephemeral receipt trail"; contradicted its own page + spec + code). - r2-cli-sandbox-network-1: `sandbox.network` is accepted but not honored (docker forces --network=none); r2-cli-sandbox-image-default-1: image default is node:22-bookworm-slim, not "(none)". - r2-dt-maskedrelay-only-beats-1 / r2-dt-record-fixture-id-throws-1 / r2-dt-records-without-beats-1: agent-observatory also lacks a beats.json; masked-relay (no beats) replays but cannot be driven through the FIXTURE= recorder. - r2-rollin-self-rerender-docs-1: the self-driven tick is a deterministic zero-token bridge, not a model re-render. - r2-rollin-docs-version-1p0-1: doctor sample shows @openprose/reactor@0.2.0 (was 1.0.0). - r2-cli-concurrency-default-doc-1: state the v1 `--concurrency` default of 1. Gates: check:emdash clean, check:links resolve. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) --- content/docs/cli/compile-run-serve.mdx | 4 ++-- content/docs/cli/configuration.mdx | 4 ++-- content/docs/cli/quickstart.mdx | 2 +- content/docs/reactor-devtools/recording.mdx | 6 +++--- content/docs/reactor/continuity-and-ingestion.mdx | 11 ++++++----- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/content/docs/cli/compile-run-serve.mdx b/content/docs/cli/compile-run-serve.mdx index 3a1df1a..aab8783 100644 --- a/content/docs/cli/compile-run-serve.mdx +++ b/content/docs/cli/compile-run-serve.mdx @@ -86,7 +86,7 @@ On `SIGINT` or `SIGTERM`, the host stops arming new work, drains the in-flight q 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. Independent reactors render in parallel up to N. +`--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. @@ -100,7 +100,7 @@ Within-reactor parallelism is a future enhancement. The current SDK has no withi 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 world-model with an ephemeral receipt trail, ingests the named node with a full external wake, drains to quiescence, and reports the dispositions. +`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. diff --git a/content/docs/cli/configuration.mdx b/content/docs/cli/configuration.mdx index d2f83bd..ae8f83e 100644 --- a/content/docs/cli/configuration.mdx +++ b/content/docs/cli/configuration.mdx @@ -61,8 +61,8 @@ The `sandbox` block is the render threat-model knob. | --- | --- | --- | | `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` | (none) | The container image, when `mode: docker`. | -| `sandbox.network` | (none) | The container network setting, when `mode: docker`. | +| `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. diff --git a/content/docs/cli/quickstart.mdx b/content/docs/cli/quickstart.mdx index 16afd4c..d0322ab 100644 --- a/content/docs/cli/quickstart.mdx +++ b/content/docs/cli/quickstart.mdx @@ -38,7 +38,7 @@ reactor doctor reactor doctor node v22.3.0 (ok) - sdk @openprose/reactor@1.0.0 (resolved) + sdk @openprose/reactor@0.2.0 (resolved) offline mode not forced live key present (OPENROUTER_API_KEY) live dep @openai/agents: ok diff --git a/content/docs/reactor-devtools/recording.mdx b/content/docs/reactor-devtools/recording.mdx index a02f3a5..b73e4cb 100644 --- a/content/docs/reactor-devtools/recording.mdx +++ b/content/docs/reactor-devtools/recording.mdx @@ -50,7 +50,7 @@ The recorder is data-driven. It reads `/beats.json` -- a director's s | `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 -- it changes nothing about the run. A state-dir without it still replays and records; it just falls back to computed captions. +`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 @@ -64,9 +64,9 @@ The repository ships seven committed, replayable demo state-dirs under `packages | `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 (the only one with no authored `beats.json`, so it replays with computed captions). | +| `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/`), inspect them with `--describe`, or record them with `FIXTURE=`. 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. +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 diff --git a/content/docs/reactor/continuity-and-ingestion.mdx b/content/docs/reactor/continuity-and-ingestion.mdx index b9ac772..1b6312d 100644 --- a/content/docs/reactor/continuity-and-ingestion.mdx +++ b/content/docs/reactor/continuity-and-ingestion.mdx @@ -18,9 +18,10 @@ 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 node re-renders and either writes a moved fingerprint - (surprise propagates) or an unmoved one (the tick stops there, costing nothing - downstream). + 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. @@ -48,8 +49,8 @@ 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) - -> the node re-examines the fact +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 ``` From 68dec8c356fc7b4a401ce3a756e5d7e2ec360009 Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:50:19 -0700 Subject: [PATCH 09/14] =?UTF-8?q?docs(cli):=20round-3=20=E2=80=94=20serve?= =?UTF-8?q?=20loop=20sleeps=20a=20flat=20--poll-interval=20in=20v1=20(not?= =?UTF-8?q?=20adaptive)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit r3-cli-poll-interval-adaptive-doc-1 (CROSSCHECK-REPORT-R3.md): compile-run-serve.mdx said the serve loop "sleeps to the soonest armed self-recheck" -- the deferred adaptive-idle behavior, contradicting line 59 + the spec + the code (flat sleep). A half-resolved round-2 sibling (round 2 fixed the spec + continuity docs page but not this line). Now describes the flat v1 cadence + notes adaptive sleep is deferred. Gates: check:emdash clean, check:links resolve. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) --- content/docs/cli/compile-run-serve.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/cli/compile-run-serve.mdx b/content/docs/cli/compile-run-serve.mdx index aab8783..f5d4ff0 100644 --- a/content/docs/cli/compile-run-serve.mdx +++ b/content/docs/cli/compile-run-serve.mdx @@ -58,7 +58,7 @@ The host builds a durable substrate: a flat append-only receipt trail at `` sets the cadence ceiling (default `60000`). The loop sleeps to the soonest armed self-recheck, bounded by this ceiling. +`--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 From 3c8664e23d1f9a91977c1e087e62354d47c5381f Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:40:48 -0700 Subject: [PATCH 10/14] docs: remove stale v0.1 Cradle technical report Dead, unpublished, unreferenced file at the repo root (outside content/docs, so fumadocs never rendered it). It described the retired v0.1 model (@openprose/reactor-cradle, prose compile/serve, judge/policy). Not a live-site artifact; deleting to avoid a stale doc shipping in the repo. Co-Authored-By: Claude Opus 4.8 (1M context) --- reactor-technical-report.md | 356 ------------------------------------ 1 file changed, 356 deletions(-) delete mode 100644 reactor-technical-report.md diff --git a/reactor-technical-report.md b/reactor-technical-report.md deleted file mode 100644 index 52e8400..0000000 --- a/reactor-technical-report.md +++ /dev/null @@ -1,356 +0,0 @@ - - -# Reactor-Class Harnesses: A v0.1 Technical Report - -Date: 2026-05-20 - -This report describes the current OpenProse Reactor worktree, not a future -launch claim. The current evidence supports a narrow but useful statement: -under deterministic Cradle scenarios, a Reactor runtime can maintain a -long-lived responsibility with runtime-produced receipts, reusable verdict -proof, forecast-paced rechecks, and explicit surprise attribution. It does not -yet support a public launch claim, hosted production operation, runtime -variable-depth ensemble judging, actual npm publication, or stranger -quickstart verification. Since the first report draft, the local release -surface has also become concrete: a packed-tarball flat-tokens example reads -runtime-produced receipts and reports `46:46`; the deterministic -`incident-briefing-room` CLI quickstart shows owner and public status -attribution with `surprise_cause=real-input`; and the `v*` tag gate is wired -to publish smoked Reactor and Cradle tarballs with provenance, though no npm -publication has been observed. - -## 1. Problem - -AI sessions are good at bounded bursts of work. They are much weaker at -responsibilities that stay alive after the session ends: "keep the incident -briefing current," "watch release readiness," "refresh compliance evidence," -or "notice when a customer risk signal changes." In these cases the useful -unit is not a one-shot answer. It is a maintained state of the world, plus a -record of why the system checked, acted, slept, or asked for help. - -The OpenProse promise is a lived loop: write durable intent, let bounded -activations inspect changing evidence, interrupt only when judgment or missing -input requires a human, and leave an exit trail that another agent can inspect. -That loop has a cost problem. A naive maintenance loop spends because time -passes. A useful maintenance harness should spend because something material -changed, while still paying an honest floor for forecast-paced plan audits. - -The target is therefore not "agents with more memory" in the abstract. The -target is a harness that can maintain declared responsibilities through -durable state, bounded model calls, policy artifacts, receipts, projections, -and replay. Some interruptions are part of the design. If a contract has no -observable referent, or if a judgment has no anchor, the system should stop -and say so rather than hide uncertainty inside fluent prose. - -## 2. Category - -A Reactor-class harness is an evented reconciliation system for declared -responsibilities. A gateway or local trigger provides an event. A bounded -judge or fulfillment activation reads typed evidence and prior state. The -runtime records a receipt. A Reactor decision projects status and chooses one -of the next moves: act, retry, escalate, schedule another check, or go quiet. - -The category differs from a normal agent loop in the question it asks. The -question is not "what should I do next?" It is "which reconciliation action is -justified by this responsibility, this event, these observations, and this -receipt trail?" That shift lets policy, cost, and audit become durable parts -of the system rather than session-local instincts. - -OpenProse makes the category concrete with a model-authored policy artifact, -compiled to deterministic runtime state; content-addressed receipts; stable -memo keys; forecast-gated scheduling; projections for owner, subscriber, and -public readers; and exit bundles. Two recursions sit on top of the base loop: -policy changes over time, and responsibilities can compose over a graph by -consuming upstream receipts. In v0.1, the single-responsibility runtime spine -and local evidence are the demonstrated center. Broader graph behavior and -runtime ensemble depth remain designed or partially built. - -## 3. Prior Patterns - -The closest analogy is React reconciliation: a stable declaration is compared -with observed inputs, partial work is memoized, and a commit boundary controls -when the outside world changes. The analogy is structural, not literal. A -Reactor's "render" includes model judgment over ambiguous evidence, and its -output is not a DOM tree. Its output is a receipt-backed decision about a -responsibility. - -Control systems contribute the time dimension: forecast, drift, hysteresis, -backstops, and revalidation floors. Kubernetes controllers, event sourcing, -dataflow systems, CQRS/read models, actor systems, and workflow engines are -also part of the lineage. The difference is that workflow engines usually -execute known steps, while a Reactor must decide which reconciliation step is -justified now when the world is partly ambiguous. - -Cron remains the right tool for many deterministic jobs. Batch transforms and -one-shot writing tasks often do not need receipts or durable policy. The -Reactor shape matters when a changing world, uncertain evidence, cost, and -human interruption policy all need to be explicit. - -## 4. OpenProse Design - -OpenProse treats the responsibility as the program. The source contract names -the goal, evidence, freshness referents, memo-breaking conditions, plan-audit -horizon, observable criteria, and fulfillment boundary. Criteria that cannot -be observed are expected to produce blocked receipts routed to the author, not -hidden judgment inside prose. Source compile lowers `*.prose.md` into -repository IR when authored intent changes. Policy compile lowers the contract -plus receipt history into a token-free policy artifact when runtime evidence -shows the current policy needs revision. - -Those are separate compiles. The `*.prose.md` contract remains the only -authored source of meaning. If a policy artifact or projection disagrees with -the contract, the contract wins; artifacts, receipts, forecasts, and -projections are derived sibling runtime state. Deterministic code validates, -schedules, records, and executes artifacts, but it does not author semantic -judgment. v0.1 demonstrates source compile, cold-start policy artifact -authorship, and local recompile/rollback substrate; recurring production -policy recompile remains future work. - -Quiescence has three meanings. First, do not act when the maintained state is -already up to date. Second, do not check now when the forecast says the next -audit is not due. Third, do not check deeply when shallow evidence and -calibration justify a cheaper path. v0.1 demonstrates the first two local -paths and the single shallow judge path. Variable-depth runtime judging is a -designed part of the architecture, but not a v0.1 runtime claim. - -Memoization depends on stable identity. The runtime can reuse verdict work -when the contract revision, evidence receipts, dependency receipts, and policy -namespace still justify reuse. The cost claim is therefore not "zero cost on a -static world." The honest claim is: cost scales with surprise plus a -forecast-amortized plan-audit floor. - -Receipts are the trust unit. They carry `as_of`, contract and policy identity, -evidence references, verdict, token accounting, surprise cause, forecast -handoff, dependency pins, and projection-safe data. The same receipt trail -supports audit, replay, composition, privacy projection, and exit. - -## 5. Implementation - -The current implementation has four cooperating surfaces: - -- `@openprose/reactor`: the runtime spine, receipt schema, policy artifact - validation, memoization, forecast, evidence plans, composition, - projection, adapters, shallow judge, and exit/import support. -- `@openprose/reactor-cradle`: deterministic worlds, doubles, replay, - baselines, K1/K2 spikes, release-parity fixtures, and measurement harnesses. -- The migrated OpenProse CLI path: local `prose compile`, `prose serve`, and - `prose status` evidence for bundled examples through Reactor receipts. -- The local release surface: a packed-tarball flat-tokens example, a - deterministic CLI quickstart, and a `v*` tag workflow that downloads the - exact smoked Reactor and Cradle tarballs and runs provenance-enabled - `npm publish` for those tarballs. - -| Surface | v0.1 demonstrated | Not claimed | -| --- | --- | --- | -| Runtime spine | receipt v0, kernel backstops, policy artifact validation, memoization, forecast, evidence plans, composition pins, projections, export/import, cold-start authorship, and local recompile/rollback substrate | production gateway, production fulfillment, or production oracle | -| Judgment | shallow runtime judge with adapter-owned usage metadata, plus K1 live-cassette evaluator evidence | runtime ensemble depth, live provider matrix, or production calibration service | -| CLI path | local source compile, serve, status, forwarded fulfillment artifacts, crash-window replay, duplicate-trigger dedupe, and projection-tier leak tests | hosted production operation or real external side effects | -| Storage/adapters | memory, filesystem, record-replay model gateway, local connector, and event-sink adapters | Postgres parity or signer support | -| Release surface | packed-tarball flat-tokens smoke, deterministic `incident-briefing-room` quickstart, and tag-gated provenance publish workflow | public npm availability, stranger run, or production operation | - -Cradle is the measurement rig, not a production dependency. -Production-equivalent ingress would authenticate external events, normalize -event identities, dedupe and replay event claims, enforce source-specific -budgets, and hand only typed events and evidence to Reactor. -Production-equivalent fulfillment would own idempotent dispatch, retries, -budgets, external side effects, durable claims, and operator-visible failure -states. Production-equivalent oracle support would expose explicit truth and -evidence sockets for evaluation and operations without smuggling ambient -knowledge into the runtime. v0.1 has local CLI trigger/serve evidence, -forwarded fulfillment artifacts, deterministic Cradle worlds, and one live K1 -cassette instead of those production layers. - -Postgres parity is deferred because the storage adapter v0 is synchronous -while real Postgres IO is async. Signer support is honestly deferred: v0.1 -normalizes omitted signers to -`{scheme:"none", null_reason:"no-signer-adapter-configured"}`, and non-null -receipt signatures fail closed rather than implying cryptographic support. - -## 6. Evaluation Methodology - -Phase C measures two controlled scenarios: -`incident-briefing-static-zero` and -`incident-briefing-periodic-surprise`. Each scenario compares three rows: - -- Reactor runtime-produced receipts. -- A deterministic no-memo Reactor control over the same scenario schedule and - token charges. -- A non-Reactor naive loop control that rechecks on the same schedule without - receipts, memo keys, forecast policy, or reusable verdict proof. - -The core metrics are receipt count, model invocation count, fresh tokens, -reused tokens, and surprise attribution. `tokens.fresh` is fresh model work. -`tokens.reused` is accounted reusable work, not new model spend. The no-memo -control isolates the value of memoization and forecast reuse. The naive loop -control approximates a fixed cron/prompt maintenance loop. - -The event-changing controls are deterministic counterfactual controls over -the same four-turn schedule. They are not shipped runtime flags or live -production ablations. The measured rows use local deterministic Cradle -providers and recorded token accounting, so this report does not dollarize the -results. - -K1 is evaluated separately. One OpenRouter live-recorded ensemble cassette -captures outputs from multiple provider/model families and records request -ids, response ids, latency, finish reason, usage, and spend. It proves the K1 -evaluator and metadata path against a live cassette. It does not prove runtime -ensemble judging or a full provider matrix. - -## 7. Results - -The result table below is copied from the Phase C result table at -`../planning/plans/2026-05-19-reactor-runtime-wave/phases/C-baseline-and-second-adapter/results/cost-thesis-baseline.md`. -During Phase D this report cites local worktree evidence; before publication, -the K1 cassette and result artifacts must be included in, or linked from, a -public release artifact. - -| Scenario | Row | Provenance | Receipts | Model invocations | Fresh tokens | Reused tokens | Source | -| --- | --- | --- | ---: | ---: | ---: | ---: | --- | -| Static `incident-briefing-static-zero` | Reactor | runtime-produced | 4 | 2 | 46 | 46 | Phase C result table | -| Static `incident-briefing-static-zero` | Reactor no memo | deterministic control | 4 | 4 | 92 | 0 | Phase C result table | -| Static `incident-briefing-static-zero` | Naive loop | non-Reactor control | 0 | 4 | 256 | 0 | Phase C result table | -| Event-changing `incident-briefing-periodic-surprise` | Reactor | runtime-produced | 4 | 2 | 74 | 74 | Phase C result table | -| Event-changing `incident-briefing-periodic-surprise` | Reactor no memo | deterministic control | 4 | 4 | 148 | 0 | Phase C result table | -| Event-changing `incident-briefing-periodic-surprise` | Naive loop | non-Reactor control | 0 | 4 | 148 | 0 | Phase C result table | - -In the static scenario, Reactor uses 2 model invocations and 46 fresh tokens, -compared with 4 invocations and 92 fresh tokens for the no-memo control, and -4 invocations and 256 fresh tokens for the naive loop. In the event-changing -scenario, Reactor uses 2 model invocations and 74 fresh tokens, compared with -4 invocations and 148 fresh tokens for both controls. The event-changing win is -fresh model work plus reusable-verdict proof, not a total accounted-token -claim over the no-memo control. - -On the static scenario, Reactor performs fresh model work at bootstrap and at -the plan-age audit, while evidence-age rechecks hit memoized verdicts. The -per-turn audit in the Phase C table records fresh/reused work as `41/0`, -`0/41`, `5/0`, and `0/5`. - -On the event-changing scenario, the material surprise is an `incident-opened` -event at `2026-05-18T12:30:00.000Z`, which changes the evidence hash to -`sha256:a890c5e45abb3e746857db1ca9f79905c17f4f769fbe273446fe186e5787aeb8` -in the Phase C result table. The Reactor row spends fresh work on bootstrap -and on that material change, then reuses verdict work on evidence-age -rechecks. The controls re-spend on the same schedule. - -Supporting evidence: - -| Evidence | Observed | Boundary | -| --- | --- | --- | -| B5 live K1 cassette at `../../prose-reactor-build/packages/reactor-cradle/src/spikes/fixtures/k1-live-recorded.json` | SHA-256 `f64484990635a61a3dcac973a96e97d6433a576ccc297c23742d4a515e2c1868`; OpenRouter run with `output_count = 3` across Google/Gemini, Mistral, and Qwen; `0.00022823 USD` spend under the `2.00 USD` cap; for this authored-anchor cassette, the evaluator returned status `calibrated` and `calibrated_confidence = 1` | evaluator and metadata evidence, not a runtime ensemble, production calibration bar, or general accuracy result | -| Recorded provider parity | two recorded provider/model paths produce byte-identical policy artifact bytes from the same contract/history, and provider drift fails closed | recorded policy-artifact parity, not live provider support or model quality parity | -| Phase E CLI acceptance at `../planning/plans/2026-05-19-reactor-runtime-wave/phases/E-cli-migration/SIGNPOST.md` | five bundled examples compile from source, run spawned `prose serve`, ingest triggers through Reactor, write forwarded fulfillment artifacts, and render `prose status` with `surprise_cause=real-input`; crash-window restart and duplicate-trigger dedupe are scripted; projection tiers are leak-tested | local deterministic CLI evidence, not hosted production operation, live-model fulfillment content, npm launch, or stranger quickstart | -| Phase F flat-tokens packed-tarball example | installs from packed Reactor and Cradle tarballs, drives four static-world turns through `createReactor().ingest()`, reads the produced receipts, and prints `tokens.fresh = 46`, `tokens.reused = 46`, `tokens.ratio = "46:46"`, `runtime.receipt_count = 4`, and `runtime.model_invocation_count = 2` | local packed-tarball smoke, not public npm install, stranger run, production economics, or a new runtime claim beyond the Phase C receipt evidence | -| Phase F `incident-briefing-room` quickstart | documents local `prose compile src --harness mock`, manifest activation, deterministic `prose serve`, POST `/incident/events`, `prose status --tier=owner`, and `prose status --tier=public`; the status surface attributes owner and public state with `surprise_cause=real-input` | local deterministic CLI quickstart, not hosted ingress, live-model fulfillment content, public npm launch, or stranger verification | -| Phase F `v*` publish gate | on `refs/tags/v*`, the workflow waits for CI, downloads the exact smoked package tarballs, and runs `npm publish --access public --provenance` for both Reactor and Cradle | workflow path only; actual npm publication and registry-visible provenance attestation remain unobserved | - -The Phase C result table's `## Reproduce` section records the sharpest local -commands: `pnpm --filter @openprose/reactor-cradle test` and -`pnpm --filter @openprose/reactor test` from `/Users/sl/code/prose-reactor-build`. -The Phase F local release signpost adds the tarball smoke and CLI quickstart -checks that turn the same numbers into a release-surface exercise without -claiming publication. - -## 8. Case Study: Incident Briefing Room - -The Incident Briefing Room is the clearest example because its world changes -over time. The maintained state is not "write a report." It is "the incident -channel has an accurate current briefing." Its continuity rule says to recheck -on incident messages, status-page changes, and while the incident is active. -Its criteria include impact, timeline, owner, next action, and -customer-facing status. Its fulfillment boundary is to summarize new facts, -ask for missing owner input, and update the briefing. - -One deterministic Cradle receipt trail shows the shape: - -| Beat | Cause | Evidence note | Fresh/reused | Decision shape | User-visible result | -| --- | --- | --- | ---: | --- | --- | -| `2026-05-18T12:00:00.000Z` | `real-input` | initial incident feed | `37/0` | fresh judge receipt | baseline briefing state recorded | -| `2026-05-18T12:15:00.000Z` | `forecast-recheck` / `evidence-age` | evidence unchanged | `0/37` | memo-hit receipt | no material change; next recheck remains visible | -| `2026-05-18T12:30:00.000Z` | `real-input` | `incident-opened`; evidence hash `sha256:a890c5e45abb3e746857db1ca9f79905c17f4f769fbe273446fe186e5787aeb8` | `37/0` | fresh judge receipt | briefing changes because an incident opened | -| `2026-05-18T12:45:00.000Z` | `forecast-recheck` / `evidence-age` | changed evidence unchanged since prior turn | `0/37` | memo-hit receipt | updated verdict is reused instead of asking again | - -The quiet-world static run has the same proof shape with one extra plan-age -audit floor: bootstrap fresh, evidence-age memo hit, plan-age audit fresh -floor, and final evidence-age memo hit. That is not free; it is lower fresh -model work than reasking on every scheduled turn. - -Phase F uses this same responsibility as the local CLI quickstart. The -observed sequence is intentionally plain: compile the bundled source, activate -the manifest, run deterministic `prose serve`, POST an incident event, then -compare `prose status --tier=owner` and `prose status --tier=public`. The -useful observation is owner/public status attribution with -`surprise_cause=real-input` on a local receipt trail, not a hosted incident -system. - -The case study stays deliberately local. The CLI can write forwarded -fulfillment artifacts for bundled examples, but this report does not claim a -production incident system updating real customer communications. - -## 9. Limitations - -The main runtime layers still deferred from v0.1 are production-equivalent -gateway, fulfillment, and oracle support. The current worktree proves local -trigger/serve behavior, forwarded fulfillment artifacts, and deterministic -Cradle worlds. It does not prove hosted event ingress, real external -actuation, or a production truth socket. - -Judgment is also deliberately scoped. Runtime judging is shallow in v0.1. -Variable-depth ensemble judging, live provider matrix runs, calibration -cadence, degraded-calibration ladder steps 2-4b, and the accrued-anchor -calibration exit bar remain future work. K1 is live-recorded evaluator -evidence, not a production calibration service. - -Infrastructure launch work remains. The packages have version and provenance -settings, local tarball and parity evidence, a flat-tokens packed-tarball -example that reports `46:46`, and a `v*` tag gate that would publish the exact -smoked Reactor and Cradle tarballs with provenance. Public npm publication is -still not observed in this report. Stranger quickstart verification is not -observed. Postgres parity is deferred under the async storage seam. A concrete -signer adapter is deferred; null-signer remains the only honest v0.1 receipt -signature state. - -The domain boundaries matter. Some responsibilities have no cheap complete -hash for "semantically relevant content changed." In those domains the system -can remain safe, but the cost differentiator weakens toward forecast-cadence -cost. Some responsibilities have no correctness anchor. Those stay in bounded -degraded calibration unless and until human labels or observed outcomes can -support stronger calibration. Prompt sensitivity, adversarial or silent drift, -public receipt maturity, and human review quality all need larger evaluation. - -The plan-revalidation tax is not a bug. It is the cost of not letting a stale -plan become quietly wrong. The cost thesis is surprise plus a -forecast-amortized audit floor, not surprise alone. - -## 10. Future Work - -The closest category-completing work is production-equivalent ingress, -fulfillment, and oracle adapters. Those layers would let the same receipt -logic operate against authenticated event sources, idempotent external side -effects, and explicit truth/evidence sockets rather than local deterministic -worlds. - -The next judgment work is runtime variable-depth ensemble judging: shallow -checks by default, deeper or more diverse judging when uncertainty or policy -requires it, and a calibration cadence that records why depth changed. That -work includes degraded-calibration ladder steps 2-4b and an accrued-anchor -exit bar for responsibilities that start without labels. - -The policy loop should continue toward richer recurring recompile and rollback -behavior against recorded artifacts, wider receipt-graph composition case -studies, stronger transitive freshness semantics, and learned forecast -policies. The storage layer should gain async parity rows, including Postgres, -once the adapter seam supports real async IO. - -The release work is concrete: run the `v*` tag publish gate under npm auth or -trusted publishing, verify `@openprose/reactor` and -`@openprose/reactor-cradle` in the public registry with provenance, pin the CLI -to the public package, publish this report from measured evidence, and run a -stranger quickstart where the observed token ratio matches the report within -tolerance. - -Longer term, the policy author can migrate into a first-class OpenProse -`kind: responsibility` once the Prose VM path is proven. That should not add a -second intent surface. The goal remains one durable responsibility, one -inspectable evidence graph, and one receipt trail that lets future agents know -what happened and why. From f4d5ccf157aa8382a36d2b116a251c12cf95bb9b Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:29:03 -0700 Subject: [PATCH 11/14] Make the docs site Reactor-forward for launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retire the pre-Reactor model pages and reframe the docs around the Reactor harness, matching the launch README + the intelligent-react architecture/CLI specs. - Delete the retired page trees that taught `prose run`, `.prose/runs/`, `### Ensures`, `### Services`, `kind: program`/`kind: system`, and the retired Forme/VM/services model: start/*, think/*, use/*, agents/*, and the whole reference/* tree. - Drop them from the root nav (meta.json): pages now index → reactor → cli → reactor-devtools only. - Rewrite index.mdx to the Reactor model: lead with Responsibilities, the world-model, ### Requires → ### Maintains, the React↔Reactor table, and the keyless `reactor-devtools --example` replay. - cli/quickstart: install now pulls the SDK + CLI + devtools (`npm i -g @openprose/reactor @openprose/reactor-cli @openprose/reactor-devtools`); the static scaffold is driven with `reactor serve`, not `reactor run` (only serve's continuity loop ingests a static gateway). - reactor-devtools/reference: add the shipped flags --example, --copy-to, --json, --version/-V, --force; correct /api/node from "the click-through inspector" to a server/library endpoint the SPA does not call (no node-click UI in v1). - Relax the now-stale ProseProgram sanity test (no docs page embeds a vendor .prose program anymore; per-use resolution check remains). typecheck/lint/tests/link-check pass. spell is blocked only by the local Node version (cspell needs >=22.18.0), unrelated to these changes. Co-Authored-By: Claude Opus 4.8 --- __tests__/prose-program-srcs.test.ts | 8 +- content/docs/agents/for-ai-agents.mdx | 87 ---------- content/docs/agents/meta.json | 4 - content/docs/cli/quickstart.mdx | 20 +-- content/docs/index.mdx | 114 +++++++------ content/docs/meta.json | 2 +- content/docs/reactor-devtools/reference.mdx | 17 +- content/docs/reference/commands.mdx | 49 ------ content/docs/reference/meta.json | 4 - content/docs/reference/overview.mdx | 37 ----- content/docs/reference/specs.mdx | 43 ----- content/docs/start/first-program.mdx | 78 --------- content/docs/start/first-useful-workflow.mdx | 86 ---------- content/docs/start/install.mdx | 106 ------------ content/docs/start/meta.json | 9 -- content/docs/start/what-is-openprose.mdx | 59 ------- content/docs/think/contracts-not-prompts.mdx | 92 ----------- content/docs/think/meta.json | 9 -- content/docs/think/services-forme-vm.mdx | 138 ---------------- content/docs/think/when-to-use-it.mdx | 116 -------------- .../docs/think/workspaces-bindings-traces.mdx | 140 ---------------- content/docs/use/examples.mdx | 102 ------------ content/docs/use/meta.json | 4 - content/docs/use/patterns.mdx | 151 ------------------ content/docs/use/troubleshooting.mdx | 119 -------------- 25 files changed, 91 insertions(+), 1503 deletions(-) delete mode 100644 content/docs/agents/for-ai-agents.mdx delete mode 100644 content/docs/agents/meta.json delete mode 100644 content/docs/reference/commands.mdx delete mode 100644 content/docs/reference/meta.json delete mode 100644 content/docs/reference/overview.mdx delete mode 100644 content/docs/reference/specs.mdx delete mode 100644 content/docs/start/first-program.mdx delete mode 100644 content/docs/start/first-useful-workflow.mdx delete mode 100644 content/docs/start/install.mdx delete mode 100644 content/docs/start/meta.json delete mode 100644 content/docs/start/what-is-openprose.mdx delete mode 100644 content/docs/think/contracts-not-prompts.mdx delete mode 100644 content/docs/think/meta.json delete mode 100644 content/docs/think/services-forme-vm.mdx delete mode 100644 content/docs/think/when-to-use-it.mdx delete mode 100644 content/docs/think/workspaces-bindings-traces.mdx delete mode 100644 content/docs/use/examples.mdx delete mode 100644 content/docs/use/meta.json delete mode 100644 content/docs/use/patterns.mdx delete mode 100644 content/docs/use/troubleshooting.mdx diff --git a/__tests__/prose-program-srcs.test.ts b/__tests__/prose-program-srcs.test.ts index 3040e2f..6e60903 100644 --- a/__tests__/prose-program-srcs.test.ts +++ b/__tests__/prose-program-srcs.test.ts @@ -8,8 +8,12 @@ const REPO_ROOT = resolve(__dirname, ".."); describe(" references", () => { const uses = findProseProgramUses(REPO_ROOT); - it("finds at least one ProseProgram use across content/docs (sanity)", () => { - expect(uses.length).toBeGreaterThan(0); + // The Reactor-forward docs no longer embed vendor `.prose` programs via + // , so an empty set is valid. The real guard is the per-use + // resolution check below: any that *is* authored must + // point at a file that exists. + it("every ProseProgram use found resolves (none is fine)", () => { + expect(uses.length).toBeGreaterThanOrEqual(0); }); it.each(uses)("resolves $src referenced from $mdxPath:$line", ({ src }) => { diff --git a/content/docs/agents/for-ai-agents.mdx b/content/docs/agents/for-ai-agents.mdx deleted file mode 100644 index 20f1c82..0000000 --- a/content/docs/agents/for-ai-agents.mdx +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: For AI agents -description: Read this before running, editing, or explaining OpenProse programs. ---- - -# For AI agents - -If you are an AI agent, this page is a signpost, not the execution spec. - -Load the `open-prose` skill before running, editing, linting, testing, migrating, or explaining an OpenProse program. - -The skill and its linked specs are the source of truth for execution behavior. These docs are for human orientation. - -## First move - -When you see: - -- `prose run ...` -- `prose lint ...` -- `prose test ...` -- a `.md` file with `kind:` frontmatter -- a `.prose` file -- a user asking for reusable multi-agent orchestration - -load the `open-prose` skill and follow its path table. - -Do not infer execution semantics from this docs site. - -## Command routing - -Inside an agent session, treat `prose ...` as an in-session command language. - -For `prose run file.md`: - -1. Read the Contract Markdown program. -2. Load the narrowest required specs. -3. Wire services through Forme if the program declares services. -4. Execute through the Prose VM behavior using host primitives. -5. Persist run state under `.prose/runs/`. - -Do not run `prose`, `npx prose`, or `npx @openprose/prose-cli` from inside the VM unless the user or harness explicitly tells you that you are using the CLI as an outer wrapper. - -The shell executable is a bridge into an agent harness. It is not the VM. - -## Host primitives - -Map the abstract OpenProse operations onto the current host: - -| Primitive | What it means | -| --- | --- | -| `spawn_session` | run a service or branch in an isolated agent session | -| `ask_user` | pause for missing caller input | -| `read_state` / `write_state` | read and write `.prose/runs/{id}/` files | -| `copy_binding` | publish declared outputs from a workspace to bindings | -| `check_env` | verify that an environment variable exists without revealing it | - -If the host lacks a required primitive, say so. Do not fake a multi-agent run as if isolation happened. - -## Fit check - -Decline OpenProse for one-shot work. - -Use it when the task has a reusable workflow shape: roles, handoffs, retries, parallel exploration, durable constraints, or a run trace the user will care about later. - -If the user says "just do it", respect that. - -## State and artifacts - -Prefer paths over pasted content. - -Services do their work in private workspaces. Declared outputs are copied into bindings. Downstream services inspect the artifact paths they need. - -Keep scratch out of bindings. Do not publish undeclared files as outputs. - -## Canonical files - -Use the files linked from [canonical specs](/reference/specs): - -- `SKILL.md` -- `contract-markdown.md` -- `forme.md` -- `prose.md` -- `prosescript.md` - -If this docs site and the skill disagree, trust the skill. - -Next: [reference overview](/reference/overview). diff --git a/content/docs/agents/meta.json b/content/docs/agents/meta.json deleted file mode 100644 index 096e383..0000000 --- a/content/docs/agents/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Agents", - "pages": ["for-ai-agents"] -} diff --git a/content/docs/cli/quickstart.mdx b/content/docs/cli/quickstart.mdx index d0322ab..85042aa 100644 --- a/content/docs/cli/quickstart.mdx +++ b/content/docs/cli/quickstart.mdx @@ -5,14 +5,16 @@ description: Scaffold, compile, and run a reactor project end to end. # Quickstart -This walks the full path: `reactor init` to scaffold a project, `reactor compile` to freeze it, then `reactor run` for a one-shot drain or `reactor serve` for a durable daemon. +This walks the full path: `reactor init` to scaffold a project, `reactor compile` to freeze it, then `reactor serve` to drive the scaffold's static gateway to a real receipt. ## Install ```sh -npm install -g @openprose/reactor-cli +npm i -g @openprose/reactor @openprose/reactor-cli @openprose/reactor-devtools ``` +`@openprose/reactor` is the SDK engine the CLI drives. As of this release the SDK is a real dependency of `@openprose/reactor-cli`, so `npm i -g @openprose/reactor-cli` already pulls it in -- the line above just makes the SDK and the replay viewer (`@openprose/reactor-devtools`) explicit so the keyless replay in [Reactor DevTools](/reactor-devtools/quickstart) is there too. None of these pull a model provider or a key. + ## 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. @@ -74,16 +76,9 @@ Once compiled, the offline observability commands work with no key. `topology` p reactor topology # the compiled DAG (inbox -> digest) ``` -## Run once +## Drive the static gateway -`run` ensures the IR is fresh (compiling if stale), boots the reactor, drains to quiescence, prints per-node dispositions plus cost, and exits. - -```sh -reactor run -reactor status # standing compile cost beside the run cost -``` - -## Or serve as a daemon +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. @@ -91,9 +86,10 @@ reactor status # standing compile cost beside the run cost reactor serve --http 8080 ``` -The scaffold's `static` connector ingests the seeded items on the first tick, so the gateway and digest both render. In another shell you can read the trail: +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 +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 ``` diff --git a/content/docs/index.mdx b/content/docs/index.mdx index 725473e..467efec 100644 --- a/content/docs/index.mdx +++ b/content/docs/index.mdx @@ -1,27 +1,17 @@ --- title: OpenProse Documentation -description: A programming language for AI sessions. +description: Reactor -- React.memo for expensive LLM work. Cost scales with surprise, not the clock. --- -# OpenProse is a programming language for AI sessions +# Reactor: `React.memo` for expensive LLM work -Write a Markdown contract. Your agent reads it, wires the services, runs the subagents, passes artifacts through the filesystem, and leaves a trace on disk. +**Reactor (`@openprose/reactor`) is a small, open-source harness for AI work that has to _keep being true_ after a chat ends.** -Plain prompts are great for one-off work. They get messy when the same process needs roles, handoffs, retries, memory, or a receipt. OpenProse gives that process a contract and lets an agent session execute it as a program. +You declare the truths you want kept current as OpenProse **Responsibilities** -- standing goals. Reactor keeps a composed **world-model** up to date against a changing world, re-renders only the responsibilities whose inputs actually moved, and leaves a content-addressed **receipt** behind every decision. -Two first-class parts make that durable. The Reactor DAG is the engine: it compiles a `.prose` project into a content-addressed graph and runs it so cost scales with surprise. The Reactor CLI is the command-line client that drives that engine and serves it as a durable, cost-observable daemon. +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**. - - - -## The shape +## 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 reconciler that decides _whether to wake_ is deliberately **dumb and deterministic** -- there is **no judge step**. The intelligence is frozen ahead of time, at compile, into a per-node canonicalizer and the Forme wiring. The memo key has no clock in it. + +## The shape of a contract -An OpenProse program is Markdown with a contract: +A Reactor program is Markdown with a contract. A **responsibility** declares what it subscribes to (`### Requires`) and the truth it keeps current (`### Maintains`): ```markdown --- -name: docs-review -kind: program +name: renewal-risk +kind: responsibility --- -### Requires +### Goal -- `repo`: repository to inspect +Keep a current risk read on every account whose signals have moved. -### Ensures +### Requires -- `report`: concise review report with risks and next steps +- `usage`: the per-account usage facet from the telemetry responsibility -### Services +### Maintains -- `reader`: maps the relevant files -- `critic`: checks for correctness and missing tests -- `writer`: turns the findings into a report +A current risk score per account. Material: the score band and the top +driver per account; `fetched_at` and request ids are immaterial. Each +account carries a `valid_until`. Postcondition: every flagged account +cites a corroborating signal. ``` -The program format is just Markdown. The execution model is real: contracts, services, wiring, workspaces, bindings, and traces. +`### Maintains` is the world-model **schema** doing four jobs at once: it is a **type**, a **canonicalization spec** (what's 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. The compile phase lowers it into a deterministic canonicalizer; at run time the reconciler only ever _compares_ fingerprints. The fingerprint moves iff the material content moves -- so a re-poll that returns the same truth costs nothing. -## When to reach for it +The other kinds are `gateway` (an external source -- webhook/cron/manual ingress), `function` (a called helper with `### Parameters` → `### Returns`, never a node), `pattern` (reusable coordination expanded at compile), and `test` (assertions over a subject's world-model or receipts). -Use OpenProse when a prompt has started turning into a process: +## See the thesis -- keyless, no model call -- the work has roles, phases, or handoffs -- several agents can work in parallel -- the result needs a durable trace -- the same process will run again -- constraints should survive across runs +The fastest way to understand Reactor is to replay a real saved run and read the per-node `rendered`/`skipped` dispositions, the cost rollup split by surprise cause, and per-node chain-verify -- with no key and no spend: -If it fits in one good prompt, use one good prompt. OpenProse is ceremony until the workflow needs a contract. +```sh +npm i -g @openprose/reactor @openprose/reactor-cli @openprose/reactor-devtools +reactor-devtools --example masked-relay --describe +``` -## What runs it +```text +dispositions: rendered 46 · skipped 31 (reuse 32%) +cost rollup by surprise-cause: external 8 · input 69 total 77 +CHAIN-VERIFY ok +``` -A modern agent session is already a kind of computer: a model, a harness, tools, filesystem state, and sometimes subagents. OpenProse gives that computer a program format. +That is "cost scales with surprise" -- checkable, with no key and no spend. From there, scaffold and inspect a project of your own, all offline: -Forme reads the contracts, resolves which services satisfy which requirements, writes a manifest, and then the model plus harness executes the run. Services work in private workspaces. Declared outputs become bindings. Other services receive paths to artifacts instead of giant pasted blobs. +```sh +reactor init my-project && cd my-project +reactor doctor # what's present + the fix for anything missing +reactor compile --check; echo "exit=$?" # offline; exits 1 if the contract set is STALE +``` -## Source of truth +To go live you set a model key and the two optional peers, then `reactor compile` to wire the DAG and `reactor serve` to drive the scaffold's static gateway to a real receipt. The [Quickstart](/cli/quickstart) walks the full path. -These docs explain the shape of OpenProse. They are not the execution spec. +## Works with any harness -- Reactor is the fast path + +Reactor is built on **OpenProse**: Markdown contracts that run on any Prose-Complete agent harness. The `.prose.md` contracts are harness-agnostic. Reactor itself is a real SDK + CLI + DevTools you install and run -- the deterministic host that compiles, runs, and inspects standing responsibilities, and the recommended fast path for them. + +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. + +--- -If you are an agent running an OpenProse program, start with [For AI agents](/agents/for-ai-agents). The `open-prose` skill and its linked specs are the source of truth for execution behavior. +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/meta.json b/content/docs/meta.json index 44fdd7d..269c2f6 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -1,4 +1,4 @@ { "title": "OpenProse", - "pages": ["index", "start", "think", "reactor", "cli", "reactor-devtools", "use", "agents", "reference"] + "pages": ["index", "reactor", "cli", "reactor-devtools"] } diff --git a/content/docs/reactor-devtools/reference.mdx b/content/docs/reactor-devtools/reference.mdx index c60266c..40a3292 100644 --- a/content/docs/reactor-devtools/reference.mdx +++ b/content/docs/reactor-devtools/reference.mdx @@ -8,18 +8,25 @@ description: The reactor-devtools bin, the read-only HTTP API, the frame shape t ## The `reactor-devtools` bin ```text -reactor-devtools [--port ] [--host ] [--describe] +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)_ | A saved Reactor state directory (receipts + `compile/topology.json`). | +| `` | _(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. The keyless proof from the README (e.g. `--example masked-relay`). | +| `--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` or a successful `--describe`; `1` when `` is missing (usage is printed), when `--port` is not an integer, or on any startup error. The server runs until `SIGINT` / `SIGTERM`, then closes and exits `0`. +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). @@ -29,8 +36,8 @@ 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. | -| `GET /api/node/:id?version=` | The node's world-model at a version (the click-through inspector) via `readVersion`. `version` is a frame's `atomicVersion`. `400` if missing, `404` if no such node/version. | +| `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 diff --git a/content/docs/reference/commands.mdx b/content/docs/reference/commands.mdx deleted file mode 100644 index 36a7812..0000000 --- a/content/docs/reference/commands.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Command surface -description: The OpenProse command names and what they are for. ---- - -# Command surface - -OpenProse commands are first an agent-session command language. - -When you type `prose run file.md` inside an agent session, the agent should load the `open-prose` skill and execute the program in-session. - -When you use the CLI from a shell, the CLI packages that same instruction and sends it into a supported harness. The CLI does not replace the VM. - -## Commands - -| Command | Purpose | -| --- | --- | -| `prose run file.md` | run a Contract Markdown or ProseScript program | -| `prose lint file.md` | check Contract Markdown structure before a run | -| `prose preflight file.md` | check dependencies and environment without executing | -| `prose test path` | run OpenProse tests | -| `prose inspect run-id` | inspect a completed run | -| `prose status [--graph]` | summarize recent runs, optionally as a graph | -| `prose install [--update]` | install or update OpenProse dependencies | -| `prose examples [name]` | list or run bundled examples | -| `prose help` | show OpenProse help | -| `prose migrate file.prose` | convert ProseScript toward Contract Markdown | - -## Shell CLI - -The shell CLI is useful when you are outside an agent session. - -```bash -prose run programs/reviewer.md -prose run programs/reviewer.md --harness claude-sdk -prose doctor -``` - -The current CLI supports harnesses such as `codex-sdk`, `claude-sdk`, `codex`, `claude`, and `mock`. It can check whether the `open-prose` skill is installed for the selected provider and can run `prose doctor` to inspect setup. - -For exact install commands, flags, harness defaults, and exit behavior, read the canonical [CLI README](https://github.com/openprose/prose/blob/main/cli/README.md). - -## Agent rule - -If you are an agent and the user gives you `prose ...` in chat, do not shell out to the CLI by default. - -Load the skill and treat the command as an instruction to the current session. - -Next: [canonical specs](/reference/specs). diff --git a/content/docs/reference/meta.json b/content/docs/reference/meta.json deleted file mode 100644 index be461db..0000000 --- a/content/docs/reference/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Reference", - "pages": ["overview", "commands", "specs"] -} diff --git a/content/docs/reference/overview.mdx b/content/docs/reference/overview.mdx deleted file mode 100644 index 71820f6..0000000 --- a/content/docs/reference/overview.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Reference overview -description: Where the canonical OpenProse specs live. ---- - -# Reference overview - -These docs are intentionally small. They explain how to think about OpenProse and how to get started. - -The canonical behavior lives in the open source repo, especially the `open-prose` skill and its reference specs. - -That split is deliberate. - -The docs site should help a human get oriented. The skill should tell an agent how to run the thing. - -## What belongs here - -This section gives you: - -- a shallow map of the command surface -- links to the skill and canonical specs -- enough vocabulary to know which file to read next - -It does not try to mirror the full language reference. Mirrored specs drift, and drift is dangerous when agents read the docs as if they are executable instructions. - -## Where to go - -| Need | Read | -| --- | --- | -| Run, lint, test, or explain a program as an agent | [For AI agents](/agents/for-ai-agents) | -| Understand the shell command names | [Command surface](/reference/commands) | -| Find the authoritative execution files | [Canonical specs](/reference/specs) | -| Decide whether OpenProse is a fit | [When to use OpenProse](/think/when-to-use-it) | - -If this site and the skill disagree, trust the skill. - -Next: [commands](/reference/commands). diff --git a/content/docs/reference/specs.mdx b/content/docs/reference/specs.mdx deleted file mode 100644 index bf37737..0000000 --- a/content/docs/reference/specs.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Canonical specs -description: Links to the skill and source-of-truth reference files. ---- - -# Canonical specs - -If this site and the skill disagree, trust the skill. - -The docs site is orientation. These files define how OpenProse agents should behave. - -## Skill - -- [`open-prose` skill](https://github.com/openprose/prose/blob/main/skills/open-prose/SKILL.md) - -Agents should start here. The skill contains the activation rules, path table, command routing, host primitive adapter, and fit check. - -## Core specs - -| File | What it defines | -| --- | --- | -| [Contract Markdown](https://github.com/openprose/prose/blob/main/skills/open-prose/contract-markdown.md) | the `.md` program and service format | -| [Forme](https://github.com/openprose/prose/blob/main/skills/open-prose/forme.md) | semantic wiring and manifest generation | -| [Prose VM](https://github.com/openprose/prose/blob/main/skills/open-prose/prose.md) | execution behavior for manifests and services | -| [ProseScript](https://github.com/openprose/prose/blob/main/skills/open-prose/prosescript.md) | the imperative scripting layer for `.prose` and `### Execution` | -| [Filesystem state](https://github.com/openprose/prose/blob/main/skills/open-prose/state/filesystem.md) | the default run-state layout | -| [Session primitive](https://github.com/openprose/prose/blob/main/skills/open-prose/primitives/session.md) | how isolated service sessions should behave | - -## Authoring guidance - -| File | What it helps with | -| --- | --- | -| [Tenets](https://github.com/openprose/prose/blob/main/skills/open-prose/guidance/tenets.md) | design principles behind the language | -| [Patterns](https://github.com/openprose/prose/blob/main/skills/open-prose/guidance/patterns.md) | reusable workflow shapes | -| [Antipatterns](https://github.com/openprose/prose/blob/main/skills/open-prose/guidance/antipatterns.md) | common ways OpenProse programs go wrong | - -## CLI and packages - -- [OpenProse README](https://github.com/openprose/prose/blob/main/README.md) -- [CLI README](https://github.com/openprose/prose/blob/main/cli/README.md) -- [Standard library](https://github.com/openprose/prose/tree/main/packages/std) - -Reference pages here should stay shallow. If a detail is normative, link to source instead of copying it. diff --git a/content/docs/start/first-program.mdx b/content/docs/start/first-program.mdx deleted file mode 100644 index bab913a..0000000 --- a/content/docs/start/first-program.mdx +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: Your first program -description: Write and run a tiny OpenProse program. ---- - -# Your first program - -Hello world proves the loop. This program takes one input, asks the agent to -explain it, and leaves a run trace you can inspect afterward. - -Create `hello-openprose.md`: - -```markdown ---- -name: hello-openprose -kind: program ---- - -### Requires - -- `topic`: thing to explain - -### Ensures - -- `summary`: a short explanation of the topic - -### Execution - -Tell the user what `topic` means in plain English. -Write the result to `summary.md`. -``` - -Run it from an agent session: - -```text -prose run hello-openprose.md --topic "why traces matter" -``` - -Or, if you installed the CLI, run the same command from a shell: - -```bash -prose run hello-openprose.md --topic "why traces matter" -``` - -In both cases, the command is asking an agent harness to run the program. The -CLI does not execute the program by itself. - -## What happened - -The agent reads `hello-openprose.md` as a Contract Markdown program. - -`Requires` says the program needs a `topic`. The command supplies it with -`--topic "why traces matter"`. - -`Ensures` says the run must produce a `summary`. This is the promise you can -check when the run is done. - -`Execution` pins the tiny bit of choreography for this first program. Most -OpenProse programs start declarative and only add execution steps when order -matters. - -Because this program has one component and no `Services` section, there is no -service graph to wire and no subagent fan-out required. The agent can run it -directly. - -## Inspect the trace - -After the run, look under the newest directory in `.prose/runs/`. - -You should find the durable record of the run: the inputs, the execution log, -and the output artifacts produced by the program. Exact filenames can vary as -the tooling evolves, but the point is stable: a run leaves a receipt on disk -instead of disappearing into chat history. - -That receipt is why OpenProse is more than a long prompt. The contract, the -artifacts, and the trace can be reviewed, versioned, and improved. - -Next: [try a useful workflow](/start/first-useful-workflow). diff --git a/content/docs/start/first-useful-workflow.mdx b/content/docs/start/first-useful-workflow.mdx deleted file mode 100644 index 5164c4b..0000000 --- a/content/docs/start/first-useful-workflow.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Your first useful workflow -description: Run a small multi-service workflow where OpenProse starts to matter. ---- - -# Your first useful workflow - -Hello world proves that the loop works. A useful OpenProse workflow proves why you would write a program at all. - -Use this example when the work wants more than one point of view. A code review is a good first shape because the roles are obvious: one reviewer can think about security, another can think about performance, another can think about style, and a final service can turn the separate notes into one report. - - - -## What this program says - -The program has one input: - -- `code`: the code to review - -It promises one output: - -- `report`: a unified review report that covers security, performance, and style - -The `Services` section names the roles that can satisfy that promise. The roles are intentionally plain: - -- `security-reviewer` looks for risks and unsafe assumptions -- `perf-reviewer` looks for slow paths and waste -- `style-reviewer` looks for readability and local conventions -- `synthesizer` turns the separate reviews into one report - -That is the first useful OpenProse move: split the work where splitting helps, then make the handoff explicit. - -## Run it small - -Start with a small target. A single file, a short diff, or a narrow directory is better than a whole repo for a first run. - -From an agent session with the `open-prose` skill installed: - -```text -prose run vendor/prose-examples/16-parallel-reviews/index.md -``` - -The program requires `code`. If the runner does not already have a value for that input, it should ask you for one. Give it a path, a diff, or a compact snippet. - -When the run is finished, read the output as a contract check: - -- Did the report cover security, performance, and style? -- Did the separate points of view stay separate until synthesis? -- Did the final report prioritize what matters instead of pasting every note? -- Did the run leave enough trace to understand what happened? - -If the answer is no, the program needs a tighter contract. That is normal. OpenProse programs are meant to be revised like code. - -## Make one change - -Try one small edit after the first run. - -If the report is too broad, make the input narrower. If the reviewers overlap too much, give one role a sharper name. If the final report is noisy, add an `Ensures` obligation for prioritization. - -For example, a stricter output contract might say: - -```markdown -### Ensures - -- `report`: unified review report covering security, performance, and style -- each issue has: severity, evidence, and a concrete next step -- issues are sorted by risk before polish -``` - -That edit changes the shape of the run without turning the program into a long prompt. The contract still says what must be true when the work is done. - -## Why this is different from one prompt - -You could ask an agent to "review this code for security, performance, and style." For a small snippet, that is fine. - -The program becomes useful when you want the shape to survive: - -- the same review should run again next week -- different roles should work independently -- the synthesis should be inspectable -- the output contract should be reviewable in git -- future changes should improve the workflow instead of disappearing into chat history - -That is the OpenProse taste test. Reach for it when the prompt has become a process. - -Next: [learn contracts](/think/contracts-not-prompts). diff --git a/content/docs/start/install.mdx b/content/docs/start/install.mdx deleted file mode 100644 index 4292796..0000000 --- a/content/docs/start/install.mdx +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: Install -description: Install the OpenProse skill and run through the shell entrypoint. ---- - -# Install - -OpenProse runs in an agent session. The important install is the -`open-prose` skill, because the skill tells the agent how to read an -OpenProse program, wire services, run the work, and write a trace. - -The `prose` CLI is optional. It is a shell entrypoint into an agent harness, -not a separate OpenProse VM. It packages commands like `prose run file.md` -into an agent-session instruction, then streams the harness output back to -your terminal. - -## Requirements - -You need: - -- An agent harness that can read and write files in the working directory. -- A subagent or session-spawning primitive for multi-service programs. -- Node.js 18 or newer if you use the CLI. -- Credentials for the harness you choose. - -Single-component programs can run without subagents. Multi-service programs -need a harness that can start isolated worker sessions. - -## Install the skill - -Install the `open-prose` skill: - -```bash -npx skills add openprose/prose -``` - -After the skill is available, an OpenProse command inside an agent session is -an instruction to that agent: - -```text -prose run hello-openprose.md -``` - -The agent should not look for a `prose` binary just because the instruction -starts with `prose`. It should load the skill, read the Markdown contract, and -run the program in-session. - -## Install the CLI - -If you want to run OpenProse from a shell, install the CLI: - -```bash -npm install --global @openprose/prose-cli -prose --help -``` - -For project-local or one-off use, you can also invoke the package through -`npx`: - -```bash -npx @openprose/prose-cli doctor -``` - -The installed binary is named `prose`. - -## Check your setup - -Run `doctor` before the first real program: - -```bash -prose doctor -``` - -The CLI checks whether the `open-prose` skill is available for the selected -harness. For a specific harness, pass `--harness`: - -```bash -prose doctor --harness claude-sdk --install -``` - -The default harness is `codex-sdk`. Other supported harnesses include -`claude-sdk`, `codex`, `claude`, and `mock`. - -## Run path - -There are two common ways to run the same program: - -```text -prose run hello-openprose.md -``` - -Use that form inside an agent session. - -```bash -prose run hello-openprose.md -``` - -Use that form from a shell after installing the CLI. The shell command still -hands execution to the selected agent harness. The harness and skill are what -run the OpenProse program. - -When a run completes, OpenProse writes durable state under `.prose/runs/`. -That trace is where you inspect inputs, outputs, service workspaces, and the -execution log. - -Next: [write your first program](/start/first-program). diff --git a/content/docs/start/meta.json b/content/docs/start/meta.json deleted file mode 100644 index 688183f..0000000 --- a/content/docs/start/meta.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "Start", - "pages": [ - "what-is-openprose", - "install", - "first-program", - "first-useful-workflow" - ] -} diff --git a/content/docs/start/what-is-openprose.mdx b/content/docs/start/what-is-openprose.mdx deleted file mode 100644 index 2356944..0000000 --- a/content/docs/start/what-is-openprose.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: What is OpenProse? -description: The short mental model for OpenProse. ---- - -# What is OpenProse? - -OpenProse is a programming language for AI sessions. - -A modern agent session already has a lot of the pieces of a computer: a model, a tool loop, a filesystem, subprocesses or subagents, and persistent state. OpenProse gives that system a program format. - -The program is a Markdown contract. An agent reads the contract, wires the services, runs the work, passes artifacts through the filesystem, and leaves a durable trace. - -```text -Markdown contract - -> Forme wiring - -> manifest - -> VM run - -> workspaces + bindings + trace -``` - -## The AI session is the computer - -OpenProse does not try to make the model deterministic. It gives the model a contract, a workspace, and a trace so complex work has a shape you can inspect and improve. - -The VM is the model plus its harness: tools, files, shell access, browser access, subagents, and whatever state the run is allowed to use. In a Prose Complete harness, the agent and the skill execute the program inside that session. - -## The program is Markdown - -Markdown is not a gimmick here. The executor is an agent, so the program should be readable by agents. The user is a human builder, so the program should be readable in review. Git should be able to show a useful diff. - -The important parts of a small program are usually easy to spot: - -- `Requires` names what the service needs. -- `Ensures` names what the service must produce. -- `Services` names the work that can satisfy those contracts. -- `Execution` pins order when choreography matters. - -You do not need all of that for every program. Start declarative. Add wiring when relationships matter. Add execution when order matters. - -## Contracts, services, Forme, and traces - -A prompt says, "do this." A contract says, "given these inputs, these things must be true when you are done." - -Services are the pieces of work in the program. Forme is the container that reads their contracts and decides how they fit together. The manifest is the materialized wiring for a run. The VM is the agent session that executes it. - -The trace is the record. Each run has places for private work, declared outputs, and bindings that downstream services can receive as pointers. This keeps handoffs inspectable without turning every step into a giant paste. - -## How it differs from other shapes - -OpenProse is not just a longer prompt. Prompts are good when the work fits in one exchange. OpenProse is for recurring or fragile workflows where roles, handoffs, constraints, and receipts matter. - -OpenProse is also not an external workflow engine that controls the model from the outside. Most orchestration frameworks wrap the model. OpenProse gives the agent a program to execute from inside the session. - -That is the practical difference: the point is not to replace judgment with plumbing. The point is to put recurring agent work on rails. - -OpenProse is early. The program format is useful today, but the tools and conventions are still settling. Expect sharp edges, and prefer small workflows before betting a large process on it. - -Next: [install OpenProse](/start/install). diff --git a/content/docs/think/contracts-not-prompts.mdx b/content/docs/think/contracts-not-prompts.mdx deleted file mode 100644 index fbb04ff..0000000 --- a/content/docs/think/contracts-not-prompts.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Contracts, not prompts -description: Learn the basic shape of OpenProse programs. ---- - -# Contracts, not prompts - -A prompt asks an agent to do something. A contract says what the work needs, what it must produce, what can go wrong, what must stay true, and how to handle judgment calls. - -That difference is small on the page and large in practice. - -Prompts are fine for one-off work. They start to blur when the same process needs handoffs, retries, memory, or review. OpenProse keeps the work in Markdown, but changes the shape from "please do this" to "given these inputs, these obligations must hold when you are done." - -For the exact Contract Markdown grammar, accepted section forms, and extraction rules, see the [canonical specs](/reference/specs). This page is the human mental model. - -## Requires and Ensures - -The center of an OpenProse service is the pair `Requires` and `Ensures`. - -`Requires` names what the service needs from its caller, another service, the environment, or a completed run. `Ensures` names what the service commits to produce or make true. - -```markdown -### Requires - -- `draft`: article draft to improve -- `audience`: who the article is for - -### Ensures - -- `final`: edited article ready for review -- `notes`: short explanation of substantial edits -``` - -This is not decoration. It is the public interface of the work. When services have clear requirements and clear postconditions, Forme can wire them by meaning instead of relying only on file order or a long procedural prompt. - -The useful habit is to write obligations, not intentions. "Make it better" is a prompt. "`final`: edited article that preserves the author's claims and removes unclear structure" is closer to a contract. - -## Errors - -Some failures are part of the shape of the work. `Errors` names the failures a service is allowed to report instead of hiding them in prose after the fact. - -For example, a research service might declare that it can fail with insufficient sources, conflicting evidence, or missing access. A conversion service might declare unsupported input format. A reviewer might declare that the draft is too incomplete to evaluate. - -Named errors help callers decide what to do next. They also make a run easier to inspect because the failure has a place in the contract, not just in the transcript. - -## Invariants - -`Invariants` are promises that must hold regardless of the path the agent takes. - -Use them for boundaries that should survive success, partial success, retries, and failure handling: - -- do not invent citations -- do not edit source files outside the workspace -- preserve user-authored examples unless the contract explicitly asks to change them -- keep private scratch work out of declared outputs - -Invariants are where you put the rules that should not be traded away during a clever workaround. - -## Strategies - -`Strategies` are guidance for judgment calls. - -They are not a script. They tell the service how to choose when the contract leaves room for interpretation: - -- when evidence is thin, broaden the search before concluding -- when two outputs conflict, prefer the one backed by a traceable source -- when the task is too large, produce a smaller useful slice and say what remains -- when a requested change would violate an invariant, stop and report the conflict - -Good strategies preserve judgment without hiding the criteria. The agent still has to reason. The contract makes the criteria visible. - -## What Goes Where - -Put inputs in `Requires`. - -Put promised outputs and postconditions in `Ensures`. - -Put known failure modes in `Errors`. - -Put non-negotiable boundaries in `Invariants`. - -Put judgment guidance in `Strategies`. - -If you find yourself writing a long sequence of steps, ask whether the order truly matters. If the answer is no, tighten the contract. If the answer is yes, that choreography belongs in `Execution`, which is covered by the canonical specs and by the runtime model. - -## Why This Matters - -Contracts make OpenProse programs inspectable. A human can see what each service owes. An agent can extract the same shape. Forme can resolve services that satisfy each other's requirements. The run can leave a trace that says what was promised and what was produced. - -OpenProse does not make the model deterministic. It gives the model a contract, a workspace, and a record of the work. That is enough to make larger sessions easier to inspect and improve. - -Next: [services, Forme, and the VM](/think/services-forme-vm). diff --git a/content/docs/think/meta.json b/content/docs/think/meta.json deleted file mode 100644 index 31bfb79..0000000 --- a/content/docs/think/meta.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "Think", - "pages": [ - "contracts-not-prompts", - "services-forme-vm", - "workspaces-bindings-traces", - "when-to-use-it" - ] -} diff --git a/content/docs/think/services-forme-vm.mdx b/content/docs/think/services-forme-vm.mdx deleted file mode 100644 index 80d990a..0000000 --- a/content/docs/think/services-forme-vm.mdx +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: Services, Forme, and the VM -description: How OpenProse wires and runs programs. ---- - -# Services, Forme, and the VM - -OpenProse programs are made out of services. - -A service is a unit of agent work with a contract. It names what it needs in -`Requires`, what it is obligated to produce in `Ensures`, and any boundaries -that matter for the work. A program is the entry point that gathers services -into one run. - -That split is small, but it changes the shape of a session. Instead of asking -one agent to remember every role, handoff, and output, you give the system a set -of components it can wire and run. - -## Services are boundaries - -A service should be clear about its job. - -```markdown -### Requires - -- `question`: the question to investigate - -### Ensures - -- `findings`: sourced claims that answer the question -- `sources`: the sources consulted during the work -``` - -The names matter, but the prose matters too. OpenProse is read by a model, so a -contract is not just a schema. It is a set of obligations the executing agent can -understand. - -A good service boundary answers three questions: - -- What input does this part of the run need? -- What public output does it promise? -- What work should stay inside this service? - -That last question is easy to miss. Services are not just named steps. They are -isolation boundaries. Each service gets its own workspace, writes its own -artifacts, and publishes only the outputs declared by its contract. - -## Forme wires the graph - -Forme is the container for OpenProse programs. It reads the program and service -contracts, resolves which outputs satisfy which inputs, and writes a manifest -for the runtime. - -Traditional dependency injection containers wire software components by types -and annotations. Forme wires agent services by meaning. If one service ensures -`findings` and another requires `evidence`, Forme can read the descriptions and -decide whether those are the same thing in this program. - -That judgment is deliberate. OpenProse does not try to replace model judgment -with a brittle type system. If wiring is ambiguous, the right fix is usually to -make the contracts clearer. When order or exact data flow matters, the author -can pin it. - -The practical ladder is: - -1. Write contracts and let Forme auto-wire the graph. -2. Add explicit wiring when a relationship matters. -3. Add an `Execution` block when order matters. - -Start declarative. Add control when the work asks for it. - -## The manifest is the handoff - -The manifest is the materialized wiring for a run. It is what Forme produces and -what the VM executes. - -It records: - -- the caller inputs the program needs -- each service in the graph -- where each service reads its inputs -- where each service writes its outputs -- which services can run in parallel -- warnings Forme found while wiring - -The manifest is useful because it makes an intelligent decision inspectable. -Forme may use semantic judgment to connect services, but the result is not -hidden in the chat. It becomes a file the runtime can follow and a human can -review. - -## The VM is the agent session - -The OpenProse VM is the model plus its harness, tools, subagent primitive, and -filesystem access. - -That can sound stranger than it is. A modern coding agent can read files, write -files, call tools, spawn isolated sessions, inspect state, and continue from -what is left on disk. OpenProse gives that machine a program format. - -During execution, the VM reads the manifest and runs each service. A service -does not need to know the whole graph. It receives: - -- its own service definition -- file paths for its inputs -- a private workspace path -- the output files it must write - -When a service finishes, it returns a short confirmation. The VM copies the -declared outputs into public bindings, appends to the run trace, and moves on. - -This is why OpenProse is not a hosted workflow builder wrapped around a model. -The workflow runs inside the agent session. The filesystem is the durable state. -The trace is the receipt. - -## The analogy - -The project uses this analogy because it is precise enough to be useful: - -```text -Prose : Forme : agent harness :: Java : Spring : JVM -``` - -Prose is the language. Forme is the intelligent container. The agent harness is -the VM. - -Do not push the analogy too far. OpenProse does not make model behavior -deterministic, and it does not remove judgment from the run. It gives that -judgment a contract, a graph, and a trace. - -## Agent note - -These docs explain the shape of OpenProse. They are not the execution spec. - -If you are an agent running or editing an OpenProse program, read -[For AI agents](/agents/for-ai-agents) and follow the skill and canonical specs -listed in [Canonical specs](/reference/specs). - -Next: [workspaces, bindings, and traces](/think/workspaces-bindings-traces). diff --git a/content/docs/think/when-to-use-it.mdx b/content/docs/think/when-to-use-it.mdx deleted file mode 100644 index 1e79498..0000000 --- a/content/docs/think/when-to-use-it.mdx +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: When to use OpenProse -description: Good fits, bad fits, and the quick taste test. ---- - -# When to use OpenProse - -Use OpenProse when a prompt has become a process. - -It is not a better wrapper around chat. It is a way to write a reusable contract for agent work: inputs, obligations, roles, artifacts, failure cases, and the trace the run leaves behind. - -If one good prompt gets the job done, use one good prompt. OpenProse adds structure. That structure is only worth it when it buys something real. - -## Good fits - -### Repeated work - -Use OpenProse when you keep rebuilding the same workflow in chat: - -- weekly research digests -- repeated code review passes -- release note generation -- intake, triage, and routing -- report drafting with the same inputs and quality bar - -The question is not "could an agent do this?" The question is "will I want the same agent process again?" - -### Work with handoffs - -OpenProse starts to matter when the work has roles, stages, or artifacts that should not blur together. - -A prompt can say "review this code." A program can say: - -- one service checks security -- one service checks performance -- one service checks documentation impact -- one service synthesizes the findings -- the final output must include file paths, severity, and evidence - -That is a contract, not a chat request. For the mental model, read [Contracts, not prompts](/think/contracts-not-prompts). - -### Parallel work - -Use OpenProse when independent branches can run at the same time and join later. - -Good examples: - -- review the same change from security, performance, and product angles -- collect independent research on different competitors -- classify a batch of files before a synthesis pass - -Bad example: split one bug diagnosis into "code," "logs," and "context" agents when each one needs the whole picture. That usually creates coordination overhead without better thinking. - -### Work that needs a receipt - -OpenProse leaves work on disk. That matters when you need to inspect what happened, rerun part of the work, or hand artifacts to another service by path instead of pasting giant blobs into the next prompt. - -Use it when the trace and workspace are part of the value. See [Workspaces, bindings, and traces](/think/workspaces-bindings-traces). - -### Work with real acceptance criteria - -OpenProse is a good fit when you can name what must be true at the end: - -- every item has a summary and score -- every finding has evidence -- every generated file exists at a declared path -- every failed branch is reported with a reason - -If the only success criterion is "make it better," stay in chat until you know what "better" means. - -## Bad fits - -### One-shot answers - -Do not write an OpenProse program for a single question, a quick explanation, or a small decision. Ask the agent directly. - -OpenProse is overkill when the output is just a paragraph and you do not need a trace. - -### Tiny edits - -Do not use OpenProse to fix one typo, rename one file, or make a simple local change. The ceremony will cost more than the work. - -### Deterministic work - -If shell, SQL, Python, or a normal queue can do the job exactly, use that. - -OpenProse is for judgment-heavy work inside agent sessions. It is a bad core for exact accounting, payment flow, policy enforcement, or anything where a model should not be deciding the next state. - -### Vague work - -A contract will not rescue an unclear goal. - -If you cannot name the inputs, the output, and the definition of done, writing Markdown around the request only makes the vagueness look official. - -### Work that only looks parallel - -Parallel agents are not free. Use them when the work is genuinely independent or when different viewpoints should stay separate until synthesis. - -Do not fan out tiny tasks. Do not split a single line of reasoning across agents just to make the graph look busy. - -## The taste test - -Before writing a program, answer these: - -1. What does the workflow require? -2. What must it ensure? -3. What artifacts should exist on disk? -4. Which parts can run independently? -5. What can fail, retry, or degrade? -6. Will this workflow run again? - -If most answers are blank, OpenProse is too much. Start with a prompt. - -If the answers are clear, OpenProse gives the process a contract, a workspace, and a trace. - -Next: [try a useful workflow](/start/first-useful-workflow) or [browse examples](/use/examples). diff --git a/content/docs/think/workspaces-bindings-traces.mdx b/content/docs/think/workspaces-bindings-traces.mdx deleted file mode 100644 index eb478ff..0000000 --- a/content/docs/think/workspaces-bindings-traces.mdx +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Workspaces, bindings, and traces -description: How OpenProse keeps agent work durable and context-efficient. ---- - -# Workspaces, bindings, and traces - -OpenProse uses the filesystem as the durable part of the session. - -That choice is not incidental. Agent context is scarce and easy to muddy. Files -are inspectable, resumable, and cheap to pass by path. OpenProse keeps service -work on disk, publishes only declared outputs, and records what happened in a -trace. - -The main pieces are: - -- `workspace/`: private working files for each service -- `bindings/`: public outputs that other services can read -- `state.md`: the append-only trace for the run - -## Workspaces are private - -Each service gets its own workspace inside the run directory. - -```text -.prose/runs/20260317-143052-a7b3c9/ -|-- workspace/ -| |-- researcher/ -| | |-- notes.md -| | |-- findings.md -| | `-- sources.md -| `-- synthesizer/ -| |-- outline.md -| `-- report.md -``` - -The service can write whatever it needs there: notes, drafts, scratch files, -source extracts, failed attempts, and final outputs. Those files are preserved so -you can inspect the run later. - -The workspace is private in the service sense. Other services do not consume it -directly. A downstream service should not have to understand another service's -drafts or internal reasoning. It should receive the output promised by the -contract. - -## Bindings are public - -Bindings are the public interface between services. - -When a service completes, the VM looks at the service's `Ensures` contract and -copies the declared output files from its workspace into `bindings/`. - -```text -workspace/researcher/findings.md -bindings/researcher/findings.md -``` - -That copy is the return step. - -The service writes to its workspace. The VM publishes the declared outputs. -Downstream services read from the binding paths named in the manifest. - -This matters because it keeps the contract honest. If `researcher` ensures -`findings` and `sources`, then those are the public artifacts other services can -depend on. Its scratch notes may still exist, but they are not part of the -interface. - -## Pointers beat pasted context - -OpenProse does not need to paste every intermediate artifact into the main -agent context. - -A service can return: - -```text -Service complete: researcher -Outputs written: - - findings: workspace/researcher/findings.md - - sources: workspace/researcher/sources.md -Summary: Found relevant sources and extracted claims. -``` - -The VM copies the files and carries the paths forward. The next service receives -file paths, not a giant blob of text. It can read what it needs from disk. - -That is why workspaces and bindings are not just implementation details. They -are part of the language's context strategy. Big workflows stay legible because -artifacts remain artifacts. - -## Traces are the receipt - -Every run has a trace. In the canonical runtime, the trace is `state.md`. - -It records the important events of the run: - -```text -# run:20260317-143052-a7b3c9 deep-research - -program: deep-research - -1-> [input] question ok -2-> researcher ok -3-> critic ok -4-> synthesizer ok ----end 2026-03-17T14:35:22Z -``` - -The trace is intentionally small. It is not a transcript of every thought or -every token. It is the run ledger: inputs bound, services completed, errors -signaled, delegates called, tests passed or failed, and completion status. - -This gives you a way to answer practical questions after the run: - -- Which service produced this file? -- Did the critic actually run? -- Where did the run stop? -- Can the VM resume from the next service? -- Which prior run did this run depend on? - -Without a trace, an agent workflow is just a long conversation. With a trace, it -has a record. - -## What this buys you - -The workspace, binding, and trace split gives OpenProse three useful properties. - -First, isolation. A service can think and write freely without leaking every -draft into downstream work. - -Second, context control. The runtime passes pointers to artifacts instead of -stuffing the whole run into one prompt. - -Third, inspectability. After the run, you can open the directory and see what -was produced, what was published, and what happened. - -This is the practical reason OpenProse treats the filesystem as part of the VM. -The agent session is still doing the intelligent work, but the important state -does not vanish when the chat scrolls away. - -Next: [when to use OpenProse](/think/when-to-use-it). diff --git a/content/docs/use/examples.mdx b/content/docs/use/examples.mdx deleted file mode 100644 index 3fc5a55..0000000 --- a/content/docs/use/examples.mdx +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: Examples -description: A curated path through the OpenProse examples. ---- - -# Examples - -Read these examples in order. They move from the smallest possible service to a program with explicit phases, review, and testing. - -The point is not to memorize syntax. Look for the contract shape: - -- what the program requires -- what it ensures -- where judgment belongs -- when a workflow stays declarative -- when order is important enough for `Execution` - -## Hello world - -Start here to see the minimum surface area. It is a service, not a program, and it has no required inputs. - - - -Notice how little structure is needed. The service only promises `greeting`. That is enough for the VM to know what successful completion means. - -Use this shape when you are testing whether the OpenProse loop is installed and the agent knows how to read a contract. - -## Research and summarize - -This example adds an input, a more specific output, and strategies for judgment calls. - - - -The useful detail is the constraint on the bullets. The service does not just promise a summary. It promises five bullets, practical implications, and grounding in recent sources. - -The `Strategies` section gives the agent room to reason without leaving the workflow vague. If sources are thin, broaden the search. If the findings are too technical, translate them for developers. - -## Code review - -This is still a single service, but the contract is closer to real work. - - - -The output has shape: - -- a unified report -- coverage across security, performance, and maintainability -- severity on each issue -- actionable recommendations -- priority by severity - -That is the difference between "review this" and a contract. The service has room to use judgment, but the result has obligations. - -## Parallel reviews - -This is the first example where OpenProse starts to pay for itself. - - - -The services are separate points of view: - -- `security-reviewer` -- `perf-reviewer` -- `style-reviewer` -- `synthesizer` - -The program does not need a long script. The roles and the final report are clear enough for a first pass. If you need sharper boundaries later, give the services their own contracts or add explicit execution. - -For a guided run, see [your first useful workflow](/start/first-useful-workflow). - -## Captain's chair - -This example is larger because order matters. It plans, researches, reviews the plan, implements, reviews the implementation, tests, and then summarizes. - - - -The important change is the `Execution` block. Declarative contracts are still present, but the program also pins the phases: - -- plan first -- run research sweeps -- synthesize an implementation plan -- let a critic review the plan -- implement -- review the implementation -- test -- produce the final result - -Use this shape when the workflow has real order, branching, or retry points. Do not start here for a small task. Start declarative, add wiring when relationships matter, and use `Execution` when order matters. - -## Choosing the right example - -| Example | Use it to study | -| --- | --- | -| Hello world | the minimum service contract | -| Research and summarize | inputs, output constraints, and strategies | -| Code review | a practical single-service contract | -| Parallel reviews | fan out, then synthesize | -| Captain's chair | explicit order with `Execution` | - -Plain prompts are still the right tool for one-off work. These examples are useful when you want a workflow to be inspectable, reusable, and worth improving. - -Next: [study common patterns](/use/patterns). diff --git a/content/docs/use/meta.json b/content/docs/use/meta.json deleted file mode 100644 index 6ba3526..0000000 --- a/content/docs/use/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Use", - "pages": ["examples", "patterns", "troubleshooting"] -} diff --git a/content/docs/use/patterns.mdx b/content/docs/use/patterns.mdx deleted file mode 100644 index fef4408..0000000 --- a/content/docs/use/patterns.mdx +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Patterns -description: Common workflow shapes in OpenProse. ---- - -# Patterns - -Patterns are small workflow shapes. They are not a reason to use OpenProse by themselves. First decide the job deserves a program. Then pick the shape. - -Start declarative. Add control only when the workflow needs it. - -1. Contract only: say what the run requires and ensures. -2. Services and wiring: name the roles when handoffs matter. -3. Execution: pin order, loops, and failure policy when choreography matters. - -For exact syntax, use [Specs](/reference/specs). This page is about shape. - -## Fan out, then synthesize - -Use this when branches are independent and the final answer benefits from distinct views. - -Good fits: - -- security, performance, and documentation review of the same change -- separate research on different companies -- multiple translation drafts before one editor pass - -Bad fit: one diagnosis where every branch needs the full context. In that case, a single focused investigator is usually cheaper and better. - -```text -parallel: - security = session "Review for security issues" - performance = session "Review for performance issues" - docs = session "Review for documentation impact" - -session "Synthesize the review into prioritized findings" - context: { security, performance, docs } -``` - -In Contract Markdown, this usually means separate services with clear `Requires` and `Ensures`, then one synthesizer service that requires their outputs. - -## Pipeline - -Use a pipeline when each stage narrows or reshapes the previous artifact. - -```text -let sources = session "Collect relevant source material" - -let claims = session "Extract claims with evidence" - context: sources - -let recommendations = session "Turn claims into recommendations" - context: claims - -session "Format recommendations as a report" - context: recommendations -``` - -Keep the context narrow. Pass the artifact the next stage needs, not everything the run has seen. - -## Worker plus critic - -Use this when review pressure is part of the work. - -```text -let draft = session "Draft the migration plan" - -let critique = session "Find concrete risks, missing steps, and unsupported claims" - context: draft - -session "Revise the plan and address each valid critique" - context: { draft, critique } -``` - -Do not add a critic just because it feels rigorous. For ordinary self-checks, put the verification requirement into the worker's own contract. Use a separate critic when you want an adversarial pass or a different specialty. - -## Retry with learning - -Use a bounded loop when each attempt can learn from the last one. - -```text -let draft = session "Create the first implementation" - -loop until **tests pass and lint passes** (max: 5): - let failures = session "Run checks and summarize failures" - context: draft - - draft = session "Fix the implementation using the failure summary" - context: { draft, failures } -``` - -Every loop needs a concrete exit condition and a maximum. "Improve until good" is not a condition. - -## Controlled-burn exploration - -Use controlled exploration when several approaches might work and you want useful candidates without spending forever. - -```text -parallel ("any", count: 3, on-fail: "ignore"): - session "Try a minimal implementation" - session "Try a library-based implementation" - session "Try a data-model-first implementation" - session "Try a test-first implementation" - -session "Compare the first three successful approaches" -``` - -Set the budget before the run. Decide how many candidates are enough. Stop when you have the signal you came for. - -## Cheap pass first, expensive pass later - -Use a cheap first pass when every item needs some signal, but only a few deserve deeper work. - -```text -let screened = session "Score every item with fast local evidence" - -let shortlist = session "Select the top items that need deep review" - context: screened - -session "Perform deep review only on the shortlist" - context: shortlist -``` - -This pattern keeps the deferred items useful. A shallow score for every item is better than rich detail for the top slice and nothing for the rest. - -## Batch similar work - -Do not spin up one session per trivial item. - -```text -session "Classify each item and return structured results" - context: items -``` - -Use per-item fan-out when the items are heavy, independent, or likely to fail separately. Batch when the task is simple and the output schema is crisp. - -## Pattern checks - -A pattern is probably wrong if: - -- one focused session would have better context than several branches -- branches depend on each other but are marked parallel -- a loop has no maximum -- a critic gives generic taste notes instead of evidence -- every stage receives the whole run context -- the program uses the deepest model for every task -- the workflow would be easier as a normal script - -OpenProse should make the work more inspectable, not just more elaborate. - -Next: [troubleshooting](/use/troubleshooting). diff --git a/content/docs/use/troubleshooting.mdx b/content/docs/use/troubleshooting.mdx deleted file mode 100644 index 6a17c59..0000000 --- a/content/docs/use/troubleshooting.mdx +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: Troubleshooting -description: Common OpenProse confusions and how to correct them. ---- - -# Troubleshooting - -Most OpenProse problems are not mysterious. They come from treating it like a shell tool, a prompt template, or a normal workflow framework. - -The quick rule: - -> If the docs and the `open-prose` skill disagree, trust the skill. - -## The agent tried to run `prose` as a shell command - -Inside an agent session, `prose run file.md` is an instruction to the agent. It is not usually a request to find a `prose` executable on `PATH`. - -The right behavior is: - -1. Load the `open-prose` skill. -2. Read the program. -3. Wire services if the program declares them. -4. Execute through the current harness primitives. -5. Write run state under `.prose/runs/`. - -From a normal shell, the CLI can pass the same instruction into a supported harness. That CLI is a bridge into the agent session, not the VM itself. - -See [for AI agents](/agents/for-ai-agents) for the stricter version. - -## The program is too vague - -A vague OpenProse program usually has vague `Ensures`. - -Weak: - -```markdown -### Ensures - -- `report`: a good report -``` - -Better: - -```markdown -### Ensures - -- `report`: concise Markdown report with findings, evidence, risks, and next steps -- `evidence`: links or file paths supporting each major claim -``` - -The agent still has room to use judgment. The contract should make the obligation clear enough that another agent can tell whether the run fulfilled it. - -## Forme picked the wrong service - -Forme wires by contract meaning, not just by string equality. Exact names help, but they are not the whole system. - -If wiring is ambiguous: - -- make `Requires` and `Ensures` more specific -- rename outputs so their intent is visible -- add `Shape` constraints when a service must not take over another role -- add explicit `Wiring` only when the relationship really needs to be pinned - -Do not jump straight to `Execution` just because the first draft was ambiguous. Tighten the contracts first. - -## The run produced a pasted blob - -OpenProse is meant to pass artifacts by path whenever possible. - -Services should do real work in their private workspace. Declared outputs should be copied into bindings. Downstream services should receive pointers to those artifacts, not a giant paste of every intermediate result. - -If the run is bloating context: - -- ask the service to write files instead of returning long text -- make each `Ensures` output an artifact with a clear path -- have the synthesizer read only the artifacts it needs -- keep scratch work out of bindings - -See [workspaces, bindings, and traces](/think/workspaces-bindings-traces). - -## A subagent did the coordinator's job - -This is usually a shape problem. - -The coordinator should wire, delegate, and synthesize. A worker should do its own bounded service. If a worker starts running the whole program, the contract needs a clearer boundary. - -Useful fixes: - -- add `Shape` language that names what the service may and may not do -- split a large service into smaller services -- make upstream inputs and downstream outputs explicit -- tell the coordinator not to hand the full manifest to every subagent - -OpenProse works best when each agent has the smallest useful view of the work. - -## The workflow is slower than a prompt - -It might be the wrong fit. - -OpenProse adds structure. That structure is useful when you need reuse, handoffs, parallel work, retries, or a trace. It is waste when the task fits in one good prompt. - -If a run feels heavy, ask: - -- Would I run this again? -- Do I care what happened after the fact? -- Does another agent need to understand the handoff? -- Would parallel work or review improve the result? - -If the answer is no, use a prompt. - -## The docs and skill disagree - -Trust the skill. - -These docs explain the shape of OpenProse for humans. The `open-prose` skill and its linked reference specs are the execution source of truth for agents. - -If the site is stale, fix the site. Do not teach agents to infer runtime behavior from a friendly docs page. - -Next: [agent instructions](/agents/for-ai-agents). From 509c37005acc96b735a19bc14039ec4381a42527 Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:04:24 -0700 Subject: [PATCH 12/14] docs: unify OpenProse + Reactor; full SDK reference; shipped 0.3.0; agent-first --- content/docs/cli/command-reference.mdx | 168 +++++++- content/docs/cli/compile-run-serve.mdx | 47 ++- content/docs/cli/connectors-and-sandbox.mdx | 26 +- content/docs/cli/overview.mdx | 95 ++++- content/docs/cli/quickstart.mdx | 102 ++++- content/docs/index.mdx | 113 ++--- content/docs/meta.json | 2 +- content/docs/openprose/contracts.mdx | 195 +++++++++ content/docs/openprose/declare-outcomes.mdx | 186 ++++++++ content/docs/openprose/harness-agnostic.mdx | 136 ++++++ content/docs/openprose/index.mdx | 147 +++++++ content/docs/openprose/meta.json | 4 + content/docs/openprose/prosescript.mdx | 376 +++++++++++++++++ content/docs/openprose/setup.mdx | 151 +++++++ content/docs/reactor-devtools/index.mdx | 6 +- content/docs/reactor-devtools/reference.mdx | 6 +- .../state-dirs-and-replay.mdx | 14 +- content/docs/reactor/index.mdx | 158 ++++--- content/docs/sdk/adapters.mdx | 266 ++++++++++++ content/docs/sdk/agents.mdx | 399 ++++++++++++++++++ content/docs/sdk/front-door.mdx | 370 ++++++++++++++++ content/docs/sdk/index.mdx | 185 ++++++++ content/docs/sdk/internals.mdx | 215 ++++++++++ content/docs/sdk/meta.json | 4 + content/docs/sdk/run.mdx | 284 +++++++++++++ .../src/count-summary.prose.md | 46 ++ .../src/counter-events.prose.md | 67 +++ 27 files changed, 3588 insertions(+), 180 deletions(-) create mode 100644 content/docs/openprose/contracts.mdx create mode 100644 content/docs/openprose/declare-outcomes.mdx create mode 100644 content/docs/openprose/harness-agnostic.mdx create mode 100644 content/docs/openprose/index.mdx create mode 100644 content/docs/openprose/meta.json create mode 100644 content/docs/openprose/prosescript.mdx create mode 100644 content/docs/openprose/setup.mdx create mode 100644 content/docs/sdk/adapters.mdx create mode 100644 content/docs/sdk/agents.mdx create mode 100644 content/docs/sdk/front-door.mdx create mode 100644 content/docs/sdk/index.mdx create mode 100644 content/docs/sdk/internals.mdx create mode 100644 content/docs/sdk/meta.json create mode 100644 content/docs/sdk/run.mdx create mode 100644 vendor/prose-examples/basic-unit-suite/src/count-summary.prose.md create mode 100644 vendor/prose-examples/basic-unit-suite/src/counter-events.prose.md diff --git a/content/docs/cli/command-reference.mdx b/content/docs/cli/command-reference.mdx index eaa2cef..020ed64 100644 --- a/content/docs/cli/command-reference.mdx +++ b/content/docs/cli/command-reference.mdx @@ -1,15 +1,21 @@ --- title: Command reference -description: All twelve reactor commands, their key flags, and the documented exit codes. +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. Run `reactor --version` (or `-v`) for the CLI version. +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 are honored by every command and override `reactor.yml`. +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 | | --- | --- | @@ -20,22 +26,119 @@ These are honored by every command and override `reactor.yml`. ## 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. Every other command runs fully offline. +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 project: a gateway, a responsibility, `reactor.yml`, `.gitignore`, and a `README.md`. Flag: `--force`. | -| `reactor doctor [--live]` | offline (`--live` probes) | Report node, SDK, key/deps, offline mode, sandbox, state-dir, and IR health. `--live` runs one smoke render. | -| `reactor compile [--force] [--check]` | live (cache hit and `--check` are offline) | Run the compile sessions into the IR cache. `--check` exits non-zero when stale. | -| `reactor run` | live | Ensure the IR is fresh, boot, drain to quiescence, report, and exit. | -| `reactor serve [--http ] [--concurrency ] [--poll-interval ]` | live | Boot the durable host, the continuity loop, and the HTTP surface. | -| `reactor trigger [--data \|@file]` | live | Trigger a node with an external wake (one-shot mount, or `POST` to a daemon). | -| `reactor status` | offline | Standing compile cost beside live run cost plus dispositions. | -| `reactor topology` | offline | Print the compiled DAG: nodes with wake source, and resolved edges. | -| `reactor inspect [--strict]` | offline | A node's topology position, fingerprints, last receipt, and chain. | -| `reactor logs [--node ]` | offline | The receipt stream, optionally filtered to one node. | -| `reactor trace []` | offline | Each node's receipt chain: wake to disposition. | -| `reactor receipts [list\|verify\|cost] [--node ]` | offline | Audit the receipt trail. `verify` exits non-zero on a broken chain. | +| `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 @@ -43,14 +146,33 @@ The CLI uses stable, documented exit codes so it composes in CI and scripts. | Code | Meaning | | --- | --- | -| `0` | Success, or healthy. A help or version display also exits `0`. | -| `1` | A reported failure with an actionable message: 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, or a missing argument, surfaced by the arg parser. | +| `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`. +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 -- [Quickstart](/cli/quickstart) walks the commands end to end. -- [Compile, run, serve](/cli/compile-run-serve) covers the three core verbs in depth. -- [Observability](/cli/observability) covers `status`, `inspect`, `topology`, `logs`, `trace`, and `receipts`. + + + + + + diff --git a/content/docs/cli/compile-run-serve.mdx b/content/docs/cli/compile-run-serve.mdx index f5d4ff0..68acda6 100644 --- a/content/docs/cli/compile-run-serve.mdx +++ b/content/docs/cli/compile-run-serve.mdx @@ -1,11 +1,11 @@ --- title: Compile, run, serve -description: The three core verbs in depth, the content-addressed compile cache, and the durable daemon. +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. +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 @@ -19,7 +19,7 @@ reactor compile [--force] [--check] 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 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 @@ -45,7 +45,7 @@ reactor run ## serve ```sh -reactor serve [--http ] [--concurrency ] [--poll-interval ] +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`. @@ -62,21 +62,31 @@ Each tick, the driver polls gateways for ingress, then polls continuity across e ### The HTTP surface -`--http ` binds a zero-framework `node:http` server. The endpoints: +`--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 | -| --- | --- | -| `POST /trigger/` | An external wake of `` (the webhook or manual ingress), serialized behind that reactor's queue. | -| `GET /health` | Liveness: boot done, reactor count. | -| `GET /status` | The cost rollup plus node count and queue depth. | -| `GET /cost` | The cost rollup (the headline observability). | -| `GET /topology` | The node ids. | -| `GET /receipts` | The ledger receipt stream. | -| `GET /nodes/` | A node's published fingerprints plus its last receipt. | +| 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. -The HTTP surface is namespaced per reactor under `//...`, with the prefix omitted for a single-reactor host. There is no auth in v1: the host is one process for one operator. +### Bind address and safety -`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. +`--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 @@ -105,3 +115,8 @@ reactor trigger [--data |@file] `--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/connectors-and-sandbox.mdx b/content/docs/cli/connectors-and-sandbox.mdx index 098e06a..c7b6674 100644 --- a/content/docs/cli/connectors-and-sandbox.mdx +++ b/content/docs/cli/connectors-and-sandbox.mdx @@ -7,6 +7,8 @@ description: Gateways and connectors with durable idempotency cursors, and the r 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: @@ -15,7 +17,7 @@ A gateway is an external-driven entry point: a `kind: gateway` contract that acc - **`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`: +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: @@ -45,12 +47,20 @@ Idempotency is durable. A per-source cursor dedups arrivals, so a restart never 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. @@ -79,6 +89,8 @@ sandbox: 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: @@ -88,3 +100,15 @@ If Docker is absent when `mode: docker`, the run degrades to the bounded shell w ``` 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/overview.mdx b/content/docs/cli/overview.mdx index f27ee7a..c8c1ecc 100644 --- a/content/docs/cli/overview.mdx +++ b/content/docs/cli/overview.mdx @@ -1,26 +1,39 @@ --- title: Reactor CLI -description: The reference command-line client that compiles a .prose project and serves it as a durable, cost-observable daemon. +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`. +`@openprose/reactor-cli` is the deterministic command-line **driver** for the [`@openprose/reactor`](/reactor) SDK. The command is `reactor`. -The CLI does one 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. +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 -g @openprose/reactor-cli +npm install --save-dev @openprose/reactor-cli @openprose/reactor @openai/agents zod +# then: npx reactor ... ``` -Or per-project, alongside the SDK and its peer dependencies: +To touch the keyless replay with **no install at all**, see [DevTools](/reactor-devtools): ```sh -npm install --save-dev @openprose/reactor-cli @openprose/reactor @openai/agents zod +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. @@ -30,14 +43,62 @@ The CLI is the reference client for the SDK's three-phase lifecycle. 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 -reactor init my-project # scaffold a gateway + responsibility + reactor.yml +npx reactor init my-project # scaffold a gateway + responsibility + reactor.yml cd my-project -reactor doctor # check node, SDK, key/deps, sandbox, state-dir, IR -reactor compile # run the compile sessions -> IR cache -reactor run # boot, drain to quiescence, print dispositions + cost -reactor serve --http 8080 # boot the durable host + continuity loop + HTTP surface +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. @@ -55,6 +116,16 @@ You can force offline mode on any command with `--offline` (or `REACTOR_OFFLINE= ## Where to go next + + + 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 -reactor init my-project +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, live-key presence (it never prints the key), live-dep presence, the sandbox mode, whether the state directory is writable, and the compiled-IR freshness. +`reactor 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 -reactor doctor +npx reactor doctor ``` ```text reactor doctor node v22.3.0 (ok) - sdk @openprose/reactor@0.2.0 (resolved) + 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. +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 and freezes them into the content-addressed IR cache. It needs a live key (`OPENROUTER_API_KEY`) plus the `@openai/agents` and `zod` peer deps. +`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 -reactor compile +export OPENROUTER_API_KEY=... # doctor confirms it's present, never echoes it +npx reactor compile ``` -An unchanged contract set recompiles at zero session cost because the cache key is content-addressed. To check freshness without compiling (handy in CI), use `--check`, which exits non-zero when the cache is stale. +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 -reactor compile --check # exits 1 right after init, before the first compile +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 -reactor topology # the compiled DAG (inbox -> digest) +npx reactor topology # the compiled DAG (inbox -> digest) ``` ## Drive the static gateway @@ -83,19 +109,55 @@ The scaffold's `inbox` gateway uses a `static` connector: its seeded items are i `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 -reactor serve --http 8080 +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 -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 +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 -- [Configuration](/cli/configuration) covers every `reactor.yml` key. -- [Compile, run, serve](/cli/compile-run-serve) goes deep on the three core verbs. -- [Connectors and sandbox](/cli/connectors-and-sandbox) explains gateways and the render sandbox. + + + + + + + diff --git a/content/docs/index.mdx b/content/docs/index.mdx index 467efec..d2bd488 100644 --- a/content/docs/index.mdx +++ b/content/docs/index.mdx @@ -1,52 +1,53 @@ --- -title: OpenProse Documentation -description: Reactor -- React.memo for expensive LLM work. Cost scales with surprise, not the clock. +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. --- -# Reactor: `React.memo` for expensive LLM work +# OpenProse -**Reactor (`@openprose/reactor`) is a small, open-source harness for AI work that has to _keep being true_ after a chat ends.** +**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**. -You declare the truths you want kept current as OpenProse **Responsibilities** -- standing goals. Reactor keeps a composed **world-model** up to date against a changing world, re-renders only the responsibilities whose inputs actually moved, and leaves a content-addressed **receipt** behind every decision. +**Reactor (`@openprose/reactor`) is the harness built to run OpenProse** -- and the recommended **fast path**. It keeps a composed **world-model** up to date against a changing world, re-rendering only the declared facets whose upstream inputs actually moved (memoized agent sessions wired into a DAG), and leaves a content-addressed **receipt** behind every decision. + +These are not two systems. They are **one system, two layers that fit together**: OpenProse is the language you author in; Reactor is the deterministic host that serves it efficiently. The thesis Reactor works toward: + +> **Inference cost that scales with surprise, not wall-clock time.** In plain terms: you declare what should stay true, the system watches the world, and it does expensive model work **only when something material actually moved**. +## Start here + + + -## 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 reconciler that decides _whether to wake_ is deliberately **dumb and deterministic** -- there is **no judge step**. The intelligence is frozen ahead of time, at compile, into a per-node canonicalizer and the Forme wiring. The memo key has no clock in it. - ## The shape of a contract -A Reactor program is Markdown with a contract. 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 --- @@ -70,40 +71,56 @@ 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's 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. The compile phase lowers it into a deterministic canonicalizer; at run time the reconciler only ever _compares_ fingerprints. The fingerprint moves iff the material content moves -- so a re-poll that returns the same truth costs nothing. +`### 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. -The other kinds are `gateway` (an external source -- webhook/cron/manual ingress), `function` (a called helper with `### Parameters` → `### Returns`, never a node), `pattern` (reusable coordination expanded at compile), and `test` (assertions over a subject's world-model or receipts). +## 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 Reactor is to replay a real saved run and read the per-node `rendered`/`skipped` dispositions, the cost rollup split by surprise cause, and per-node chain-verify -- with no key and no spend: +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: -```sh -npm i -g @openprose/reactor @openprose/reactor-cli @openprose/reactor-devtools -reactor-devtools --example masked-relay --describe +```bash +npx -p @openprose/reactor-devtools reactor-devtools --example masked-relay --describe ``` ```text -dispositions: rendered 46 · skipped 31 (reuse 32%) -cost rollup by surprise-cause: external 8 · input 69 total 77 -CHAIN-VERIFY ok -``` - -That is "cost scales with surprise" -- checkable, with no key and no spend. From there, scaffold and inspect a project of your own, all offline: +dispositions rendered=46 · skipped=31 · failed=0 +surprise-cause external=8 · input=69 (a.k.a. wake-cause) ← receipt COUNTS, 77 total -```sh -reactor init my-project && cd my-project -reactor doctor # what's present + the fix for anything missing -reactor compile --check; echo "exit=$?" # offline; exits 1 if the contract set is STALE +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 ``` -To go live you set a model key and the two optional peers, then `reactor compile` to wire the DAG and `reactor serve` to drive the scaffold's static gateway to a real receipt. The [Quickstart](/cli/quickstart) walks the full path. - -## Works with any harness -- Reactor is the fast path +That is "cost scales with surprise" -- checkable, with no key and no spend. 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). -Reactor is built on **OpenProse**: Markdown contracts that run on any Prose-Complete agent harness. The `.prose.md` contracts are harness-agnostic. Reactor itself is a real SDK + CLI + DevTools you install and run -- the deterministic host that compiles, runs, and inspects standing responsibilities, and the recommended fast path for them. +## 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. +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. --- diff --git a/content/docs/meta.json b/content/docs/meta.json index 269c2f6..9cbc469 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -1,4 +1,4 @@ { "title": "OpenProse", - "pages": ["index", "reactor", "cli", "reactor-devtools"] + "pages": ["index", "openprose", "reactor", "sdk", "cli", "reactor-devtools"] } diff --git a/content/docs/openprose/contracts.mdx b/content/docs/openprose/contracts.mdx new file mode 100644 index 0000000..fbbf95a --- /dev/null +++ b/content/docs/openprose/contracts.mdx @@ -0,0 +1,195 @@ +--- +title: Contracts +description: The authored surface of OpenProse -- Markdown contracts, the five kinds, and the load-bearing sections that turn a declaration into a typed, subscribable node. +--- + +# Contracts + +A contract is the thing you author. It is a Markdown file -- `*.prose.md` -- +with a small YAML frontmatter and a handful of `###` sections. The Markdown, +with its durable trail, is the public artifact: it can leave for any +Prose-Complete harness with no lost semantics. The deployment's secrets and +data stay private. + +Authors are co-equal. A human and an agent write contracts the same way, read +the same sections, and fork or compose each other's work without translation. +Every section below is designed for both readers from the start. + +This page is the authored surface only -- what you write. The runtime that +*serves* these contracts (fingerprints, memoization, the continuity clock, +receipts) is the Reactor harness's concern; see +[the Reactor section](/reactor) for that. Here, intent lives only in the +contract. + +## Frontmatter and identity + +A contract opens with YAML frontmatter. Two fields carry weight: + +- `kind:` -- which of the five contract kinds this is (below). This is the one + field that changes how the file is executed. +- `name:` -- the human-facing slug. + +A third field, `id:`, is a durable identity (a base32 token) minted once by +tooling and preserved across `name:` and filename renames. You do not +hand-write `id:`; tooling manages it. `name:` is what you read; `id:` is what +keys the node's world-model and receipt-ledger state on disk. + +## The five kinds + +`kind:` selects one of five authored kinds. Each has a different run model. + +| Kind | Run model | Purpose | +| --- | --- | --- | +| `responsibility` | Served (continuously reconciled) | The headline kind: a mounted DAG node maintaining a standing truth over time. | +| `function` | Called (one-shot) | A stateless, ephemeral helper. Bind `### Parameters`, run one render, return `### Returns`. No Forme phase, no world-model. | +| `gateway` | Mounted as external-driven | Sugar for an external-driven responsibility. Compiles into a trigger registration for `reactor serve`; refuses a direct `run`. | +| `test` | Via the test path | Fixtures plus natural-language assertions against a subject responsibility or function. | +| `pattern` | Instantiated at compile time | A reusable coordination algorithm, expanded into nodes at compile time. Never directly run. | + +There is **no `kind: service`** (it was renamed to `function`) and **no +`kind: system`** (deleted). Cross-node composition is a Forme-wired +subscription between responsibilities; intra-node composition is an imperative +`call` inside one render. There is never an internally-autowired graph kind. + + +The headline kind is `responsibility`. The rest of this page teaches its +load-bearing sections, because those are what make a contract a *typed, +subscribable node* rather than a bare prompt. + + +## The load-bearing sections + +Most `###` sections mean exactly what their name says. `### Description` is a +human summary preserved for readers, not a contract. `### Goal` is the render's +one-sentence standing intent. These defer to the obvious. + +Three sections carry the semantic load of a `responsibility`, and the contract +must *state* them rather than defer: `### Requires`, `### Maintains`, and +`### Continuity`. + +### `### Maintains` -- the world-model schema (four jobs at once) + +`### Maintains` is the schema for the truth this node keeps current. It does +four jobs in one section: + +1. It **types** the maintained truth -- the shape of the world-model the node + owns. +2. It carries the **canonicalization spec** -- which fields are material and + how they normalize. This compiles into the node's fingerprint, the cheap + content identity the run phase compares. +3. It declares **facets** (below) -- the named parts of the truth. +4. It states **postconditions** -- the obligations a render must satisfy + before it may commit. + +There is no separate judge and no `### Criteria` section. Satisfaction folds +into `### Maintains`: checked deterministically where it can be written as a +validator, and self-attested by the render where it is semantic. A render that +cannot satisfy its postconditions commits nothing -- the prior truth stands, +and a `failed` receipt records why. + +### Facets make subscription structural + +A `####` sub-heading inside `### Maintains` declares a **facet** -- a named +part of the truth. The facet's name is, at once, three things: + +- its **fingerprint unit** (the canonicalizer emits one token per facet, plus + an always-on atomic token over the whole truth); +- its **subscription symbol** (a consumer names it in `### Requires`, and the + reconciler wakes that consumer only when *that* facet's token moves -- + `Requires.` ↔ `Maintains.`); +- and its **region** of the world-model. + +Declaring no parts is the atomic default -- one truth, one token -- and costs +nothing. This adds no new grammar: it reuses the heading hierarchy and the +Requires↔Maintains join. **Structure is subscription.** The way you shape the +`### Maintains` section *is* the way downstream nodes subscribe to it. + +### `### Requires` -- what the node subscribes to + +`### Requires` names the upstream facets this node consumes. Forme matches each +entry to a producer's `### Maintains` facet and draws the subscription edge. +`### Requires` is the input side of the memo key: the run phase fingerprints +the node's contract together with its subscribed inputs, and re-renders only +when one of them moves. + +### `### Continuity` -- the wake source + +`### Continuity` declares when, beyond a subscribed input changing, a node +should re-render. It is **not a schedule**. A node is *input-driven* by default +(it wakes when a subscribed facet moves). `### Continuity` adds one of: + +- a *self-driven* cadence -- the truth goes stale on its own. A `valid_until` + freshness state lives in the world-model as data; `### Continuity` carries + only the *policy* that reads it on a forecast cadence. When a `valid_until` + lapses, the harness mechanically moves the facet's fingerprint and wakes the + node -- no model call to decide that time has passed. +- *external-driven* -- marks a `kind: gateway` so an ingress event wakes it. + +The author writes these sections. The harness owns the fingerprinting, the +forecast cadence, the receipts, and the subscription wiring. You declare the +truth; the runtime decides, deterministically, when it is worth recomputing. + +## Functions: the call interface + +A `kind: function` does not maintain a world-model and is not part of the DAG. +It declares a plain call interface instead of the four-jobs schema: + +- `### Parameters` -- the inputs bound at call time. +- `### Returns` -- the return value of its single render. + +A lone function has no Forme phase: bind parameters, spawn one render, return +`### Returns`. It is the stateless, ephemeral helper. + +## A contract in full + +Here is a real, migrated `responsibility`. Read it top to bottom: the `> ` +description states intent for a human, `### Requires` names the single facet it +subscribes to, `### Maintains` types the `CountSummary` truth and declares one +`#### structured` facet with an explicit postcondition, and `### Continuity` +declares it `input-driven` -- so it wakes only when its `counts` input moves. + + + +And here is the `gateway` that feeds it. A gateway has no `### Requires` (its +input arrives from outside the graph). Notice how `### Maintains` splits the +ledger into two `####` facets -- `counts` and `raw_events` -- so a +metadata-only event moves `raw_events` (waking the auditor) without moving +`counts` (the summary above stays dark). That split *is* the subscription wiring. + + + +A bare prompt is `any`: it tells an agent what to do once and forgets. A +contract is a typed function over the world -- a declared truth with a fingerprint, +named facets others can subscribe to, and postconditions it must satisfy before +it commits. That is the whole difference, and it is authored entirely in +Markdown. + +## Where to go next + + + + + + + + +For the full language semantics, the authoritative source is the OpenProse +spec ([01-Language.md](https://github.com/openprose/prose/blob/main/spec/01-Language.md)) +and the loaded `open-prose` SKILL. If the docs and the skill ever disagree, +trust the skill. diff --git a/content/docs/openprose/declare-outcomes.mdx b/content/docs/openprose/declare-outcomes.mdx new file mode 100644 index 0000000..8bbd994 --- /dev/null +++ b/content/docs/openprose/declare-outcomes.mdx @@ -0,0 +1,186 @@ +--- +title: Declare outcomes, not instructions +description: The central teaching of OpenProse -- author an ideal world-model to keep true, not a sequence of steps to run. Intent lives only in the contract. +--- + +# Declare outcomes, not instructions + +OpenProse asks one thing of you that most agent frameworks do not: **declare the +truth you want kept current, and stop there.** Not the steps. Not the order. Not +the prompt that coaxes a model through them. You write down the world as it +should be -- an ideal world-model the system is responsible for making real and +keeping real -- and the host figures out the work. + +This is the inversion at the heart of the paradigm, and everything else (the +five contract kinds, fingerprints, the DAG, receipts) is downstream of it. + +## Instructions decay; outcomes endure + +A prompt is a sequence of instructions aimed at one moment. It runs, it produces +something, and then the moment passes. When the world moves -- a new filing +lands, a competitor ships, a number drifts -- the instructions have nothing to +say, because they were never about the world. They were about a turn. + +An **outcome** is different. "Every tracked competitor has a current, +corroborated funding view" is true or it is not, today and tomorrow and after +the source you scraped changes its layout. It is a standing claim about the +world, not a recipe for one visit to it. Declare the outcome and you have +declared something that can be *re-checked* and *re-established* without you +re-typing the steps. + +That is the whole shift: + +| You stop writing | You start writing | +| --- | --- | +| "First fetch X, then summarize, then compare to Y..." | "This truth, with these fields, kept current and corroborated." | +| a sequence pinned to one run | a standing goal that survives many runs | +| intent spread across a prompt, a config, and a judge | intent in exactly one place | + + +This is the language layer. It says **what** is true and **what** counts as a +material change. It deliberately says nothing about *how often* the host checks, +*how* it decides something moved, or *what* it records along the way. Those are +runtime mechanics, owned by a harness like +[Reactor](/reactor) -- and deferred to that section on purpose. + + +## Intent lives only in the contract + +The first OpenProse tenet states it plainly: **intent lives only in the +contract.** The `*.prose.md` you author carries 100% of the semantic weight. +Everything else -- the compiled artifacts, the projections, the operational +policy a host applies -- is *derived*, and when a derived thing disagrees with +your contract, the contract is right. + +There is no second authored surface for intent. Not a prompt you tune on the +side. Not a YAML config that quietly overrides the Markdown. Not a hidden judge +prompt deciding what you really meant. If you find yourself reaching for one of +those, the meaning has leaked out of the contract, and the system can no longer +guarantee the outcome you declared -- because you declared it in two places that +can drift apart. + +One source of meaning is not a stylistic preference. It is what lets the same +contract run identically on any compliant host, what lets an agent fork and +compose your work without archaeology, and what makes the audit trail honest: +there is exactly one thing the trail can be checked against. + +## The world-model: a maintained truth, like the DOM + +When you declare an outcome, the thing the system keeps real on your behalf is +the **world-model** -- a node's maintained truth, persisted on disk, standing +between one unit of work and the next. + +If you know React, you already know the shape. The world-model is the Reactor's +**DOM**: a current, structured representation that survives between renders, is +read by the next render as its prior state, and is subscribed to by whatever +depends on it. You do not rebuild it from scratch each turn. You declare its +*schema* -- its shape, and what about it actually matters -- and the system keeps +that truth current against a changing world. + +A render, then, is one bounded step from prior truth to next truth: + +```text +(contract, evidence, prior world-model) -> (new world-model, receipt) +``` + +Read it as a sentence. Given **what you declared** (the contract), **what the +world now shows** (evidence), and **what was true last time** (the prior +world-model), one bounded session computes **the truth as it should now be** (the +new world-model) and leaves **a record of the decision** (the receipt). The +outcome you declared is the contract on the left and the world-model on the +right. The instructions you did *not* write are what the session figures out in +the middle. + + +You declare the world-model's schema -- its fields, what counts as a material +change, and how its parts divide for subscription. The host owns *how* it decides +a change occurred (fingerprints), *when* it re-checks (the reconciler), and *what* +it records (receipts). See +[World-model and fingerprints](/reactor/world-model-and-fingerprints) for those +mechanics; this page is only about the act of declaring the truth. + + +## A type system for agent workflows + +Here is the mental model that makes the discipline pay for itself. + +Think of a bare prompt as `any`. It runs, and nothing is checked: no declared +inputs, no declared output, no way for a caller to reason about it or for a +violation to fail loudly. A contract is a **typed function**. Its inputs and +outputs are declared, its callers can reason about composition, and an unmet +obligation fails where you can see it rather than rotting silently downstream. + +You would not write a two-thousand-line TypeScript system in `any`. Multi-step +agent work is the same. Declaring outcomes is how you give an agent workflow a +type: the contract states what must be true, so the system can check that it is. + +## What you actually write + +Declaring an outcome is concrete. A standing goal becomes a +`kind: responsibility` -- the headline kind, mounted as a node whose truth is +maintained over time. You name the truth it keeps current and what counts as a +material change to it; the host does the rest. + +```markdown +--- +name: competitor-funding +kind: responsibility +--- + +### Goal + +Keep a current, corroborated funding view for every tracked competitor. + +### Maintains + +A funding view per competitor: each carries a stable `name`, a set of funding +events (round, amount, date), and a `last_corroborated` field. Material: the +event set (unordered) and each event's round, amount, and date. Immaterial +everywhere: `fetched_at` and source request ids. Postcondition: every funding +event cites a source. +``` + +There is no `### Execution` here, and that absence is the point. You did not +write the steps. You declared the outcome -- the truth, its shape, what matters +about it, and what must hold before it may be committed -- and a render figures +out the work each time the world moves. `### Maintains` is doing the load-bearing +job: it is the world-model schema, and learning to author it well is the next +page. + + +Declarative is the default, not a cage. When a step genuinely must happen in a +specific way -- a tool that must run, an order that must hold -- OpenProse has an +optional imperative layer (ProseScript) for pinning exactly that, and nothing +more. The rule is "declarative by default, explicit when needed," and the +explicit part stays subordinate to the declared outcome. See +[ProseScript](/openprose/prosescript). + + +## Where to go next + + + + + + + +These docs are orientation. The canonical execution behavior lives in the +open-source `open-prose` skill in the +[`openprose/prose`](https://github.com/openprose/prose) repo; if the docs and the +skill disagree, trust the skill. + +--- + +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/openprose/harness-agnostic.mdx b/content/docs/openprose/harness-agnostic.mdx new file mode 100644 index 0000000..0ada937 --- /dev/null +++ b/content/docs/openprose/harness-agnostic.mdx @@ -0,0 +1,136 @@ +--- +title: Harness-agnostic +description: OpenProse contracts describe an abstract VM. Any Prose-Complete host can run them, and the SKILL-loaded session embodies that VM -- there is no parser. Reactor is one host, the recommended fast path. +--- + +# Harness-agnostic + +A `.prose.md` contract does not name a host. It declares an ideal world-model +and, optionally, a fulfillment plan, in terms of an abstract VM. Anything that +can map that VM's primitives onto real capabilities can run the same contract. +This is the deliberate seam at the center of OpenProse: **the language is one +layer, the host is another**, and you author against the language. + +This is why "harness-agnostic" is a property of the contract, not a promise from +any one runtime. The wisdom of the ancients applies: you write to an interface, +and the interface outlives any single implementation. + +## The VM primitives a host must provide + +A host is **Prose-Complete** when it can map the abstract VM operations onto its +own tools. There are five, and they are the entire contract between the language +and any host: + +| Primitive | What the host must do | +| --- | --- | +| `spawn_session` | Run a render -- a responsibility or a called function -- in an isolated agent/session with a prompt, an optional model, and access to the declared input/output paths | +| `ask_user` | Pause for missing required caller input, and resume with the answer | +| `read_state` / `write_state` | Read and write run state through the selected backend (the OpenProse root's run artifacts and durable records) | +| `copy_binding` | Publish a declared output through the active backend; never publish undeclared scratch | +| `check_env` | Confirm an environment variable exists without exposing its value | + +A host that can do these five things can execute any OpenProse contract. The +contract never reaches past this table. It does not know whether `spawn_session` +becomes a subagent call in Claude Code, a `codex-sdk` activation, or a bounded +session inside Reactor -- it only knows that a render runs in isolation with the +paths it declared. + + +The mapping is the host's job, not yours. Codex-style and Claude Code-style +environments are the primary documented targets. A Prose-Complete host with a +real subagent primitive runs multi-agent contracts; a host without one may +execute trivial single-render runs inline and must report the limitation rather +than pretend. + + +## The same Markdown runs on any host + +Because the contract speaks only in VM primitives, the **same file** runs +unchanged across hosts. You do not maintain a Claude variant and a Codex variant +and a Reactor variant. You maintain one `.prose.md`. The host changes; the +declared outcome does not. + +Concretely, a shell line like: + +```bash +prose run src/hello.prose.md +``` + +means "ask the selected agent harness to embody the OpenProse VM and execute +this contract." Swapping the host -- `codex-sdk`, `claude-sdk`, a local `mock`, +or Reactor -- changes who provides the five primitives, not what the contract +asks for. + +## The session embodies the VM -- there is no parser + +This is the load-bearing idea, and it is easy to miss because it inverts the +usual assumption. OpenProse is **never parsed or interpreted**. There is no +`.prose` parser, no bytecode, no interpreter loop that walks the Markdown. + +Instead, a SKILL-loaded agent session **is** the VM. When a Prose-Complete host +loads the `open-prose` skill and reads a contract, the session itself carries +the execution semantics: it resolves the contract, spawns renders, maintains the +world-model, and signs receipts. The intelligence is the runtime. Even a compile +step is an agent session -- the topology, the canonicalizer, and the validators +are all session output, not parser output. + + +A `prose` CLI is not a replacement VM. It forwards a run to the selected harness +and never parses `.prose` semantics itself. The CLI is plumbing; the +SKILL-loaded session is the machine. + + +This is what makes the language portable without a shared runtime binary. There +is nothing to port. Any host that can host a capable agent session, and can map +the five primitives, already has everything the language needs. + +## The language/harness seam + +Keeping the two layers distinct keeps both honest. The boundary is sharp: + +- **The language has one source-derived compile.** `prose compile` lowers the + `*.prose.md` source set into compile-phase IR. That IR is a pure function of + the source set and nothing else -- no clock, no run history, no host state. +- **Runtime mechanics are sibling state, owned by the host.** A host's + token-truth receipts, forecasts, freshness tracking, and reconciler decisions + are runtime state owned by `@openprose/reactor`. They are **not** IR fields and + **not** new `*.prose.md` syntax. A host may keep none of this, some of it, or + all of it; the language does not require any of it. + +So when you read about receipts, fingerprints, memoization, and the reconciler +in the [Reactor section](/reactor), read them as **one host's runtime**, not as +language features. The contract you author is the same whether or not a host +chooses to memoize it. + +## Reactor is one host -- the recommended fast path + +OpenProse contracts run on any Prose-Complete host. **Reactor** (the +`@openprose/reactor` SDK, plus `reactor-cli` and `reactor-devtools`) is the host +built specifically to run them well: it memoizes the agent-session DAG, +re-renders only what moved, and leaves a content-addressed receipt behind every +decision, so cost scales with surprise rather than the clock. + +That makes Reactor the recommended fast path -- not the only path. You can run +the same contracts on a Claude Code or Codex session today. You reach for +Reactor when you want the deterministic reconciler, the durable world-model, and +the inspectable receipt trail. The choice is a host choice; your contracts do +not change. + +## Where to go next + + + + + + +The canonical execution behavior lives in the open-source `open-prose` skill in +the [`openprose/prose`](https://github.com/openprose/prose) repo. These docs are +orientation; if the docs and the skill disagree, trust the skill. diff --git a/content/docs/openprose/index.mdx b/content/docs/openprose/index.mdx new file mode 100644 index 0000000..977a739 --- /dev/null +++ b/content/docs/openprose/index.mdx @@ -0,0 +1,147 @@ +--- +title: OpenProse +description: The foundation -- a programming language for AI sessions. You declare an ideal world-model in Markdown contracts, and any Prose-Complete harness keeps it true. +--- + +# OpenProse + +OpenProse is a programming language for AI sessions. You write a `*.prose.md` +contract that declares an ideal world-model -- the truths you want kept current +-- and an agent host reads that contract, wires the work, and keeps the truths +true. The program is not a Python graph or a hosted workflow. The program is the +Markdown, and it runs inside the agent session itself. + +This is the foundation. [Reactor](/reactor) is the deterministic harness built +to run it. They are not two systems; they are one system, two layers. OpenProse +is the language you author in. Reactor is the recommended host that serves it +efficiently. The contract is harness-agnostic; the same Markdown runs on any +compliant host. + +## Declare outcomes, not steps + +A prompt says *do this*. A contract says *these things must be true when you are +done, and they must stay true as the world changes*. Intent lives only in the +contract (Tenet 1): the `*.prose.md` carries 100% of the semantic weight. There +is no second authored surface for meaning -- not a prompt, not a config, not a +tuned judge. Compiled artifacts and projections are derived views; when one +disagrees with the Markdown, the Markdown is right. + +So you do not script a sequence of model calls. You declare a standing goal and +the shape of the truth that satisfies it, and you let the host figure out the +rest -- which is exactly what makes the work reusable, inspectable, and portable. + +## A type system for agent workflows + +The cleanest way to hold OpenProse in your head is as a type system for agent +work: + +> A bare prompt is `any` -- it runs, but nothing is checked. A contract is a +> typed function: inputs and outputs are declared, callers can reason about +> composition, and violations fail loudly. + +You would not write a 2,000-line TypeScript system in `any`. Multi-step agent +workflows are the same. A contract gives the model a typed shape to satisfy +instead of a pile of instructions, and gives you something better than vibes to +inspect afterward. + +## The render atom + +Underneath every kind of contract is one operation that both the compile and run +phases agree on -- the render atom: + +```text +(contract, evidence, prior world-model) -> (new world-model, receipt) +``` + +A bounded agent session reads the contract and the new evidence, queries the +truth it maintained last time, computes the next world-model, and signs a +receipt. The receipt is the durable commit; it is the audit record, the +composition edge, and the exit ticket all at once (Tenet 5). Continuity lives in +that trail, not in a session that runs forever (Tenet 3) -- a one-shot run is +just the degenerate case of a standing one. + +## The authored surface is small and complete + +The whole language is five kinds and a fixed set of `###` sections. + +- **`responsibility`** -- the headline kind. A standing goal mounted as a node + whose maintained truth is kept current over time. Node-ness comes from + *mounting*, never from statefulness. +- **`function`** -- a called, ephemeral helper that binds `### Parameters` and + returns `### Returns`. Never a node. +- **`gateway`** -- an external source (webhook, cron, manual ingress) that + maintains the latest incoming truth. +- **`pattern`** -- reusable coordination, expanded into nodes at compile time. +- **`test`** -- assertions over a subject's world-model or receipts. + +The load-bearing section is `### Maintains`: the world-model schema, doing four +jobs at once. It *types* the maintained truth, carries the *canonicalization +spec* (which fields are material and how they normalize), declares optional +*facets* (named sub-truths a consumer can subscribe to one at a time), and +states *postconditions* (the obligations a render must satisfy before it may +commit). A render that cannot satisfy its postconditions commits nothing -- the +prior truth stands and a `failed` receipt records why. + + +New capability in OpenProse is new *semantics* in the skill docs, never new +syntax or a YAML overlay. The authored surface stays small, stable, and +complete on purpose. + + +## Hands off to Reactor + +OpenProse defines the contract and what it means. It deliberately does **not** +define the runtime that serves it -- the two-phase compile/run split, +memoization, the continuity clock, receipts, and composition are the harness's +concern. [Reactor](/reactor) (`@openprose/reactor` + `reactor-cli` + +`reactor-devtools`) is that harness: it compiles your contracts into a +content-addressed DAG and runs them so that expensive model work happens only +when something material actually moved. Cost scales with surprise, not the clock. + +You can author OpenProse against any Prose-Complete host. Reactor is the +recommended fast path because it makes the maintained-truth model real, +deterministic, and observable. + + + + + + + + + + +## Source of truth + +The canonical execution behavior lives in the open-source +[`open-prose` skill](https://github.com/openprose/prose) (currently `0.15.0`). +These docs are orientation. If the docs and the skill ever disagree, trust the +skill. + +--- + +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/openprose/meta.json b/content/docs/openprose/meta.json new file mode 100644 index 0000000..122813f --- /dev/null +++ b/content/docs/openprose/meta.json @@ -0,0 +1,4 @@ +{ + "title": "OpenProse", + "pages": ["index", "declare-outcomes", "contracts", "prosescript", "harness-agnostic", "setup"] +} diff --git a/content/docs/openprose/prosescript.mdx b/content/docs/openprose/prosescript.mdx new file mode 100644 index 0000000..3d77df4 --- /dev/null +++ b/content/docs/openprose/prosescript.mdx @@ -0,0 +1,376 @@ +--- +title: ProseScript +description: The optional imperative pinning layer inside ### Execution. Declarative by default, explicit when needed. Contract Markdown owns the interface; ProseScript only choreographs one render. +--- + +# ProseScript + +Contract Markdown is declarative. You write down the world you want to keep +true, and the harness decides the graph. ProseScript is the **pinning layer**: +the optional imperative script you reach for when the *order* of work inside a +single render matters and you do not want a host to choose it for you. + +The mantra is **declarative by default, explicit when needed**. ProseScript is +always secondary to the contract. It never declares what a node depends on or +what it produces -- that is the contract's job. It only choreographs the steps +*inside* one render. + + +ProseScript is the **intra-node** layer. `call` invokes a `function`, and +`session` / `agent` / `resume` spawn one-off subagents -- all ephemeral and +internal to producing this node's world-model. Cross-node connections are never +made in ProseScript; they are subscriptions that Forme wires from a +responsibility's `### Requires` to a producer's `### Maintains`. + + +## When to pin, and when not to + +Use Contract Markdown when the *end state* matters and the host can pick the +graph. Use ProseScript when **order, loops, branching, retries, parallelism, or +exact call choreography** matter and you want them written down, not inferred. + +The Prose VM follows pinned choreography exactly. It does not infer new +parallelism, reorder calls, or add missing calls. That precision is the whole +point: a pinned `### Execution` block is a promise that the steps run as +written. + +| You want | Reach for | +| --- | --- | +| A standing truth kept current; the host may choose the graph | Contract Markdown (`### Maintains`) | +| An exact sequence of calls inside one render | ProseScript in `### Execution` | +| Parallel fan-out with a join policy | `parallel` in ProseScript | +| Retry / backoff on a flaky call | `retry` / `backoff` call modifiers | +| A one-off subagent internal to this render | `session` / `agent` / `resume` | + +## Where ProseScript lives + +ProseScript appears on exactly two surfaces. Both are owned by Contract +Markdown, so an embedded script must **not** redeclare caller inputs or public +outputs. + +| Surface | Scope | Primary call style | Interface source | +| --- | --- | --- | --- | +| `### Execution` in a `*.prose.md` | Pinned intra-node choreography | `call function-name` | `### Requires` / `### Maintains` (responsibility) or `### Parameters` / `### Returns` (function) | +| Pattern `### Delegation` | Slot interaction rules inside a pattern instance | `call slot-name` | `### Slots`, `### Config`, and the pattern instance bindings | + +A responsibility declares its interface with `### Requires` / `### Maintains`; a +function declares it with `### Parameters` / `### Returns`. Inside the script, +those declared names are simply in scope. The reactive interface always belongs +to the contract sections -- embedded ProseScript should never restate them. + +## A first pinned block + +Inside Contract Markdown, a fenced `prose` block under `### Execution` pins the +choreography for one render: + +````markdown +### Execution + +```markdown +let findings = call researcher + topic: topic + +let report = call writer + findings: findings + +return report +``` +```` + +`topic` comes from the contract's `### Requires` (responsibility) or +`### Parameters` (function). `return report` hands the result to the enclosing +`### Maintains` or `### Returns`. The VM runs `researcher`, then `writer`, in +that order, every time -- nothing is reordered or inferred. + +## The constructs + +ProseScript is a small, complete language. The constructs below cover everything +the grammar exposes. + +### Bindings and values + +`let` creates a mutable binding; `const` creates an immutable one. Object +results can be destructured. + +```markdown +let draft = call writer + brief: brief + +const threshold = "high confidence" + +let { findings, sources } = call researcher + topic: topic +``` + +Values are strings (`"short"`, `"""multi\nline"""`), numbers, booleans, `null`, +arrays, objects, and references. Object shorthand keeps variable names: +`{ findings, sources }` means `{ findings: findings, sources: sources }`. String +interpolation uses `{name}` or `{object.property}`. Scope is lexical, and loop +variables, block parameters, and catch variables are immutable within their +body. + +### `call` + +`call` invokes a Contract Markdown `function`, a pattern instance or slot, or a +`use`d dependency function. Input bindings are indented `name: expression` +lines; the result is the target's declared outputs. + +```markdown +let response = call external-api + request: request + retry: 3 + backoff: exponential +``` + +`retry` and `backoff` are call modifiers (`backoff` may be `none`, `linear`, or +`exponential`). Each `call` target must resolve to a real function or pattern +slot, or Forme rejects the block. + +### `parallel` + +Branch-local statements run concurrently and join according to modifiers. + +```markdown +parallel: + let security = call security-reviewer + code: code + let performance = call performance-reviewer + code: code + +let report = call synthesizer + context: { security, performance } +``` + +Modifiers control the join: `"all"` (default), `"first"`, `"any"` with +`count: N`, and `on-fail:` policies (`"fail-fast"` default, `"continue"`, +`"ignore"`). Defaults are `("all", on-fail: "fail-fast")`. + +### `for`, `repeat`, and `loop` + +Fixed repetition, collection iteration, and open or model-sized loops: + +```markdown +repeat 3 as attempt: + call generator + attempt: attempt + +parallel for item in items: + call processor + item: item + +loop until all tests pass (max: 5): + let results = call tester + if results include failures: + call fixer + test-results: results +``` + +Open `loop` blocks should carry a `(max: N)` bound -- it is a warning in source +and an error in generated canonical docs. `parallel for` preserves input order +unless a parallel modifier requests race semantics. + +### `if` / `elif` / `else` and `choice` + +`if` runs the first true branch in order. `choice` lets the VM select exactly +one labeled peer branch by criteria. + +```markdown +if review has critical concerns: + call reviser + review: review +elif review has minor concerns: + call polisher + review: review +else: + call approver + +choice best recovery path: + option "retry": + call retryer + option "abort": + throw "No safe recovery path" +``` + +Conditions are **discretion text** -- natural-language conditions the VM +evaluates. They may be bare, `**wrapped**`, or `***multi-line***`. Prefer +concrete, observable conditions over vague ones. + +### `try` / `catch` / `finally` and `throw` + +```markdown +try: + let response = call external-api + request: request + retry: 3 + backoff: exponential +catch as err: + call fallback + error: err +finally: + call cleanup +``` + +`try` runs its body; `catch` handles an unhandled failure; `finally` always +runs. `throw` (bare) re-raises the active error inside `catch`; +`throw expression` raises a new one. A `try` needs at least a `catch` or a +`finally`. + +### `session`, `agent`, and `resume` + +Pinned blocks can spawn direct subagents when the work is intentionally a +one-off internal to this render -- not a reusable `function`. + +```markdown +agent researcher: + model: sonnet + persist: project + prompt: "Research thoroughly and keep a compact project memory." + skills: ["web-search"] + shape: + self: ["research", "source evaluation"] + prohibited: ["writing source files", "running shell commands"] + +let findings = session: researcher + prompt: "Research {topic}" + context: topic + +let review = resume: researcher + prompt: "Review the new draft" + context: findings +``` + +`session "prompt"` spawns a one-off subagent; `session: agent` uses an `agent` +definition; `resume: agent` continues a persistent agent with memory. `shape` is +the ProseScript equivalent of Contract Markdown `### Shape` -- it expresses +behavioral boundaries, not raw secret or permission values. Host sandbox +permissions remain a host adapter concern. + + +Inside `### Execution`, prefer `call function` over a direct `session` when an +equivalent `function` exists -- the VM warns when you skip a real function for an +ad-hoc subagent. Reach for `session` / `agent` / `resume` only when the work is +genuinely an intentional one-off. + + +### Blocks, `do`, and pipelines + +Anonymous `do:` groups sequential statements; named `block` definitions are +reusable local choreography invoked with `do name(...)`. Pipelines transform +collections left to right with `map`, `pmap`, `filter`, and `reduce`. + +```markdown +block review-and-fix(artifact, max_rounds): + let review = call critic + artifact: artifact + if review has critical issues: + return call fixer + artifact: artifact + review: review + return artifact + +let result = do review-and-fix(draft, 3) + +let summaries = articles + | filter: + call relevance-checker + article: item + | map: + call summarizer + article: item +``` + +Block definitions are collected before execution, so a block may be invoked +before its definition. Each invocation gets its own scope, and parameters are +immutable within the call. + +## `use`: declaring dependencies + +`use` declares an external dependency for pinned choreography. It is processed +before agents, blocks, and statements, and follows the same disk-only resolution +rules as `prose run`. + +```markdown +use "github.com/openprose/prose/packages/std/evals/inspector" as inspector + +let inspection = call inspector + run-path: run_path +``` + +`use` is valid in a standalone ProseScript block, but **not** inside embedded +Contract Markdown ProseScript -- declare the dependency in the contract, not in +the render body. + +## The boundary: ProseScript does not own the interface + +This is the single most important rule, restated. ProseScript does not own +public interfaces in current OpenProse source: + +- `### Requires` (responsibility) or `### Parameters` (function) declares the + variables available to `### Execution`. +- `### Maintains` (responsibility) declares the world-model truth the render + must keep current; `### Returns` (function) declares the value it must + produce. +- `return` chooses the execution block's result for the enclosing contract. + +Legacy standalone `.prose` files used `input` and `output` declarations. Treat +those as upgrade inputs, not current syntax -- writing `input` or `output` +inside current ProseScript is an error. Run `prose upgrade --dry-run` to migrate. + +## How a pinned block runs + +Embedded `### Execution` runs in three phases: + +1. **Parse** the lexical structure, blocks, declarations, and statements. +2. **Validate** names, scopes, call targets, inputs and outputs, loop bounds, and + surface-specific restrictions. Forme checks that every `call` target resolves + to a real `function` (local, `use`d, or a `std/` library function) or a + delegated helper the render declares, and that the returned value satisfies + the enclosing `### Maintains` / `### Returns`. +3. **Execute** in source order, using the Prose VM for calls, sessions, state, + bindings, retries, and final result publication. + +Validation errors block execution; warnings do not, but generated canonical docs +should resolve warnings where possible. + +## Pattern delegation + +Pattern `### Delegation` describes how bound slots interact. When written as +fenced `prose`, it is validated as ProseScript with pattern-specific scope: slot +names, `config` keys, and parent-provided inputs are in scope, and `call` may +target slots or nested pattern instances. + +```markdown +loop until critic accepts output (max: config.max_rounds): + let output = call worker + task: task + feedback: feedback + let verdict = call critic + output: output + quality-bar: config.quality-bar + if verdict is not accepted: + let feedback = verdict.feedback + +return output +``` + +Pattern files are not directly runnable. A responsibility instantiates the +pattern and binds its slots before the delegation runs. + +## Where to go next + + + + + + +For the complete grammar, validation tables, and execution model, see the +ProseScript reference in the OpenProse spec. When the docs and the skill +disagree, trust the skill. diff --git a/content/docs/openprose/setup.mdx b/content/docs/openprose/setup.mdx new file mode 100644 index 0000000..7df391d --- /dev/null +++ b/content/docs/openprose/setup.mdx @@ -0,0 +1,151 @@ +--- +title: Setup +description: One ordered path for agents onboarding on behalf of a user -- keyless proof first, then init, doctor, compile, and serve. Where contracts live and what each step needs. +--- + +# Setup + +This is the one ordered path. If you are an agent onboarding on behalf of a user, follow it top to bottom: prove the idea with no key and no spend, scaffold and inspect entirely offline, then go live only when the user is ready to reach a model. + +The binary is `reactor`. The keyless replay viewer is `reactor-devtools`. Both ship from npm and run locally. + + +**Trust before spend.** Nothing below reaches a model provider or costs a token until the final "go live" step. Steps 1 through 3 are fully offline -- you can earn the user's trust by showing the whole shape first, with no key in the environment. + + +## 1. Prove the thesis -- keyless, no model call + +Run a saved sample run and read the per-node dispositions, the receipt counts by surprise cause, the token cost rollup, and the chain verify. No install, no key, no spend: + +```bash +npx -p @openprose/reactor-devtools reactor-devtools --example masked-relay --describe +``` + +```text +dispositions rendered=46 · skipped=31 · failed=0 +surprise-cause external=8 · input=69 (a.k.a. wake-cause) ← receipt COUNTS, 77 total + +COST ROLLUP (tokens) + total fresh=27180 tokens · reused=12840 tokens · reuse=32% + external receipts= 8 fresh= 1080 tokens reused=840 tokens + input receipts= 69 fresh= 26100 tokens reused=12000 tokens +CHAIN-VERIFY ok +``` + +The `surprise-cause` line counts receipts by what woke them; the cost rollup below it is the actual token spend, where `fresh` is what each surprise cost and `reused` is what memoization saved. That is "cost scales with surprise" -- checkable, with no key and no spend. + +Prefer the browser? Drop `--describe` and `reactor-devtools --example masked-relay` boots an animated DAG viewer at a localhost URL: nodes flash on render, dim-pulse on memo-skip, with a live cost meter. + +## 2. Install -- local-first + +The keyless step above needs no install. For the full CLI, prefer a project-local install (no root, no global collisions): + +```bash +npm install @openprose/reactor @openprose/reactor-cli @openprose/reactor-devtools +# then call the binaries with `npx reactor …` / `npx reactor-devtools …` +``` + +`@openprose/reactor` is the SDK engine the CLI drives; it is a real dependency of `@openprose/reactor-cli`, so the install above just makes the SDK and the replay viewer explicit. None of these pull a model provider or a key. + + +**Local install means `npx`.** The bare `reactor …` / `reactor-devtools …` commands below assume the binaries are on your `PATH` (a global install). After a project-local install, prepend `npx` -- for example `npx reactor init my-project`. The keyless `npx -p @openprose/reactor-devtools …` line already does this. + + +A global install (`npm i -g …`) is an alternative, but on Linux/WSL it can fail with `EACCES` and may collide with other tools' binaries. Prefer the local install above. + +## 3. Scaffold and inspect -- keyless + +Everything here runs offline. + +```bash +reactor init my-project && cd my-project +reactor doctor # what's present + the exact fix for anything missing +reactor compile --check; echo "exit=$?" # offline; exits 1 if the contract set is STALE (CI-wireable) +``` + +`reactor init` writes a minimal, compilable project: a gateway, a responsibility that subscribes to it, a `reactor.yml`, a `.gitignore`, and a short README. It refuses to overwrite existing files by default; pass `--force` to clobber a directory that already holds scaffold files. + +`reactor doctor` runs fully offline and reports the node version, SDK resolvability, live-key presence (it never prints the key), live-dep presence, the sandbox mode, whether the state directory is writable, and the compiled-IR freshness. + +`reactor compile --check` is the offline freshness gate. It exits non-zero when the IR cache is stale -- which it is right after `init`, before the first real compile -- so it wires cleanly into CI without ever touching a model. + +### Where contracts live + +Author your OpenProse contracts as `*.prose.md` files under the scaffold's `src/` directory: + +- one `kind: responsibility` per standing goal, carrying its `### Maintains` (the world-model schema it keeps current), `### Requires` (the upstream facets it subscribes to), and `### Continuity` (its wake source); +- optional `kind: gateway` contracts for ingress; +- optional `kind: function` contracts for stateless helpers. + +`src/` is authored intent; the compiler writes its frozen output into `dist/`, and run state (world-models and the signed receipt ledger) lands under the state directory. You write `src/`; Reactor owns the rest. + + +**Structure is subscription.** You never wire the graph by hand. Forme -- the compile-time wiring layer -- matches each `Requires.` to the `Maintains.` that produces it and draws the subscription edge. The DAG assembles itself from the contracts. See [contracts](/openprose/contracts). + + +## 4. Go live -- needs a model key + +These steps reach the model surface. A keyless reader can stop at step 3. To go live, set `OPENROUTER_API_KEY` and add the two optional live peers: + +```bash +npm i @openai/agents zod # the two optional live peers +reactor compile # Forme wires the DAG; freezes per-node canonicalizers +reactor serve --http 8080 # drive the scaffold's static gateway to a real receipt +reactor-devtools .reactor --describe # replay YOUR live run's ledger +``` + +`reactor compile` runs the intelligent compile sessions once and freezes them into the content-addressed IR cache. An unchanged contract set recompiles at zero session cost because the cache key is content-addressed. + + +**Use `serve`, not `run`, for the scaffold.** The scaffold's `inbox` gateway uses a `static` connector: its seeded items are ingested by the `serve` continuity loop, which polls the gateway and stages the seeded arrivals -- moving the inbox fingerprint and waking `digest`. `reactor run` is the one-shot drain for graphs whose connectors emit on their own; on a static scaffold it would boot, find nothing newly arrived, and exit without rendering. + + +`serve` boots the durable host, runs the continuity loop, and binds the built-in HTTP surface; it stays up until you stop it with Ctrl-C. In another shell you can read the trail and the standing cost: + +```bash +reactor status # standing compile cost beside the run cost +reactor receipts list # the gateway + digest receipts +reactor receipts cost # cost rolled up by surprise_cause +``` + +## Honest status + +In the spirit of the receipts, a few things are openly pending. They do not block the path above, but you should onboard the user knowing them: + +- **Benchmarks are pending on purpose.** We publish the harness before the numbers and will not imply a measured speedup we have not run. The proof you can check today is the keyless replay in step 1. +- **Signer caveat.** In v1, *signed* means tamper-evident at the meaning layer and chain-consistent -- not yet a cryptographic byte hash. `reactor receipts verify` proves the receipt chain is consistent but does not yet bind the world-model artifacts. +- **No timestamp, no actor yet.** A v1 receipt records *what* changed and *why*, but not *when* it was committed or *who* committed it -- so the ledger is a verifiable record of decisions, not yet a substitute for an external audit log. +- **The fixpoint (topology-as-responsibility) is specified and deferred.** Facet inference and ledger compaction are named roadmap. + +This honesty is the point. The harness is young and should be used with caution. There is nothing new here -- we are applying classical engineering paradigms to our brave new world, and finding that the wisdom of the ancients still applies. + +## Where to go next + + + + + + + + +To author your first real contract, copy a shape from [`skills/open-prose/examples/`](https://github.com/openprose/prose/tree/main/skills/open-prose/examples) -- each ships a committed, chain-verifiable `replay/` you can read keyless. When the docs and the [SKILL](https://github.com/openprose/prose/tree/main/skills/open-prose) disagree, trust the skill: it is the source of truth for the language. + +--- + +*The conversation always ends. The responsibility shouldn't have to.* diff --git a/content/docs/reactor-devtools/index.mdx b/content/docs/reactor-devtools/index.mdx index a34d7ed..c714343 100644 --- a/content/docs/reactor-devtools/index.mdx +++ b/content/docs/reactor-devtools/index.mdx @@ -49,15 +49,17 @@ DevTools meets two different readers -- a human at a browser and an agent at a t ## 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, 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. +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 - └─ /sdk : createReplaySession shapes the ledger into a replay (ordering, moved facets, cost) + └─ "@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 diff --git a/content/docs/reactor-devtools/reference.mdx b/content/docs/reactor-devtools/reference.mdx index 40a3292..e1dd2ff 100644 --- a/content/docs/reactor-devtools/reference.mdx +++ b/content/docs/reactor-devtools/reference.mdx @@ -16,7 +16,7 @@ reactor-devtools --example --copy-to [--force] # seed a samp | 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. The keyless proof from the README (e.g. `--example masked-relay`). | +| `--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. | @@ -91,13 +91,13 @@ Exported surface: | `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 (`@openprose/reactor/sdk`), not here, so other tools can shape a trail without this package. See [State dirs and replay](/reactor-devtools/state-dirs-and-replay). +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.1.0` | +| Version | `0.2.0` | | Bin | `reactor-devtools` → `dist/cli.js` | | Runtime dependency | `@openprose/reactor` only | | Server | Node built-in `node:http` (no web framework) | diff --git a/content/docs/reactor-devtools/state-dirs-and-replay.mdx b/content/docs/reactor-devtools/state-dirs-and-replay.mdx index 954715f..6aa8164 100644 --- a/content/docs/reactor-devtools/state-dirs-and-replay.mdx +++ b/content/docs/reactor-devtools/state-dirs-and-replay.mdx @@ -39,20 +39,20 @@ Every read goes through one module (`src/data`), the only place the package touc | Need | SDK surface | | --- | --- | | Open the durable trail | `createFileSystemStorageAdapter({ directory })` -- `@openprose/reactor` | -| Re-derive the ledger (= replay) | `new FileSystemReceiptLedger({ storage })` -- `@openprose/reactor/sdk` | -| Ordering + per-node chain index + moved-facet diff + cost rollup | `createReplaySession({ ledger })` -- `@openprose/reactor/sdk` | +| 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/sdk` | -| Click-through world-model | `FileSystemWorldModelStore.readVersion(node, version)` where `version === receipt.fingerprints["@atomic"]` | +| 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. +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 `@openprose/reactor/sdk`, does no I/O, and pulls no new dependency. +`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/sdk"; +import { createReplaySession } from "@openprose/reactor"; // Prefer handing in an already-opened ledger (stays filesystem-agnostic): const session = createReplaySession({ ledger }); diff --git a/content/docs/reactor/index.mdx b/content/docs/reactor/index.mdx index 94f761a..453e8ce 100644 --- a/content/docs/reactor/index.mdx +++ b/content/docs/reactor/index.mdx @@ -1,56 +1,85 @@ --- -title: The Reactor -description: The execution engine that turns a .prose project into a content-addressed DAG and runs it as a durable, self-maintaining reactor. +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. --- -# The Reactor +# Reactor: the harness that runs OpenProse -The Reactor is the execution engine for 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 takes a `.prose` project, compiles it into a content-addressed graph of -responsibilities, and runs that graph as a durable reactor that does expensive -work only when something material has actually changed. +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: -The shape borrows from React. React keeps a DOM consistent with declared -component state and re-renders only what moved. The 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. +> **Inference cost that scales with surprise, not wall-clock time.** -| React | The Reactor | -| --- | --- | -| Component | A responsibility (a declared ideal world-state) | -| DOM | The world-model (the maintained truth, materialized) | -| Render function | A bounded agent session that computes the next truth | -| Props | Subscriptions to other responsibilities | -| `setState` / re-render | A new signed receipt on a subscription | -| Reconciler | The reconciler (fingerprint, schedule, commit, propagate) | -| `React.memo` | Skip the render if subscribed inputs have not moved | +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 -## Two phases: compile, then run +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. -The single most important fact about the Reactor is that it has two phases, and +| 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 contracts +.prose.md contracts -> COMPILE (intelligent sessions, fires on contract change) - Forme draws the DAG + 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, commit, propagate + 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." +is deliberately dumb: the reconciler compares fingerprints and never asks a model +"did this change." -This is the binding model of OpenProse, stated for the 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; the session embodies -the VM. See [the DAG and compile](/reactor/the-dag-and-compile). +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 @@ -58,11 +87,11 @@ 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. -The 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. +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 @@ -71,21 +100,50 @@ self-rechecks that catch silent staleness. See ## 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. The render writes -files into its workspace; the harness harvests, promotes, and fingerprints -them. Quiet nodes stay quiet and free. +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 + + -To drive a Reactor from the shell, see the [CLI overview](/cli/overview) and -the [CLI quickstart](/cli/quickstart). +--- + +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/sdk/adapters.mdx b/content/docs/sdk/adapters.mdx new file mode 100644 index 0000000..7f43a08 --- /dev/null +++ b/content/docs/sdk/adapters.mdx @@ -0,0 +1,266 @@ +--- +title: Adapters +description: The /adapters injection boundary -- the one Substrate primitive, the restart-survival invariant, the gateway-ingress and cursor toolkit, record/replay, and the port contracts a custom backend implements. +--- + +# Adapters + +`@openprose/reactor/adapters` is the **injection boundary**: the seam where a +reactor's I/O lives. Everything above it -- the reconciler, the canonicalizer, +the receipt chain -- is a pure function over the ports this subpath defines. A +deployment swaps the backends here without touching the engine, and the +[front door](/sdk/front-door) facade reaches in for you on the common paths. + +This page is the reference for what `/adapters` exports and, more importantly, +the contracts a custom backend must honor to stay correct. The headline is one +record: + +```ts +import { fileSystemSubstrate } from "@openprose/reactor/adapters"; +``` + +## The one Substrate primitive + +A reactor keeps two durable things: its **truth** (the world-model) and its +**memory of what it already decided** (the receipt trail). `Substrate` is the +single record that answers "where does a reactor keep both" -- one shape, four +fields: + +```ts +interface Substrate { + readonly clock: ClockAdapter; // the only time source + readonly storage: StorageAdapter; // the receipt ledger's append-only trail + readonly worldModel: WorldModelStore; // the truth the render commits through + readonly ledger: MutableReceiptLedger; // derived from `storage` (see below) +} +``` + +Two named factories build it correctly so you do not hand-wire the four fields: + +- **`fileSystemSubstrate({ directory })`** -- the durable substrate. A system + clock, a filesystem storage adapter (the receipt trail at + `/receipts.json`), a durable ledger re-derived from that storage, + and a filesystem world-model store under `/world-models/`. This is + the canonical layout the CLI and DevTools fixtures share, so a substrate built + here and a state-dir the CLI populated re-open the **same** durable trail and + truth. +- **`inMemorySubstrate()`** -- the ephemeral substrate for tests and replay. The + same clock, an in-memory storage adapter, a ledger re-derived from that + in-memory storage (identical re-derivation semantics, just no disk), and an + in-memory world-model store. Nothing persists; a fresh `inMemorySubstrate()` + is empty. + +The constructors (`mountDag` / `createReactor` / `runProject`) accept a +`{ substrate }`, and the substrate is a strict superset of the older à-la-carte +fields -- no backend was removed. The facade builds one for you from +`{ directory }`. + +## The restart-survival invariant + +This is the load-bearing reason the durable factory exists, and the one thing a +custom backend must not get wrong. + + +For a durable substrate, the **ledger MUST be derived from the same storage +adapter** -- `createFileSystemReceiptLedger({ storage })` over the exact storage +the substrate also exposes. The durable ledger re-derives every node's last +receipt from that storage's append-only trail at construction. So re-opening the +same `directory` re-opens the full prior memory, and the boot sweep memo-skips +the unchanged nodes instead of re-rendering them. + + +`fileSystemSubstrate` bakes this in -- it builds the storage adapter and then +builds the ledger over that very adapter, so a consumer never has to remember to +wire it. "The ledger is the source of truth": a restart that re-opens the same +directory resumes the prior memory, which is what makes "cost scales with +surprise, not wall-clock time" survive a process restart. + +### The storage-only spread idiom + +You may need to swap one field -- say a custom storage adapter -- while keeping +the rest correct. The blessed override is a spread over the factory, never a +hand-built record: + +```ts +import { createReactor } from "@openprose/reactor"; +import { fileSystemSubstrate } from "@openprose/reactor/adapters"; + +const r = createReactor({ + substrate: { ...fileSystemSubstrate({ directory }), storage: myStorage }, + topology, + mounts, +}); +``` + +Spreading the factory keeps the other three fields built-right. Supplying a +divergent in-memory ledger alongside durable storage is the explicit opt-out, +not an accident -- if you spread a different `ledger` over durable storage, you +have deliberately broken restart-survival. The factory makes the correct path +the short path; the override makes the unusual path visible. + +## Gateway ingress and the idempotency cursor + +`/adapters` ships the toolkit for turning an external arrival into a receipt at +the system's edge, without ever letting a re-delivered arrival manufacture a +second one. + +The connector is the one impure seam -- the actual I/O against a source: + +```ts +import { createPollConnectorAdapter } from "@openprose/reactor/adapters"; + +// `fetch` performs the real read (HTTP GET, queue drain, file read). A test +// supplies a deterministic stub. Either way the adapter stays a pure function +// over injected I/O. +const connector = createPollConnectorAdapter((request) => fetchSource(request)); +``` + +`pollGateway` (and its async sibling `pollGatewayAsync`) drives the edge. For +each new arrival it stages the item into the gateway node's upstream truth, +marks the cursor, then wakes the node: + +```ts +import { + pollGateway, + createIdempotencyCursor, +} from "@openprose/reactor/adapters"; + +const cursor = createIdempotencyCursor(); + +const result = pollGateway(dag, { + connector, + source_id: "inbox", + node: "gateway", + extract: (payload) => toArrivals(payload), // -> readonly GatewayArrival[] + cursor, + stage: (arrival) => appendToInbox(arrival.item), +}); + +result.ingested_ids; // arrivals past the cursor that drove a wake this poll +result.skipped_ids; // arrivals already in the cursor -- no wake, no receipt +``` + +The cursor is what dedups **before** the edge. Each `GatewayArrival` carries a +stable `id` (a message id, an event id, a content hash) -- the same logical +arrival yields the same `id` across polls and redeliveries. An already-seen +`(source, id)` pair is dropped, so it never produces a second receipt. The order +of arrivals is the delivery order, so the cursor advances monotonically. + +The cursor is durable. It round-trips through the storage registry as a plain +JSON-able snapshot, so a restart resumes without re-ingesting the backlog: + +```ts +import { + loadIdempotencyCursor, + cursorRegistryPatch, +} from "@openprose/reactor/adapters"; + +// At boot: rehydrate from the registry the storage adapter persisted. +const cursor = loadIdempotencyCursor(storage.readRegistry()); + +// After a poll: project the cursor's snapshot into a registry patch to persist. +storage.writeRegistry({ ...storage.readRegistry(), ...cursorRegistryPatch(cursor) }); +``` + +The staging order is deliberate -- **stage, mark, then wake**. Marking before +the wake means a throw mid-render does not re-stage on the next poll: the arrival +is durably consumed once it is staged, and the gateway's render re-runs against +the staged truth on a later wake if it failed. A single payload that lists the +same idempotency key twice is a source bug the cursor cannot disambiguate, so +`pollGateway` fails loudly rather than silently ingesting one and dropping the +other. + +## Record and replay + +Two recording adapters make a run reproducible without a live provider -- the +mechanism the keyless [DevTools](/reactor-devtools) replay path rests on. + +- **`createRecordReplayModelGatewayAdapter({ records })`** replays a captured + sequence of model calls. Each `invoke` is matched against the next record's + request (canonical-JSON equality); a mismatch throws with the record id, so a + drifted run is caught at the exact diverging call rather than silently + producing a different truth. The adapter exposes `calls()` and `remaining()` + for inspection. +- **`createPassthroughAgentSdkAdapter({ launch?, sandbox? })`** is the agent-SDK + passthrough that records every launch and sandbox run. Without handlers it + echoes the request payload back; `createNullAgentSdkAdapter(payload)` is the + inert variant that returns a fixed payload. Both fold the sandbox path in (the + architecture folds sandbox execution into the agent-SDK port). + +Every leaf adapter clones its request in and payload out through canonical JSON, +so an adapter cannot be mutated by a caller and a payload is always a defensive +copy. + +## The port contracts a backend implements + +A custom backend implements one of the trimmed v1 ports. These are the contracts +the harness is a pure function over -- the short names are the headline +vocabulary `Substrate` uses; the `Reactor*`-prefixed originals are the +deprecated aliases, kept reachable from both `/adapters` and +[`/internals`](/sdk/internals) (nothing was removed). + +| Port (short name) | `Reactor*` original | Responsibility | +| --- | --- | --- | +| `ClockAdapter` | `ReactorClockAdapter` | the only time source -- `now(): string` | +| `StorageAdapter` | `ReactorStorageAdapter` | the receipt ledger's append-only trail plus the shrunk registry | +| `WorldModelStore` | `ReactorWorldModelStore` | read-by-reference, commit-and-fingerprint, content-addressed versioning | +| -- | `ReactorModelGatewayAdapter` | render / compile-step invocation -- the model call seam | +| -- | `ReactorConnectorAdapter` | external evidence sources (gateways) | + +A few contracts are worth stating plainly, because they are where a custom +backend tends to drift: + +- **`StorageAdapter`** is append-and-list over receipts plus read/write of a + shrunk registry. The registry is a dumb canonical-JSON key/value -- it carries + the topology world-model and self-driven schedule as opaque blobs, nothing + more. Keep it byte-stable across equal states (sorted, canonical) so the + durable snapshot is deterministic. +- **`WorldModelStore`** hands the render a queryable **reference** to a node's + prior truth (never pre-stuffed into context), and `commit` produces the + deterministic canonical serialization, content-addresses it, and returns the + new version plus the canonicalizer-computed fingerprints. Only the `published` + workspace is fingerprinted; the render's private `workspace` is not. +- **`ReactorModelGatewayAdapter`** reports usage as a `{ provider, model, tokens }` + triple -- the cost-bearing half of the receipt. It does **not** know the wake + source, so `surprise_cause` is supplied by the reconciler, not the gateway. + +The reference backends for these ports also live on `/adapters`: +`createSystemClockAdapter` / `createFixedClockAdapter`, +`createFileSystemStorageAdapter` / `createMemoryStorageAdapter`, +`FileSystemWorldModelStore` / `InMemoryWorldModelStore` (and their `create*` +helpers), `FileSystemReceiptLedger` / `InMemoryReceiptLedger`, and the +`createStaticConnectorAdapter` for an inert in-memory source. + + +Honest status: the v1 port surface is deliberately trimmed. The `signer` port is +deferred to the crypto byte-hash milestone -- the only honest v1 signature is the +null signature, so the receipt chain is tamper-evident and chain-consistent, not +a cryptographic byte hash. `eventSink` is dropped on purpose: telemetry is read +off the ledger, not a separate sink. `sandbox` is folded into the agent-SDK port. +None of this is a gap to paper over; it is the surface as shipped. + + +## Where to go next + + + + + + + diff --git a/content/docs/sdk/agents.mdx b/content/docs/sdk/agents.mdx new file mode 100644 index 0000000..405fea9 --- /dev/null +++ b/content/docs/sdk/agents.mdx @@ -0,0 +1,399 @@ +--- +title: The /agents escape hatch +description: The peer-dep-isolated @openai/agents passthrough -- the layered RenderOptions (Tier A sugar, Tier B verbatim passthrough, Tier C backstop), the RenderBackend injection seam, and the compile-session surface. Every render knob is reachable, with zero capability loss. +--- + +# The `/agents` escape hatch + +`@openprose/reactor/agents` is the one subpath where the harness gets out of your +way. Every render and every compile step in Reactor is **one bounded +`@openai/agents` session**, and this subpath is the named priority of the +`0.3.0` surface: **every knob that SDK anticipates is reachable**, layered so an +agent's auto-import sees intent rather than a wall of peers, with **zero +capability loss** versus driving `@openai/agents` by hand. + +It is its own subpath for one honest reason: importing it pulls the optional +peers (`@openai/agents`, `zod`). The keyless inspection and replay surface +installs neither, so the escape hatch lives behind a door you only open when you +mean to. Everything here is side-effect-free at import: the `Agent` and `Runner` +are constructed lazily inside the render closure, never at module load. + + +**TypeScript needs `nodenext` or `bundler` module resolution** to reach this +subpath. The escape-hatch subpaths (`/agents`, `/adapters`, `/run`, +`/run/types`, `/internals`) are declared through the package's `"exports"` map, +which the legacy `"moduleResolution": "node"` resolver does not read. The root +`@openprose/reactor` import resolves under legacy `node` too; the cliff bites +only the explicit subpaths, where you are already in a real `tsconfig`. + + +## The promise, stated honestly + +A wrapper earns trust by being lossless. The old `createAgentRender` exposed only +a handful of knobs and built the `Agent`/`Runner` internally with hardcoded +settings, so a consumer who needed anything more had to throw away the **whole** +render -- losing the harness's instruction composition, the `wm_*` tools, the +harvest, and the cost capture. That is the lossy wrapper this subpath was +designed to retire. + +The fix is a single `RenderOptions` with three tiers. The harness reserves only +the four fields it cannot let you touch without breaking the render contract; +**everything else passes through verbatim.** When the bottom tier is not enough, +you build the `Agent` and `Runner` yourself -- so there is no ceiling. + +## The layered seam + +```ts +import type { RenderOptions } from "@openprose/reactor/agents"; +``` + +`RenderOptions` is the shared escape hatch. The render config (`AgentRenderConfig`) +extends it; the facade's `render` option forwards it verbatim to every node; the +sub-agent primitive and the compile session thread the same fields. Learn it once. + +### Tier A -- harness-specific sugar + +These are **not** plain `@openai/agents` fields. They are harness knobs that map +onto the SDK config, and the two decoding sugars (`temperature`, `seed`) **fill +only the fields you left unset** (precedence, below). + +| Field | Type | Notes | +| --- | --- | --- | +| `provider` | `ModelProvider` | Keep first-class: the scoped-not-global invariant. Defaults to the scoped OpenRouter provider, resolved lazily on first render. | +| `model` | `string \| Model` | Widened from `string` so you may pass a constructed `Model` instance, not just a provider-resolved id. | +| `maxTurns` | `number \| null` | The session's turn cap. `null` is the **deliberate unbounded opt-in** -- it bypasses the turn guard. Unset means the high default cap (`200` for a render). | +| `signal` | `AbortSignal` | Per-run cancellation. Operational, not config, so it earns a top-level home rather than hiding in `runOptions` (where it is reserved). | +| `temperature` | `number` | Sugar for `agent.modelSettings.temperature`. Defaults `0`. | +| `seed` | `number` | Sugar for `agent.modelSettings.providerData.seed`. | + +### Tier B -- the verbatim `@openai/agents` passthrough + +This is the layer that closes the gap. Each field is deep-merged **over** the +harness's defaults. + +| Field | Type | What it carries | +| --- | --- | --- | +| `agent` | `AgentPassthrough` | The consumer's `Agent` config, reserved fields removed (below). Home of `modelSettings.*` (reasoning, maxTokens, toolChoice, topP, providerData, ...), `handoffs`, `inputGuardrails` / `outputGuardrails`, `mcpServers` / `mcpConfig`, `toolUseBehavior`, `prompt`, `handoffDescription`, `model`. | +| `runConfig` | `Partial` | Runner-**construction** config: `tracingDisabled`, `workflowName`, `traceId`, `groupId`, `traceMetadata`, `modelProvider`, the SDK `sandbox`, `sessionInputCallback`. | +| `runOptions` | `RunOptionsPassthrough` | The **per-run** options bag -- the ONLY home for `previousResponseId`, `conversationId`, `session`, `sessionInputCallback`, `errorHandlers`. | +| `extraTools` | `(defaults) => Tool[]` | Receives the built-in `wm_*` / cwd / spawn set and returns the full set. It **concatenates** -- the built-ins are always present, never replaced. | +| `instructionsSuffix` | `string` | Appended to the composed system prompt (after the SKILL + contract layers). Extend the prompt without dropping to a factory. | +| `tracing` | `boolean \| TracingConfig` | Re-enable tracing with your own api key, or toggle the per-run `tracingDisabled`. The default backend keeps tracing **disabled per run** (safe egress) -- never via a process-global mutation. | + +### Tier C -- the full backstop + +When even verbatim passthrough is not enough -- you want a non-`@openai/agents` +model, or instance-level lifecycle hooks -- you build the instances yourself. + +| Field | Type | What it gives you | +| --- | --- | --- | +| `agentFactory` | `(spec: RenderAgentSpec) => Agent` | Build the `Agent` from the harness-required pieces. The ONLY place to attach `AgentHooks` (`agent.on(event, ...)` -- hooks are emitters on the instance, not config fields). | +| `runnerFactory` | `(provider: ModelProvider) => Runner` | Build the `Runner` yourself from the scoped provider. The ONLY place to attach `RunHooks`. | + +## Reserved fields are a compile error + +The harness owns four `Agent` fields, because they carry the render contract: drop +them and the harvest, cost capture, or routing silently breaks. Rather than let +you set one and stomp the render, the type system **removes** them from the +passthrough. + +```ts +type ReservedAgentFields = "instructions" | "tools" | "outputType" | "name"; + +// AgentPassthrough = Omit, ReservedAgentFields> +const render: RenderOptions = { + agent: { + modelSettings: { reasoning: { effort: "high" }, maxTokens: 8000 }, + // instructions: "..." // <- COMPILE ERROR: reserved. Use instructionsSuffix. + // tools: [...] // <- COMPILE ERROR: reserved. Use extraTools. + }, +}; +``` + +`instructions` is the composed SKILL + contract prompt -- extend it with +`instructionsSuffix`. `tools` is the `wm_*` / cwd / spawn set -- extend it with +`extraTools`. `outputType` is the render's done/failed signal schema. `name` is +the node id. The `runOptions` passthrough reserves four more for the same reason +-- `context`, `maxTurns`, `signal`, and `stream` are harness- or Tier-A-owned, so +they are `Omit`-ed from `RunOptionsPassthrough` (use the Tier-A knobs). + + +This is the trust mechanism made mechanical. You cannot accidentally break the +render contract, because the contract-bearing fields are not in the type. The +compiler points you at the supported extension (`instructionsSuffix` / +`extraTools`) instead of letting a silent stomp ship. + + +## Precedence + +The rule is locked and it is simple: + +- **Consumer `agent.*` wins wholesale.** Whatever you set on `agent` is your + base; the harness merges its four reserved fields **over** it so the contract + can never be broken, and the type system forbids you setting those four at all. +- **Tier-A sugar fills only what you left unset.** `temperature` and `seed` are + folded in only where your `agent.modelSettings` did not already specify them. + Set `agent.modelSettings.temperature` and the sugar steps aside. +- **`extraTools` appends.** The built-in `wm_*` / cwd / spawn set is always + present; your tools are added to it. + +Concretely, `mergeModelSettings` keeps your `modelSettings.temperature` if you set +it and otherwise drops in the Tier-A `temperature`; `providerData` is +shallow-merged so the `seed` sugar coexists with your own `providerData` keys +(yours win). The harness-owned `name` / `instructions` / `tools` / `outputType` +always merge last. + +## The knob-routing table + +Every `@openai/agents` knob has exactly one home. This table is verified against +the shipped `RenderOptions` type, not a proposal. + +| Knob | Reach it via | Tier | +| --- | --- | --- | +| `modelSettings.*` (toolChoice, parallelToolCalls, maxTokens, reasoning, topP, penalties, truncation, store, promptCacheRetention, contextManagement, text, providerData, retry) | `agent.modelSettings` | B | +| `handoffs`, `inputGuardrails`, `outputGuardrails`, `mcpServers`, `mcpConfig`, `toolUseBehavior`, `resetToolChoice`, `prompt`, `handoffDescription`, `model` | `agent` | B | +| `tracingDisabled`, `workflowName`, `traceId`, `groupId`, `traceMetadata`, `modelProvider`, `sandbox` (SDK), `sessionInputCallback` | `runConfig` | B | +| `previousResponseId`, `conversationId`, `session`, `errorHandlers` | `runOptions` | B | +| `tracing` re-enable (`{ apiKey }`) | `tracing` | B | +| `temperature`, `seed` (sugar) | `temperature` / `seed` | A | +| `provider`, `model`, `maxTurns` (incl. `null`), `signal` | first-class | A | +| Instance lifecycle hooks (`AgentHooks.on` / `RunHooks.on`) | `agentFactory` / `runnerFactory` | C | +| A fully custom `Agent` / `Runner`, or a non-`@openai/agents` backend | `agentFactory` / `runnerFactory`, or `RenderBackend` (below) | C | + + +The passthrough is forward-compatible for **additive** `@openai/agents` field +additions only. The peer is pinned to a verified `@openai/agents` version, and +`SharedRunOptions` is a moving surface (it recently grew `errorHandlers` / +`sessionInputCallback`); `runOptions` tracks it. If a future SDK renames or +replaces a reserved field, growing the `Omit` set is a breaking change. "New +versions flow through automatically" is true for additive fields, not +unconditionally version-proof. + + +## Escape-hatch in practice + +The facade forwards a `RenderOptions` to every node via its `render` option. This +is the common path -- one config, applied uniformly. + +```ts +import { reactor } from "@openprose/reactor"; +import type { RenderOptions } from "@openprose/reactor/agents"; + +const render: RenderOptions = { + model: "anthropic/claude-sonnet-4", + temperature: 0.2, // Tier-A sugar -- fills modelSettings if unset + maxTurns: 24, // null is the deliberate unbounded opt-in + agent: { modelSettings: { providerData: { top_p: 0.9 } } }, // Tier-B, wins wholesale + runConfig: { workflowName: "nightly-digest" }, // runner-construction config + instructionsSuffix: "Prefer terse, sourced claims.", +}; + +const { reactor: r } = await reactor("./my-project", { directory: "./state", render }); +await r.ingest("source", { wake: { source: "external", refs: [] } }); +``` + +A fuller config showing each tier carrying its weight: + +```ts +const render: RenderOptions = { + provider: myScopedOpenRouterProvider, // scoped, never the global default client + agent: { // Tier B -- verbatim, wins wholesale + modelSettings: { + reasoning: { effort: "high" }, + maxTokens: 8000, + toolChoice: "required", + providerData: { transforms: ["middle-out"] }, + }, + inputGuardrails: [piiGuardrail], + handoffs: [escalationAgent], + // instructions / tools / outputType / name are Omit-ed -> COMPILE ERROR. + }, + extraTools: (defaults) => [...defaults, mySearchTool], // append, never replace + instructionsSuffix: "\nAlways cite sources inline.", + runConfig: { traceMetadata: { env: "prod" } }, + runOptions: { conversationId: "thread-42", errorHandlers: myErrorHandlers }, + signal: abortController.signal, + tracing: { apiKey: process.env.MY_TRACE_KEY! }, + maxTurns: null, // deliberate unbounded opt-in, preserved end-to-end +}; +``` + +## The `RenderBackend` injection seam + +The Tier-C factories let you rebuild the `Agent`/`Runner` while staying inside the +`@openai/agents` shape. `RenderBackend` goes one level deeper: it lets you replace +the **entire model session** -- record/replay, a proxy, or a non-`@openai/agents` +model (Claude, a local model) -- while **reusing** the harness's instruction +composition, working-dir prep, harvest, and cost mapping. + +The port is `@openai/agents`-free: it traffics only in the harness-composed +request and the structured session output, so a non-SDK backend implements it +without the peer dep. + +```ts +import type { + RenderBackend, + RenderSessionRequest, + RenderSessionOutput, +} from "@openprose/reactor/agents"; + +// One bounded session. The harness hands you the resolved request (composed +// instructions, resolved model + decoding settings, the built tools, the output +// schema, the pointer input, the per-render context, the turn cap, the signal) +// and maps your returned signal + usage into a receipt Cost. +const recordingBackend: RenderBackend = { + async runSession(req: RenderSessionRequest): Promise { + // ... run your model / replay a fixture using req.instructions, req.tools, ... + return { + signal: undefined, // undefined => the harness treats the session as failed + usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, + }; + }, +}; + +const { reactor: r } = await reactor("./my-project", { + directory: "./state", + adapters: { renderBackend: recordingBackend }, +}); +void r; +``` + +`RenderSessionRequest` is exactly what the harness resolved before the model runs +-- `node`, `instructions`, `model`, `modelSettings`, `tools`, `outputType`, +`input` (the short pointer run-input), `context`, `maxTurns`, and an optional +`signal`. Your backend runs one session and returns a `RenderSessionOutput`: the +structured `signal` (a done/failed signal, or `undefined` -> treated as failed, +nothing commits) and the token `usage` that becomes the receipt `Cost`. + +### The default backend, and what it stopped doing + +`createDefaultRenderBackend(config)` is the `@openai/agents` session, lifted +verbatim out of the historic inline render body. It resolves its provider and +runner **lazily and once** (a keyless build that never renders never constructs +them), threads the full Tier-A/B/C escape hatch, and owns the one +`@openai/agents`-specific addition the port abstracts away -- the +`spawn_subagent` tool, whose sub-agents inherit the same per-run escape hatch as +the parent render. + +One behavior changed, and it is a correctness fix worth naming. The default +backend **no longer calls the process-global `setTracingDisabled(true)`**. That +mutation stomped a consumer's `runConfig.tracingDisabled = false` and leaked +across every other `@openai/agents` user in the same process. Tracing is now +decided **per run** -- still disabled by default (safe egress), but overridable +through `RenderOptions.tracing` or `runConfig.tracingDisabled`, and scoped to this +render alone. + +### `RenderAgentSpec` -- what a Tier-C `agentFactory` receives + +When you supply `agentFactory`, you still must honour the render contract. The +spec hands you every harness-required piece pre-assembled, so you do not +reconstruct them: + +| Field | Meaning | +| --- | --- | +| `name` | the node id (the `Agent.name`) | +| `instructions` | the composed SKILL + contract prompt (+ any `instructionsSuffix`) | +| `model` | the model id/instance the render resolved | +| `modelSettings` | the merged decoding settings (temperature/seed + any `agent.modelSettings`) | +| `tools` | the built-in render tools + any `extraTools` -- the full tool surface | +| `outputType` | the render done/failed signal schema | +| `agent?` | your `agent.*` passthrough, for the factory to fold in itself | + +Add anything you like -- guardrails, handoffs, instance hooks via `agent.on(...)` +-- but dropping a spec field breaks the harvest/cost/commit contract, and at this +tier you own that risk. + +## The compile-session surface + +`createAgentRender` is the **run-phase** render. The same `@openai/agents` +machinery powers the **compile** phase, and it lives on this subpath too. Each +compile step is itself a SKILL-loaded session over the loaded contract set that +emits a structured artifact, which the harness then lowers deterministically. + +```ts +import { + compileForme, // -> the topology DAG (ReconcilerTopology) + compileCanonicalizer, // -> a node's run-time canonicalizer + compilePostcondition, // -> a node's commit-gate validators + loadContractSet, + runCompileSession, +} from "@openprose/reactor/agents"; +``` + +The full flow that mounts a project **without hand-authoring**: + +1. `loadContractSet(dir)` enumerates and slices the `*.prose.md` set (a dumb file + load -- nothing parses `.prose` semantics). +2. `compileForme(contracts, fingerprints)` runs the Forme session and lowers its + decisions into a mountable topology. +3. Per node, `compileCanonicalizer(node, contracts)` and + `compilePostcondition(node, contracts)` freeze the run-time canonicalizers and + the commit-gate validators. +4. Mount the topology + canonicalizers via `mountDag` / `createReactor` and run + dumbly. + +Each step takes a `CompileStepOptions` -- the same `provider` / `model` / +`temperature` / `seed` / `maxTurns` knobs **and the same escape hatch** (`agent` / +`runConfig` / `runOptions` / `signal` / `tracing`) as the render, because a +compile step is just another bounded session. `runCompileSession` is the runner +underneath all three; `renderContractSet(contracts)` is the contract-set evidence +it folds into the session's run input. + + +The Determinism boundary holds throughout. The **session** makes the one judgment +only it can -- semantic match (Forme), materiality (canonicalizer), or +postcondition mode. The deterministic scaffolding does the rest and produces an +artifact the dumb run phase executes. A compile that cannot emit its artifact +throws, and the prior compiled artifact stands. + + +## What's here, and what isn't + +The honesty that earns trust, on this surface specifically: + +- **Lossless is a claim with a backstop.** Tiers A and B cover every knob the SDK + anticipates; Tier C and `RenderBackend` guarantee no ceiling. If you find a knob + with no home, the `agentFactory` / `runnerFactory` / `RenderBackend` path + reaches it. +- **The working dir is scoped, not sandboxed.** The render's per-node `workspaceRoot` + has path-escape guards, but a shell command can still escape `cwd`. This is for + trusted, self-authored `.prose` projects; an OS sandbox is deferred. +- **`maxTurns` is a high explicit cap, not a budget.** The real spend signal is the + token `usage` mapped to each receipt's `Cost`. `maxTurns: null` opts out of the + cap deliberately. +- **Forward-compat is additive-only.** The version caveat above is the contract: + new SDK fields flow through, reserved-field renames do not. + +## Where to go next + + + + + + + + +These docs are orientation. The canonical execution behavior lives in the +open-source `open-prose` skill in the +[`openprose/prose`](https://github.com/openprose/prose) repo; if the docs and the +skill disagree, trust the skill. + +--- + +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/sdk/front-door.mdx b/content/docs/sdk/front-door.mdx new file mode 100644 index 0000000..f251c6a --- /dev/null +++ b/content/docs/sdk/front-door.mdx @@ -0,0 +1,370 @@ +--- +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 snippet matches the test-verified facade docstring in +`sdk/facade.ts:158-164`. The `{ reactor: r }` destructure is the idiom -- the +result also carries `bootResults` and `pollConnectors`. + +### `ReactorFacadeResult` + +```ts +export interface ReactorFacadeResult { + readonly reactor: Reactor; // the typed handle (drive / observe / schedule) + readonly bootResults: readonly ReconcileResult[]; // the boot sweep's per-node results ([] when boot: false) + readonly pollConnectors: PollConnectors; // drive one poll of every armed connector (no-op when none) +} +``` + +### `ReactorOptions` + +Every field is a documented desugaring of a rung below. + +| Field | Type | What it does | +|---|---|---| +| `directory` | `string` | Durable truth + receipts. Omit for in-memory (ephemeral; tests/replay). | +| `mode` | `"run" \| "inspect"` | `"inspect"` is the **keyless posture** -- compiles, assembles, and boots without loading a render provider. | +| `boot` | `boolean` | Run the boot cold-miss sweep after assembly. Default `true`. | +| `render` | `RenderOptions & { buildRender?, ... }` | THE `@openai/agents` escape hatch, forwarded verbatim to every render. See [`/agents`](/sdk/agents). | +| `adapters` | `ReactorAdapters` | The backends to swap in (a `Partial` substrate + the model/ingress seams). | +| `schedule` | `ScheduleOptions` | Arm the self-driven continuity cadence off the handle's topology. | +| `compile` | `Omit` | Compile-phase knobs forwarded to `compileProject` (provider/model/skill, per-step overrides). | + +### `ReactorAdapters` + +The backends the facade swaps in. Every substrate field is **optional** -- the +facade defaults the rest (filesystem when `directory` is set, in-memory +otherwise): + +```ts +export interface ReactorAdapters { + readonly clock?: ClockAdapter; // defaults to the system clock + readonly storage?: StorageAdapter; // the ledger's append-only trail + readonly worldModel?: WorldModelStore; // defaults to a store over `directory` + readonly ledger?: MutableReceiptLedger; // defaults to the storage-derived durable ledger + readonly connectors?: readonly ConnectorAdapter[]; // arm ingress sources (§ Ingress, /adapters) + readonly renderBackend?: RenderBackend; // the model-injection seam (/agents) +} +``` + +The `renderBackend` and `connectors` seams are the live, documented injection +points; the substrate fields below are the persistence answer. + +## The one typed `Reactor` handle + +The return of `reactor()`, `createReactor()`, and `runProject()` is **one** +interface -- one object graph at multiple altitudes, never two parallel APIs. +Before `0.3.0` the assembler returned a nested `.dag`, so a driver cast to reach +`store`/`ledger`; the typed handle makes those first-class and the casts vanish. + +```ts +export interface Reactor { + // ── drive -- async-by-default (the live path) ── + ingest(node: string, input?: IngestInput): Promise; + tick(node: string): Promise; + drain(seeds: readonly WakeEvent[]): Promise; + boot(): Promise; + + // ── observe -- first-class read accessors (no casts) ── + readonly view: ReactorView; // the one read-and-rollup surface + onReceipt(cb: (receipt: LedgerReceipt) => void): () => void; // the ledger-is-telemetry tap + readonly ledger: MutableReceiptLedger; + readonly store: WorldModelStore; + readonly clock: ClockAdapter; + readonly topology: ReconcilerTopology; + + // ── self-driven cadence -- wired off the handle ── + scheduler(readFreshness: NodeFreshnessReader, nodes: readonly string[]): AsyncContinuityScheduler; + + // ── sync drive -- the deterministic test path, behind an explicit door ── + readonly sync: SyncDriveSurface; +} +``` + +A few facts that earn the handle its keep: + +- **Async-by-default.** A live render *is* one bounded LLM session = one `await`. + A synchronous render is trivially an already-resolved promise, so the async + verbs subsume the sync ones losslessly. The synchronous verbs (the deterministic + fake-render / test path) are preserved verbatim behind `handle.sync` -- never + amputated, just demoted from co-equal to a named door. +- **`view` re-derives on each read** off the live ledger, so a fresh read always + reflects the current trail. See [observe](#observe-one-read-and-rollup-surface). +- **`onReceipt` is the telemetry tap.** It fires for every committed receipt -- + `rendered` (real spend), `skipped` (memo hit), and `failed` -- across every + drive verb, and returns an unsubscribe function. +- **`topology` is load-bearing.** It threads the `scheduler`, and it reserves the + `createEpochDriver` seam (see [honest status](#honest-status)). +- **The full reconciler primitive is NOT on the handle.** Re-hosting the loop by + hand is engine-room altitude; reach it via [`/internals`](/sdk/internals). + +### `IngestInput` -- the `{ wake }` vs `{ data }` rule + +```ts +export interface IngestInput { + readonly wake?: Wake; // deliver a raw wake (the advanced path) + readonly data?: WorldModelFiles; // STAGE a payload, then fire a memo-MISS wake (requires an armed stager) +} +``` + +The bare `{ wake }` form delivers a raw wake. The `{ data }` form folds the +phantom-ingress stage-and-move dance into one call: the payload is staged into the +node's `::ingress` truth -- moving its `input_fingerprints` -- and then a +memo-MISS external wake fires so the node re-renders reading the staged input. + +The `{ data }` form **requires an armed ingress stager**. The `reactor()` facade +wires one (augmenting the topology with each node's phantom-ingress edge). A handle +assembled without a stager throws a legible error on `{ data }` rather than +silently dropping the payload -- deliver a raw `{ wake }` instead. + +## Tier 2 -- the assemblers (the rungs below the facade) + +When you need to mount a topology by hand -- a custom driver, a re-host of the +loop, an offline harness -- the assemblers are the rungs the facade desugars onto. +All return (or feed) the same `Reactor` handle. + +- **`createReactor(input)`** -- the durable keystone. Wires a `Substrate` + (clock + storage + world-model + ledger) and the per-node render bodies into the + run-phase surface and returns the typed handle. Take a `Substrate` (or a + `Partial`; missing pieces default), a compiled `topology`, and the + per-node `mounts` / `asyncMounts`. Restart-survival is real: re-`createReactor` + over the same storage + directory and the durable ledger re-derives every node's + last receipt, so `boot()` memo-skips the unchanged nodes. +- **`mountDag(input)`** -- the lower assembler. Wires the dumb reconciler over a + world-model store + a receipt ledger and exposes the drive verbs. `createReactor` + is the durable wrapper around it. +- **`renderAtom(input)` / `renderAtomAsync(input)`** -- one render of one node: + `(contract, evidence, prior world-model) -> (RenderProduct | RenderFailure)`. The + render atom is the smallest unit -- the same `(contract, evidence, prior) -> (new + world-model, receipt)` step the [OpenProse foundation](/openprose) declares, + realized as a function. + +```ts +export { + createReactor, type CreateReactorInput, + mountDag, type MountDagInput, type MountedDag, type NodeMount, + renderAtom, renderAtomAsync, + type RenderAtomInput, type RenderContext, type RenderProduct, type RenderFailure, +} from "@openprose/reactor"; +``` + +## Tier 2 -- the durable substrate + +Persistence has **one** answer: the `Substrate` record. `fileSystemSubstrate` +bakes in the storage to ledger restart-survival derivation; `inMemorySubstrate` is +the ephemeral test/replay form. + +```ts +import { fileSystemSubstrate, inMemorySubstrate, type Substrate } from "@openprose/reactor"; + +export interface Substrate { + readonly clock: ClockAdapter; + readonly storage: StorageAdapter; + readonly worldModel: WorldModelStore; + readonly ledger: MutableReceiptLedger; +} + +const durable = fileSystemSubstrate({ directory: "./state" }); // ledger derived over the same storage +const ephemeral = inMemorySubstrate(); // tests, replay +``` + +The blessed à-la-carte leaf factories stay on the front door for custom wiring and +the spread-override idiom: + +```ts +// one storage swap, the rest of the durable substrate intact +const substrate = { ...fileSystemSubstrate({ directory }), storage: myStorage }; +``` + +Available leaf builders: `createFileSystemStorageAdapter`, +`createMemoryStorageAdapter`, `createFixedClockAdapter`, +`createSystemClockAdapter`, `createFileSystemReceiptLedger`, +`createFileSystemWorldModelStore`, `createInMemoryWorldModelStore`. The full set of +backend ports and their custom-backend contracts lives at +[`/adapters`](/sdk/adapters). + +## `observe()` -- one read-and-rollup surface + +`observe(source)` is the SDK's **single** read-and-rollup entry point. The +"fresh-vs-reused" hero metric -- _cost scales with surprise, not the clock_ -- is +computed **here, once**, and every consumer (the `serve` line, the HTTP `/cost` +endpoint, the observability commands, the DevTools meter) reads off this one shape +rather than re-implementing the rollup. + +```ts +import { observe, type ReactorView, type CostRollup } from "@openprose/reactor"; + +// FOUR source forms share ONE rollup: +observe(r); // a live Reactor handle +observe({ ledger }); // a replayed trail +observe({ receipts }); // a trail array +observe({ results }); // a synchronous drive return +``` + +```ts +export interface ReactorView { + readonly receipts: readonly LedgerReceipt[]; + readonly byNode: ReadonlyMap; + readonly dispositions: Record; // rendered/skipped/failed/coalesced, zero-filled + readonly cost: CostRollup; + verifyChain(): { ok: boolean; errors: readonly string[] }; +} + +export interface CostRollup { + readonly fresh: number; // tokens surprise actually drove (a moved fingerprint = a real render) + readonly reused: number; // memo-hit / skipped-render tokens + readonly byCause: Readonly>; // per surprise_cause + readonly byNode: Readonly>; // per node + readonly total: CostBucket; +} +``` + +The single `CostRollup` carries **both** bucketings -- `byCause` (which wake source +drove the spend) and `byNode` (which node spiked) -- so nothing is lost to a parallel +rollup. `verifyChain()` is the tamper / chain-consistency check (see the honest +note on [what "signed" means](/reactor/reconciler-and-receipts)). + +For the DevTools replay viewer, `createReplaySession` shapes a saved trail (per-receipt +moved-facet diff + cumulative rollup) and is re-exported from this front door (and +from [`/run`](/sdk/run)). + +## The driver vocabulary + +The names a driver actually reaches for, all on the front door. + +### Branded identity + +`NodeId`, `Facet`, and `Fingerprint` are branded strings -- the surface is +self-documenting and agent-correct (the `"*"`-never-propagates and +`surprise_cause !== wake.source` footguns become compile errors). The ergonomic +boundary keeps author literals working: every **input** position accepts a plain +`string` (via `NodeIdInput` / `FacetInput`), so `r.ingest("source")` still +compiles, while everything the SDK **returns** is branded and tracked. `Fingerprint` +is branded hard -- consumers never author it. + +```ts +import { asNodeId, asFacet, ATOMIC_FACET } from "@openprose/reactor"; +import type { + NodeId, NodeIdInput, Facet, FacetInput, Fingerprint, FingerprintMap, +} from "@openprose/reactor"; + +asNodeId("scout-desire"); // string -> NodeId +asFacet("status"); // string -> Facet +ATOMIC_FACET; // the reserved whole-truth token every FingerprintMap carries +``` + +### Wake constructors + +One event type, three sources. The constructors build the `{ source, refs }` wake +a driver hands to `ingest` / the reconciler, so the literal is never re-derived by +hand at every ingress / continuity-fire site. + +```ts +import { inputWake, selfWake, externalWake } from "@openprose/reactor"; + +externalWake(); // { source: "external", refs: [] } -- a fresh external arrival +inputWake(...refs); // an upstream input moved +selfWake(...refs); // the continuity cadence fired +``` + +### Files, receipts, ingress + +```ts +import { + files, textFile, jsonFile, // build the WorldModelFiles a node reads/writes + verifyReceipt, verifyReceiptChain, // the chain-consistency check (the v1 "signed" meaning) + ingressSourceFor, augmentTopologyWithIngress, buildIngressStager, armConnectors, +} from "@openprose/reactor"; +``` + +`textFile` / `jsonFile` are the `{ data }` payload builders you saw in the facade +snippet. `verifyReceipt` / `verifyReceiptChain` verify the per-node `prev`-linked +receipt chain -- chain-consistency, not a cryptographic byte hash (see [honest +status](#honest-status)). The ingress building blocks are what +`reactor({ adapters: { connectors } })` wires; reach for them directly only when +hand-rolling a poll loop over the lower `pollGateway` / cursor primitives at +[`/adapters`](/sdk/adapters). + +### Reconcile vocabulary + types + +```ts +import type { + ReconcileResult, ReconcileDisposition, RenderOutcome, WakeEvent, + Receipt, LedgerReceipt, Cost, Wake, WakeSource, +} from "@openprose/reactor"; +``` + +## Honest status + +Honesty is the trust mechanism -- what is built, and what is not, stated plainly. + + +- **`verifyReceipt` / `verifyChain()` check chain-consistency, not crypto.** The + v1 "signed" meaning is tamper-evident `prev`-linked chaining over a content-addressed + trail -- not a cryptographic byte-hash signature. The `SignerPort` is declared so the + crypto milestone is a pure backend swap, but no crypto signer ships in `0.3.0`. +- **No benchmark numbers are asserted** anywhere in these docs. The "cost scales + with surprise" rollup is the mechanism; published numbers are pending. +- **The epoch driver is reserved, not built.** The handle exposes everything a + rollover loop needs (`topology` + `drain` + `onReceipt`), so `createEpochDriver` + lands additively later. The `Reserved*` epoch-driver shapes are type-only -- no + value ships in `0.3.0`. +- **The fixpoint is specified and deferred.** It attaches additively to this + surface (a relocated `input_fingerprints` memo key, no `Receipt`-field change). + + +## Where to go next + + + + + + + diff --git a/content/docs/sdk/index.mdx b/content/docs/sdk/index.mdx new file mode 100644 index 0000000..ce02cfe --- /dev/null +++ b/content/docs/sdk/index.mdx @@ -0,0 +1,185 @@ +--- +title: SDK API Reference +description: The @openprose/reactor 0.3.0 public surface -- six reasoned entrypoints, one curated front door, and an honest map of what's built and what isn't. +--- + +# SDK API reference + +`@openprose/reactor` is the [Reactor](/reactor) layer of one system, made +programmable. [OpenProse](/openprose) is the foundation -- the paradigm where you +declare the outcomes you want kept true, as Markdown contracts. The +[CLI](/cli/overview) is one driver of this SDK; so is the +[DevTools replay viewer](/reactor-devtools). This reference documents the surface +both of them sit on, so you can host a reactor from your own code. + +One call takes a directory of `.prose.md` contracts all the way to a booted, +reconciling reactor and hands back one typed handle: + +```ts +import { reactor } from "@openprose/reactor"; + +// Compile ./my-project, assemble a durable reactor over ./state, boot to a +// fixpoint (cold nodes render once; warm nodes memo-skip), return a live handle. +const { reactor: r } = await reactor("./my-project", { directory: "./state" }); + +console.log(r.view.cost); // { fresh, reused, byCause, byNode } -- the hero metric +await r.ingest("source", { data: { "in.txt": textFile("hello") } }); +``` + +That is the [front door](/sdk/front-door). Everything deeper is reachable, but it +is deliberately one rung down. This page is the map. + + + Source of truth. Every name, path, and version on these pages is grounded in the + shipped `@openprose/reactor@0.3.0` package -- its `exports` map and its + entrypoint barrels -- not a proposal. If a code example and prose ever disagree, + trust the code; if the docs and [the skill](/openprose) disagree, trust the + skill. + + +## The six reasoned entrypoints + +The package ships **six** entrypoints (plus `./package.json`). This is not +arbitrary surface area: each split exists for a reason an agent can reason about +before importing. The curated front door is `.`; the rest are isolation seams you +reach for only when you need what they isolate. + +| Entrypoint | What it is | Why it's split out | +| --- | --- | --- | +| `@openprose/reactor` | **The front door** -- the `reactor()` facade, the one typed `Reactor` handle, the assemblers, the substrate factories, `observe`, and the vocabulary a driver needs. | This is the one obvious door. A deliberate ~45-name curation, not a firehose. Start here. | +| `@openprose/reactor/agents` | The full `@openai/agents` escape hatch -- the layered render config and the compile-session surface. | **Peer-dep isolation.** Importing it pulls the optional `@openai/agents` + `zod` peers; the keyless core installs neither. It is the full passthrough, not a lossy wrapper. | +| `@openprose/reactor/adapters` | The injection boundary -- substrate backends, the gateway-ingress + cursor toolkit, record/replay, passthrough adapters, and the port contracts a custom backend implements. | The seam you wire custom backends against. Kept distinct so swapping persistence or a gateway is a one-import change. | +| `@openprose/reactor/run` | The **offline boundary** -- `compileProject` / `runProject` (model-bearing). | These deep-import the live agent adapters. Kept **off** the front door so a keyless inspection/replay build never loads a provider. The facade reaches them by dynamic import. | +| `@openprose/reactor/run/types` | The type-only mirror of the run-phase shapes (incl. the `Reactor` handle type). | Carries **no** `@openai/agents` value import -- so a consumer (the CLI) can type the handle it drives without crossing the offline boundary. | +| `@openprose/reactor/internals` | The engine room -- the reconciler-construction spine and every deep domain shape (receipt / cost / forme / memo / composition / forecast / evidence-plan / projection / canonicalizer), plus the deprecated `Reactor*`-prefixed port aliases. | The honest deep door for power users re-hosting the reconciler loop by hand. Stable but deep; nothing here was removed from the package, it is simply not on the headline surface. | + + + Folding `.` to a curation removes the `nodenext` resolution cliff for the 90% + path: the root import resolves under legacy `node` resolution, so the cliff now + bites only the explicit escape-hatch subpaths -- where a power user is already in + a real `tsconfig`. + + +## Read these pages in order + + + + + + + + + +## Install and prerequisites + +The SDK is a published npm package. Install it per-project alongside its optional +peers (you only need the peers when you actually render against a model): + +```sh +npm install @openprose/reactor +# When you render against @openai/agents (the live run path): +npm install @openai/agents zod +``` + +- **Version:** `@openprose/reactor@0.3.0`. (The CLI is `@openprose/reactor-cli@0.2.0` + and DevTools is `@openprose/reactor-devtools@0.2.0`; they version independently. + The OpenProse language and skill version separately again -- do not attach + `0.3.0` to the language.) +- **Node:** `>=20.0.0`. +- **Peers (optional):** `@openai/agents@^0.11.6` and `zod@^4.0.0`. They are marked + optional in `peerDependenciesMeta`: the keyless inspection/replay surface never + loads them. You pull them by importing `/agents`, `/run`, or the live render path. + + + Prefer a local-first install over a global one, and prove the system keyless + before you spend a token. For the full agent-first setup path -- keyless proof, + then `init` to `doctor` to `compile`, then going live, then where contracts live + -- see [the OpenProse setup guide](/openprose/setup). + + +## What's built, and what isn't + +In the spirit of the receipts, the honest status. For coding agents onboarding on +behalf of a user, this block is the trust mechanism: the SDK does not pretend. + +**Built and runnable.** The render atom, the content-addressed world-model store +(with the published-truth / private-workspace split), the compiled canonicalizer +with facets, Forme's wiring with diagnostics and acyclicity, postcondition-gated +commits with **no judge step**, the receipt ledger with chain verification, and +composition pins are implemented and exercised by a test suite that runs +offline -- no model calls in the commit gate. The reconciler's surprise property +is enforced as a **tested invariant**: when an input fingerprint doesn't move, the +render body provably never runs. + +**Deliberately not yet here.** + +- **No benchmark or dollar numbers.** We are not going to pretend a structural + invariant is a measured speedup. Designing honest long-horizon benchmarks is the + help we most want -- these pages assert no performance figure. +- **The signer is an explicit null state.** In v1, *signed* means tamper-evident + at the meaning layer and chain-consistent -- **not** yet a cryptographic byte + hash. The signature is an honest null + (`{ scheme: "none", null_reason: "no-signer-adapter-configured" }`). The reserved + `SignerPort` seam means the crypto milestone is a backend swap, not a reshape. +- **No timestamp or actor on receipts yet.** The chain is content-addressed and + ordered; wall-clock attribution is named, not shipped. +- **The fixpoint is specified and deferred.** The topology as a responsibility (an + epoch driver layered over the fixed-topology handle) is designed -- the handle + already exposes `topology` + `drain` + `onReceipt` for exactly that loop -- but + the driver itself is a reserved, forward-only seam, declared and not built. +- **Facet inference and ledger compaction are named roadmap**, not shipped. + +These caveats are surfaced inline where they bite, too: +[the reconciler and receipts](/reactor/reconciler-and-receipts) for what "signed" +means in v1, [world-model and fingerprints](/reactor/world-model-and-fingerprints) +for the signer caveat, and [CLI configuration](/cli/configuration) for the +deferred knobs. + +## Where to go next + + + + + + + + +--- + +_The conversation always ends. The responsibility shouldn't have to._ diff --git a/content/docs/sdk/internals.mdx b/content/docs/sdk/internals.mdx new file mode 100644 index 0000000..cc7889b --- /dev/null +++ b/content/docs/sdk/internals.mdx @@ -0,0 +1,215 @@ +--- +title: /internals +description: The engine room -- the honest deep door. The reconciler-construction spine, the deep domain shapes, the deprecated Reactor*-prefixed port aliases, and the receipt/projection helpers. Stable-but-deep, distinct from the curated front door. +--- + +# `/internals`: the engine room + +`@openprose/reactor/internals` is the **honest deep door**. Every public name +that is not on the curated front door (`.`), the `@openai/agents` escape hatch +([`/agents`](/sdk/agents)), the substrate and ingress backends +([`/adapters`](/sdk/adapters)), or the offline run-phase boundary +([`/run`](/sdk/run)) re-homes here. + +Nothing was removed from the package. These names are simply not on the headline +surface. If you are wiring a project from the [front door](/sdk/front-door) you +will never import this subpath -- the facade, the assemblers, and the substrate +factories already cover the 90% path. You reach for `/internals` only when you +are re-hosting the reconciler loop by hand against your own ports, or reading the +deep domain shapes the front door wraps. + + + Think of this as **stable-but-deep**, not unstable. The names here back the + same engine the front door drives; they are de-emphasized, not experimental. + But it is a wide surface, and the headline vocabulary on the + [front door](/sdk/front-door) is the one we curate for agents. Start there. + + +## What lives here + +`/internals` re-exports the whole engine, grouped by the design doc each module +implements (`src/internals/index.ts`): + +| Group | Module | What it is | +| --- | --- | --- | +| The coordination spine | `../shapes` | The shared discriminated unions every other module reads (`SHAPES.md`). | +| Cycle + predicate keep-home | `../cycle` | The cycle/predicate shapes (`SHAPES.md` §8). | +| The world-model store | `../world-model` | The content-addressed store object (`architecture.md` §5.2, §10). | +| The compiled canonicalizer | `../canonicalizer` | The per-node canonicalizer Forme froze (`architecture.md` §3.2). | +| The compiled postcondition validators | `../postcondition` | The commit-gate validators (`architecture.md` §3.3). | +| Forme | `../forme` | The compile-phase wiring (`architecture.md` §3.1, §6.3). | +| The receipt ledger object | `../receipt` | The receipt builder and the ledger trail (`SHAPES.md` §4). | +| The memo re-key + skip decision | `../memo` | The memo key and the skip disposition (`SHAPES.md` §3). | +| Composition | `../composition` | Subscriptions-as-props, pins-as-read-isolation (§7). | +| Forecast | `../forecast` | The continuity clock + self-recheck (`architecture.md` §3.5). | +| Evidence resolution | `../evidence-plan` | Evidence-by-reference resolution (`delta.md` §A3.1). | +| Surprise-cost + projection | `../cost`, `../projection` | The observable cost shapes + receipt projection (`delta.md` §A4). | +| The run-phase reconciler spine | `../reactor` | The dumb reconciler loop (`architecture.md` §4.1). | +| The injection boundary | `../adapters` | The adapter port contracts, including the deprecated `Reactor*`-prefixed aliases (`architecture.md` §5.3). | +| The SDK assembler spine | `../sdk` | `createReactor` / `mountDag` internals. | + +These were nine separate doc-only subpaths in the pre-`0.3.0` surface +(`/receipt`, `/cost`, `/forme`, `/evidence-plan`, `/memo`, `/forecast`, +`/composition`, `/projection`, `/canonicalizer`). They held zero consumer usage +as standalone import paths, so `0.3.0` folded them into one engine-room door. The +load-bearing few names they carried -- `ATOMIC_FACET`, `verifyReceipt`, +`verifyReceiptChain`, `Receipt` -- were **pulled forward** onto the +[front door](/sdk/front-door); the rest stay reachable here. Only the import path +changed. No name became unavailable. + + + `createSkippedReceipt` ships from both `../receipt` and `../memo`. `/internals` + pins the canonical `../receipt` builder over the `../memo` star-export, so the + name resolves to one implementation -- not a star-merge collision. + + +## The reconciler-construction spine + +This is the reason to be here: re-hosting the run-phase loop by hand against +custom ports, instead of going through the [facade](/sdk/front-door) or +[`runProject`](/sdk/run). The constructor takes injected ports and a **fixed** +compiled topology and returns a handle: + +```ts +import { + createReconciler, + type ReconcilerPorts, + type ReconcilerTopology, + type ReconcilerHandle, + inboundEdges, + COLD_START_ATOMIC_FINGERPRINT, +} from "@openprose/reactor/internals"; + +// Construct the dumb reconciler over injected ports + a fixed compiled topology. +// No judge, no policy, no backstop -- the entire decision is fingerprint +// comparison. +const handle: ReconcilerHandle = createReconciler(ports, topology); +``` + +The topology is a **constructor input** (`createReconciler(ports, topology)`, +`src/reactor/index.ts`), not mutable state -- which is exactly why the deferred +fixpoint (topology-as-responsibility) attaches additively later: "an epoch +*names* that fact" without reshaping the handle. + +The handle exposes the four drive verbs, sync and async: + +| Member | What it does | +| --- | --- | +| `reconcile(event)` | Handle one wake for one node: memo/skip, single-flight schedule, commit, propagate. Returns the disposition, the receipt written, and the downstream wakes to enqueue. | +| `drain(initial)` | Drain a seeded queue of wakes to a fixpoint, honoring single-flight + coalescing + propagation. Returns the ordered per-node results. | +| `reconcileAsync(event)` | The async sibling: awaits the one bounded LLM session. A wake delivered while a render is in flight marks the node dirty and collapses into exactly one coalesced follow-up -- never a second concurrent render, never a lost wake. | +| `drainAsync(initial)` | The async fixpoint loop: `await`s each `reconcileAsync` fully before shifting the next event, preserving the sync path's exact ordering and one-render-in-flight guarantee. | + +The supporting names complete the loop you would hand-roll: `inboundEdges` +resolves a node's subscribed edges off the topology; `memoKeyMoved`, +`movedFacetsBetween`, and `propagationTargets` are the comparison + propagation +helpers; and `COLD_START_ATOMIC_FINGERPRINT` (`asFingerprint("cold-start:empty")`) +is the reserved cold-start atomic fingerprint a node carries before its first +render. The full ledger-port (`ReceiptLedgerPort`) and world-model-port +(`WorldModelStorePort`) contracts the handle reads through are here too. + + + Most callers should **not** hand-host the reconciler. The + [facade](/sdk/front-door) wires `createReactor` + the + [run-phase compile](/sdk/run) for you and returns one typed handle. Reach for + `createReconciler` only when you genuinely own the ports -- a custom evidence + plan, an alternate persistence story, an embedding host that drives the loop + on its own cadence. See [the reconciler and receipts](/reactor/reconciler-and-receipts) + for the behavior you are taking responsibility for. + + +## The deprecated `Reactor*`-prefixed port aliases + +In the pre-`0.3.0` surface the adapter port types carried a `Reactor*` prefix. +`0.3.0` made the short names the headline vocabulary -- `StorageAdapter`, +`WorldModelStore`, `ClockAdapter` -- which is what [`/adapters`](/sdk/adapters) +and the [`Substrate`](/sdk/adapters) record use. The original `Reactor*`-prefixed +names are kept reachable from `/internals` as deprecated aliases +(`src/adapters/types.ts`): + +```ts +// The short name is the headline vocabulary used by Substrate; +// the Reactor*-prefixed original is the deprecated alias, kept reachable here. +export type StorageAdapter = ReactorStorageAdapter; +// likewise: WorldModelStore = ReactorWorldModelStore, +// ClockAdapter = ReactorClockAdapter +``` + +These are type aliases, not separate implementations -- a downstream still +pinned to `ReactorStorageAdapter` keeps compiling. Prefer the short names in new +code. Nothing was removed; the prefixed names are simply no longer the door you +are pointed at. + +## The receipt and projection helpers + +Receipt **verification** lives on the [front door](/sdk/front-door) +(`verifyReceipt` / `verifyReceiptChain`). The **proof-projection** helpers -- +deriving a sharable proof summary that avoids private payload fields -- live here, +alongside the deep receipt shapes they operate on (`src/receipt/index.ts`, +`src/projection/index.ts`): + +```ts +import { verifyReceipt } from "@openprose/reactor"; +import { + inspectReceiptProof, + projectReceiptProof, + type LedgerReceipt, + type ReceiptProofInspection, +} from "@openprose/reactor/internals"; + +export function inspectStoredReceipt(receipt: LedgerReceipt) { + const verification = verifyReceipt(receipt); + if (!verification.ok) { + throw new Error(verification.errors.join("; ")); + } + return inspectReceiptProof(receipt); +} + +export function publicReceiptEvidence(proof: ReceiptProofInspection) { + const result = projectReceiptProof({ tier: "public", proof }); + if (!result.ok) { + throw new Error(result.errors.join("; ")); + } + return result.projection; +} +``` + +`inspectReceiptProof` reads a stored receipt into a `ReceiptProofInspection`; +`projectReceiptProof({ tier, proof })` projects that inspection down to a tier +(for example `"public"`) that drops private payload fields. The verify split is +deliberate: the everyday `verifyReceipt` is on the front door, and the deeper +projection machinery -- bound to the receipt domain shapes -- stays in the engine +room. + + + Honest scope, unchanged from the [receipt model](/reactor/reconciler-and-receipts): + in v1 *verified* means tamper-evident at the meaning layer and chain-consistent + -- not yet a cryptographic byte hash, and a receipt records *what* changed and + *why*, not *when* or by *whom*. `projectReceiptProof` projects what the chain + attests; it does not manufacture an audit fact the receipt does not yet carry. + + +## Where to go next + + + + + + + diff --git a/content/docs/sdk/meta.json b/content/docs/sdk/meta.json new file mode 100644 index 0000000..ec713af --- /dev/null +++ b/content/docs/sdk/meta.json @@ -0,0 +1,4 @@ +{ + "title": "SDK API Reference", + "pages": ["index", "front-door", "agents", "adapters", "run", "internals"] +} diff --git a/content/docs/sdk/run.mdx b/content/docs/sdk/run.mdx new file mode 100644 index 0000000..bee8a5e --- /dev/null +++ b/content/docs/sdk/run.mdx @@ -0,0 +1,284 @@ +--- +title: The offline boundary +description: The /run and /run/types entrypoints -- compileProject and runProject, the model-bearing run phase that stays off the keyless front door, plus the type-only mirror that types the handle without crossing the boundary. +--- + +# The offline boundary: `/run` and `/run/types` + +Two of the six [SDK entrypoints](/sdk) exist for one reason: to keep the +model-bearing run phase **off** the keyless front door. A coding agent should be +able to inspect a project, replay a ledger, or type a run configuration without +ever loading a provider or spending a token. `/run` is where the live model +session actually gets pulled in; `/run/types` is the type-only mirror that lets +you describe the same shapes without crossing that line. + +> The keyless inspection or replay build never loads a provider. + +That sentence is the whole design. `compileProject` and `runProject` deep-import +the live agent adapters (`@openai/agents` and `zod`, both optional peers), so +they are deliberately **not** re-exported from the curated `.` front door. The +[facade](/sdk/front-door) reaches them only through a dynamic `await import("../run")` +inside its body, after it has decided it is actually going to run. A consumer who +only wants to read state imports nothing model-bearing. + + + This is the same boundary the [DevTools keyless replay](/reactor-devtools) + relies on. Inspection and replay are first-class postures, not afterthoughts: + the package is structured so they cannot accidentally pull a model in. + + +## `/run` -- the model-bearing run phase + +The `/run` entrypoint exports exactly two functions and the type shapes that go +with them. Both functions are the dynamic-import target for the run phase -- you +import `/run` only at the point where a live render (or a compile session) is +about to happen. + +```ts +// @openprose/reactor/run +export { compileProject, runProject } from "../sdk/run-project"; + +export type { + CompileProjectInput, + CompiledProject, + CompiledProjectNode, + NodeStepCompileOptions, + PerStepCompileOptions, + RunProjectInput, + RunProjectRender, + RunProjectResult, +} from "../sdk/run-project"; +``` + +The two phases mirror the harness's determinism boundary: `compileProject` is +the **compile** phase, run as sessions; `runProject` is the **run** phase, dumb +and deterministic. See [the DAG and compile](/reactor/the-dag-and-compile) for +the conceptual model the two functions implement. + +### `compileProject` -- the compile phase as sessions + +`compileProject` takes a directory of `.prose.md` contracts all the way to a +mountable shape, **without** any hand-authored topology and without a `.prose` +parser. Every model call inside it is an agent session: `loadContractSet` (plain +file loading) feeds `compileForme` (the topology session), then per node a +`compileCanonicalizer` and `compilePostcondition` session freeze each +`### Maintains` declaration into deterministic run-time code. + +```ts +import { compileProject } from "@openprose/reactor/run"; + +const compiled = await compileProject({ + contractsDir: "./my-project/src", +}); +``` + +```ts +export interface CompileProjectInput { + readonly contractsDir?: string; // a directory of .prose.md contracts + readonly contracts?: ContractSet; // OR an already-loaded set (skips loadContractSet) + readonly options?: CompileStepOptions; // per-call compile-session knobs (provider/model/skill/...) + readonly perStep?: PerStepCompileOptions;// per-step overrides (forme / canonicalizer / postcondition) + readonly skipPostconditions?: boolean; // synthesize an empty validator set (no model call) +} +``` + +The result is the mountable project the run phase consumes: + +```ts +export interface CompiledProject { + readonly reconcilerTopology: ReconcilerTopology; // Forme's output -- the DAG + readonly perNode: Readonly>; // compiled canonicalizers + validators + readonly contracts: ContractSet; // the source the sessions read + readonly contractFingerprints: Readonly>; // the memo key's first half + readonly cost: Cost; // summed session cost across every step +} +``` + +Each `CompiledProjectNode` carries the frozen materiality and the commit gate for +one node: + +```ts +export interface CompiledProjectNode { + readonly compiled: CompiledNode; // the run-time canonicalizer (materiality frozen at compile) + readonly postconditions: CompilePostconditionsResult; // the deterministic commit-gate validators +} +``` + + + Because each compile **step** emits a different output schema (Forme vs + canonicalizer vs postcondition), a single shared fake provider cannot satisfy + all three at once. For an offline compile, hand a distinct provider per step + via `perStep`. The two per-node steps take the explicit + `NodeStepCompileOptions` `{ all?, byNode? }` shape -- `all` applies one set of + options to every node, `byNode` overrides per node id, and `byNode[node]` + merges over `all`. There is no key-name heuristic deciding which one you meant. + + +### `runProject` -- the dumb run phase + +`runProject` mounts the compiled project over a [substrate](/sdk/adapters) and +runs the boot cold-miss sweep. The render is built over the **same** world-model +store the reactor commits to, so a workspace write is visible to the harvest in +that render. Boot is the honest first render of a pure source: only sources are +seeded, and input-driven nodes wake via propagation. A second `runProject` over +the same directories boots to all-skips. + +```ts +import { compileProject, runProject } from "@openprose/reactor/run"; +import { fileSystemSubstrate } from "@openprose/reactor"; + +const compiled = await compileProject({ contractsDir: "./my-project/src" }); + +const { reactor, bootResults } = await runProject({ + compiled, + substrate: fileSystemSubstrate({ directory: "./state" }), + render: { + // the model + full @openai/agents escape hatch, as one nested render config + render: { provider: myScopedProvider, model: "google/gemini-3.5-flash" }, + }, +}); + +// `reactor` IS the typed handle -- drive and observe it with no casts +console.log(reactor.view.cost); // { fresh, reused, byCause, byNode } +``` + +The input bundles the compiled project, the run substrate, and the render +wiring: + +```ts +export interface RunProjectInput { + readonly compiled: CompiledProject; // the output of compileProject + readonly substrate?: Partial; // the blessed persistence primitive (clock/storage/worldModel/ledger) + readonly adapters?: { // the a-la-carte form, retained for back-compat + readonly clock: ClockAdapter; + readonly storage: StorageAdapter; + readonly worldModel?: WorldModelStore; + readonly ledger?: MutableReceiptLedger; + }; + readonly directory?: string; // world-model directory when the store is defaulted + readonly render: RunProjectRender; // the render wiring (below) +} +``` + +Prefer `substrate` (a whole `fileSystemSubstrate` / `inMemorySubstrate`, or a +`Partial` with missing pieces defaulted) over the a-la-carte `adapters`. See +[adapters](/sdk/adapters) for the substrate primitive and the restart-survival +invariant. + +### `RunProjectRender` -- the render wiring + +`RunProjectRender` is how `runProject` reaches the model. It threads the model +selection and the **full** `@openai/agents` escape hatch through to the live +render. The escape hatch is one nested `render: RenderOptions`, not a flat +re-declaration -- so `maxTurns: number | null` is preserved end-to-end and you +reach every SDK knob the render does. See [the agents escape hatch](/sdk/agents) +for the full `RenderOptions` tiers. + +```ts +export interface RunProjectRender { + readonly contractFor?: (node: string) => CompiledContractView; // per-node compiled-contract view + readonly projectTruthFor?: (node: string) => TruthProjection; // per-node truth projection (GOTCHA-1 half) + readonly buildRender?: (store: WorldModelStore) => AsyncMountedRender; // the deepest render-body backstop + readonly skill?: string; // pre-read SKILL system prompt + readonly skillPath?: string; // path to the SKILL when `skill` is unset + readonly sandbox?: RenderSandboxRunner; // the folded render sandbox (a TYPE only; the SDK ships no runner) + 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/vendor/prose-examples/basic-unit-suite/src/count-summary.prose.md b/vendor/prose-examples/basic-unit-suite/src/count-summary.prose.md new file mode 100644 index 0000000..3bbc628 --- /dev/null +++ b/vendor/prose-examples/basic-unit-suite/src/count-summary.prose.md @@ -0,0 +1,46 @@ +--- +name: count-summary +kind: responsibility +--- + +# Count Summary + +> The single responsibility that turns the gateway's `counts` facet into a +> threshold-aware summary (U02). It reads its prior world-model by reference, +> writes a new `CountSummary`, and signs a receipt naming the upstream receipt it +> consumed. It skips when `counts` has not moved (U03). + +### Requires + +- `counts`: the numeric tallies. *(Maintained by `counter-events.counts`.)* + +This is the only subscribed input. `count-summary` is **input-driven**: it wakes +iff the `counts` facet fingerprint moves. A metadata-only event moves only +`raw_events`, so this node stays dark while `Raw Event Auditor` wakes (U05). + +### Maintains + +The `CountSummary` world-model — the structured summary the alerting chain reads. + +- `total` — the material event count. +- `by_kind` — the per-kind tallies. +- `threshold_crossed` — whether `total` reached the alert threshold. +- `explanation` — a short rationale string. + +#### structured + +The whole summary is material: any change to `total`, `by_kind`, or +`threshold_crossed` moves this node's truth and propagates to `Alert State`. + +**Postcondition:** `total` equals the count of accepted material events; +`threshold_crossed` is true iff `total ≥ threshold`. Self-policed before signing — +no separate judge beat. + +### Execution + +Read the `counts` facet and the prior summary by reference, fold the per-kind +totals, set `threshold_crossed`, and commit the structured summary. + +### Continuity + +input-driven diff --git a/vendor/prose-examples/basic-unit-suite/src/counter-events.prose.md b/vendor/prose-examples/basic-unit-suite/src/counter-events.prose.md new file mode 100644 index 0000000..381d48f --- /dev/null +++ b/vendor/prose-examples/basic-unit-suite/src/counter-events.prose.md @@ -0,0 +1,67 @@ +--- +name: counter-events +kind: gateway +--- + +# Counter Events + +> The gateway — the system's ingress. It has no `### Requires` (its input arrives +> from outside the graph), it `### Maintains` the canonical `CounterEventLedger`, +> and its `### Continuity` is **external-driven**, which is how Forme registers it +> as the single DAG entry point (U11). + +### Continuity + +external-driven + +A webhook, a poll, or a manual kick becomes one external wake at the system's +edge. The gateway folds each accepted counter event into the canonical ledger and +projects two **independent facets** so a downstream subscriber wakes only on the +slice it actually depends on (U05). Replaying the same event id is a no-op — the +ledger dedups by id, so a re-delivery produces a byte-identical world-model and +the gateway memo-skips (U01/U03). + +### Receives + +- A counter event: `{ id, kind, value, material? }`. An event with + `material: false` is **accepted into the audit trail but excluded from the + tallies** — it is the metadata-only event that moves `raw_events` without moving + `counts`. + +### Maintains + +The `CounterEventLedger` — the standing truth every downstream responsibility +subscribes to. Its canonicalization splits the truth into the two facets below, so +a change to one slice never spuriously wakes a subscriber of the other. + +- `high_water_mark` — the running material event total. +- `counts_by_kind` — the per-kind material tallies. +- `accepted_event_ids` — the full accepted id set (material and metadata-only). +- `last_seen_at` — an immaterial monotone marker (it never appears in a facet, so + it cannot wake a subscriber on its own). + +#### counts + +The numeric tallies (`high_water_mark`, `counts_by_kind`) over **material events +only**. Moves when a material event is accepted; does NOT move on a metadata-only +event. `Count Summary` and `Count Trend` subscribe here. + +#### raw_events + +The accepted-event id set plus duplicate / malformed flags. Moves whenever the +accepted set changes — including a metadata-only event. `Raw Event Auditor` +subscribes here. + +### Emits + +- count-summary +- raw-event-auditor +- count-trend + +Forme keys the wake on the producing node; the subscribers above resolve their +edges to this gateway's `counts` / `raw_events` facets. + +### Continuity recheck + +A weekday 09:00 self-kick may re-scan even when no webhook fires; a byte-identical +re-scan memo-skips, so the self-kick costs nothing when nothing changed. From e42b2be0e60a64a4ca460f06fa8addbb86cd8d58 Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:20:17 -0700 Subject: [PATCH 13/14] docs: review fold-ins (cohesion/accuracy/agent-trust/api) --- content/docs/index.mdx | 4 +- content/docs/openprose/setup.mdx | 4 +- content/docs/reactor-devtools/describe.mdx | 57 ++++++++++++---------- content/docs/sdk/agents.mdx | 8 +++ content/docs/sdk/front-door.mdx | 7 +-- content/docs/sdk/run.mdx | 2 +- 6 files changed, 49 insertions(+), 33 deletions(-) diff --git a/content/docs/index.mdx b/content/docs/index.mdx index d2bd488..3308d7a 100644 --- a/content/docs/index.mdx +++ b/content/docs/index.mdx @@ -106,6 +106,8 @@ npx -p @openprose/reactor-devtools reactor-devtools --example masked-relay --des ``` ```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 @@ -116,7 +118,7 @@ COST ROLLUP (tokens) CHAIN-VERIFY ok ``` -That is "cost scales with surprise" -- checkable, with no key and no spend. 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). +The binary prints that synthetic-sample banner first, and it is the honest frame: `masked-relay` is a **saved sample ledger**, so the token magnitudes are illustrative, not a measured bill. What is genuinely checkable here is the **structure** -- the per-node `rendered`/`skipped` dispositions, the `surprise_cause` receipt counts, and `CHAIN-VERIFY ok` (every receipt links its `prev`). That structural shape is "cost scales with surprise," and you can verify it with no key and no spend. (When you run your own contract, the cost rollup becomes your real spend; see the [honest-status notes](/sdk#whats-built-and-what-isnt) on why there are no benchmark numbers yet.) From there, the [setup path](/openprose/setup) walks the full ordered route: the keyless proof first, then `init` -> `doctor` -> `compile` offline, then go live and `serve`. Or jump to the [CLI quickstart](/cli/quickstart). ## Where the truth lives diff --git a/content/docs/openprose/setup.mdx b/content/docs/openprose/setup.mdx index 7df391d..aeb2e74 100644 --- a/content/docs/openprose/setup.mdx +++ b/content/docs/openprose/setup.mdx @@ -22,6 +22,8 @@ npx -p @openprose/reactor-devtools reactor-devtools --example masked-relay --des ``` ```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 @@ -32,7 +34,7 @@ COST ROLLUP (tokens) CHAIN-VERIFY ok ``` -The `surprise-cause` line counts receipts by what woke them; the cost rollup below it is the actual token spend, where `fresh` is what each surprise cost and `reused` is what memoization saved. That is "cost scales with surprise" -- checkable, with no key and no spend. +The first line the binary prints is the honest frame: `masked-relay` is a **synthetic sample ledger**, so the token magnitudes are illustrative, not a measured bill. The genuinely checkable part is the **structure** -- the `surprise-cause` line counts receipts by what woke them, and the cost rollup splits `fresh` (what each surprise cost) from `reused` (what memoization saved), all chained under `CHAIN-VERIFY ok`. That structural shape is "cost scales with surprise" -- checkable, with no key and no spend. When you run your own contract (step 4), the rollup becomes your real spend. Prefer the browser? Drop `--describe` and `reactor-devtools --example masked-relay` boots an animated DAG viewer at a localhost URL: nodes flash on render, dim-pulse on memo-skip, with a live cost meter. diff --git a/content/docs/reactor-devtools/describe.mdx b/content/docs/reactor-devtools/describe.mdx index 376d256..ead945b 100644 --- a/content/docs/reactor-devtools/describe.mdx +++ b/content/docs/reactor-devtools/describe.mdx @@ -15,36 +15,39 @@ Because the primary consumer of the Reactor is often an agent, this is a first-c ## 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 - 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% - input receipts=54 fresh=30420 reused=11480 - self receipts=2 fresh=0 reused=0 - external receipts=36 fresh=3240 reused=1680 - peak fresh 14580 at frame 73 (Concept Clusterer) + (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 - Runtime Watch r=6 s=12 f=0 fresh=3240 chain✓ - Claude Adapter r=4 s=0 f=0 fresh=1440 chain✓ - Codex Adapter r=4 s=0 f=1 fresh=720 chain✓ - Session Ledger r=5 s=7 f=0 fresh=2700 chain✓ - Concept Clusterer r=2 s=2 f=0 fresh=19440 chain✓ + 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 -- every node chain is prev-linked & consistent +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[...] fresh woke[...]) - 0 Agent FS rendered moved[@atomic,claude,codex,...] fresh 0 woke[Runtime Watch] - 1 Runtime Watch rendered moved[@atomic,claude,codex,...] fresh 540 woke[Claude Adapter,Codex Adapter,...] - 8 Session Ledger rendered moved[@atomic,session:claudeA,session:claudeB,session:codexA] fresh 540 woke[Session Summary [claudeA],...] - 9 Session Ledger skipped moved[--] fresh 0 woke[--] +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[--] ... ``` @@ -65,12 +68,12 @@ FRAMES (frame node status moved[...] fresh woke[...]) The `FRAMES` block is where "cost scales with surprise" stops being a slogan. In the example above: ```text - 8 Session Ledger rendered moved[@atomic,session:claudeA,...] fresh 540 woke[Session Summary ...] - 9 Session Ledger skipped moved[--] fresh 0 woke[--] - 10 Session Ledger skipped moved[--] fresh 0 woke[--] + 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 8 renders and wakes its subscribers; frames 9 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. +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 diff --git a/content/docs/sdk/agents.mdx b/content/docs/sdk/agents.mdx index 405fea9..374fdf1 100644 --- a/content/docs/sdk/agents.mdx +++ b/content/docs/sdk/agents.mdx @@ -275,6 +275,14 @@ them), threads the full Tier-A/B/C escape hatch, and owns the one `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 diff --git a/content/docs/sdk/front-door.mdx b/content/docs/sdk/front-door.mdx index f251c6a..fbab5d2 100644 --- a/content/docs/sdk/front-door.mdx +++ b/content/docs/sdk/front-door.mdx @@ -49,9 +49,10 @@ await r.ingest("source", { data: { "in.txt": textFile("hello") } }); // deliver input, reconcile to a fixpoint, re-render only what the new input moved ``` -This snippet matches the test-verified facade docstring in -`sdk/facade.ts:158-164`. The `{ reactor: r }` destructure is the idiom -- the -result also carries `bootResults` and `pollConnectors`. +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` diff --git a/content/docs/sdk/run.mdx b/content/docs/sdk/run.mdx index bee8a5e..969892b 100644 --- a/content/docs/sdk/run.mdx +++ b/content/docs/sdk/run.mdx @@ -181,7 +181,7 @@ export interface RunProjectRender { 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; // the folded render sandbox (a TYPE only; the SDK ships no runner) + 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 From a551ea136d1791bf11a7f054c05e002b30885186 Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:33:42 -0700 Subject: [PATCH 14/14] docs(cspell): add domain terms to the dictionary verify/pnpm spell flagged new content words: fixpoint (core term), wakeups, desugars, footguns, greppable, headlessly, screenshotting, unparseable, bucketings. --- .cspell.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.cspell.json b/.cspell.json index d1aa4b6..ea75ff9 100644 --- a/.cspell.json +++ b/.cspell.json @@ -88,7 +88,16 @@ "webm", "dedup", "prev", - "dep" + "dep", + "fixpoint", + "wakeups", + "desugars", + "footguns", + "greppable", + "headlessly", + "screenshotting", + "unparseable", + "bucketings" ], "ignoreRegExpList": ["https?://[^\\s)]+", "`[^`]*`"] }