robot_lab 0.0.4 → 0.0.7

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/README.md +64 -6
  4. data/Rakefile +2 -1
  5. data/docs/api/core/index.md +41 -46
  6. data/docs/api/core/memory.md +200 -154
  7. data/docs/api/core/network.md +13 -3
  8. data/docs/api/core/robot.md +38 -26
  9. data/docs/api/core/state.md +55 -73
  10. data/docs/api/index.md +7 -28
  11. data/docs/api/messages/index.md +35 -20
  12. data/docs/api/messages/text-message.md +67 -21
  13. data/docs/api/messages/tool-call-message.md +80 -41
  14. data/docs/api/messages/tool-result-message.md +119 -50
  15. data/docs/api/messages/user-message.md +48 -24
  16. data/docs/architecture/core-concepts.md +10 -15
  17. data/docs/concepts.md +5 -7
  18. data/docs/examples/index.md +2 -2
  19. data/docs/getting-started/configuration.md +80 -0
  20. data/docs/guides/building-robots.md +10 -9
  21. data/docs/guides/creating-networks.md +49 -0
  22. data/docs/guides/index.md +0 -5
  23. data/docs/guides/rails-integration.md +244 -162
  24. data/docs/guides/streaming.md +118 -138
  25. data/docs/index.md +0 -8
  26. data/examples/03_network.rb +10 -7
  27. data/examples/08_llm_config.rb +40 -11
  28. data/examples/09_chaining.rb +45 -6
  29. data/examples/11_network_introspection.rb +30 -7
  30. data/examples/12_message_bus.rb +1 -1
  31. data/examples/14_rusty_circuit/heckler.rb +14 -8
  32. data/examples/14_rusty_circuit/open_mic.rb +5 -3
  33. data/examples/14_rusty_circuit/scout.rb +14 -31
  34. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +1 -1
  35. data/examples/16_writers_room/display.rb +158 -0
  36. data/examples/16_writers_room/output/.gitignore +4 -0
  37. data/examples/16_writers_room/output/README.md +69 -0
  38. data/examples/16_writers_room/output/opus_001.md +263 -0
  39. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  40. data/examples/16_writers_room/output/opus_002.md +245 -0
  41. data/examples/16_writers_room/output/opus_002_notes.log +546 -0
  42. data/examples/16_writers_room/output/opus_002_screenplay.md +7989 -0
  43. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +993 -0
  44. data/examples/16_writers_room/prompts/screenplay_writer.md +66 -0
  45. data/examples/16_writers_room/prompts/writer.md +37 -0
  46. data/examples/16_writers_room/room.rb +186 -0
  47. data/examples/16_writers_room/tools.rb +173 -0
  48. data/examples/16_writers_room/writer.rb +121 -0
  49. data/examples/16_writers_room/writers_room.rb +256 -0
  50. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  51. data/lib/robot_lab/memory.rb +8 -32
  52. data/lib/robot_lab/network.rb +13 -20
  53. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  54. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  55. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  56. data/lib/robot_lab/robot.rb +56 -420
  57. data/lib/robot_lab/run_config.rb +184 -0
  58. data/lib/robot_lab/state_proxy.rb +2 -12
  59. data/lib/robot_lab/task.rb +8 -1
  60. data/lib/robot_lab/utils.rb +39 -0
  61. data/lib/robot_lab/version.rb +1 -1
  62. data/lib/robot_lab.rb +29 -8
  63. data/mkdocs.yml +0 -11
  64. metadata +21 -20
  65. data/docs/api/adapters/anthropic.md +0 -121
  66. data/docs/api/adapters/gemini.md +0 -133
  67. data/docs/api/adapters/index.md +0 -104
  68. data/docs/api/adapters/openai.md +0 -134
  69. data/docs/api/history/active-record-adapter.md +0 -275
  70. data/docs/api/history/config.md +0 -284
  71. data/docs/api/history/index.md +0 -128
  72. data/docs/api/history/thread-manager.md +0 -194
  73. data/docs/guides/history.md +0 -359
  74. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  75. data/lib/robot_lab/adapters/base.rb +0 -85
  76. data/lib/robot_lab/adapters/gemini.rb +0 -193
  77. data/lib/robot_lab/adapters/openai.rb +0 -160
  78. data/lib/robot_lab/adapters/registry.rb +0 -81
  79. data/lib/robot_lab/errors.rb +0 -70
  80. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  81. data/lib/robot_lab/history/config.rb +0 -115
  82. data/lib/robot_lab/history/thread_manager.rb +0 -93
  83. data/lib/robot_lab/robotic_model.rb +0 -324
@@ -2,9 +2,9 @@
2
2
 
3
3
  Stream LLM responses in real-time for better user experience.
4
4
 
5
- ## Basic Streaming
5
+ ## Streaming via Callbacks
6
6
 
7
- Pass a block to `robot.run` to receive streaming events:
7
+ RobotLab robots support streaming through callback methods inherited from RubyLLM::Agent. Register callbacks before calling `run`:
8
8
 
9
9
  ```ruby
10
10
  robot = RobotLab.build(
@@ -12,89 +12,105 @@ robot = RobotLab.build(
12
12
  system_prompt: "You are a creative storyteller."
13
13
  )
14
14
 
15
- robot.run("Tell me a story about a brave robot") do |event|
16
- puts event.inspect
15
+ # Register streaming callback
16
+ robot.on_new_message do |message|
17
+ print message.content if message.content
17
18
  end
19
+
20
+ result = robot.run("Tell me a story about a brave robot")
18
21
  ```
19
22
 
20
- ## Event Types
23
+ ## Available Callbacks
21
24
 
22
- ### Text Deltas
25
+ ### on_new_message
23
26
 
24
- Receive text as it is generated:
27
+ Called when the assistant starts generating a new message, with streaming chunks:
25
28
 
26
29
  ```ruby
27
- robot.run("Tell me a story") do |event|
28
- if event[:event] == "text.delta"
29
- print event[:data][:content]
30
- end
30
+ robot.on_new_message do |message|
31
+ print message.content if message.content
31
32
  end
32
33
  ```
33
34
 
34
- ### Tool Calls
35
+ ### on_end_message
35
36
 
36
- Know when tools are being called:
37
+ Called when the assistant finishes a message:
37
38
 
38
39
  ```ruby
39
- robot.run("What's the weather in Tokyo?") do |event|
40
- case event[:event]
41
- when "tool_call.start"
42
- puts "\nCalling: #{event[:data][:name]}"
43
- when "tool_call.complete"
44
- puts "Done: #{event[:data][:result]}"
45
- end
40
+ robot.on_end_message do |message|
41
+ puts "\n--- Response complete ---"
42
+ puts "Content length: #{message.content&.length}"
46
43
  end
47
44
  ```
48
45
 
49
- ### Lifecycle Events
46
+ ### on_tool_call
50
47
 
51
- Track execution lifecycle:
48
+ Called when the LLM invokes a tool:
52
49
 
53
50
  ```ruby
54
- robot.run("Help me with my task") do |event|
55
- case event[:event]
56
- when "run.started"
57
- puts "Starting..."
58
- when "run.completed"
59
- puts "Completed!"
60
- when "run.failed"
61
- puts "Failed: #{event[:data][:error]}"
62
- end
51
+ robot.on_tool_call do |tool_call|
52
+ puts "Calling tool: #{tool_call.name}"
63
53
  end
64
54
  ```
65
55
 
66
- ## Event Reference
56
+ ### on_tool_result
67
57
 
68
- | Event | Description | Data |
69
- |-------|-------------|------|
70
- | `run.started` | Execution began | `run_id` |
71
- | `run.completed` | Execution finished | `run_id` |
72
- | `run.failed` | Error occurred | `run_id`, `error` |
73
- | `text.delta` | Text content chunk | `content` |
74
- | `tool_call.start` | Tool execution starting | `name`, `input` |
75
- | `tool_call.complete` | Tool execution done | `name`, `result` |
58
+ Called when a tool returns its result:
59
+
60
+ ```ruby
61
+ robot.on_tool_result do |tool_call, result|
62
+ puts "Tool #{tool_call.name} returned: #{result}"
63
+ end
64
+ ```
76
65
 
77
- ## Comprehensive Event Handling
66
+ ## Comprehensive Callback Setup
78
67
 
79
- Handle all event types in a single block:
68
+ Register all callbacks for full visibility:
80
69
 
81
70
  ```ruby
82
- robot.run("Analyze this data and generate a report") do |event|
83
- case event[:event]
84
- when "text.delta"
85
- print event[:data][:content]
86
- when "tool_call.start"
87
- puts "\n[Tool] Calling: #{event[:data][:name]}"
88
- when "tool_call.complete"
89
- puts "[Tool] Done: #{event[:data][:name]}"
90
- when "run.completed"
91
- puts "\n--- Complete ---"
92
- when "run.failed"
93
- puts "\n[Error] #{event[:data][:error]}"
94
- end
71
+ robot = RobotLab.build(
72
+ name: "assistant",
73
+ system_prompt: "You are helpful.",
74
+ local_tools: [WeatherTool]
75
+ )
76
+
77
+ robot.on_new_message do |message|
78
+ print message.content if message.content
79
+ end
80
+
81
+ robot.on_end_message do |_message|
82
+ puts "\n--- Done ---"
83
+ end
84
+
85
+ robot.on_tool_call do |tool_call|
86
+ puts "\n[Tool] Calling: #{tool_call.name}"
87
+ end
88
+
89
+ robot.on_tool_result do |tool_call, result|
90
+ puts "[Tool] #{tool_call.name} returned: #{result}"
95
91
  end
92
+
93
+ result = robot.run("What's the weather in Tokyo?")
96
94
  ```
97
95
 
96
+ ## Streaming via Chat Block
97
+
98
+ For more control, pass a block directly to `chat.ask` (the underlying RubyLLM method):
99
+
100
+ ```ruby
101
+ robot = RobotLab.build(
102
+ name: "chat_bot",
103
+ system_prompt: "You are a helpful assistant."
104
+ )
105
+
106
+ # Use the underlying chat directly with a streaming block
107
+ robot.chat.ask("Tell me a story") do |chunk|
108
+ print chunk.content if chunk.content
109
+ end
110
+ ```
111
+
112
+ Note: Using `chat.ask` directly bypasses Robot's memory resolution and tool hierarchy. Use callbacks with `robot.run` when you need those features.
113
+
98
114
  ## Web Integration
99
115
 
100
116
  ### Rails Action Cable
@@ -107,9 +123,15 @@ class ChatChannel < ApplicationCable::Channel
107
123
  system_prompt: "You are a helpful chat assistant."
108
124
  )
109
125
 
110
- robot.run(data["message"]) do |event|
111
- transmit(event)
126
+ robot.on_new_message do |message|
127
+ transmit({ event: "text.delta", content: message.content }) if message.content
112
128
  end
129
+
130
+ robot.on_end_message do |_message|
131
+ transmit({ event: "run.completed" })
132
+ end
133
+
134
+ robot.run(data["message"])
113
135
  end
114
136
  end
115
137
  ```
@@ -128,9 +150,15 @@ class StreamController < ApplicationController
128
150
  system_prompt: "You are helpful."
129
151
  )
130
152
 
131
- robot.run(params[:message]) do |event|
132
- response.stream.write("data: #{event.to_json}\n\n")
153
+ robot.on_new_message do |message|
154
+ response.stream.write("data: #{message.content}\n\n") if message.content
155
+ end
156
+
157
+ robot.on_end_message do |_message|
158
+ response.stream.write("data: [DONE]\n\n")
133
159
  end
160
+
161
+ robot.run(params[:message])
134
162
  ensure
135
163
  response.stream.close
136
164
  end
@@ -142,38 +170,17 @@ end
142
170
  ```ruby
143
171
  # Using Faye WebSocket
144
172
  ws.on :message do |msg|
145
- robot.run(msg.data) do |event|
146
- ws.send(event.to_json)
173
+ robot.on_new_message do |message|
174
+ ws.send(message.content) if message.content
147
175
  end
148
- end
149
- ```
150
-
151
- ## Buffering
152
176
 
153
- Buffer content for batch processing:
154
-
155
- ```ruby
156
- buffer = []
157
-
158
- robot.run("Generate a long response") do |event|
159
- if event[:event] == "text.delta"
160
- buffer << event[:data][:content]
161
-
162
- # Flush every 10 chunks
163
- if buffer.size >= 10
164
- process_batch(buffer.join)
165
- buffer.clear
166
- end
167
- end
177
+ robot.run(msg.data)
168
178
  end
169
-
170
- # Final flush
171
- process_batch(buffer.join) if buffer.any?
172
179
  ```
173
180
 
174
181
  ## Progress Tracking
175
182
 
176
- Track streaming progress:
183
+ Track streaming progress with callbacks:
177
184
 
178
185
  ```ruby
179
186
  class StreamProgress
@@ -182,49 +189,31 @@ class StreamProgress
182
189
  @tools = 0
183
190
  end
184
191
 
185
- def handle(event)
186
- case event[:event]
187
- when "text.delta"
188
- @chars += event[:data][:content].length
192
+ attr_reader :chars, :tools
193
+
194
+ def attach(robot)
195
+ robot.on_new_message do |message|
196
+ @chars += message.content.length if message.content
189
197
  print "\rReceived #{@chars} characters..."
190
- when "tool_call.start"
198
+ end
199
+
200
+ robot.on_tool_call do |tool_call|
191
201
  @tools += 1
192
- puts "\nTool call ##{@tools}: #{event[:data][:name]}"
202
+ puts "\nTool call ##{@tools}: #{tool_call.name}"
193
203
  end
194
204
  end
195
205
  end
196
206
 
197
207
  progress = StreamProgress.new
208
+ progress.attach(robot)
198
209
 
199
- robot.run("Process this complex request") do |event|
200
- progress.handle(event)
201
- end
202
- ```
203
-
204
- ## Error Handling
205
-
206
- Handle streaming errors gracefully:
207
-
208
- ```ruby
209
- robot.run("Analyze this") do |event|
210
- case event[:event]
211
- when "run.failed"
212
- log_error(event[:data][:error])
213
- notify_user("An error occurred")
214
- when "text.delta"
215
- begin
216
- broadcast(event)
217
- rescue BroadcastError => e
218
- # Client disconnected, but continue processing
219
- logger.warn "Broadcast failed: #{e.message}"
220
- end
221
- end
222
- end
210
+ result = robot.run("Process this complex request")
211
+ puts "\nTotal: #{progress.chars} chars, #{progress.tools} tool calls"
223
212
  ```
224
213
 
225
214
  ## Without Streaming
226
215
 
227
- When streaming is not needed, simply call `run` without a block:
216
+ When streaming is not needed, simply call `run` without registering callbacks:
228
217
 
229
218
  ```ruby
230
219
  # No streaming - returns RobotResult directly
@@ -234,33 +223,23 @@ puts result.last_text_content
234
223
 
235
224
  ## Best Practices
236
225
 
237
- ### 1. Handle All Event Types
226
+ ### 1. Register Callbacks Before Run
238
227
 
239
228
  ```ruby
240
- robot.run("Hello") do |event|
241
- case event[:event]
242
- when "text.delta" then handle_delta(event)
243
- when "tool_call.start" then show_tool_indicator(event)
244
- when "tool_call.complete" then hide_tool_indicator(event)
245
- when "run.completed" then finalize_response
246
- when "run.failed" then show_error(event)
247
- end
248
- end
229
+ # Correct: register first, then run
230
+ robot.on_new_message { |msg| print msg.content if msg.content }
231
+ robot.run("Hello")
249
232
  ```
250
233
 
251
- ### 2. Provide User Feedback
234
+ ### 2. Handle Errors in Callbacks
252
235
 
253
236
  ```ruby
254
- robot.run("Process my request") do |event|
255
- case event[:event]
256
- when "run.started"
257
- show_typing_indicator
258
- when "text.delta"
259
- update_message(event[:data][:content])
260
- when "tool_call.start"
261
- show_status("Looking up information...")
262
- when "run.completed"
263
- hide_typing_indicator
237
+ robot.on_new_message do |message|
238
+ begin
239
+ broadcast(message.content) if message.content
240
+ rescue BroadcastError => e
241
+ # Client disconnected, but continue processing
242
+ logger.warn "Broadcast failed: #{e.message}"
264
243
  end
265
244
  end
266
245
  ```
@@ -269,9 +248,10 @@ end
269
248
 
270
249
  ```ruby
271
250
  begin
272
- robot.run("Hello") do |event|
273
- stream_to_client(event)
251
+ robot.on_new_message do |message|
252
+ stream_to_client(message.content) if message.content
274
253
  end
254
+ robot.run("Hello")
275
255
  ensure
276
256
  close_stream_connection
277
257
  end
data/docs/index.md CHANGED
@@ -68,14 +68,6 @@ Each robot is backed by a persistent LLM chat, configured with keyword arguments
68
68
 
69
69
  [:octicons-arrow-right-24: Memory System](guides/memory.md)
70
70
 
71
- - :material-history:{ .lg .middle } **Conversation History**
72
-
73
- ---
74
-
75
- Persist and restore conversation threads for long-running interactions.
76
-
77
- [:octicons-arrow-right-24: History Guide](guides/history.md)
78
-
79
71
  </div>
80
72
 
81
73
  ## Quick Example
@@ -39,33 +39,36 @@ class ClassifierRobot < RobotLab::Robot
39
39
  end
40
40
  end
41
41
 
42
- # Create specialized robots
42
+ # Shared RunConfig — all robots in this network use the same model
43
+ shared_config = RobotLab::RunConfig.new(model: "claude-3-haiku-20240307")
44
+
45
+ # Create specialized robots (no model: needed — inherited from RunConfig)
43
46
  classifier = ClassifierRobot.new(
44
47
  name: "classifier",
45
48
  template: :classifier,
46
- model: "claude-3-haiku-20240307"
49
+ config: shared_config
47
50
  )
48
51
 
49
52
  billing_robot = RobotLab.build(
50
53
  name: "billing",
51
54
  template: :billing,
52
- model: "claude-3-haiku-20240307"
55
+ config: shared_config
53
56
  )
54
57
 
55
58
  technical_robot = RobotLab.build(
56
59
  name: "technical",
57
60
  template: :technical,
58
- model: "claude-3-haiku-20240307"
61
+ config: shared_config
59
62
  )
60
63
 
61
64
  general_robot = RobotLab.build(
62
65
  name: "general",
63
66
  template: :general,
64
- model: "claude-3-haiku-20240307"
67
+ config: shared_config
65
68
  )
66
69
 
67
- # Create network with optional task routing
68
- network = RobotLab.create_network(name: "support_network") do
70
+ # Create network with optional task routing and shared config
71
+ network = RobotLab.create_network(name: "support_network", config: shared_config) do
69
72
  task :classifier, classifier, depends_on: :none
70
73
  task :billing, billing_robot, depends_on: :optional
71
74
  task :technical, technical_robot, depends_on: :optional
@@ -77,20 +77,43 @@ puts "-" * 70
77
77
  puts "Configuration hierarchy (highest priority first):"
78
78
  puts
79
79
  puts " Per-Robot (override global settings for a specific robot):"
80
- puts " 9. Run-time context: kwargs to robot.run re-render template"
81
- puts " 8. with_* methods: robot.with_temperature(0.9).ask(...)"
82
- puts " 7. Constructor params: Robot.new(model: ..., temperature: ...)"
83
- puts " 6. Template front matter: model, temperature, etc. in .md YAML header"
80
+ puts " 11. Run-time context: kwargs to robot.run re-render template"
81
+ puts " 10. with_* methods: robot.with_temperature(0.9).ask(...)"
82
+ puts " 9. Constructor params: Robot.new(model: ..., temperature: ...)"
83
+ puts " 8. Template front matter: model, temperature, etc. in .md YAML header"
84
+ puts " 7. Robot RunConfig: config: passed to Robot constructor"
85
+ puts
86
+ puts " Network/Task (shared defaults for a group of robots):"
87
+ puts " 6. Task RunConfig: config: passed to task() in network"
88
+ puts " 5. Network RunConfig: config: passed to create_network()"
84
89
  puts
85
90
  puts " Global (apply to all robots unless overridden):"
86
- puts " 5. Environment variables: ROBOT_LAB_RUBY_LLM__MODEL, etc."
87
- puts " 4. Project config: ./config/robot_lab.yml"
88
- puts " 3. User config: ~/.config/robot_lab/config.yml"
89
- puts " 2. Environment overrides: #{env} section in defaults.yml"
90
- puts " 1. Bundled defaults: lib/robot_lab/config/defaults.yml"
91
+ puts " 4. Environment variables: ROBOT_LAB_RUBY_LLM__MODEL, etc."
92
+ puts " 3. Project config: ./config/robot_lab.yml"
93
+ puts " 2. User config: ~/.config/robot_lab/config.yml"
94
+ puts " 1. Bundled defaults: lib/robot_lab/config/defaults.yml + env overrides"
95
+ puts "-" * 70
96
+ puts
97
+
98
+ # --- RunConfig demonstration ---
99
+ puts "-" * 70
100
+ puts "RunConfig: Shared Operational Defaults"
91
101
  puts "-" * 70
92
102
  puts
93
103
 
104
+ shared = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
105
+ puts " shared = RunConfig.new(model: \"claude-sonnet-4\", temperature: 0.5)"
106
+ puts " shared.to_h => #{shared.to_h.inspect}"
107
+ puts
108
+
109
+ creative = RobotLab::RunConfig.new(temperature: 0.9)
110
+ merged = shared.merge(creative)
111
+ puts " creative = RunConfig.new(temperature: 0.9)"
112
+ puts " merged = shared.merge(creative)"
113
+ puts " merged.to_h => #{merged.to_h.inspect}"
114
+ puts " (model inherited from shared, temperature overridden by creative)"
115
+ puts
116
+
94
117
  # Create a robot using the configuration
95
118
  robot = RobotLab.build(
96
119
  name: "config_demo",
@@ -124,9 +147,15 @@ puts <<~FOOTER
124
147
  #{"=" * 70}
125
148
  Configuration demonstrated successfully!
126
149
 
127
- Configuration flows through two layers:
150
+ Configuration flows through three layers:
128
151
  Global (MywayConfig): YAML defaults, env overrides, XDG, env vars
129
- Per-Robot: template front matter, constructor params, with_* methods
152
+ Network/Task (RunConfig): shared defaults for groups of robots
153
+ Per-Robot: RunConfig, template front matter, constructor params, with_*
154
+
155
+ RunConfig lets you express shared defaults that flow through the hierarchy:
156
+ shared = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
157
+ network = RobotLab.create_network(name: "team", config: shared) { ... }
158
+ robot = RobotLab.build(name: "bot", config: shared, temperature: 0.9)
130
159
 
131
160
  Example environment variable overrides:
132
161
  ROBOT_LAB_RUBY_LLM__MODEL=gpt-4
@@ -136,10 +136,49 @@ prev2 = show_config(robot2)
136
136
  puts
137
137
 
138
138
  # =============================================================================
139
- # Section 5: Bare robot with chaining
139
+ # Section 5: RunConfig as alternative to individual kwargs
140
140
  # =============================================================================
141
141
 
142
- puts "--- Section 5: Bare Robot with Chaining ---"
142
+ puts "--- Section 5: RunConfig as Alternative to Individual kwargs ---"
143
+ puts
144
+
145
+ # Instead of passing model:, temperature:, etc. individually,
146
+ # use a RunConfig to express shared defaults.
147
+ shared = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
148
+
149
+ puts "RunConfig: #{shared.to_h.inspect}"
150
+ puts
151
+
152
+ # Robot inherits from RunConfig; constructor kwargs still override
153
+ robot3 = RobotLab.build(
154
+ name: "runconfig_demo",
155
+ template: :configurable,
156
+ context: { task_type: "analysis" },
157
+ config: shared,
158
+ temperature: 0.8 # overrides RunConfig's 0.5
159
+ )
160
+
161
+ puts "Robot with config: shared, temperature: 0.8"
162
+ puts "(RunConfig sets 0.5, constructor overrides to 0.8)"
163
+ prev3 = show_config(robot3)
164
+ puts
165
+
166
+ # RunConfig merge semantics
167
+ creative = RobotLab::RunConfig.new(temperature: 0.9, max_tokens: 2000)
168
+ merged = shared.merge(creative)
169
+
170
+ puts "Merge: shared.merge(creative)"
171
+ puts " shared: #{shared.to_h.inspect}"
172
+ puts " creative: #{creative.to_h.inspect}"
173
+ puts " merged: #{merged.to_h.inspect}"
174
+ puts " (model inherited, temperature overridden, max_tokens added)"
175
+ puts
176
+
177
+ # =============================================================================
178
+ # Section 6: Bare robot with chaining
179
+ # =============================================================================
180
+
181
+ puts "--- Section 6: Bare Robot with Chaining ---"
143
182
  puts
144
183
 
145
184
  # A bare robot has no template. Configure entirely via chaining.
@@ -158,10 +197,10 @@ prev_bare = show_config(bare, prev_bare)
158
197
  puts
159
198
 
160
199
  # =============================================================================
161
- # Section 6: with_template() on an existing robot
200
+ # Section 7: with_template() on an existing robot
162
201
  # =============================================================================
163
202
 
164
- puts "--- Section 6: with_template() on Existing Robot ---"
203
+ puts "--- Section 7: with_template() on Existing Robot ---"
165
204
  puts
166
205
 
167
206
  # You can apply a template to a robot after creation.
@@ -172,10 +211,10 @@ show_config(bare, prev_bare)
172
211
  puts
173
212
 
174
213
  # =============================================================================
175
- # Section 7: AskUser tool for gathering template parameters
214
+ # Section 8: AskUser tool for gathering template parameters
176
215
  # =============================================================================
177
216
 
178
- puts "--- Section 7: AskUser for Template Parameters ---"
217
+ puts "--- Section 8: AskUser for Template Parameters ---"
179
218
  puts
180
219
  puts "The :configurable template declares `task_type: general` in its front"
181
220
  puts "matter. That default is offered to the user — they can accept it by"
@@ -55,16 +55,22 @@ puts "Example 11: Network Visualization & Introspection"
55
55
  puts "=" * 70
56
56
  puts
57
57
 
58
+ # Shared RunConfig for all robots in this network
59
+ shared_config = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
60
+
61
+ # Per-task RunConfig override for the writer (higher creativity)
62
+ creative_config = RobotLab::RunConfig.new(temperature: 0.9)
63
+
58
64
  # Build robots (no LLM calls, just instances)
59
65
  classifier = RobotLab.build(name: "classifier", system_prompt: "Classify input")
60
66
  analyst = RobotLab.build(name: "analyst", system_prompt: "Analyze data")
61
67
  writer = RobotLab.build(name: "writer", system_prompt: "Write summary")
62
68
 
63
- # Build network with dependencies and per-task config
64
- network = RobotLab.create_network(name: "demo_pipeline") do
69
+ # Build network with RunConfig, dependencies, and per-task config
70
+ network = RobotLab.create_network(name: "demo_pipeline", config: shared_config) do
65
71
  task :classify, classifier, depends_on: :none
66
72
  task :analyze, analyst, context: { depth: "deep" }, depends_on: [:classify]
67
- task :write, writer, depends_on: [:analyze]
73
+ task :write, writer, config: creative_config, depends_on: [:analyze]
68
74
  end
69
75
 
70
76
  # =============================================================================
@@ -179,10 +185,27 @@ ap network["analyze"].to_h
179
185
  puts
180
186
 
181
187
  # =============================================================================
182
- # Section 5: Broadcast
188
+ # Section 5: RunConfig Introspection
189
+ # =============================================================================
190
+
191
+ puts "--- Section 5: RunConfig Introspection ---"
192
+ puts
193
+
194
+ puts "Network RunConfig (shared defaults):"
195
+ ap network.config.to_h
196
+ puts
197
+
198
+ puts "Merged effective config for :write task (network + task override):"
199
+ effective = shared_config.merge(creative_config)
200
+ ap effective.to_h
201
+ puts " model inherited from network, temperature overridden by task"
202
+ puts
203
+
204
+ # =============================================================================
205
+ # Section 6: Broadcast
183
206
  # =============================================================================
184
207
 
185
- puts "--- Section 5: Broadcast ---"
208
+ puts "--- Section 6: Broadcast ---"
186
209
  puts
187
210
 
188
211
  broadcast_messages = []
@@ -207,10 +230,10 @@ end
207
230
  puts
208
231
 
209
232
  # =============================================================================
210
- # Section 6: Shared memory access
233
+ # Section 7: Shared memory access
211
234
  # =============================================================================
212
235
 
213
- puts "--- Section 6: Shared Network Memory ---"
236
+ puts "--- Section 7: Shared Network Memory ---"
214
237
  puts
215
238
 
216
239
  puts "network.memory is a #{network.memory.class}"
@@ -31,7 +31,7 @@ class Comedian < RobotLab::Robot
31
31
  with_temperature(temp)
32
32
  joke = run(message.content.to_s).reply.strip
33
33
  puts " Bob [##{@attempts}, t=#{"%.1f" % temp}]: #{joke}"
34
- reply(message, joke)
34
+ send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
35
35
  end
36
36
  end
37
37