The Collaboration Tracer
The Collaboration Tracer: lifecycle hooks record every message the orchestrator sends and every reply that comes back, then the run is drawn as a sequence diagram. Object-oriented programs are conversations; this makes the conversation visible.
Observability & Ops
Round 3
Sandi Metz
exit 0
bundle exec ruby examples/collaboration_tracer.rb
a real captured run
COLLABORATION TRACE (8 messages)
Orchestrator Researcher Writer Editor
| | | |
|-------------->| | |
| perform(resea | | |
|<--------------| | |
| done: "3 fact | | |
|------------------------------>| |
| perform(writer) | |
|------------------------------>| |
| here's "3 facts about fibe... | |
|<------------------------------| |
| done: "draft built on: 3 ..." | |
|---------------------------------------------->|
| perform(editor) | |
|---------------------------------------------->|
| here's "draft built on: 3 ..."| |
|<----------------------------------------------|
| done: "tightened: draft b..." | |
| | | |
read it like a conversation: every arrow is a message, every
reply flows back before the next collaborator is addressed.
source
# frozen_string_literal: true # The Collaboration Tracer: lifecycle hooks record every message the # orchestrator sends and every reply that comes back, then the run is # drawn as a sequence diagram. Object-oriented programs are # conversations; this makes the conversation visible. # # bundle exec ruby examples/collaboration_tracer.rb # # Runs offline: a three-agent editorial pipeline, traced. require class="s">"bundler/setup" require class="s">"agentic" PIPELINE = { class="s">"Researcher" => {work: ->(_prev) { class="s">"3 facts about fibers" }}, class="s">"Writer" => {work: ->(prev) { class="s">"draft built on: #{prev}" }}, class="s">"Editor" => {work: ->(prev) { class="s">"tightened: #{prev.split(":class="s">").first}" }} }.freeze trace = [] hooks = { before_task_execution: ->(task_id:, task:) { trace << {from: class="s">"Orchestrator", to: task.description, label: class="s">"perform(#{task.description.downcase})"} unless task.dependency_outputs.empty? task.dependency_outputs.each_value do |output| trace << {from: class="s">"Orchestrator", to: task.description, label: class="s">"here's \"#{output.to_s[0, 18]}...\class="s">""} end end }, after_task_success: ->(task_id:, task:, result:, duration:) { trace << {from: task.description, to: class="s">"Orchestrator", label: class="s">"done: \"#{result.output.to_s[0, 18]}...\class="s">""} } } orchestrator = Agentic:class="y">:PlanOrchestrator.new(lifecycle_hooks: hooks) previous = nil PIPELINE.each do |role, spec| task = Agentic:class="y">:Task.new( description: role, agent_spec: {class="s">"name" => role, class="s">"instructions" => class="s">"collaborate"}, payload: spec[class="y">:work] ) orchestrator.add_task(task, previous ? [previous] : [], agent: ->(t) { t.payload.call(t.dependency_outputs.values.first) }) previous = task end orchestrator.execute_plan # --- render the conversation as a sequence diagram -------------------------- actors = [class="s">"Orchestrator"] + PIPELINE.keys width = 16 positions = actors.each_with_index.to_h { |actor, i| [actor, i * width + width / 2] } line_width = actors.size * width puts class="s">"COLLABORATION TRACE (#{trace.size} messages)" puts puts actors.map { |a| a.center(width) }.join puts positions.values.each_with_object(class="s">" " * line_width) { |pos, line| line[pos] = class="s">"|" } trace.each do |message| from_pos = positions[message[class="y">:from]] to_pos = positions[message[class="y">:to]] left, right = [from_pos, to_pos].minmax # the arrow line, with lifelines drawn through line = class="s">" " * line_width positions.each_value { |pos| line[pos] = class="s">"|" } (left + 1...right).each { |i| line[i] = class="s">"-" } line[(from_pos < to_pos) ? right - 1 : left + 1] = (from_pos < to_pos) ? class="s">">" : class="s">"<" puts line # the label line label = class="s">" " * line_width positions.each_value { |pos| label[pos] = class="s">"|" } text = message[class="y">:label][0, right - left - 3] label[left + 2, text.length] = text puts label end puts positions.values.each_with_object(class="s">" " * line_width) { |pos, line| line[pos] = class="s">"|" } puts puts class="s">"read it like a conversation: every arrow is a message, every" puts class="s">"reply flows back before the next collaborator is addressed."