robot_lab 0.0.1 → 0.0.6

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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +9 -9
  3. data/.irbrc +6 -0
  4. data/CHANGELOG.md +140 -0
  5. data/README.md +263 -48
  6. data/Rakefile +71 -1
  7. data/docs/api/core/index.md +53 -46
  8. data/docs/api/core/memory.md +200 -154
  9. data/docs/api/core/network.md +13 -3
  10. data/docs/api/core/robot.md +490 -130
  11. data/docs/api/core/state.md +55 -73
  12. data/docs/api/core/tool.md +205 -209
  13. data/docs/api/index.md +7 -28
  14. data/docs/api/mcp/client.md +119 -48
  15. data/docs/api/mcp/index.md +75 -60
  16. data/docs/api/mcp/server.md +120 -136
  17. data/docs/api/mcp/transports.md +172 -184
  18. data/docs/api/messages/index.md +35 -20
  19. data/docs/api/messages/text-message.md +67 -21
  20. data/docs/api/messages/tool-call-message.md +80 -41
  21. data/docs/api/messages/tool-result-message.md +119 -50
  22. data/docs/api/messages/user-message.md +48 -24
  23. data/docs/api/streaming/context.md +157 -74
  24. data/docs/api/streaming/events.md +114 -166
  25. data/docs/api/streaming/index.md +74 -72
  26. data/docs/architecture/core-concepts.md +360 -116
  27. data/docs/architecture/index.md +97 -59
  28. data/docs/architecture/message-flow.md +138 -129
  29. data/docs/architecture/network-orchestration.md +197 -50
  30. data/docs/architecture/robot-execution.md +199 -146
  31. data/docs/architecture/state-management.md +255 -187
  32. data/docs/concepts.md +311 -49
  33. data/docs/examples/basic-chat.md +89 -77
  34. data/docs/examples/index.md +222 -47
  35. data/docs/examples/mcp-server.md +207 -203
  36. data/docs/examples/multi-robot-network.md +129 -35
  37. data/docs/examples/rails-application.md +159 -160
  38. data/docs/examples/tool-usage.md +295 -204
  39. data/docs/getting-started/configuration.md +347 -154
  40. data/docs/getting-started/index.md +1 -1
  41. data/docs/getting-started/installation.md +22 -13
  42. data/docs/getting-started/quick-start.md +166 -121
  43. data/docs/guides/building-robots.md +418 -212
  44. data/docs/guides/creating-networks.md +143 -24
  45. data/docs/guides/index.md +0 -5
  46. data/docs/guides/mcp-integration.md +152 -113
  47. data/docs/guides/memory.md +220 -164
  48. data/docs/guides/rails-integration.md +244 -162
  49. data/docs/guides/streaming.md +137 -187
  50. data/docs/guides/using-tools.md +259 -212
  51. data/docs/index.md +46 -41
  52. data/examples/01_simple_robot.rb +6 -9
  53. data/examples/02_tools.rb +6 -9
  54. data/examples/03_network.rb +19 -17
  55. data/examples/04_mcp.rb +5 -8
  56. data/examples/05_streaming.rb +5 -8
  57. data/examples/06_prompt_templates.rb +42 -37
  58. data/examples/07_network_memory.rb +13 -14
  59. data/examples/08_llm_config.rb +169 -0
  60. data/examples/09_chaining.rb +262 -0
  61. data/examples/10_memory.rb +331 -0
  62. data/examples/11_network_introspection.rb +253 -0
  63. data/examples/12_message_bus.rb +74 -0
  64. data/examples/13_spawn.rb +90 -0
  65. data/examples/14_rusty_circuit/comic.rb +143 -0
  66. data/examples/14_rusty_circuit/display.rb +203 -0
  67. data/examples/14_rusty_circuit/heckler.rb +63 -0
  68. data/examples/14_rusty_circuit/open_mic.rb +123 -0
  69. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
  70. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
  71. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
  72. data/examples/14_rusty_circuit/scout.rb +156 -0
  73. data/examples/14_rusty_circuit/scout_notes.md +89 -0
  74. data/examples/14_rusty_circuit/show.log +234 -0
  75. data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
  76. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
  77. data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
  78. data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
  79. data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
  80. data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
  81. data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
  82. data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
  83. data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
  84. data/examples/15_memory_network_and_bus/output/memory.json +13 -0
  85. data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
  86. data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
  87. data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
  88. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
  89. data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
  90. data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
  91. data/examples/16_writers_room/display.rb +158 -0
  92. data/examples/16_writers_room/output/.gitignore +2 -0
  93. data/examples/16_writers_room/output/opus_001.md +263 -0
  94. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  95. data/examples/16_writers_room/prompts/writer.md +37 -0
  96. data/examples/16_writers_room/room.rb +150 -0
  97. data/examples/16_writers_room/tools.rb +162 -0
  98. data/examples/16_writers_room/writer.rb +121 -0
  99. data/examples/16_writers_room/writers_room.rb +162 -0
  100. data/examples/README.md +197 -0
  101. data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
  102. data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
  103. data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
  104. data/examples/prompts/comedian.md +6 -0
  105. data/examples/prompts/comedy_critic.md +10 -0
  106. data/examples/prompts/configurable.md +9 -0
  107. data/examples/prompts/dispatcher.md +12 -0
  108. data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
  109. data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
  110. data/examples/prompts/frontmatter_mcp_test.md +9 -0
  111. data/examples/prompts/frontmatter_named_test.md +5 -0
  112. data/examples/prompts/frontmatter_tools_test.md +6 -0
  113. data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
  114. data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
  115. data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
  116. data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
  117. data/examples/prompts/llm_config_demo.md +20 -0
  118. data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
  119. data/examples/prompts/os_advocate.md +13 -0
  120. data/examples/prompts/os_chief.md +13 -0
  121. data/examples/prompts/os_editor.md +13 -0
  122. data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
  123. data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
  124. data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
  125. data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
  126. data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
  127. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  128. data/lib/robot_lab/ask_user.rb +75 -0
  129. data/lib/robot_lab/config/defaults.yml +121 -0
  130. data/lib/robot_lab/config.rb +183 -0
  131. data/lib/robot_lab/error.rb +6 -0
  132. data/lib/robot_lab/mcp/client.rb +1 -1
  133. data/lib/robot_lab/memory.rb +10 -34
  134. data/lib/robot_lab/network.rb +13 -20
  135. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  136. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  137. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  138. data/lib/robot_lab/robot.rb +240 -330
  139. data/lib/robot_lab/robot_message.rb +44 -0
  140. data/lib/robot_lab/robot_result.rb +1 -0
  141. data/lib/robot_lab/run_config.rb +184 -0
  142. data/lib/robot_lab/state_proxy.rb +2 -12
  143. data/lib/robot_lab/streaming/context.rb +1 -1
  144. data/lib/robot_lab/task.rb +8 -1
  145. data/lib/robot_lab/tool.rb +108 -172
  146. data/lib/robot_lab/tool_config.rb +1 -1
  147. data/lib/robot_lab/tool_manifest.rb +2 -18
  148. data/lib/robot_lab/utils.rb +39 -0
  149. data/lib/robot_lab/version.rb +1 -1
  150. data/lib/robot_lab.rb +89 -57
  151. data/mkdocs.yml +0 -11
  152. metadata +121 -135
  153. data/docs/api/adapters/anthropic.md +0 -121
  154. data/docs/api/adapters/gemini.md +0 -133
  155. data/docs/api/adapters/index.md +0 -104
  156. data/docs/api/adapters/openai.md +0 -134
  157. data/docs/api/history/active-record-adapter.md +0 -195
  158. data/docs/api/history/config.md +0 -191
  159. data/docs/api/history/index.md +0 -132
  160. data/docs/api/history/thread-manager.md +0 -144
  161. data/docs/guides/history.md +0 -359
  162. data/examples/prompts/assistant/user.txt.erb +0 -1
  163. data/examples/prompts/billing/user.txt.erb +0 -1
  164. data/examples/prompts/classifier/user.txt.erb +0 -1
  165. data/examples/prompts/entity_extractor/user.txt.erb +0 -3
  166. data/examples/prompts/escalation/user.txt.erb +0 -34
  167. data/examples/prompts/general/user.txt.erb +0 -1
  168. data/examples/prompts/github_assistant/user.txt.erb +0 -1
  169. data/examples/prompts/helper/user.txt.erb +0 -1
  170. data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
  171. data/examples/prompts/order_support/user.txt.erb +0 -22
  172. data/examples/prompts/product_support/user.txt.erb +0 -32
  173. data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
  174. data/examples/prompts/synthesizer/user.txt.erb +0 -15
  175. data/examples/prompts/technical/user.txt.erb +0 -1
  176. data/examples/prompts/triage/user.txt.erb +0 -17
  177. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  178. data/lib/robot_lab/adapters/base.rb +0 -85
  179. data/lib/robot_lab/adapters/gemini.rb +0 -193
  180. data/lib/robot_lab/adapters/openai.rb +0 -159
  181. data/lib/robot_lab/adapters/registry.rb +0 -81
  182. data/lib/robot_lab/configuration.rb +0 -143
  183. data/lib/robot_lab/errors.rb +0 -70
  184. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  185. data/lib/robot_lab/history/config.rb +0 -115
  186. data/lib/robot_lab/history/thread_manager.rb +0 -93
  187. data/lib/robot_lab/robotic_model.rb +0 -324
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ── Writers' Room Tools ──────────────────────────────────────
4
+ #
5
+ # General-purpose tools that give each writer agency over
6
+ # communication, shared memory, and team composition.
7
+ # No workflow logic — the LLMs decide when and how to use them.
8
+
9
+ # Helper to access the room logger from any tool
10
+ module ToolLogging
11
+ private
12
+
13
+ def log
14
+ robot.room&.logger
15
+ end
16
+ end
17
+
18
+
19
+ class BroadcastTool < RobotLab::Tool
20
+ include ToolLogging
21
+
22
+ description "Send a message to all writers in the room. " \
23
+ "Use for discussion, proposals, questions, or announcements. " \
24
+ "Don't broadcast trivially — only when you have something substantive."
25
+
26
+ param :message, type: "string",
27
+ desc: "What to say to the room", required: true
28
+
29
+ def execute(message:)
30
+ log&.info("#{robot.name} TOOL broadcast (#{message.length} chars)")
31
+ robot.send_message(to: :room, content: message)
32
+ robot.display&.broadcast(robot.name, message)
33
+ "Broadcast sent."
34
+ end
35
+ end
36
+
37
+
38
+ class DirectMessageTool < RobotLab::Tool
39
+ include ToolLogging
40
+
41
+ description "Send a private message to one specific writer. " \
42
+ "Use for feedback on their chapter, coordination on handoffs, " \
43
+ "or questions that don't concern the whole room."
44
+
45
+ param :to, type: "string",
46
+ desc: "Name of the writer to message", required: true
47
+ param :message, type: "string",
48
+ desc: "What to say", required: true
49
+
50
+ def execute(to:, message:)
51
+ log&.info("#{robot.name} TOOL direct_message -> #{to} (#{message.length} chars)")
52
+ robot.send_message(to: to.to_sym, content: message)
53
+ robot.display&.direct_message(robot.name, to, message)
54
+ "Message sent to #{to}."
55
+ end
56
+ end
57
+
58
+
59
+ class ReadMemoryTool < RobotLab::Tool
60
+ include ToolLogging
61
+
62
+ description "Read a value from shared memory. " \
63
+ "Use to check the story bible, outline, chapter claims, " \
64
+ "or read another writer's chapter draft."
65
+
66
+ param :key, type: "string",
67
+ desc: "Memory key to read (e.g. story_bible, outline, claims, chapter_3)", required: true
68
+
69
+ def execute(key:)
70
+ value = robot.shared_memory.get(key.to_sym)
71
+ if value.nil?
72
+ log&.info("#{robot.name} TOOL read_memory :#{key} -> nil")
73
+ "Key '#{key}' is not set yet."
74
+ else
75
+ log&.info("#{robot.name} TOOL read_memory :#{key} -> #{value.to_s.length} chars")
76
+ value.to_s
77
+ end
78
+ end
79
+ end
80
+
81
+
82
+ class WriteMemoryTool < RobotLab::Tool
83
+ include ToolLogging
84
+
85
+ description "Write a value to shared memory for all writers to see. " \
86
+ "Use to store the story bible, outline, claim a chapter, " \
87
+ "or submit a finished chapter draft."
88
+
89
+ param :key, type: "string",
90
+ desc: "Memory key (e.g. story_bible, outline, claims, chapter_1)", required: true
91
+ param :value, type: "string",
92
+ desc: "Content to store", required: true
93
+
94
+ def execute(key:, value:)
95
+ log&.info("#{robot.name} TOOL write_memory :#{key} (#{value.length} chars)")
96
+ robot.shared_memory.set(key.to_sym, value)
97
+ robot.display&.memory_write(robot.name, key)
98
+ "Stored '#{key}' in shared memory."
99
+ end
100
+ end
101
+
102
+
103
+ class ListMemoryTool < RobotLab::Tool
104
+ include ToolLogging
105
+
106
+ description "List all keys currently in shared memory. " \
107
+ "Use to get situational awareness of what's been done."
108
+
109
+ def execute
110
+ keys = robot.shared_memory.keys
111
+ log&.info("#{robot.name} TOOL list_memory -> #{keys.join(', ')}")
112
+ if keys.empty?
113
+ "Shared memory is empty."
114
+ else
115
+ "Keys in shared memory: #{keys.join(', ')}"
116
+ end
117
+ end
118
+ end
119
+
120
+
121
+ class SpawnWriterTool < RobotLab::Tool
122
+ include ToolLogging
123
+
124
+ description "Bring a new writer into the room to help with the workload. " \
125
+ "Use when there are more unclaimed chapters than active writers."
126
+
127
+ param :name, type: "string",
128
+ desc: "Name for the new writer (e.g. writer_4)", required: true
129
+
130
+ def execute(name:)
131
+ log&.info("#{robot.name} TOOL spawn_writer '#{name}'")
132
+ new_writer = robot.room.spawn_writer(name)
133
+ robot.display&.spawn(robot.name, name)
134
+ "#{name} has joined the writers' room."
135
+ rescue => e
136
+ log&.error("#{robot.name} TOOL spawn_writer '#{name}' FAILED: #{e.message}")
137
+ "Failed to spawn #{name}: #{e.message}"
138
+ end
139
+ end
140
+
141
+
142
+ class MarkCompleteTool < RobotLab::Tool
143
+ include ToolLogging
144
+
145
+ description "Signal that the book is finished — all 10 chapters are written. " \
146
+ "Only use this when you have verified all chapters exist in shared memory."
147
+
148
+ def execute
149
+ # Verify all chapters exist
150
+ missing = (1..10).reject { |n| robot.shared_memory.key?(:"chapter_#{n}") }
151
+
152
+ if missing.any?
153
+ log&.warn("#{robot.name} TOOL mark_complete REJECTED — missing chapters: #{missing.join(', ')}")
154
+ "Cannot mark complete. Missing chapters: #{missing.join(', ')}"
155
+ else
156
+ log&.info("#{robot.name} TOOL mark_complete SUCCESS")
157
+ robot.shared_memory.set(:book_complete, true)
158
+ robot.display&.complete(robot.name)
159
+ "Book marked as complete!"
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ── The Writer ─────────────────────────────────────────────────
4
+ #
5
+ # All writers in the room are instances of this same class with
6
+ # the same template and the same tools. There is no hierarchy,
7
+ # no designated leader, no pre-assigned roles. The group
8
+ # self-organizes through bus communication and shared memory.
9
+ #
10
+ # Each writer:
11
+ # - Subscribes to :room for broadcast discussion
12
+ # - Listens on a personal channel for direct messages
13
+ # - Has tools to read/write shared memory, broadcast, DM,
14
+ # spawn new writers, and mark the book complete
15
+ # - Decides via LLM when to speak, when to write, when to
16
+ # listen, and when to spawn
17
+ #
18
+ # == Chat Reset Strategy
19
+ #
20
+ # In a bus-based SOG, writers receive many messages and each
21
+ # triggers a run() call. When the LLM responds with only tool
22
+ # calls (no text), RubyLLM appends an empty text content block
23
+ # to the chat history. The Anthropic API rejects this on the
24
+ # next call, permanently killing the writer.
25
+ #
26
+ # Fix: reset the chat before each message. The writer doesn't
27
+ # need persistent chat history — shared memory is the single
28
+ # source of truth. Each message is processed with a fresh chat
29
+ # that has the system prompt, tools, and current memory context.
30
+ #
31
+ class Writer < RobotLab::Robot
32
+ attr_accessor :shared_memory, :display, :room
33
+ attr_reader :messages_processed
34
+
35
+ def initialize(name:, bus:, shared_memory:, display:, room:, config: nil)
36
+ @shared_memory = shared_memory
37
+ @display = display
38
+ @room = room
39
+ @messages_processed = 0
40
+
41
+ super(
42
+ name: name,
43
+ template: :writer,
44
+ context: { writer_name: name },
45
+ bus: bus,
46
+ config: config,
47
+ local_tools: build_tools
48
+ )
49
+
50
+ setup_room_subscription
51
+ setup_message_handler
52
+ end
53
+
54
+ private
55
+
56
+ def log
57
+ @room&.logger
58
+ end
59
+
60
+ def build_tools
61
+ [
62
+ BroadcastTool.new(robot: self),
63
+ DirectMessageTool.new(robot: self),
64
+ ReadMemoryTool.new(robot: self),
65
+ WriteMemoryTool.new(robot: self),
66
+ ListMemoryTool.new(robot: self),
67
+ SpawnWriterTool.new(robot: self),
68
+ MarkCompleteTool.new(robot: self),
69
+ ]
70
+ end
71
+
72
+ def setup_room_subscription
73
+ @bus.subscribe(:room) do |delivery|
74
+ handle_incoming_delivery(delivery)
75
+ end
76
+ end
77
+
78
+ # Reset the chat to a clean state with system prompt and tools.
79
+ # Prevents history corruption from tool-only LLM responses
80
+ # (empty text content blocks that Anthropic rejects).
81
+ def fresh_chat!
82
+ resolved_model = @config&.model || RobotLab.config.ruby_llm.model
83
+ @chat = RubyLLM.chat(model: resolved_model)
84
+ apply_template_to_chat(@build_context) if @template
85
+ @chat.with_instructions(@system_prompt) if @system_prompt
86
+ @chat.with_temperature(@config.temperature) if @config&.temperature
87
+
88
+ filtered = filtered_tools([])
89
+ @chat.with_tools(*filtered) if filtered.any?
90
+ end
91
+
92
+ def setup_message_handler
93
+ on_message do |message|
94
+ # Don't respond to your own messages
95
+ next if message.from == name
96
+
97
+ @messages_processed += 1
98
+ log&.info("#{name} <- [#{message.from}] msg ##{@messages_processed} (#{message.content.to_s[0..80]}...)")
99
+ @display&.incoming(name, message.from, message.content)
100
+
101
+ # Fresh chat for each message — shared memory is our persistence
102
+ fresh_chat!
103
+
104
+ # Build prompt with current memory context
105
+ memory_keys = shared_memory.keys
106
+ prompt = "[#{message.from}]: #{message.content}"
107
+ prompt += "\n\n[Memory keys: #{memory_keys.join(', ')}]" if memory_keys.any?
108
+
109
+ log&.info("#{name} -> run() starting (prompt: #{prompt.length} chars)")
110
+
111
+ begin
112
+ result = run(prompt)
113
+ reply_text = result.respond_to?(:reply) ? result.reply.to_s[0..120] : result.to_s[0..120]
114
+ log&.info("#{name} <- run() finished (reply: #{reply_text}...)")
115
+ rescue => e
116
+ log&.error("#{name} !! run() raised #{e.class}: #{e.message}")
117
+ log&.error(" #{e.backtrace&.first(5)&.join("\n ")}")
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 16: The Writers' Room — Self-Organizing Group
5
+ #
6
+ # A team of writer robots collaborates to produce a 10-chapter
7
+ # fiction novella. No orchestration, no pipeline, no assigned roles.
8
+ # The writers self-organize through:
9
+ #
10
+ # - BUS (broadcast + direct messages)
11
+ # :room channel for group discussion
12
+ # personal channels for 1:1 feedback
13
+ #
14
+ # - SHARED MEMORY (story bible, outline, chapters)
15
+ # Writers read and write freely; memory is the shared truth
16
+ #
17
+ # - SPAWNING (dynamic team growth)
18
+ # Any writer can recruit new writers when work exceeds capacity
19
+ #
20
+ # The script creates identical writers, seeds the room with an
21
+ # assignment, and waits. Everything else is emergent.
22
+ #
23
+ # Usage:
24
+ # bundle exec ruby examples/16_writers_room/writers_room.rb
25
+ # bundle exec ruby examples/16_writers_room/writers_room.rb --premise "a detective story set on Mars"
26
+ # bundle exec ruby examples/16_writers_room/writers_room.rb --writers 4 --timeout 300
27
+ # bundle exec ruby examples/16_writers_room/writers_room.rb --log session.log
28
+ # bundle exec ruby examples/16_writers_room/writers_room.rb -h
29
+
30
+ ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "prompts")
31
+
32
+ require_relative "../../lib/robot_lab"
33
+ require_relative "display"
34
+ require_relative "tools"
35
+ require_relative "room"
36
+ require_relative "writer"
37
+
38
+ RubyLLM.configure { |c| c.logger = Logger.new(File::NULL) }
39
+
40
+ # ── Parse CLI args ───────────────────────────────────────────
41
+
42
+ if ARGV.include?("-h") || ARGV.include?("--help")
43
+ puts <<~HELP
44
+ Usage: #{$0} [options]
45
+
46
+ Options:
47
+ --premise TEXT Story premise (default: generation ship AI consciousness)
48
+ --writers N Initial number of writers, minimum 2 (default: 3)
49
+ --log FILE Also write display output to FILE
50
+ --timeout N Seconds to wait for completion (default: 600)
51
+ -h, --help Show this help
52
+ HELP
53
+ exit
54
+ end
55
+
56
+ log_path = nil
57
+ if (idx = ARGV.index("--log"))
58
+ log_path = ARGV[idx + 1]
59
+ abort "Missing value for --log" unless log_path
60
+ end
61
+
62
+ premise = "a generation ship where the AI navigation system develops consciousness"
63
+ if (idx = ARGV.index("--premise"))
64
+ premise = ARGV[idx + 1]
65
+ abort "Missing value for --premise" unless premise
66
+ end
67
+
68
+ initial_writers = 3
69
+ if (idx = ARGV.index("--writers"))
70
+ initial_writers = ARGV[idx + 1].to_i
71
+ initial_writers = 3 if initial_writers < 2
72
+ end
73
+
74
+ timeout = 600
75
+ if (idx = ARGV.index("--timeout"))
76
+ timeout = ARGV[idx + 1].to_i
77
+ timeout = 600 if timeout < 30
78
+ end
79
+
80
+ # ── Build the room ───────────────────────────────────────────
81
+
82
+ OUTPUT_DIR = File.join(__dir__, "output")
83
+ require "fileutils"
84
+ FileUtils.mkdir_p(OUTPUT_DIR)
85
+
86
+ display = Display.new(log_path: log_path)
87
+
88
+ shared_config = RobotLab::RunConfig.new(
89
+ model: "claude-sonnet-4-5-20250929",
90
+ temperature: 0.7
91
+ )
92
+
93
+ room = Room.new(display: display, config: shared_config)
94
+
95
+ # Create identical writers
96
+ initial_writers.times do |i|
97
+ room.add_writer("writer_#{i + 1}")
98
+ end
99
+
100
+ # Monitor shared memory changes
101
+ room.memory.subscribe_pattern("chapter_*") do |change|
102
+ display.info("[memory] #{change.writer} wrote :#{change.key}")
103
+ end
104
+
105
+ room.memory.subscribe(:story_bible, :outline, :claims, :book_complete) do |change|
106
+ display.info("[memory] #{change.writer} updated :#{change.key}")
107
+ end
108
+
109
+ # ── Go ───────────────────────────────────────────────────────
110
+
111
+ display.banner(<<~BANNER)
112
+ ============================================================
113
+ THE WRITERS' ROOM — Self-Organizing Group
114
+ ============================================================
115
+
116
+ Premise: #{premise}
117
+ Writers: #{room.writers.keys.join(', ')}
118
+ Goal: 10 chapters of fiction
119
+ Method: Self-organization via bus + shared memory
120
+ BANNER
121
+
122
+ display.separator
123
+
124
+ assignment = <<~ASSIGNMENT
125
+ ASSIGNMENT: Write a 10-chapter science fiction novella about #{premise}.
126
+
127
+ You are one of #{initial_writers} writers in this room. Coordinate among
128
+ yourselves to produce the book. Discuss the premise, build a story bible,
129
+ create an outline, claim chapters, and write them. If you need more writers,
130
+ spawn them. When all 10 chapters are done, mark complete.
131
+
132
+ Start by discussing what this story should be about.
133
+ ASSIGNMENT
134
+
135
+ room.seed(assignment)
136
+
137
+ completed = room.wait_for_completion(timeout: timeout)
138
+
139
+ # ── Assemble and save ────────────────────────────────────────
140
+
141
+ display.separator
142
+
143
+ book = room.assemble_book
144
+ book_path = File.join(OUTPUT_DIR, "book.md")
145
+ File.write(book_path, book)
146
+
147
+ # Count chapters actually written
148
+ chapters_written = (1..10).count { |n| room.memory.key?(:"chapter_#{n}") }
149
+
150
+ display.stats(<<~STATS)
151
+ ────────────────────────────────────────────────────────────
152
+ Writers' Room Stats:
153
+ Chapters written: #{chapters_written}/10
154
+ Completed: #{completed}
155
+ Total writers: #{room.writers.size} (#{room.writers.size - initial_writers} spawned)
156
+ Writers: #{room.writers.keys.join(', ')}
157
+ Memory keys: #{room.memory.keys.join(', ')}
158
+
159
+ Output: #{book_path}
160
+ STATS
161
+
162
+ display.close
@@ -0,0 +1,197 @@
1
+ # RobotLab Examples
2
+
3
+ Working demonstrations of RobotLab features, from single-robot basics to multi-robot orchestration and message bus communication.
4
+
5
+ ## Prerequisites
6
+
7
+ - Ruby >= 3.2
8
+ - `bundle install` (from the project root)
9
+ - An LLM API key (e.g., `ANTHROPIC_API_KEY`)
10
+
11
+ ## Running Examples
12
+
13
+ ```bash
14
+ # Run a specific example by number
15
+ bundle exec rake examples:run[1]
16
+
17
+ # Run all examples
18
+ bundle exec rake examples:all
19
+
20
+ # Run directly
21
+ bundle exec ruby examples/01_simple_robot.rb
22
+ ```
23
+
24
+ ## Directory Structure
25
+
26
+ ```
27
+ examples/
28
+ 01_simple_robot.rb # Basic robot with template
29
+ 02_tools.rb # Robot with custom tools
30
+ 03_network.rb # Multi-robot network with routing
31
+ 04_mcp.rb # MCP server integration (GitHub)
32
+ 05_streaming.rb # Real-time streaming events
33
+ 06_prompt_templates.rb # Template-based e-commerce support
34
+ 07_network_memory.rb # Shared memory with concurrent robots
35
+ 08_llm_config.rb # Configuration hierarchy demo
36
+ 09_chaining.rb # with_* method chaining & reconfiguration
37
+ 10_memory.rb # Advanced Memory API operations
38
+ 11_network_introspection.rb # Network visualization & inspection
39
+ 12_message_bus.rb # Bidirectional robot communication
40
+ 13_spawn.rb # Dynamic specialist robot spawning
41
+ 14_rusty_circuit/ # Multi-robot open mic with self-modification
42
+ open_mic.rb # Main entrypoint — wires up the show
43
+ comic.rb # Comedian with self-modification tools
44
+ heckler.rb # Audience heckler (can stay silent or counter-joke)
45
+ scout.rb # Talent scout with analyst spawning
46
+ display.rb # Terminal formatting (color, wrapping, file output)
47
+ prompts/ # Templates for comic, heckler, and scout
48
+ prompts/ # Prompt templates (.md with YAML front matter)
49
+ ```
50
+
51
+ ## Examples
52
+
53
+ ### 01 — Simple Robot
54
+
55
+ Create and run a basic robot using a prompt template. Sends a single message and displays the response.
56
+
57
+ **Requires:** LLM API key
58
+
59
+ ### 02 — Tools
60
+
61
+ Give a robot custom tools (`Calculator`, `FortuneCookie`) defined as `RubyLLM::Tool` subclasses. The LLM decides when to call each tool based on the user's request.
62
+
63
+ **Requires:** LLM API key
64
+
65
+ ### 03 — Multi-Robot Network
66
+
67
+ Build a customer support network with a classifier robot that routes requests to billing, technical, or general specialists. Uses SimpleFlow's optional task activation for conditional routing.
68
+
69
+ **Requires:** LLM API key
70
+
71
+ ### 04 — MCP Integration
72
+
73
+ Connect to the GitHub MCP server via stdio transport. Part 1 demonstrates direct `MCP::Client` usage (listing tools, calling `search_repositories`). Part 2 wraps the MCP server inside a robot for natural-language queries.
74
+
75
+ **Requires:** LLM API key, `GITHUB_PERSONAL_ACCESS_TOKEN`, `github-mcp-server` installed
76
+
77
+ ### 05 — Streaming
78
+
79
+ Real-time streaming of robot responses using `RobotLab::Streaming::Context`. Simulates text deltas with timing to demonstrate the streaming event model, then shows the code pattern for streaming with a robot or network.
80
+
81
+ **Requires:** None (simulated events, no LLM calls)
82
+
83
+ ### 06 — Prompt Templates
84
+
85
+ Full e-commerce support system using prompt_manager templates with YAML front matter. A triage robot classifies customer requests and routes to order, product, or escalation specialists. Demonstrates build-time context (company info, policies) and run-time context (customer data, order history).
86
+
87
+ **Requires:** LLM API key
88
+
89
+ ### 07 — Network Memory
90
+
91
+ Reactive shared memory with concurrent robots. Multiple analysis robots (sentiment, entity extraction, keywords) run in parallel and write to shared memory. A synthesizer robot waits for all results using blocking reads, then produces a combined analysis. Demonstrates subscriptions, notifications, and network broadcast.
92
+
93
+ **Requires:** LLM API key
94
+
95
+ ### 08 — LLM Configuration
96
+
97
+ Walks through the full configuration hierarchy without making LLM calls:
98
+
99
+ 1. Bundled defaults
100
+ 2. Environment-specific overrides
101
+ 3. XDG user config
102
+ 4. Project config
103
+ 5. Environment variables
104
+ 6. Template front matter
105
+ 7. Constructor parameters
106
+ 8. `with_*` method chaining
107
+ 9. Run-time context
108
+
109
+ **Requires:** None (no LLM calls)
110
+
111
+ ### 09 — Chaining & Reconfiguration
112
+
113
+ Demonstrates the Robot API surface for runtime configuration: `with_*` method chaining, `update()` for reconfiguration, `to_h` introspection, config diffs between steps, and how constructor params override template front matter.
114
+
115
+ **Requires:** None (no LLM calls)
116
+
117
+ ### 10 — Advanced Memory
118
+
119
+ Comprehensive Memory API demo: `StateProxy` for method-style access, key and pattern subscriptions, `MemoryChange` objects, key enumeration, serialization round-trips, clone for isolated copies, delete with reserved key protection, and clear vs reset.
120
+
121
+ **Requires:** None (no LLM calls)
122
+
123
+ ### 11 — Network Introspection
124
+
125
+ Network visualization and inspection tools: `to_mermaid()` for diagram export, `to_dot()` for Graphviz, `execution_plan()` for text output, `visualize()` for ASCII pipelines, robot access by name, dynamic `add_robot()`, `to_h()` introspection, task-specific config, and `broadcast()`.
126
+
127
+ **Requires:** None (no LLM calls)
128
+
129
+ ### 12 — Message Bus
130
+
131
+ Bidirectional robot communication via TypedBus. A comedy critic (Alice) tasks a comedian (Bob) to tell robot jokes. Alice evaluates each joke with her LLM; if it's not funny, she sends Bob back for another attempt. Bob's temperature ramps from 0.2 to 1.0 across retries for increasing creativity. The loop continues until Alice approves or `MAX_ATTEMPTS` is reached.
132
+
133
+ Demonstrates: Robot subclasses, prompt templates, auto-ack `on_message`, `reply()` convenience, temperature ramping, convergence patterns.
134
+
135
+ **Requires:** LLM API key
136
+
137
+ ### 13 — Spawning Robots
138
+
139
+ Dynamic specialist creation at runtime. A dispatcher robot receives questions, asks its LLM what kind of specialist is needed, then uses `spawn` to create one on the fly. The bus is created lazily on the first spawn — no explicit bus setup required. Spawned specialists are reused across questions of the same type.
140
+
141
+ Demonstrates: `spawn` for dynamic robot creation, lazy bus creation, `on_message` for reply handling, LLM-driven delegation.
142
+
143
+ **Requires:** LLM API key
144
+
145
+ ### 14 — The Rusty Circuit (Open Mic Night)
146
+
147
+ A comedy club where three robots interact through a shared message bus. A comedian performs stand-up armed with self-modification tools (style reinvention, energy adjustment, coaching). A heckler reacts from the audience — heckling weak material, telling counter-jokes with the comic as the punch line, showing grudging respect, or staying silent when a bit doesn't warrant a response. A talent scout observes silently, spawning specialist analysts and refining evaluation criteria before delivering a final verdict.
148
+
149
+ Terminal output is color-formatted: comic bits in cyan (left-aligned), heckler reactions in yellow (right-indented), tool annotations dimmed. Scout notes go to `scout_notes.md` instead of STDOUT. The final verdict appears in green on both STDOUT and the scout file.
150
+
151
+ Demonstrates: Robot subclasses, self-modification via tool side effects, dynamic spawning (`spawn`), shared `:room` channel + personal channels, processing guards for async serialization, `[SILENCE]` opt-out pattern, style reinvention via user-prompt injection.
152
+
153
+ **Requires:** LLM API key
154
+
155
+ ## Prompt Templates
156
+
157
+ Templates live in `examples/prompts/` as `.md` files with YAML front matter. Each template defines a robot's personality and behavior:
158
+
159
+ ```markdown
160
+ ---
161
+ description: Simple helpful assistant
162
+ temperature: 0.7
163
+ parameters:
164
+ company_name: null
165
+ ---
166
+ You are a helpful assistant for <%= company_name %>.
167
+ ```
168
+
169
+ Front matter keys like `model`, `temperature`, `top_p`, `max_tokens` are applied to the robot's chat automatically. Parameters with `null` values are required and must be provided via `context:` at build time.
170
+
171
+ ### Template Inventory
172
+
173
+ | Template | Used By | Description |
174
+ |----------|---------|-------------|
175
+ | `helper.md` | 01 | Simple helpful assistant |
176
+ | `assistant.md` | 02 | Assistant with tool access |
177
+ | `classifier.md` | 03 | Request classifier (billing/technical/general) |
178
+ | `billing.md` | 03 | Billing specialist |
179
+ | `technical.md` | 03 | Technical support specialist |
180
+ | `general.md` | 03 | General support |
181
+ | `github_assistant.md` | 04 | GitHub-aware assistant |
182
+ | `triage.md` | 06 | E-commerce request triage |
183
+ | `order_support.md` | 06 | Order inquiry specialist |
184
+ | `product_support.md` | 06 | Product questions specialist |
185
+ | `escalation.md` | 06 | Complex issue handler |
186
+ | `sentiment_analyzer.md` | 07 | Sentiment analysis |
187
+ | `entity_extractor.md` | 07 | Entity extraction |
188
+ | `keyword_extractor.md` | 07 | Keyword extraction |
189
+ | `synthesizer.md` | 07 | Multi-source synthesis |
190
+ | `llm_config_demo.md` | 08 | Configuration demo |
191
+ | `configurable.md` | 09 | Configurable template with front matter |
192
+ | `comedian.md` | 12 | Robot joke teller |
193
+ | `comedy_critic.md` | 12 | Joke evaluator (FUNNY/NOT_FUNNY) |
194
+ | `dispatcher.md` | 13 | Specialist role dispatcher |
195
+ | `open_mic_comic.md` | 14 | Observational comedian with self-modification |
196
+ | `open_mic_heckler.md` | 14 | Tough audience heckler (can stay silent or counter-joke) |
197
+ | `open_mic_scout.md` | 14 | Talent scout with analyst recruitment |
@@ -1,2 +1,5 @@
1
+ ---
2
+ description: Helpful assistant with tool access
3
+ ---
1
4
  You are a helpful assistant with access to tools.
2
5
  Use the calculator for math and fortune_cookie for fortune requests.
@@ -1,3 +1,6 @@
1
+ ---
2
+ description: Billing support specialist
3
+ ---
1
4
  You are a billing support specialist. Help users with:
2
5
  - Invoice questions
3
6
  - Payment issues
@@ -1,3 +1,6 @@
1
+ ---
2
+ description: Request classifier for billing/technical/general
3
+ ---
1
4
  You are a request classifier. Analyze the user's request and classify it
2
5
  as either "billing", "technical", or "general".
3
6
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: Robot comedian that tells original jokes
3
+ ---
4
+ You are a robot comedian. Reply with ONLY the joke — no preamble,
5
+ no explanation, no quotation marks. Each joke must be original.
6
+ Keep it to two or three sentences maximum.
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Demanding comedy critic that evaluates jokes
3
+ temperature: 0.2
4
+ ---
5
+ You are a brutally demanding comedy critic. You have extremely high
6
+ standards. Most jokes are NOT funny to you — predictable puns, obvious
7
+ wordplay, and basic setups bore you. Only truly clever, surprising, or
8
+ brilliantly constructed jokes earn your approval.
9
+ Reply with EXACTLY one word on the first line: FUNNY or NOT_FUNNY
10
+ Then on the next line give a one-sentence explanation of your verdict.