AgentCanvas / Pages / Developer Guide / Core / Decisions / Dataflow / ADR-dataflow-008
2026-06-09 21:32
Date
2026-06-09 21:32
Status
accepted
Field
dataflow
Revises
ADR-dataflow-006 (pivot count 3 → 2)

Partial revision of ADR-dataflow-006. That ADR introduced the separate initialize pivot to cure iterIn's dual-role multi-fire problem. This ADR folds initialize back into iterIn's input side — but keeps everything -006 actually fixed: the init_/iterout_ prefix-separation of synthesised slots, and the three temporal phases (pre-loop / in-loop / after-loop). What changes is purely where the init slots are authored (on the iterIn, not on a separate node) and the node-type count.

Context

ADR-dataflow-006 split the run-start collection role out of iterIn into a separate initialize sink, giving three pivot node types per loop scope (initialize, iterIn, iterOut). Two things made that split look heavier than it needed to be in hindsight. (a) initialize is already half-dissolved: its ports are not consumed as a node at all — _synthesize_iterin_ports (graph_def.py) folds the paired initialize.ports into iterIn's unified config.ports surface as init_<X> slots at load, and the executor's run-start transfer just re-prefixes and writes those into iterIn.port_slots. The separate node is effectively a wire-collection anchor for slots iterIn already owns. (b) The platform's north star is AAS search-space regularisation (paper #1): the searchable graph vocabulary should be as small and regular as possible, and a pivot type that adds no expressiveness inflates it. The triggering observation (user): the seed nodes feeding initialize could just feed iterIn's left/input side directly, and iterIn would "perfectly take over" the run-start role.

Decision

Make iterIn two-sided and retire initialize as an authored node. iterIn's left side carries the run-start init inputs (captured once, at run start), its right side the per-iteration loop-carry outputs; iterOut is unchanged. Concretely:

Alternatives

(a) Keep three pivots — rejected; initialize adds a node type without adding expressiveness, and the AAS search benefits from a smaller, more regular pivot vocabulary. (b) Hard-eliminate initialize and reject any graph still using it (as ADR-031 did for init_ports) — rejected; keeping the node-type registered plus an execution-time absorb lets unmigrated / legacy graphs load and run unchanged, and makes JSON migration opt-in per file rather than a flag day. (c) Keep initialize at runtime but synthesise it as a hidden internal node at load (authoring drops it, runtime keeps it) — rejected; this retains the runtime concept and injects synthetic nodes that muddy logging / replay / scope-analysis (which key on node ids), the opposite of the user's "iterIn takes over" mental model and of the regularisation goal. (d) Force after-loop work through an iterOut output side at the same time (full left/right symmetry on both pivots) — deferred; investigation showed the post-loop pass has no clean terminating-iterOut fire on 3 of 4 exit paths and the two production post_loop graphs feed their evaluate from body nodes, so "everything through iterOut" would stale their inputs. The after-loop stage stays as the existing config.post_loop + _post_loop_pass.

Rationale

The change is safe because the property ADR-dataflow-006 actually relied on — the init_/iterout_ prefix-separation of iterIn's synthesised slots (the real fix for the v3 "obs-freeze" dual-write bug) — is preserved unchanged; it lives on iterIn's port surface, not on the separate node. Relaxing -006's "iterIn takes no canvas wires" invariant is therefore sound: init values now arrive as ordinary incoming edges, but they land in disjoint init_<X> slots, so they cannot collide with the iterout_<X> loop-carry writes. Because the executor already routes iterIn-targeted edges into port_slots, the runtime delta is essentially edge-rewiring plus deleting one node type — small and contained. Behaviour is provably preserved: for all 17 graphs the synthesised iterIn port surface equals the pre-change HEAD version and the flatten+absorb exec form is byte-identical; a 10-episode mapgpt-mp3d eval (gpt-5-mini, 10 workers) completed clean on the migrated graph. The temporal phase decomposition is intact — pre-loop = iterIn's left inputs (captured once), in-loop = the body, after-loop = the existing post-loop pass — so this is a vocabulary reduction (3 → 2 pivot types), not a semantics change.

Affected docs

core/glossary.md (Initialize entry → deprecated/folded; iterIn now two-sided; new initPorts term), core/architecture.md (§4.1 Execution Engine — three-pivot → two-sided iteration), capabilities/graph-execution-engine.md + capabilities/graph-expressible-agents.md (three-pivot framing), core/codebase-map.md (new _absorb_initialize_pivots helper; scripts/migrations/migrate_initialize_to_initports.py), decisions/dataflow/index.md + decisions/index.md (this entry).