Skip to content

05 โ€“ State Fork and Lineage

State is immutable: nodes never modify it. They produce new states via fork(), which carries lineage, confidence, and history forward.


๐ŸŽฏ Goal

Walk a branch tree and inspect how states are related, how confidence compounds, and how history records the exact node path taken.


๐Ÿงฑ Setup

from dagpipe import Payload, Schema, State, Node

class ScoreState(State):
    schema = Schema({"score": int | float, "label": str | None})

class AddBonus(Node):
    id = "score.bonus"

    def resolve(self, state):
        yield self.fork(state, confidence_delta=0.1,
                        payload_update={"score": state.get("score") + 10})

Node.fork passes node_id=self.id, so history records the point of derivation automatically.


๐Ÿ” Forking

root = ScoreState(payload=Payload({"score": 50}))

s1 = root.fork(
    payload_update={"score": 60},
    confidence_delta=0.1,
    node_id="score.bonus",
    metadata_update={"source": "manual"},
)

s2 = s1.fork(payload_update={"label": "high"})
Field root s1 s2
depth 0 1 2
score 50 60 60
confidence 1.0 1.1 1.1
history () ('score.bonus',) ('score.bonus',)
parent None root s1

๐Ÿ” Lineage

lineage() walks parents back to the root, root-first:

1
2
3
4
states = s2.lineage()
assert states == (root, s1, s2)
assert states[0] is root
assert states[-1] is s2

๐Ÿ” Immutability guarantees

1
2
3
4
assert root.get("label") is None       # s2's update didn't touch root
assert s1.get("label") is None         # fork never mutates its input
assert root.confidence == 1.0          # confidence travels forward only
assert root is s1.parent               # parent is by reference

๐Ÿ” Metadata & confidence

  • confidence_delta is added (parent.confidence + delta), so a chain of bonuses compounds: 1.0 โ†’ 1.1 โ†’ 1.2.
  • metadata_update is shallow-merged into metadata โ€” untouched states share the same metadata dict (no copy unless a fork changes it).
  • payload_update keys are dot-paths; they are validated by validate_update against the state's schema before copying.

๐Ÿ’ก Tips

  • Use Node.fork in resolve() โ€” it records node_id into history for free.
  • Fork parameters are keyword-only (payload_update, confidence_delta, node_id, metadata_update) โ€” there is no positional API.
  • A root state built directly (ScoreState(payload=...)) validates against schema in __post_init__ โ€” you can't create an invalid root.
  • Since states are immutable and share structure, forking many branches is cheap โ€” copy only the modified branch (see Payload.update).