agentic examples

The Flaky API Drill

The Flaky API Drill: a task that times out twice before succeeding, run under a retry policy with exponential backoff and a journal. The timeline shows every attempt, every backoff gap, and the journal proves the whole ordeal - failures included - survived to disk.

Testing & Verification Round 3 Mike Perham exit 0

source on github

bundle exec ruby examples/flaky_api_drill.rb

a real captured run

FLAKY API DRILL (max 3 retries, exponential backoff from 100ms)

   82ms  > attempt 1 calling the flaky API...
ERROR: Task execution failed: upstream took too long
   85ms  x attempt failed: TimeoutError (upstream took too long)
  207ms  > attempt 2 calling the flaky API...
ERROR: Task execution failed: upstream took too long
  210ms  x attempt failed: TimeoutError (upstream took too long)
  400ms  > attempt 3 calling the flaky API...
  400ms  + sync:accounts succeeded: {"synced"=>42}
  407ms  + audit:trail succeeded: "audited 42 accounts"

plan: completed in 329ms, 3 attempts for one success

the journal remembers the whole ordeal:
  2 failed attempts and 2 successes on disk
  completed?("sync:accounts") => true  (by name - a rerun tomorrow gets new task ids and still knows)
  completed?("audit:trail")   => true

source

# frozen_string_literal: true

# The Flaky API Drill: a task that times out twice before succeeding,
# run under a retry policy with exponential backoff and a journal.
# The timeline shows every attempt, every backoff gap, and the journal
# proves the whole ordeal - failures included - survived to disk.
#
#   bundle exec ruby examples/flaky_api_drill.rb
#
# Runs offline; the flakiness is scripted so the drill is repeatable.

require class="s">"bundler/setup"
require class="s">"agentic"
require class="s">"tmpdir"

# The error class name must match the retry policy's retryable_errors
class TimeoutError < StandardError; end

JOURNAL = File.join(Dir.tmpdir, class="s">"agentic_flaky_drill.journal.jsonl")
File.delete(JOURNAL) if File.exist?(JOURNAL)

journal = Agentic:class="y">:ExecutionJournal.new(path: JOURNAL)
started = Process.clock_gettime(Process:class="y">:CLOCK_MONOTONIC)
stamp = -> { format(class="s">"%5dms", (Process.clock_gettime(Process:class="y">:CLOCK_MONOTONIC) - started) * 1000) }

timeline_hooks = journal.lifecycle_hooks(
  after_task_failure: ->(task_id:, task:, failure:, duration:) {
    puts class="s">"#{stamp.call}  x attempt failed: #{failure.type} (#{failure.message})"
  },
  after_task_success: ->(task_id:, task:, result:, duration:) {
    puts class="s">"#{stamp.call}  + #{task.description} succeeded: #{result.output.inspect}"
  }
)

orchestrator = Agentic:class="y">:PlanOrchestrator.new(
  concurrency_limit: 1,
  lifecycle_hooks: timeline_hooks,
  retry_policy: {
    max_retries: 3,
    retryable_errors: [class="s">"TimeoutError"],
    backoff_strategy: class="y">:exponential,
    backoff_base: 0.1
  }
)

# Fails twice with a retryable timeout, then delivers
attempts = 0
sync = Agentic:class="y">:Task.new(
  description: class="s">"sync:accounts",
  agent_spec: {class="s">"name" => class="s">"AccountSync", class="s">"instructions" => class="s">"sync"},
  payload: nil
)
orchestrator.add_task(sync, agent: ->(_t) {
  attempts += 1
  puts class="s">"#{stamp.call}  > attempt #{attempts} calling the flaky API..."
  raise TimeoutError, class="s">"upstream took too long" if attempts < 3

  {class="s">"synced" => 42}
})

# An innocent bystander task, to show the plan keeps moving
audit = Agentic:class="y">:Task.new(
  description: class="s">"audit:trail",
  agent_spec: {class="s">"name" => class="s">"Auditor", class="s">"instructions" => class="s">"audit"}
)
orchestrator.add_task(audit, [sync], agent: ->(t) {
  class="s">"audited #{t.output_of(sync)["syncedclass="s">"]} accounts"
})

puts class="s">"FLAKY API DRILL (max 3 retries, exponential backoff from 100ms)"
puts
result = orchestrator.execute_plan
puts
puts class="s">"plan: #{result.status} in #{(result.execution_time * 1000).round}ms, " \
  class="s">"#{attempts} attempts for one success"

state = Agentic:class="y">:ExecutionJournal.replay(path: JOURNAL)
failures = state.events.count { |e| e[class="y">:event] == class="s">"task_failed" }
successes = state.events.count { |e| e[class="y">:event] == class="s">"task_succeeded" }
puts
puts class="s">"the journal remembers the whole ordeal:"
puts class="s">"  #{failures} failed attempts and #{successes} successes on disk"
puts class="s">"  completed?(\"sync:accounts\class="s">") => #{state.completed?("sync:accountsclass="s">")}  (by name - " \
  class="s">"a rerun tomorrow gets new task ids and still knows)"
puts class="s">"  completed?(\"audit:trail\class="s">")   => #{state.completed?("audit:trailclass="s">")}"