🧪 test(flows): the authoring model end to end over the durable engine - #194
Merged
Conversation
…Error The authoring half of the plan: seven closure-free node variants (Succeed, All, Map, AndThen, FlowCall, ActivityCall, Branch) with digested deferred functions in out-of-band tables, the Planned<T> branded placeholder whose proxy throws GraphBuildError on any computation (toPrimitive, valueOf, toString, toJSON, application), and the GraphBuildError vocabulary. Docs: docs/specs/Concepts/Unified Flow Authoring.md (cherry picked from commit 64624ac)
Activity.make gains a string-first overload: a named, schema-typed atom (payload/success/error/tier/idempotencyKey) whose implementation attaches separately via .toLayer(execute) through the existing registration and activity machinery, and whose .call(payload) records an ActivityCall node for plans. The inline options form is unchanged. Docs: docs/specs/Concepts/Unified Flow Authoring.md (cherry picked from commit b883814)
…otations Flow.make accepts an optional plan-time body (required once the migration completes), Flow.prototype.call records a FlowCall node, the trampoline outcome vocabulary lands as pure data (Flow.done / flow.to / Flow.park over the existing Waiting reasons), and Capabilities / EffectsDeclaration / Placement join the annotation keys. Docs: docs/specs/Concepts/Unified Flow Authoring.md Docs: docs/specs/Concepts/Trampoline Loops.md (cherry picked from commit ad7df62)
roninjin10
force-pushed
the
flow-body-required
branch
from
August 12, 2026 05:48
95b82ec to
da8e044
Compare
Evaluates a body against its real payload with strict Planned placeholders for step results, expands branch arms as static topology, splices inline flow calls with capability intersection, rejects recursive calls with recursion_requires_boundary, reads effects/capabilities/placement from the authoring annotations, and emits NodeDrafts that Plan.compile keys directly. Docs: docs/specs/Concepts/Unified Flow Authoring.md (cherry picked from commit f8bf150)
Interpreter.layer(flow) registers a generic handler over the FlowRuntime port: Graph.build against the real payload, dependency-order driving of the compiled drafts, activity calls through the existing persistence machinery with declared implementations resolved from an Implementations table, branch predicates on real values running only the taken arm, Ref-path projection threading, and journaled replay on execution-id reuse. Bodied flows reject toLayer; behavior comes from exactly one place. Docs: docs/specs/Concepts/Unified Flow Authoring.md (cherry picked from commit 42ee5ce) (cherry picked from commit 72f9b1e)
A bodied flow's root value settles as Complete (done), Suspended (park, existing Waiting vocabulary), or the new encoded Handoff (to): the engine plans the target and runs it as the next round. Rounds chain under one lineage via parent_run_id with deterministic (lineage, ordinal) execution ids, the settle-and-create seam is one storage transaction, maxRounds is declared on the origin flow and enforced at the handoff with a typed terminal failure, and the root caller's polling follows handoff chains transparently to the final value. Docs: docs/specs/Concepts/Trampoline Loops.md (cherry picked from commit acafd2e) (cherry picked from commit e1f2cc8)
flow.child(payload) records a non-expanded boundary node whose material carries the child's tag, payload, and declared envelope. The interpreter runs it as a genuine child execution with a deterministic id derived from the parent execution and node address, parent edges and suspension propagate through the existing machinery, and placement conflicts on inline .call() now error with placement_requires_boundary directing to .child(). Docs: docs/specs/Concepts/Unified Flow Authoring.md (cherry picked from commit f09bc16) (cherry picked from commit b4ec945)
The branch move applied to the error channel: Node.catch protects a subgraph with a failure arm that is plan-visible via an explicit failure edge reason, filters errors by digested schema (missing filter = the full typed error channel), never intercepts defects or compensation failures, and settles the protected node's planned reference with the caught error so failure-arm payloads consume it without re-running failed work. Docs: docs/specs/Concepts/Unified Flow Authoring.md (cherry picked from commit 47ffb00) (cherry picked from commit 37d8dc2)
Every handler-flow in packages/flow is gone: a Flow.make declaration with a toLayer handler was the attachment-decides shape the authoring model rejects, so each one splits into the two nouns it always was — a declared Activity that does the work, and a bodied Flow whose body names it. Suites drive them through Interpreter.layer over one shared Activity.layerImplementations table, added to the runtime fixture as layerWired. The fixture also threads a dispatch's interrupted flag back to the flow, the way it already threads the waiting classification: a recorded interruption is a durable outcome, and the flag DurableDeferred.await sets on the instance in scope must reach the flow or a terminal outcome reads as an external suspension. FlowDefinitionGaps' definition-level withRollback cases pinned the legacy shape and now pin the new one: Flow.Execution<Tag> is a phantom only Flow.toLayer discharges, so the combinator is reached from a declared activity's implementation through a named cast that says so. Docs: docs/specs/Concepts/Unified Flow Authoring.md Depends-on: 417035e (cherry picked from commit 9e7b75e) (cherry picked from commit 2dd7f2d)
…un model Engine and engine-store test fixtures move off handler-style flows onto declared activities, bodied flows, Interpreter.layer, and shared activity implementation tables. Behavior-identical; no production sources changed; run-store had no legacy call sites. (cherry picked from commit e043ff4) (cherry picked from commit 9afadd5)
… call sites Every remaining handler-flow outside packages/flow, engine, engine-store, and run-store moves to the two-noun model: a declared Activity that does the work, and a bodied Flow whose body names it, driven by Interpreter.layer over one Activity.layerImplementations table. The seven examples are documentation as much as code, so each declared atom is named for what it does rather than wrapped as "<flow>/activity": Greet, Bundle, Assess, Release, Measure, Post, CompileTarget. Where the old handler WAS a single sealed activity (05, and the fork suite's fork-once), the inline activity becomes the declaration itself, so the journal shape and the sealed cache key are unchanged and the fork still replays with one dispatch. Where it was a composite (02, 03, 04, 06), the composite becomes the declared atom's implementation and the inner inline activities stay exactly as they were. Example 01 now provides NodeCrypto: an activity dispatch is recorded under a derived step identity, so the memory engine needs a Crypto that a handler-only flow never asked for. Example 03's Summary field is renamed handlerEntries -> stepEntries because the counter now counts the step implementation's entries. The time-travel EngineIntegration fixture keeps its composite handler as ONE declared activity rather than four body nodes, so the evidence its assertions fold stays the shape it was written against; the body's own step adds one dispatch, and the four attempt/anchor/fork counts move from 3 to 4 and 2 to 3 accordingly. SqlForkExecution drops engine.register for the ordinary layer composition over Layer.succeed(FlowRuntime, engine). Docs: docs/specs/Concepts/Unified Flow Authoring.md Depends-on: e043ff4 (cherry picked from commit a3f2bda) (cherry picked from commit bfd8891)
The README and the introduction both opened on a handler-flow, which is the shape example 01 no longer has. Both quick starts now declare the Activity, attach its implementation, and give the Flow a body that names it, over Interpreter.layer and the implementation table — the same program the example runs. The examples page follows: the 01 row, the 03 assertion, and the reading order say declaration, implementation, and body instead of handler, and the 06 row drops the entry counts the added step node changed. Docs: README.md, docs/pages/index.md, docs/pages/examples.md Depends-on: a3f2bda (cherry picked from commit e41c07b) (cherry picked from commit d4b4d3c)
The two nouns stop overlapping. `Flow.make` requires `body`, so every declaration carries the pure plan-time function that IS its behavior, and `Flow.Any.body` is no longer optional. `toLayer` leaves the flow surface entirely: an Activity carries an implementation, attached separately as a Layer, and a Flow carries a body, driven by `Interpreter.layer`. Attaching a second, opaque behavior to a flow is not refused at run time any more — there is nothing to attach it with. What the optional-body stage needed goes with it. `Flow.Bodied` described a flow that had a body, which is now every flow, so `Flow.Flow` is that type. `Flow.BodyDefinesBehavior` was the defect `toLayer` raised on a bodied flow. `InterpreterError`'s `missing_body` code and `Interpreter.layer`'s check are gone because the argument has a body by construction. Graph building drops its body-less callee branches: an inline call is spliced unless its declaration did not survive beside its AST, which is now the only leaf inline call and the only call with no body digest to fold in. The runtime's own registration seam stays, internal. `Activity.toLayer` still registers the activity's flow form so an activity is executable as a durable execution of its own; that form is now an ordinary bodied flow whose body is the one call to the activity, registered through `FlowRuntime.register` directly rather than through a flow-level `toLayer`. Call sites: no legacy handler-flow survived waves A-C, but the engine and engine-store suites that drive `FlowRuntime.register` directly declare flows as port fixtures, where the handler under test is the behavior. Those declarations take an inert body. `execute`, `poll`, `interrupt`, and `resume` are unchanged. Docs: docs/specs/Concepts/Unified Flow Authoring.md Depends-on: e41c07b (cherry picked from commit 10ca198) (cherry picked from commit 27f0f20)
…oved The requirement `Flow.make` now enforces is asserted where a reader looks for it. A declaration that omits `body` does not compile, pinned by the directive tsc rejects when the call succeeds, and `Flow.Any["body"]` is non-nullable, so no consumer is left an optional-body branch to write. Beside it, what 10ca198 removed is pinned absent: `Flow.Bodied` no longer resolves as a type, `BodyDefinesBehavior` is off the flow namespace, and `InterpreterError`'s code union is the five codes that remain. `@smthrs/plan` gets the breaking-change entry the flow one implies. A flow call's `KeyMaterial.body` no longer has a body-less shape, so `StepKey.content` folds a body digest into every flow-call node whose declaration survived beside its AST, and the keys `Plan.compile` derives for the calls that carried none move with it. The prose that still described a flow handler is corrected. `withRollback` applies inside the flow execution; the implementations table is what a composition driving behavior registered directly with the runtime does not need; and the four site pages that showed a body-less `Flow.make` or a flow-level `toLayer` now show an activity carrying the implementation and `Interpreter.layer` driving the body. Each sample was compiled against the real packages before it landed. Docs: docs/specs/Concepts/Unified Flow Authoring.md, docs/pages/architecture.md, docs/pages/api/flow.md, docs/pages/api/flows.md, docs/pages/api/engine.md Depends-on: 10ca198 (cherry picked from commit 95b82ec) (cherry picked from commit da8e044)
One suite in the barrel package that drives the real SQLite-backed stores: the counter-to-100 lineage and its parent_run_id chain, a lineage re-driven after the worker that opened a round went away, both branch arms on real values, the planned-value refusals and the field access they still allow, the recursion refusal and the handoff it points at, a .child() boundary with its parent edge and suspension, Sleep and WaitFor parking and waking, the round budget, both Node.catch arms, and Graph.build to Plan.compile to PlanStore with a PlanDiff of round-over-round growth. Docs: docs/specs/Concepts/Unified Flow Authoring.md, docs/specs/Concepts/Trampoline Loops.md Depends-on: 95b82ec (cherry picked from commit a4f2936) (cherry picked from commit 55430c6)
roninjin10
force-pushed
the
flow-body-required
branch
from
August 12, 2026 06:51
da8e044 to
4734448
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The end-to-end suite over the durable engine:
packages/flows/test/e2e-authoring.test.tsexercises the whole decided API through real@smthrs/*resolution — all ten scenarios:parent_run_idchain, ordinals, final valuerecursion_requires_boundary.child(): real child execution, parent edge, suspension, replaySleeppark/wake · 8.WaitForpark/wakemaxRounds→ typed terminal failure ·Node.catcharmsGraph.build→Plan.compile→PlanStore→PlanDiffround-over-round growthNo source changes — tests only.
Depends-on: #193
Verification
packages/flows3 files / 171 tests at 100% coverage (independently re-verified); conformance 117