The Mirror Plan
The Mirror Plan: every task ships its own inverse, so every plan has a REFLECTION - same graph with the arrows flipped, undo agents in place of do agents - and running plan-then-mirror returns the world to its exact initial state, byte for byte. Which sounds like a party trick until a plan fails halfway with real side effects already committed: then the mirror of the completed prefix is the compensation saga you'd otherwise write by hand at 3am, and the strangest thing about …
Reliability & Recovery
Round 19
Luca Guidi
exit 0
bundle exec ruby examples/mirror_plan.rb
a real captured run
THE MIRROR PLAN (every plan carries its own reflection)
act 1 - the plan runs forward: lamp reserved, card charged, listing live
world changed: true (inventory 2, ledger 1 entries)
...then its mirror runs (same steps, arrows flipped, undo for do):
world restored byte-for-byte: true
act 2 - the same plan dies at step 3 (payment provider said no... late):
completed before the crash: reserve stock, charge card
stock is reserved and money is TAKEN - the classic 3am state.
the mirror of the COMPLETED PREFIX runs (a saga, auto-derived):
world restored byte-for-byte: true
act 3 - mirror(mirror(plan)) == plan: true (reflection is an involution;
the mandala next door would like a word)
the honest fine print: this works because every step declared an
inverse that TRULY inverts - pop for push, += for -=. some real
actions have no inverse (you cannot unsend an email; you can only
send an apology), and the discipline the mirror imposes is exactly
that: it forces you to ANSWER, per step, 'what is the undo?' -
and steps with no answer get quarantined behind the ones that
have one. compensation isn't a framework feature you buy; it's a
question you agree to keep answering. the mirror just makes the
question mandatory, which is the most architecture anything in
this file does.
source
# frozen_string_literal: true # The Mirror Plan: every task ships its own inverse, so every plan # has a REFLECTION - same graph with the arrows flipped, undo agents # in place of do agents - and running plan-then-mirror returns the # world to its exact initial state, byte for byte. Which sounds like # a party trick until a plan fails halfway with real side effects # already committed: then the mirror of the completed prefix is the # compensation saga you'd otherwise write by hand at 3am, and the # strangest thing about it is that it was sitting in the graph all # along, wearing the plan's clothes backwards. # # bundle exec ruby examples/mirror_plan.rb # # Runs offline; exits 1 unless both mirrors restore the world. require class="s">"bundler/setup" require class="s">"agentic" Agentic.logger.level = class="y">:fatal # Each step declares its action AND its inverse, over a shared world STEPS = [ {name: class="s">"reserve stock", do: ->(w) { w[class="y">:inventory][class="s">"lamp"] -= 1 }, undo: ->(w) { w[class="y">:inventory][class="s">"lamp"] += 1 }}, {name: class="s">"charge card", do: ->(w) { w[class="y">:ledger] << {charge: 4900} }, undo: ->(w) { w[class="y">:ledger].pop }}, {name: class="s">"publish listing", do: ->(w) { w[class="y">:published] << class="s">"lamp" }, undo: ->(w) { w[class="y">:published].delete(class="s">"lamp") }}, {name: class="s">"notify warehouse", do: ->(w) { w[class="y">:outbox] << class="s">"pick lamp" }, undo: ->(w) { w[class="y">:outbox].pop }} ].freeze def run(steps, world, direction:, fail_at: nil) orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 1, retry_policy: {max_retries: 0, retryable_errors: []}) ordered = (direction == class="y">:mirror) ? steps.reverse : steps completed = [] previous = nil ordered.each do |step| action = (direction == class="y">:mirror) ? step[class="y">:undo] : step[class="y">:do] task = Agentic:class="y">:Task.new(description: class="s">"#{direction}: #{step[class="y">:name]}", agent_spec: {class="s">"name" => step[class="y">:name], class="s">"instructions" => class="s">"w"}) orchestrator.add_task(task, previous ? [previous] : [], agent: ->(_t) { raise Agentic:class="y">:Errors:class="y">:LlmAuthenticationError, class="s">"payment provider said no" if step[class="y">:name] == fail_at action.call(world) completed << step class="y">:done }) previous = task end orchestrator.execute_plan completed end def snapshot(world) = Marshal.dump(world) fresh_world = -> { {inventory: {class="s">"lamp" => 3}, ledger: [], published: [], outbox: []} } puts class="s">"THE MIRROR PLAN (every plan carries its own reflection)" puts # --- act 1: forward, then the full mirror - the world round-trips ------------------- world = fresh_world.call genesis = snapshot(world) run(STEPS, world, direction: class="y">:forward) changed = snapshot(world) != genesis puts class="s">" act 1 - the plan runs forward: lamp reserved, card charged, listing live" puts class="s">" world changed: #{changed} (inventory #{world[class="y">:inventory]["lampclass="s">"]}, ledger #{world[class="y">:ledger].size} entries)" run(STEPS, world, direction: class="y">:mirror) restored_full = snapshot(world) == genesis puts class="s">" ...then its mirror runs (same steps, arrows flipped, undo for do):" puts class="s">" world restored byte-for-byte: #{restored_full}" puts # --- act 2: the plan fails halfway - mirror only what completed --------------------- world = fresh_world.call genesis = snapshot(world) completed = run(STEPS, world, direction: class="y">:forward, fail_at: class="s">"publish listing") puts class="s">" act 2 - the same plan dies at step 3 (payment provider said no... late):" puts class="s">" completed before the crash: #{completed.map { |s| s[class="y">:name] }.join(", class="s">")}" puts class="s">" stock is reserved and money is TAKEN - the classic 3am state." run(completed, world, direction: class="y">:mirror) restored_partial = snapshot(world) == genesis puts class="s">" the mirror of the COMPLETED PREFIX runs (a saga, auto-derived):" puts class="s">" world restored byte-for-byte: #{restored_partial}" puts # --- act 3: the mirror of the mirror is the plan ------------------------------------ involution = STEPS.reverse.reverse == STEPS puts class="s">" act 3 - mirror(mirror(plan)) == plan: #{involution} (reflection is an involution;" puts class="s">" the mandala next door would like a word)" puts failures = [] failures << class="s">"forward plan changed nothing" unless changed failures << class="s">"full mirror failed to restore" unless restored_full failures << class="s">"compensation failed to restore" unless restored_partial failures << class="s">"reflection is not an involution" unless involution puts class="s">" the honest fine print: this works because every step declared an" puts class="s">" inverse that TRULY inverts - pop for push, += for -=. some real" puts class="s">" actions have no inverse (you cannot unsend an email; you can only" puts class="s">" send an apology), and the discipline the mirror imposes is exactly" puts class="s">" that: it forces you to ANSWER, per step, 'what is the undo?' -" puts class="s">" and steps with no answer get quarantined behind the ones that" puts class="s">" have one. compensation isn't a framework feature you buy; it's a" puts class="s">" question you agree to keep answering. the mirror just makes the" puts class="s">" question mandatory, which is the most architecture anything in" puts class="s">" this file does." exit(failures.empty? ? 0 : 1)