The Round Trip
The Round Trip: serialize a plan's graph to JSON, rebuild a fresh orchestrator from the JSON, and prove the rebuilt topology is isomorphic to the original - same shape, same labels, new ids. A projection you can't invert is a projection you can't trust with your plans.
Data & Pipelines
Round 6
Xavier Noria
exit 0
bundle exec ruby examples/plan_roundtrip.rb
a real captured run
THE WIRE FORMAT
{
"tasks": [
"gather",
"check",
"weave",
"ship"
],
"edges": [
{
"from": "check",
"to": "weave",
"label": null
},
{
"from": "gather",
"to": "weave",
"label": "threads"
},
{
"from": "weave",
"to": "ship",
"label": null
}
]
}
THE VERDICT
round trip is faithful: 3 edges, labels intact,
topological order preserved (gather -> check -> weave -> ship)
task ids are per-process and correctly absent from the wire format -
identity travels as description, structure travels as edges, and
needs: labels survive because graph[:edges] carries them.
source
# frozen_string_literal: true # The Round Trip: serialize a plan's graph to JSON, rebuild a fresh # orchestrator from the JSON, and prove the rebuilt topology is # isomorphic to the original - same shape, same labels, new ids. A # projection you can't invert is a projection you can't trust with # your plans. # # bundle exec ruby examples/plan_roundtrip.rb # # Runs offline; prints the wire format and the verdict. require class="s">"bundler/setup" require class="s">"agentic" require class="s">"json" def step(name) Agentic:class="y">:Task.new(description: name, agent_spec: {class="s">"name" => name, class="s">"instructions" => class="s">"work"}) end # --- an original plan with every edge flavor --------------------------------- original = Agentic:class="y">:PlanOrchestrator.new gather = step(class="s">"gather") check = step(class="s">"check") weave = step(class="s">"weave") ship = step(class="s">"ship") original.add_task(gather) original.add_task(check) original.add_task(weave, [check], needs: {threads: gather}) original.add_task(ship, [weave]) # --- serialize: graph -> wire format (ids replaced by descriptions) --------- def serialize(graph) names = graph[class="y">:tasks].transform_values(&class="y">:description) { class="s">"tasks" => graph[class="y">:order].map { |id| names[id] }, class="s">"edges" => graph[class="y">:edges].map { |e| {class="s">"from" => names[e[class="y">:from]], class="s">"to" => names[e[class="y">:to]], class="s">"label" => e[class="y">:label]&.to_s} } } end # --- deserialize: wire format -> a fresh orchestrator ------------------------ def deserialize(data) orchestrator = Agentic:class="y">:PlanOrchestrator.new tasks = data[class="s">"tasks"].to_h { |name| [name, step(name)] } data[class="s">"tasks"].each do |name| edges_in = data[class="s">"edges"].select { |e| e[class="s">"to"] == name } plain = edges_in.reject { |e| e[class="s">"label"] }.map { |e| tasks.fetch(e[class="s">"from"]) } named = edges_in.select { |e| e[class="s">"label"] } .to_h { |e| [e[class="s">"label"].to_sym, tasks.fetch(e[class="s">"from"])] } orchestrator.add_task(tasks[name], plain, needs: named.empty? ? nil : named) end orchestrator end wire = JSON.pretty_generate(serialize(original.graph)) rebuilt = deserialize(JSON.parse(wire)) puts class="s">"THE WIRE FORMAT" puts wire.gsub(/^/, class="s">" ") puts # --- the isomorphism check: compare shapes, not ids -------------------------- def shape(graph) names = graph[class="y">:tasks].transform_values(&class="y">:description) { order: graph[class="y">:order].map { |id| names[id] }, edges: graph[class="y">:edges].map { |e| [names[e[class="y">:from]], names[e[class="y">:to]], e[class="y">:label]] }.sort_by(&class="y">:to_s) } end before = shape(original.graph) after = shape(rebuilt.graph) puts class="s">"THE VERDICT" if before == after puts class="s">" round trip is faithful: #{before[class="y">:edges].size} edges, labels intact," puts class="s">" topological order preserved (#{after[class="y">:order].join(" -> class="s">")})" else puts class="s">" DRIFT DETECTED:" puts class="s">" before: #{before.inspect}" puts class="s">" after: #{after.inspect}" exit 1 end puts puts class="s">"task ids are per-process and correctly absent from the wire format -" puts class="s">"identity travels as description, structure travels as edges, and" puts class="s">"needs: labels survive because graph[class="y">:edges] carries them."