agentic examples

The Allocation Audit

The Allocation Audit: every object is a promissory note the GC collects on later. This audit counts exactly what each framework operation allocates (GC.stat's total_allocated_objects is an exact counter, not a sample) and where the GC actually runs during a plan. Latency spikes that "come from nowhere" come from here.

Observability & Ops Round 11 Koichi Sasada (ko1) exit 0

source on github

bundle exec ruby examples/allocation_audit.rb

a real captured run

ALLOCATION AUDIT (objects per operation, exact via GC.stat)

  Task.new                         18 objects
  validator: happy path            37 objects
  validator: rejection            448 objects   ########
  graph snapshot (10 tasks)       381 objects   #######
  to_json_schema                   29 objects
    ...graph, isolated             96 objects   (snapshot alone, build subtracted)

  a full 10-task plan allocates 1592 objects and triggered 0 GC run(s).

  reading the audit like a VM person: the happy-path validation
  (37 objects) is what you multiply by requests-per-second -
  37 x 1000 rps is 37000 promissory notes a second, and the GC
  collects on schedule whether you budgeted or not. rejection costs
  12x the happy path in objects (exceptions carry backtraces;
  error paths are allocation paths), and the graph snapshot's
  96 objects of dup+freeze buy the immutability every round-8
  tool leans on - that's not waste, that's a purchase. allocation
  isn't evil; UNBUDGETED allocation is. now there's a budget.

source

# frozen_string_literal: true

# The Allocation Audit: every object is a promissory note the GC
# collects on later. This audit counts exactly what each framework
# operation allocates (GC.stat's total_allocated_objects is an exact
# counter, not a sample) and where the GC actually runs during a
# plan. Latency spikes that "come from nowhere" come from here.
#
#   bundle exec ruby examples/allocation_audit.rb
#
# Runs offline; counts are exact for this Ruby version.

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

Agentic.logger.level = class="y">:fatal

def allocations(iterations = 100)
  yield # warm: first call pays memoization, schema compilation, caches
  GC.start
  before = GC.stat(class="y">:total_allocated_objects)
  iterations.times { yield }
  (GC.stat(class="y">:total_allocated_objects) - before) / iterations
end

def task_named(name)
  Agentic:class="y">:Task.new(description: name, agent_spec: {class="s">"name" => class="s">"w", class="s">"instructions" => class="s">"work"})
end

SPEC = Agentic:class="y">:CapabilitySpecification.new(
  name: class="s">"audit", description: class="s">"x", version: class="s">"1.0.0",
  inputs: {mode: {type: class="s">"string", required: true, enum: %w[a b]},
           weight: {type: class="s">"number", required: true, min: 1, max: 100}},
  rules: {fits: {relation: class="y">:sum_lte, fields: [class="y">:weight], limit: 100}}
)
VALIDATOR = Agentic:class="y">:CapabilityValidator.new(SPEC)

def ten_task_orchestrator
  orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 4)
  previous = nil
  10.times do |i|
    task = task_named(class="s">"t#{i}")
    orchestrator.add_task(task, previous ? [previous] : [], agent: ->(_t) { class="y">:ok })
    previous = task
  end
  orchestrator
end

puts class="s">"ALLOCATION AUDIT (objects per operation, exact via GC.stat)"
puts

rows = {
  class="s">"Task.new" => -> { task_named(class="s">"x") },
  class="s">"validator: happy path" => -> { VALIDATOR.validate_inputs!(mode: class="s">"a", weight: 50) },
  class="s">"validator: rejection" => -> {
    begin
      VALIDATOR.validate_inputs!(mode: class="s">"z", weight: 500)
    rescue Agentic:class="y">:Errors:class="y">:ValidationError
      nil
    end
  },
  class="s">"graph snapshot (10 tasks)" => -> { ten_task_orchestrator.graph },
  class="s">"to_json_schema" => -> { SPEC.to_json_schema }
}

counts = rows.transform_values { |op| allocations(&op) }
counts.each do |label, objects|
  puts format(class="s">"  %-28s %6d objects   %s", label, objects, class="s">"#" * [objects / 50, 40].min)
end

# The graph row includes building the orchestrator - separate the two
build_only = allocations { ten_task_orchestrator }
graph_only = counts[class="s">"graph snapshot (10 tasks)"] - build_only
puts format(class="s">"  %-28s %6d objects   (snapshot alone, build subtracted)", class="s">"  ...graph, isolated", graph_only)

# --- where the GC actually fires during a plan ---------------------------------
orchestrator = ten_task_orchestrator
GC.start
gc_before = GC.count
allocated_before = GC.stat(class="y">:total_allocated_objects)
orchestrator.execute_plan
plan_allocations = GC.stat(class="y">:total_allocated_objects) - allocated_before
gc_runs = GC.count - gc_before

puts
puts format(class="s">"  a full 10-task plan allocates %d objects and triggered %d GC run(s).", plan_allocations, gc_runs)
puts
per_call = counts[class="s">"validator: happy path"]
puts class="s">"  reading the audit like a VM person: the happy-path validation"
puts class="s">"  (#{per_call} objects) is what you multiply by requests-per-second -"
puts class="s">"  #{per_call} x 1000 rps is #{per_call * 1000} promissory notes a second, and the GC"
puts class="s">"  collects on schedule whether you budgeted or not. rejection costs"
puts class="s">"  #{counts["validator: rejectionclass="s">"] / per_call}x the happy path in objects (exceptions carry backtraces;"
puts class="s">"  error paths are allocation paths), and the graph snapshot's"
puts class="s">"  #{graph_only} objects of dup+freeze buy the immutability every round-8"
puts class="s">"  tool leans on - that's not waste, that's a purchase. allocation"
puts class="s">"  isn't evil; UNBUDGETED allocation is. now there's a budget."