self_agency 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +40 -0
- data/.irbrc +22 -0
- data/CHANGELOG.md +5 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +177 -0
- data/Rakefile +8 -0
- data/docs/api/configuration.md +85 -0
- data/docs/api/errors.md +166 -0
- data/docs/api/index.md +37 -0
- data/docs/api/self-agency-module.md +198 -0
- data/docs/architecture/overview.md +181 -0
- data/docs/architecture/security.md +101 -0
- data/docs/assets/images/self_agency.gif +0 -0
- data/docs/assets/images/self_agency.mp4 +0 -0
- data/docs/development/contributing.md +45 -0
- data/docs/development/setup.md +81 -0
- data/docs/development/testing.md +70 -0
- data/docs/examples/autonomous-robots.md +109 -0
- data/docs/examples/basic-examples.md +237 -0
- data/docs/examples/collaborative-robots.md +98 -0
- data/docs/examples/full-workflow.md +100 -0
- data/docs/examples/index.md +36 -0
- data/docs/getting-started/installation.md +71 -0
- data/docs/getting-started/quick-start.md +94 -0
- data/docs/guide/configuration.md +113 -0
- data/docs/guide/generating-methods.md +146 -0
- data/docs/guide/how-to-use.md +144 -0
- data/docs/guide/lifecycle-hooks.md +86 -0
- data/docs/guide/prompt-templates.md +189 -0
- data/docs/guide/saving-methods.md +84 -0
- data/docs/guide/scopes.md +74 -0
- data/docs/guide/source-inspection.md +96 -0
- data/docs/index.md +77 -0
- data/examples/01_basic_usage.rb +27 -0
- data/examples/02_multiple_methods.rb +43 -0
- data/examples/03_scopes.rb +40 -0
- data/examples/04_source_inspection.rb +46 -0
- data/examples/05_lifecycle_hook.rb +55 -0
- data/examples/06_configuration.rb +97 -0
- data/examples/07_error_handling.rb +103 -0
- data/examples/08_class_context.rb +64 -0
- data/examples/09_method_override.rb +52 -0
- data/examples/10_full_workflow.rb +118 -0
- data/examples/11_collaborative_robots/atlas.rb +31 -0
- data/examples/11_collaborative_robots/echo.rb +30 -0
- data/examples/11_collaborative_robots/main.rb +190 -0
- data/examples/11_collaborative_robots/nova.rb +71 -0
- data/examples/11_collaborative_robots/robot.rb +119 -0
- data/examples/12_autonomous_robots/analyst.rb +193 -0
- data/examples/12_autonomous_robots/collector.rb +78 -0
- data/examples/12_autonomous_robots/main.rb +166 -0
- data/examples/12_autonomous_robots/planner.rb +125 -0
- data/examples/12_autonomous_robots/robot.rb +284 -0
- data/examples/generated/from_range_class.rb +3 -0
- data/examples/generated/mean_instance.rb +4 -0
- data/examples/generated/median_instance.rb +15 -0
- data/examples/generated/report_singleton.rb +3 -0
- data/examples/generated/standard_deviation_instance.rb +8 -0
- data/examples/lib/message_bus.rb +57 -0
- data/examples/lib/setup.rb +8 -0
- data/lib/self_agency/configuration.rb +76 -0
- data/lib/self_agency/errors.rb +35 -0
- data/lib/self_agency/generator.rb +47 -0
- data/lib/self_agency/prompts/generate/system.txt.erb +15 -0
- data/lib/self_agency/prompts/generate/user.txt.erb +13 -0
- data/lib/self_agency/prompts/shape/system.txt.erb +26 -0
- data/lib/self_agency/prompts/shape/user.txt.erb +10 -0
- data/lib/self_agency/sandbox.rb +17 -0
- data/lib/self_agency/saver.rb +62 -0
- data/lib/self_agency/validator.rb +64 -0
- data/lib/self_agency/version.rb +5 -0
- data/lib/self_agency.rb +315 -0
- data/mkdocs.yml +156 -0
- data/sig/self_agency.rbs +4 -0
- metadata +163 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# 12_autonomous_robots/main.rb — Autonomous Robots Demo
|
|
5
|
+
#
|
|
6
|
+
# Demonstrates:
|
|
7
|
+
# - Three-layer LLM approach: decompose goal -> generate helpers -> generate orchestrator
|
|
8
|
+
# - Robots receive "what" not "how" — LLM decides method names, algorithms, data structures
|
|
9
|
+
# - Pipeline execution: collect landmarks -> analyze data -> plan itinerary
|
|
10
|
+
#
|
|
11
|
+
# Contrast with Demo 11:
|
|
12
|
+
# Demo 11 dictates method names and algorithms in the task description.
|
|
13
|
+
# Demo 12 gives only a high-level goal and lets the LLM decide everything.
|
|
14
|
+
#
|
|
15
|
+
# Requires a running Ollama instance with the configured model.
|
|
16
|
+
|
|
17
|
+
require_relative "robot"
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Create message bus and autonomous robots
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
puts "=== Autonomous Robots — City Landmarks Tour Pipeline ==="
|
|
24
|
+
puts ""
|
|
25
|
+
|
|
26
|
+
bus = MessageBus.new
|
|
27
|
+
|
|
28
|
+
collector = Robot.new(
|
|
29
|
+
name: "Collector",
|
|
30
|
+
goal: "Return an Array of 8 Hashes representing fictional city landmarks. " \
|
|
31
|
+
"Each Hash has Symbol keys :name (String), :type (String, e.g. 'museum'), " \
|
|
32
|
+
":duration (Integer, 30..180 minutes), :rating (Float, 1.0..5.0). " \
|
|
33
|
+
"Do NOT wrap the Array in an outer Hash.",
|
|
34
|
+
bus: bus
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
puts ""
|
|
38
|
+
|
|
39
|
+
analyst = Robot.new(
|
|
40
|
+
name: "Analyst",
|
|
41
|
+
goal: "Analyze landmark data. The input is an Array of Hashes, each with keys " \
|
|
42
|
+
":name (String), :type (String), :duration (Integer), :rating (Float). " \
|
|
43
|
+
"Return a Hash with three keys: " \
|
|
44
|
+
":statistics (a Hash with :avg_rating, :avg_duration, :total_duration, :count), " \
|
|
45
|
+
":ranked (the landmarks Array sorted by :rating descending), " \
|
|
46
|
+
":by_type (a Hash grouping landmarks by :type)",
|
|
47
|
+
bus: bus,
|
|
48
|
+
receives_input: true
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
puts ""
|
|
52
|
+
|
|
53
|
+
planner = Robot.new(
|
|
54
|
+
name: "Planner",
|
|
55
|
+
goal: "Create a formatted one-day tour itinerary. The input is a Hash with keys " \
|
|
56
|
+
":statistics, :ranked, and :by_type. " \
|
|
57
|
+
"Use :ranked (an Array of Hashes with :name, :type, :duration, :rating) " \
|
|
58
|
+
"to select top-rated landmarks that fit within 360 total minutes of visit time. " \
|
|
59
|
+
"Return a String with a formatted itinerary listing each stop with its name, " \
|
|
60
|
+
"type, duration, and rating",
|
|
61
|
+
bus: bus,
|
|
62
|
+
receives_input: true
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Display capabilities and generated source
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
puts ""
|
|
70
|
+
puts "=== Capabilities Summary ==="
|
|
71
|
+
[collector, analyst, planner].each do |robot|
|
|
72
|
+
puts "#{robot.name}: #{robot.capabilities.inspect}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
puts ""
|
|
76
|
+
puts "=== Generated Source Code ==="
|
|
77
|
+
[collector, analyst, planner].each do |robot|
|
|
78
|
+
robot.generation_log.each do |entry|
|
|
79
|
+
puts "--- #{robot.name}##{entry[:method_name]} ---"
|
|
80
|
+
puts entry[:code]
|
|
81
|
+
puts ""
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Execute the pipeline
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
puts "=== Executing Pipeline ==="
|
|
90
|
+
puts ""
|
|
91
|
+
|
|
92
|
+
# Step 1: Collector builds a landmark catalog (no input)
|
|
93
|
+
puts "Step 1: Collector builds landmark catalog..."
|
|
94
|
+
collector_result = collector.perform_task
|
|
95
|
+
collector.send_message(to: "Analyst", content: collector_result)
|
|
96
|
+
puts "Collector produced: #{collector_result.class}"
|
|
97
|
+
puts ""
|
|
98
|
+
|
|
99
|
+
# Step 2: Analyst processes the catalog
|
|
100
|
+
puts "Step 2: Analyst analyzes landmark data..."
|
|
101
|
+
analyst_input = analyst.inbox.last&.dig(:content)
|
|
102
|
+
analyst_result = analyst.perform_task(analyst_input)
|
|
103
|
+
analyst.send_message(to: "Planner", content: analyst_result)
|
|
104
|
+
puts "Analyst produced: #{analyst_result.class}"
|
|
105
|
+
puts ""
|
|
106
|
+
|
|
107
|
+
# Step 3: Planner creates the itinerary
|
|
108
|
+
puts "Step 3: Planner creates tour itinerary..."
|
|
109
|
+
planner_input = planner.inbox.last&.dig(:content)
|
|
110
|
+
final_itinerary = planner.perform_task(planner_input)
|
|
111
|
+
puts ""
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Display results
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
puts "=== Final Tour Itinerary ==="
|
|
118
|
+
puts final_itinerary.to_s
|
|
119
|
+
puts ""
|
|
120
|
+
|
|
121
|
+
bus.print_log
|
|
122
|
+
puts ""
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Generation statistics
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
puts "=== Generation Statistics ==="
|
|
129
|
+
total_methods = 0
|
|
130
|
+
total_lines = 0
|
|
131
|
+
[collector, analyst, planner].each do |robot|
|
|
132
|
+
methods = robot.generation_log.size
|
|
133
|
+
lines = robot.generation_log.sum { |e| e[:code].lines.size }
|
|
134
|
+
total_methods += methods
|
|
135
|
+
total_lines += lines
|
|
136
|
+
puts "#{robot.name}: #{methods} method(s), #{lines} lines of generated code"
|
|
137
|
+
end
|
|
138
|
+
puts "Total: #{total_methods} methods, #{total_lines} lines of generated code"
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Repair statistics (if any)
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
all_repairs = [collector, analyst, planner].flat_map(&:repair_log)
|
|
145
|
+
unless all_repairs.empty?
|
|
146
|
+
puts ""
|
|
147
|
+
puts "=== Repair Statistics ==="
|
|
148
|
+
all_repairs.each do |entry|
|
|
149
|
+
status = entry[:success] ? "SUCCESS" : "FAILED"
|
|
150
|
+
puts " [#{status}] #{entry[:method_name]} — #{entry[:error]}"
|
|
151
|
+
end
|
|
152
|
+
puts "Total repair attempts: #{all_repairs.size}, " \
|
|
153
|
+
"successful: #{all_repairs.count { |e| e[:success] }}, " \
|
|
154
|
+
"failed: #{all_repairs.count { |e| !e[:success] }}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Save generated robots as subclasses
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
puts ""
|
|
162
|
+
puts "=== Saving Generated Robots ==="
|
|
163
|
+
[collector, analyst, planner].each do |robot|
|
|
164
|
+
path = robot._save!(as: robot.name)
|
|
165
|
+
puts "#{robot.name}: saved to #{path}"
|
|
166
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "robot"
|
|
4
|
+
|
|
5
|
+
class Planner < Robot
|
|
6
|
+
# def select_top_landmarks(ranked_landmarks, max_minutes=360)
|
|
7
|
+
# # Select landmarks that fit within the maximum time limit
|
|
8
|
+
# # Input: ranked_landmarks - Array of Hashes with :name, :type, :duration, :rating
|
|
9
|
+
# # max_minutes - Integer representing maximum time in minutes
|
|
10
|
+
# # Output: Array of Hashes representing selected landmarks
|
|
11
|
+
#
|
|
12
|
+
# selected = []
|
|
13
|
+
# total_time = 0
|
|
14
|
+
#
|
|
15
|
+
# ranked_landmarks.each do |landmark|
|
|
16
|
+
# if total_time + landmark[:duration] <= max_minutes
|
|
17
|
+
# selected << landmark
|
|
18
|
+
# total_time += landmark[:duration]
|
|
19
|
+
# else
|
|
20
|
+
# break
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# selected
|
|
25
|
+
# end
|
|
26
|
+
def select_top_landmarks(ranked_landmarks, max_minutes=360)
|
|
27
|
+
selected = []
|
|
28
|
+
total_time = 0
|
|
29
|
+
|
|
30
|
+
ranked_landmarks.each do |landmark|
|
|
31
|
+
if total_time + landmark[:duration] <= max_minutes
|
|
32
|
+
selected << landmark
|
|
33
|
+
total_time += landmark[:duration]
|
|
34
|
+
else
|
|
35
|
+
break
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
selected
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# def format_itinerary_section(landmarks)
|
|
43
|
+
# # Format a section of the itinerary with landmark details
|
|
44
|
+
# # Input: landmarks - Array of Hashes with :name, :type, :duration, :rating
|
|
45
|
+
# # Output: String representing formatted itinerary section
|
|
46
|
+
#
|
|
47
|
+
# return "No landmarks selected" if landmarks.empty?
|
|
48
|
+
#
|
|
49
|
+
# lines = ["Landmarks to Visit:"]
|
|
50
|
+
#
|
|
51
|
+
# landmarks.each_with_index do |landmark, index|
|
|
52
|
+
# lines << "#{index + 1}. #{landmark[:name]} (#{landmark[:type]}) -- #{landmark[:duration]} minutes -- Rating: #{landmark[:rating]}"
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# lines.join("\n")
|
|
56
|
+
# end
|
|
57
|
+
def format_itinerary_section(landmarks)
|
|
58
|
+
return "No landmarks selected" if landmarks.empty?
|
|
59
|
+
|
|
60
|
+
lines = ["Landmarks to Visit:"]
|
|
61
|
+
|
|
62
|
+
landmarks.each_with_index do |landmark, index|
|
|
63
|
+
lines << "#{index + 1}. #{landmark[:name]} (#{landmark[:type]}) -- #{landmark[:duration]} minutes -- Rating: #{landmark[:rating]}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
lines.join("\n")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# def generate_itinerary_string(statistics, ranked_landmarks, by_type)
|
|
70
|
+
# # Generate the complete formatted itinerary string
|
|
71
|
+
# # Input: statistics - Hash with statistical information
|
|
72
|
+
# # ranked_landmarks - Array of Hashes with :name, :type, :duration, :rating
|
|
73
|
+
# # by_type - Hash grouping landmarks by type
|
|
74
|
+
# # Output: String representing the complete formatted itinerary
|
|
75
|
+
#
|
|
76
|
+
# selected_landmarks = select_top_landmarks(ranked_landmarks)
|
|
77
|
+
# itinerary_section = format_itinerary_section(selected_landmarks)
|
|
78
|
+
#
|
|
79
|
+
# "#{itinerary_section}"
|
|
80
|
+
# end
|
|
81
|
+
def generate_itinerary_string(statistics, ranked_landmarks, by_type)
|
|
82
|
+
selected_landmarks = select_top_landmarks(ranked_landmarks)
|
|
83
|
+
itinerary_section = format_itinerary_section(selected_landmarks)
|
|
84
|
+
itinerary_section
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Fix the Ruby singleton method 'execute_task' on this Robot instance.
|
|
88
|
+
#
|
|
89
|
+
# Robot's overall goal: Create a formatted one-day tour itinerary. The input is a Hash with keys :statistics, :ranked, and :by_type. Use :ranked (an Array of Hashes with :name, :type, :duration, :rating) to select top-rated landmarks that fit within 360 total minutes of visit time. Return a String with a formatted itinerary listing each stop with its name, type, duration, and rating
|
|
90
|
+
# Generated capabilities on this object: [:select_top_landmarks, :format_itinerary_section, :generate_itinerary_string, :execute_task]
|
|
91
|
+
#
|
|
92
|
+
# Current source code of 'execute_task':
|
|
93
|
+
# def execute_task(input)
|
|
94
|
+
# ranked = input[:ranked]
|
|
95
|
+
# filtered_landmarks = select_top_landmarks(ranked)
|
|
96
|
+
# formatted_sections = filtered_landmarks.map { |landmark| format_itinerary_section(landmark) }
|
|
97
|
+
# generate_itinerary_string(formatted_sections)
|
|
98
|
+
# end
|
|
99
|
+
#
|
|
100
|
+
# Runtime error:
|
|
101
|
+
# NoMethodError: undefined method '[]' for nil
|
|
102
|
+
#
|
|
103
|
+
# Backtrace (top 5):
|
|
104
|
+
# (eval at /Users/dewayne/sandbox/git_repos/madbomber/self_agency/lib/self_agency.rb:265):2:in 'execute_task'
|
|
105
|
+
# /Users/dewayne/sandbox/git_repos/madbomber/self_agency/examples/12_autonomous_robots/robot.rb:72:in 'Robot#perform_task'
|
|
106
|
+
# ./main.rb:110:in '<main>'
|
|
107
|
+
#
|
|
108
|
+
# This method takes no external input.
|
|
109
|
+
#
|
|
110
|
+
# Produce a corrected version of this method that avoids the error.
|
|
111
|
+
# Keep the same method name and signature. Only define this one method.
|
|
112
|
+
# Fix the bug while preserving the method's intent.
|
|
113
|
+
def execute_task(input)
|
|
114
|
+
return "" if input.nil?
|
|
115
|
+
|
|
116
|
+
ranked = input[:ranked]
|
|
117
|
+
return "" if ranked.nil?
|
|
118
|
+
|
|
119
|
+
filtered_landmarks = select_top_landmarks(ranked)
|
|
120
|
+
return "" if filtered_landmarks.nil? || filtered_landmarks.empty?
|
|
121
|
+
|
|
122
|
+
formatted_sections = filtered_landmarks.map { |landmark| format_itinerary_section(landmark) }
|
|
123
|
+
generate_itinerary_string(formatted_sections)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# robot.rb — Autonomous Robot with three-layer LLM self-generation
|
|
4
|
+
#
|
|
5
|
+
# Layer 1 (Decompose): Direct RubyLLM.chat().ask() breaks a high-level goal
|
|
6
|
+
# into a JSON array of helper method specs. The LLM decides method names,
|
|
7
|
+
# parameter signatures, and algorithms — the user only states "what", not "how".
|
|
8
|
+
# Layer 2 (Generate Helpers): Loops through specs, calls _() for each.
|
|
9
|
+
# SelfAgency handles shape -> generate -> validate -> sandbox eval.
|
|
10
|
+
# Layer 3 (Generate Orchestrator): A final _() call generates an execute_task
|
|
11
|
+
# method that calls the helpers in whatever order the LLM decides.
|
|
12
|
+
|
|
13
|
+
require "json"
|
|
14
|
+
require_relative "../lib/message_bus"
|
|
15
|
+
require_relative "../lib/setup"
|
|
16
|
+
|
|
17
|
+
class Robot
|
|
18
|
+
include SelfAgency
|
|
19
|
+
|
|
20
|
+
MAX_REPAIR_ATTEMPTS = 3
|
|
21
|
+
|
|
22
|
+
attr_reader :name, :goal, :bus, :inbox, :capabilities, :generation_log, :repair_log
|
|
23
|
+
|
|
24
|
+
def initialize(name:, goal:, bus:, receives_input: false)
|
|
25
|
+
@name = name
|
|
26
|
+
@goal = goal
|
|
27
|
+
@bus = bus
|
|
28
|
+
@receives_input = receives_input
|
|
29
|
+
@inbox = []
|
|
30
|
+
@capabilities = []
|
|
31
|
+
@generation_log = []
|
|
32
|
+
@repair_log = []
|
|
33
|
+
|
|
34
|
+
bus.register(self)
|
|
35
|
+
|
|
36
|
+
# Layer 1 — Decompose goal into helper method specs
|
|
37
|
+
puts "#{@name}: Decomposing goal..."
|
|
38
|
+
specs = decompose_task(goal)
|
|
39
|
+
puts "#{@name}: LLM decomposed goal into #{specs.size} helper(s)"
|
|
40
|
+
|
|
41
|
+
# Layer 2 — Generate each helper method via _()
|
|
42
|
+
specs.each do |spec|
|
|
43
|
+
description = spec["description"]
|
|
44
|
+
puts "#{@name}: Generating helper — #{description[0, 80]}..."
|
|
45
|
+
|
|
46
|
+
begin
|
|
47
|
+
defined_methods = _(description, scope: :singleton)
|
|
48
|
+
@capabilities.concat(defined_methods)
|
|
49
|
+
puts "#{@name}: Generated #{defined_methods.inspect}"
|
|
50
|
+
rescue SelfAgency::Error => e
|
|
51
|
+
puts "#{@name}: Failed to generate helper: #{e.message}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Layer 3 — Generate orchestrator that calls the helpers
|
|
56
|
+
puts "#{@name}: Generating orchestrator for #{@capabilities.size} helper(s)..."
|
|
57
|
+
generate_orchestrator
|
|
58
|
+
|
|
59
|
+
puts "#{@name}: Ready with capabilities: #{@capabilities.inspect}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def perform_task(input = nil)
|
|
63
|
+
attempts = 0
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
arity = method(:execute_task).arity
|
|
67
|
+
puts "#{@name}: Running execute_task (input: #{input.class})"
|
|
68
|
+
|
|
69
|
+
if arity == 0
|
|
70
|
+
execute_task
|
|
71
|
+
else
|
|
72
|
+
execute_task(input)
|
|
73
|
+
end
|
|
74
|
+
rescue => error
|
|
75
|
+
attempts += 1
|
|
76
|
+
puts "#{@name}: Error (attempt #{attempts}/#{MAX_REPAIR_ATTEMPTS}): #{error.class}: #{error.message}"
|
|
77
|
+
|
|
78
|
+
if attempts < MAX_REPAIR_ATTEMPTS
|
|
79
|
+
repair_method(error, input)
|
|
80
|
+
retry
|
|
81
|
+
else
|
|
82
|
+
puts "#{@name}: MALFUNCTION — failed after #{MAX_REPAIR_ATTEMPTS} repair attempts: #{error.message}"
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def receive_message(from:, content:)
|
|
89
|
+
@inbox << { from: from, content: content }
|
|
90
|
+
puts "#{@name}: Received message from #{from}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def send_message(to:, content:)
|
|
94
|
+
@bus.deliver(from: @name, to: to, content: content)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def broadcast(content:)
|
|
98
|
+
@bus.broadcast(from: @name, content: content)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def on_method_generated(method_name, scope, code)
|
|
102
|
+
@generation_log << { method_name: method_name, scope: scope, code: code }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
# Self-repair: identify the failing method from the backtrace, regenerate it via LLM.
|
|
108
|
+
# Includes goal, capabilities, input shape, and sibling source for full context.
|
|
109
|
+
def repair_method(error, input = nil)
|
|
110
|
+
failing_method = identify_failing_method(error)
|
|
111
|
+
current_source = find_generated_source(failing_method)
|
|
112
|
+
puts "#{@name}: Repairing #{failing_method.inspect}"
|
|
113
|
+
|
|
114
|
+
# Build a snapshot of all generated method sources for context
|
|
115
|
+
sibling_sources = @generation_log
|
|
116
|
+
.select { |e| e[:method_name] != failing_method }
|
|
117
|
+
.map { |e| " def #{e[:method_name]}(...) — see generation log" }
|
|
118
|
+
.join("\n")
|
|
119
|
+
|
|
120
|
+
# Describe the input data shape so the LLM understands what it's working with
|
|
121
|
+
input_shape = describe_data_shape(input)
|
|
122
|
+
|
|
123
|
+
description = <<~DESC
|
|
124
|
+
Fix the Ruby singleton method '#{failing_method}' on this Robot instance.
|
|
125
|
+
|
|
126
|
+
Robot's overall goal: #{@goal}
|
|
127
|
+
Generated capabilities on this object: #{@capabilities.inspect}
|
|
128
|
+
|
|
129
|
+
Current source code of '#{failing_method}':
|
|
130
|
+
#{current_source}
|
|
131
|
+
|
|
132
|
+
Runtime error:
|
|
133
|
+
#{error.class}: #{error.message}
|
|
134
|
+
|
|
135
|
+
Backtrace (top 5):
|
|
136
|
+
#{error.backtrace&.first(5)&.join("\n ")}
|
|
137
|
+
|
|
138
|
+
#{input_shape}
|
|
139
|
+
|
|
140
|
+
Produce a corrected version of this method that avoids the error.
|
|
141
|
+
Keep the same method name and signature. Only define this one method.
|
|
142
|
+
Fix the bug while preserving the method's intent.
|
|
143
|
+
DESC
|
|
144
|
+
|
|
145
|
+
defined_methods = _(description, scope: :singleton)
|
|
146
|
+
@repair_log << { method_name: failing_method, error: "#{error.class}: #{error.message}", success: true }
|
|
147
|
+
puts "#{@name}: Repaired #{failing_method} -> #{defined_methods.inspect}"
|
|
148
|
+
rescue SelfAgency::Error => e
|
|
149
|
+
@repair_log << { method_name: failing_method, error: "#{error.class}: #{error.message}", success: false }
|
|
150
|
+
puts "#{@name}: Repair generation failed: #{e.message}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Scan the backtrace for a method name that matches one of our generated capabilities.
|
|
154
|
+
# Handles both Ruby <3.4 (`method') and Ruby >=3.4 ('Class#method') backtrace formats.
|
|
155
|
+
def identify_failing_method(error)
|
|
156
|
+
return :execute_task unless error.backtrace
|
|
157
|
+
|
|
158
|
+
error.backtrace.each do |line|
|
|
159
|
+
match = line.match(/in ['`](?:\w+#)?(\w+)'/)
|
|
160
|
+
next unless match
|
|
161
|
+
|
|
162
|
+
method_name = match[1].to_sym
|
|
163
|
+
return method_name if @capabilities.include?(method_name) && method_name != :execute_task
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
:execute_task
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Look up the most recent generated source for a method from the generation log.
|
|
170
|
+
def find_generated_source(method_name)
|
|
171
|
+
entry = @generation_log.reverse.find { |e| e[:method_name] == method_name }
|
|
172
|
+
entry ? entry[:code] : "(source not found)"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Build a human-readable description of the input data shape for the repair prompt.
|
|
176
|
+
# Shows class, keys, and a sample element so the LLM understands the actual structure.
|
|
177
|
+
def describe_data_shape(input)
|
|
178
|
+
return "This method takes no external input." if input.nil?
|
|
179
|
+
|
|
180
|
+
lines = ["The execute_task method received input of type #{input.class}."]
|
|
181
|
+
|
|
182
|
+
case input
|
|
183
|
+
when Hash
|
|
184
|
+
lines << "Top-level keys: #{input.keys.inspect}"
|
|
185
|
+
input.each do |key, value|
|
|
186
|
+
case value
|
|
187
|
+
when Array
|
|
188
|
+
lines << " input[#{key.inspect}] is an Array with #{value.size} element(s)."
|
|
189
|
+
if value.first.is_a?(Hash)
|
|
190
|
+
lines << " Sample element keys: #{value.first.keys.inspect}"
|
|
191
|
+
lines << " Sample element: #{value.first.inspect[0, 200]}"
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
lines << " input[#{key.inspect}] is a #{value.class}: #{value.inspect[0, 100]}"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
when Array
|
|
198
|
+
lines << "Array with #{input.size} element(s)."
|
|
199
|
+
if input.first.is_a?(Hash)
|
|
200
|
+
lines << "Sample element keys: #{input.first.keys.inspect}"
|
|
201
|
+
lines << "Sample element: #{input.first.inspect[0, 200]}"
|
|
202
|
+
end
|
|
203
|
+
else
|
|
204
|
+
lines << "Value preview: #{input.inspect[0, 200]}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
lines.join("\n")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Layer 1 — Ask the LLM to decompose a high-level goal into helper method specs.
|
|
211
|
+
# Returns an Array of Hashes with "description" keys.
|
|
212
|
+
def decompose_task(goal)
|
|
213
|
+
cfg = SelfAgency.configuration
|
|
214
|
+
chat = RubyLLM.chat(model: cfg.model, provider: cfg.provider)
|
|
215
|
+
|
|
216
|
+
prompt = <<~PROMPT
|
|
217
|
+
You are an autonomous task decomposition engine. Given a high-level goal,
|
|
218
|
+
decide what Ruby helper methods are needed to accomplish it. YOU choose
|
|
219
|
+
the method names, parameters, algorithms, and data structures.
|
|
220
|
+
|
|
221
|
+
Return a JSON array of method specifications. Each element must have:
|
|
222
|
+
- "description": a precise Ruby method specification including the method
|
|
223
|
+
name you chose (snake_case), parameter names and types, return type,
|
|
224
|
+
and a detailed algorithm. This description will be given to a Ruby code
|
|
225
|
+
generator, so be precise and complete.
|
|
226
|
+
|
|
227
|
+
Rules:
|
|
228
|
+
- Choose clear, descriptive method names
|
|
229
|
+
- Design clean data structures (prefer Hashes with Symbol keys and Arrays)
|
|
230
|
+
- Each method should do one thing well
|
|
231
|
+
- Methods should be composable — later methods can use output of earlier ones
|
|
232
|
+
- Do NOT include an orchestrator method — only helper methods
|
|
233
|
+
- Keep to 2-4 helper methods maximum
|
|
234
|
+
|
|
235
|
+
Respond with ONLY the JSON array. No markdown fences, no explanation.
|
|
236
|
+
|
|
237
|
+
Goal: #{goal}
|
|
238
|
+
PROMPT
|
|
239
|
+
|
|
240
|
+
response = chat.ask(prompt)
|
|
241
|
+
raw = response.content.to_s.strip
|
|
242
|
+
|
|
243
|
+
raw = raw.gsub(/<think>.*?<\/think>/m, "")
|
|
244
|
+
raw = raw.sub(/\A```\w*\n?/, "").sub(/\n?```\s*\z/, "")
|
|
245
|
+
raw.strip!
|
|
246
|
+
|
|
247
|
+
JSON.parse(raw)
|
|
248
|
+
rescue JSON::ParserError => e
|
|
249
|
+
puts "#{@name}: Failed to parse decomposition: #{e.message}"
|
|
250
|
+
[]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Layer 3 — Generate an orchestrator method that calls the helpers.
|
|
254
|
+
# The LLM decides how to wire the helpers together to achieve the goal.
|
|
255
|
+
def generate_orchestrator
|
|
256
|
+
helper_list = @capabilities.map(&:to_s).join(", ")
|
|
257
|
+
input_clause = if @receives_input
|
|
258
|
+
"It takes one parameter (input) which is the data passed in from a previous stage."
|
|
259
|
+
else
|
|
260
|
+
"It takes no parameters."
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
description = <<~DESC
|
|
264
|
+
An instance method named 'execute_task' that orchestrates the following
|
|
265
|
+
goal: #{@goal}
|
|
266
|
+
|
|
267
|
+
#{input_clause}
|
|
268
|
+
|
|
269
|
+
Available helper methods on this object: #{helper_list}
|
|
270
|
+
|
|
271
|
+
Call the helper methods in whatever order makes sense to accomplish the goal.
|
|
272
|
+
Return the final result. Do NOT define the helper methods — they already exist.
|
|
273
|
+
Only define the execute_task method itself.
|
|
274
|
+
DESC
|
|
275
|
+
|
|
276
|
+
begin
|
|
277
|
+
defined_methods = _(description, scope: :singleton)
|
|
278
|
+
@capabilities.concat(defined_methods)
|
|
279
|
+
puts "#{@name}: Generated orchestrator #{defined_methods.inspect}"
|
|
280
|
+
rescue SelfAgency::Error => e
|
|
281
|
+
puts "#{@name}: Failed to generate orchestrator: #{e.message}"
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
def median
|
|
2
|
+
return 0.0 if @data.nil? || @data.empty?
|
|
3
|
+
|
|
4
|
+
sorted_data = @data.sort
|
|
5
|
+
length = sorted_data.length
|
|
6
|
+
|
|
7
|
+
if length.odd?
|
|
8
|
+
middle_index = length / 2
|
|
9
|
+
sorted_data[middle_index].to_f
|
|
10
|
+
else
|
|
11
|
+
right_middle_index = length / 2
|
|
12
|
+
left_middle_index = right_middle_index - 1
|
|
13
|
+
(sorted_data[left_middle_index] + sorted_data[right_middle_index]).to_f / 2.0
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# message_bus.rb — Simple pub/sub message bus for robot collaboration
|
|
4
|
+
#
|
|
5
|
+
# Provides point-to-point delivery and broadcast messaging between
|
|
6
|
+
# registered robots. No SelfAgency dependency — plain infrastructure.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MessageBus
|
|
10
|
+
attr_reader :log
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@robots = {}
|
|
14
|
+
@log = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def register(robot)
|
|
18
|
+
@robots[robot.name] = robot
|
|
19
|
+
puts "MessageBus: registered robot '#{robot.name}'"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def deliver(from:, to:, content:)
|
|
23
|
+
recipient = @robots[to]
|
|
24
|
+
unless recipient
|
|
25
|
+
puts "MessageBus: unknown recipient '#{to}'"
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@log << { from: from, to: to, content: content }
|
|
30
|
+
recipient.receive_message(from: from, content: content)
|
|
31
|
+
puts "MessageBus: #{from} -> #{to} (#{content_preview(content)})"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def broadcast(from:, content:)
|
|
35
|
+
@robots.each do |name, robot|
|
|
36
|
+
next if name == from
|
|
37
|
+
|
|
38
|
+
@log << { from: from, to: name, content: content }
|
|
39
|
+
robot.receive_message(from: from, content: content)
|
|
40
|
+
end
|
|
41
|
+
puts "MessageBus: #{from} -> broadcast (#{content_preview(content)})"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def print_log
|
|
45
|
+
puts "=== Message Bus Log (#{@log.size} messages) ==="
|
|
46
|
+
@log.each_with_index do |entry, i|
|
|
47
|
+
puts " #{i + 1}. #{entry[:from]} -> #{entry[:to]}: #{content_preview(entry[:content])}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def content_preview(content)
|
|
54
|
+
text = content.to_s
|
|
55
|
+
text.length > 80 ? "#{text[0, 77]}..." : text
|
|
56
|
+
end
|
|
57
|
+
end
|