AgentCanvas / Pages / Developer Guide / Core / Decisions / Dataflow / ADR-dataflow-009
2026-06-11 15:58
Date
2026-06-11 15:58
Status
accepted
Field
dataflow
Revises
ADR-dataflow-008 (executes its deferred alternative (d): after-loop work now rides iterOut's output side; config.post_loop retired)

Symmetric completion of ADR-dataflow-008. That ADR made iterIn two-sided (init inputs on the left, loop-carry outputs on the right) and explicitly deferred the mirror move on iterOut. This ADR performs it: iterOut gains a stop halt input on its loop side and a final_* output side that fires exactly once at scope termination. The standalone termination node type and the config.post_loop / terminationCondition mechanisms are removed entirely.

Context

Termination and evaluation were authored as ordinary dataflow nodes, but their correctness requirements are phase-shaped: a termination check must be consulted exactly once per iteration, and an evaluate node's verdict must reflect the terminal iteration's values. The executor held this together with four special cases โ€” a termination defer guard, a settle-pass skip for termination nodes, a latched_done latch, and the config.post_loop flag with its pop-without-clear guard โ€” the same shape of accumulated special-casing that preceded the initialize removal in ADR-dataflow-008. The cost was real, not aesthetic: TODO #64 (the terminal step's success never reached the evaluate snapshot; smartway_ce recorded SR 0.000 with 5/20 episodes actually succeeding), the explore-eqa post-loop upstream-dependency collapse (SR 0.43 โ†’ 0 when a re-fired evaluate resolved a node-fed input to None), a zombie terminationCondition field threaded through backend and frontend but read by nothing, and silent episode completion at step_count=0 still reporting completed.

Decision

Make iterOut two-sided and retire the termination node type (hard removal โ€” graphs carrying one are rejected at load with a migration hint, mirroring the initialize precedent). Concretely:

Alternatives

(a) Keep termination as authoring sugar, lowered to the stop port at load โ€” rejected (user decision): it preserves a node type that adds no expressiveness, inflating the AAS search vocabulary the same way initialize did. (b) A finalPorts trimming option so authors could expose only a subset on the final side โ€” rejected (user decision): the final side defaults to mirroring all carry ports; an unconsumed final_* handle costs nothing. (c) Keep in-loop evaluate nodes with a latch for per-step metric streams โ€” rejected (user decision): per-step telemetry is the viewer sinks' job (step.info โ†’ viewer, opt-in); the verdict comes only from the final side, so "evaluate fires mid-loop with stale terminal values" is no longer expressible. (d) Soft-deprecate with an execution-time absorb (the -008 treatment of initialize) โ€” rejected: all 24 in-repo graphs were migrated in the same change by tools/migrate_termination.py, so there is no legacy population to keep alive, and a hard validator error with a migration hint is clearer than silent rewriting.

Rationale

The refactor converts episode-verdict correctness from a scheduling accident into a structural guarantee: the final side emits the terminal iteration's values by construction, so the #64 class of bug (verdict snapshot lags the terminal step) cannot be written in the new vocabulary, and the band purity rule makes the explore-eqa class (band node starved by a loop-body dependency) a load-time validator error instead of a silent SR collapse. Four executor special cases disappear outright, and the pivot vocabulary stays at two node types with symmetric two-sided semantics โ€” pre-loop = iterIn's init side, in-loop = the body, after-loop = iterOut's final side โ€” completing the regularisation arc that ADR-dataflow-006 started and -008 advanced. Verified by 117 backend tests (including a #64 regression test asserting the after-loop consumer receives the terminal step's value, budget-exhaustion emission, and best-effort error-path emission), tsc --noEmit clean, and 24/24 + 1 composite in-repo graphs migrated and loading clean. Real-eval re-verification (smartway_ce expected SR โ‰ˆ 0.25) rides the existing TODO #64/#65 re-verification wave.

Affected docs

Updated in the same change-set (commits 7095f196 + 3760c002): core/glossary.html (Termination โ†’ Removed; new stop port / Final side / After-loop band terms), design-docs/graph/graph-executor.html (Part I rewritten: guards 4 โ†’ 2, Decide/Settle/Record, ยง4.6 trace, ยง5.1 after-loop band, ยง8 exit-paths table), capabilities/graph-execution-engine.html (ยง9, ยง10.5), design-docs/graph/loop-control-system.html (historical note), nodesets/env/template.html (decision E), nodesets/method/explore-eqa.html, core/roadmap.html (#62/#63/#64 notes), .claude/tutorials/skill-graph-json.md, .claude/commands/architect/myloop seeds + AAS mirror, decisions/dataflow/index.html + decisions/index.html (this entry).