Skip to content

JSON Serialization

Chains round-trip to JSON so you can persist, share, and reload them.

chain.save("my_chain.json") # write to disk
loaded = ReasoningChain.load("my_chain.json")
d = chain.to_dict(); ReasoningChain.from_dict(d)
s = chain.to_json(); ReasoningChain.from_json(s)

from_dict runs full validation (cycles, dependency references, reference-syntax warnings).

from_dict(data, use_typed_steps=False) rebuilds legacy StepDescription objects by default. Pass use_typed_steps=True (or use from_dict_typed(data)) to rebuild the typed step classes instead — preferred for new code.

to_dict() stamps two fields:

  • format_version: int — bumped when the wire shape changes.
  • carl_version: str — informational (the mmar-carl version that wrote it).

The compatibility contract:

SituationBehaviour
Read olderA chain saved at format_version = N stays loadable on every CARL that ships FORMAT_VERSION ≥ N.
Read newerA newer wire format raises ChainFormatNewerError(required, this) — MAESTRO catches it and prompts the user to upgrade.
Unknown step typeFails loudly rather than silently dropping the step.

Some fields are intentionally not serialized because they hold live objects: sub_chain (handoff), agents (supervisor), metrics, cache.key_fn, and the callbacks on ReasoningContext. Re-attach these in code after loading.

Results round-trip too — persist an execution losslessly and reload it later (this is what RunRecord wraps):

result = chain.execute(context)
result.save("result.json") # lossless by default
loaded = ReasoningResult.load("result.json")
d = result.to_dict(full=True) # full=True keeps every step's detail
ReasoningResult.from_dict(d)
s = result.to_json(); ReasoningResult.from_json(s)

Per-step results serialize individually via StepExecutionResult.to_dict(truncate=False) / from_dict — truncate controls whether long step outputs are clipped.