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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +40 -0
  4. data/.irbrc +22 -0
  5. data/CHANGELOG.md +5 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +177 -0
  9. data/Rakefile +8 -0
  10. data/docs/api/configuration.md +85 -0
  11. data/docs/api/errors.md +166 -0
  12. data/docs/api/index.md +37 -0
  13. data/docs/api/self-agency-module.md +198 -0
  14. data/docs/architecture/overview.md +181 -0
  15. data/docs/architecture/security.md +101 -0
  16. data/docs/assets/images/self_agency.gif +0 -0
  17. data/docs/assets/images/self_agency.mp4 +0 -0
  18. data/docs/development/contributing.md +45 -0
  19. data/docs/development/setup.md +81 -0
  20. data/docs/development/testing.md +70 -0
  21. data/docs/examples/autonomous-robots.md +109 -0
  22. data/docs/examples/basic-examples.md +237 -0
  23. data/docs/examples/collaborative-robots.md +98 -0
  24. data/docs/examples/full-workflow.md +100 -0
  25. data/docs/examples/index.md +36 -0
  26. data/docs/getting-started/installation.md +71 -0
  27. data/docs/getting-started/quick-start.md +94 -0
  28. data/docs/guide/configuration.md +113 -0
  29. data/docs/guide/generating-methods.md +146 -0
  30. data/docs/guide/how-to-use.md +144 -0
  31. data/docs/guide/lifecycle-hooks.md +86 -0
  32. data/docs/guide/prompt-templates.md +189 -0
  33. data/docs/guide/saving-methods.md +84 -0
  34. data/docs/guide/scopes.md +74 -0
  35. data/docs/guide/source-inspection.md +96 -0
  36. data/docs/index.md +77 -0
  37. data/examples/01_basic_usage.rb +27 -0
  38. data/examples/02_multiple_methods.rb +43 -0
  39. data/examples/03_scopes.rb +40 -0
  40. data/examples/04_source_inspection.rb +46 -0
  41. data/examples/05_lifecycle_hook.rb +55 -0
  42. data/examples/06_configuration.rb +97 -0
  43. data/examples/07_error_handling.rb +103 -0
  44. data/examples/08_class_context.rb +64 -0
  45. data/examples/09_method_override.rb +52 -0
  46. data/examples/10_full_workflow.rb +118 -0
  47. data/examples/11_collaborative_robots/atlas.rb +31 -0
  48. data/examples/11_collaborative_robots/echo.rb +30 -0
  49. data/examples/11_collaborative_robots/main.rb +190 -0
  50. data/examples/11_collaborative_robots/nova.rb +71 -0
  51. data/examples/11_collaborative_robots/robot.rb +119 -0
  52. data/examples/12_autonomous_robots/analyst.rb +193 -0
  53. data/examples/12_autonomous_robots/collector.rb +78 -0
  54. data/examples/12_autonomous_robots/main.rb +166 -0
  55. data/examples/12_autonomous_robots/planner.rb +125 -0
  56. data/examples/12_autonomous_robots/robot.rb +284 -0
  57. data/examples/generated/from_range_class.rb +3 -0
  58. data/examples/generated/mean_instance.rb +4 -0
  59. data/examples/generated/median_instance.rb +15 -0
  60. data/examples/generated/report_singleton.rb +3 -0
  61. data/examples/generated/standard_deviation_instance.rb +8 -0
  62. data/examples/lib/message_bus.rb +57 -0
  63. data/examples/lib/setup.rb +8 -0
  64. data/lib/self_agency/configuration.rb +76 -0
  65. data/lib/self_agency/errors.rb +35 -0
  66. data/lib/self_agency/generator.rb +47 -0
  67. data/lib/self_agency/prompts/generate/system.txt.erb +15 -0
  68. data/lib/self_agency/prompts/generate/user.txt.erb +13 -0
  69. data/lib/self_agency/prompts/shape/system.txt.erb +26 -0
  70. data/lib/self_agency/prompts/shape/user.txt.erb +10 -0
  71. data/lib/self_agency/sandbox.rb +17 -0
  72. data/lib/self_agency/saver.rb +62 -0
  73. data/lib/self_agency/validator.rb +64 -0
  74. data/lib/self_agency/version.rb +5 -0
  75. data/lib/self_agency.rb +315 -0
  76. data/mkdocs.yml +156 -0
  77. data/sig/self_agency.rbs +4 -0
  78. 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,3 @@
1
+ def self.from_range(low, high)
2
+ new((low..high).to_a)
3
+ end
@@ -0,0 +1,4 @@
1
+ def mean
2
+ return Float::NAN if @data.empty?
3
+ @data.sum.to_f / @data.size
4
+ 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,3 @@
1
+ def report
2
+ "Data count: #{@data.length}\nMean: #{self.mean}\nMedian: #{self.median}\nStandard deviation: #{self.standard_deviation}"
3
+ end
@@ -0,0 +1,8 @@
1
+ def standard_deviation
2
+ return 0.0 if @data.nil? || @data.empty?
3
+
4
+ mean = @data.sum.to_f / @data.length
5
+ squared_differences = @data.map { |x| (x - mean) ** 2 }
6
+ variance = squared_differences.sum.to_f / squared_differences.length
7
+ Math.sqrt(variance)
8
+ 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
@@ -0,0 +1,8 @@
1
+ require_relative "../../lib/self_agency"
2
+
3
+ SelfAgency.configure do |config|
4
+ config.provider = :ollama
5
+ config.model = "qwen3-coder:30b"
6
+ config.api_base = "http://localhost:11434/v1"
7
+ config.request_timeout = 120
8
+ end