robot_lab 0.3.0 → 0.3.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 (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +1 -1
  4. data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
  5. data/lib/robot_lab/version.rb +1 -1
  6. metadata +3 -253
  7. data/.architecture/AGENTS.md +0 -32
  8. data/.architecture/config.yml +0 -8
  9. data/.architecture/members.yml +0 -60
  10. data/.architecture/reviews/feature-free-will.md +0 -490
  11. data/.architecture/reviews/overall-codebase.md +0 -427
  12. data/.claude/settings.local.json +0 -57
  13. data/.codex/config.toml +0 -2
  14. data/.envrc +0 -6
  15. data/.github/workflows/deploy-github-pages.yml +0 -52
  16. data/.irbrc +0 -6
  17. data/.loki +0 -65
  18. data/.quality/reek_baseline.txt +0 -43
  19. data/.rubocop.yml +0 -10
  20. data/Archspec.rb +0 -44
  21. data/CLAUDE.md +0 -141
  22. data/COMMITS.md +0 -196
  23. data/Rakefile +0 -193
  24. data/_typos.toml +0 -21
  25. data/agent2agent_review.md +0 -192
  26. data/agentf_improvements.md +0 -253
  27. data/agents.md +0 -14
  28. data/docs/api/core/index.md +0 -147
  29. data/docs/api/core/memory.md +0 -578
  30. data/docs/api/core/network.md +0 -553
  31. data/docs/api/core/result.md +0 -211
  32. data/docs/api/core/robot.md +0 -1667
  33. data/docs/api/core/state.md +0 -145
  34. data/docs/api/core/tool.md +0 -520
  35. data/docs/api/errors.md +0 -323
  36. data/docs/api/hooks.md +0 -469
  37. data/docs/api/index.md +0 -137
  38. data/docs/api/mcp/client.md +0 -387
  39. data/docs/api/mcp/index.md +0 -267
  40. data/docs/api/mcp/server.md +0 -252
  41. data/docs/api/mcp/transports.md +0 -339
  42. data/docs/api/messages/index.md +0 -126
  43. data/docs/api/messages/text-message.md +0 -170
  44. data/docs/api/messages/tool-call-message.md +0 -206
  45. data/docs/api/messages/tool-result-message.md +0 -242
  46. data/docs/api/messages/user-message.md +0 -232
  47. data/docs/api/skills.md +0 -423
  48. data/docs/api/streaming/context.md +0 -280
  49. data/docs/api/streaming/events.md +0 -185
  50. data/docs/api/streaming/index.md +0 -156
  51. data/docs/api/support.md +0 -846
  52. data/docs/architecture/core-concepts.md +0 -575
  53. data/docs/architecture/index.md +0 -252
  54. data/docs/architecture/message-flow.md +0 -366
  55. data/docs/architecture/network-orchestration.md +0 -478
  56. data/docs/architecture/robot-execution.md +0 -396
  57. data/docs/architecture/state-management.md +0 -410
  58. data/docs/assets/css/custom.css +0 -56
  59. data/docs/assets/images/robot_lab.jpg +0 -0
  60. data/docs/concepts.md +0 -641
  61. data/docs/examples/basic-chat.md +0 -258
  62. data/docs/examples/index.md +0 -425
  63. data/docs/examples/mcp-server.md +0 -403
  64. data/docs/examples/multi-robot-network.md +0 -476
  65. data/docs/examples/tool-usage.md +0 -468
  66. data/docs/getting-started/configuration.md +0 -645
  67. data/docs/getting-started/index.md +0 -56
  68. data/docs/getting-started/installation.md +0 -197
  69. data/docs/getting-started/quick-start.md +0 -293
  70. data/docs/guides/building-robots.md +0 -1187
  71. data/docs/guides/creating-networks.md +0 -736
  72. data/docs/guides/hooks.md +0 -1206
  73. data/docs/guides/index.md +0 -78
  74. data/docs/guides/knowledge.md +0 -219
  75. data/docs/guides/mcp-integration.md +0 -740
  76. data/docs/guides/memory.md +0 -458
  77. data/docs/guides/observability.md +0 -733
  78. data/docs/guides/streaming.md +0 -323
  79. data/docs/guides/using-tools.md +0 -780
  80. data/docs/index.md +0 -280
  81. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  82. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  83. data/examples/.envrc +0 -3
  84. data/examples/01_simple_robot.rb +0 -31
  85. data/examples/02_tools.rb +0 -109
  86. data/examples/03_network.rb +0 -118
  87. data/examples/04_mcp.rb +0 -217
  88. data/examples/05_streaming.rb +0 -137
  89. data/examples/06_prompt_templates.rb +0 -325
  90. data/examples/07_network_memory.rb +0 -313
  91. data/examples/08_llm_config.rb +0 -162
  92. data/examples/09_chaining.rb +0 -251
  93. data/examples/10_memory.rb +0 -315
  94. data/examples/11_network_introspection.rb +0 -239
  95. data/examples/12_message_bus.rb +0 -67
  96. data/examples/13_spawn.rb +0 -85
  97. data/examples/14_rusty_circuit/.envrc +0 -1
  98. data/examples/14_rusty_circuit/.gitignore +0 -1
  99. data/examples/14_rusty_circuit/README.md +0 -1
  100. data/examples/14_rusty_circuit/comic.rb +0 -149
  101. data/examples/14_rusty_circuit/display.rb +0 -217
  102. data/examples/14_rusty_circuit/heckler.rb +0 -65
  103. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  104. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  105. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  106. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  107. data/examples/14_rusty_circuit/scout.rb +0 -165
  108. data/examples/14_rusty_circuit/show.log +0 -234
  109. data/examples/15_memory_network_and_bus/.envrc +0 -1
  110. data/examples/15_memory_network_and_bus/README.md +0 -66
  111. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  112. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  113. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  114. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  115. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  116. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  117. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  118. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  119. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  120. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  121. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  122. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  123. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  124. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  125. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  126. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  127. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  128. data/examples/16_writers_room/.envrc +0 -1
  129. data/examples/16_writers_room/display.rb +0 -158
  130. data/examples/16_writers_room/output/.gitignore +0 -4
  131. data/examples/16_writers_room/output/README.md +0 -69
  132. data/examples/16_writers_room/output/opus_001.md +0 -263
  133. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  134. data/examples/16_writers_room/output/opus_002.md +0 -245
  135. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  136. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  137. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  138. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  139. data/examples/16_writers_room/prompts/writer.md +0 -37
  140. data/examples/16_writers_room/room.rb +0 -186
  141. data/examples/16_writers_room/tools.rb +0 -173
  142. data/examples/16_writers_room/writer.rb +0 -121
  143. data/examples/16_writers_room/writers_room.rb +0 -256
  144. data/examples/17_skills.rb +0 -234
  145. data/examples/18_rails/.envrc +0 -3
  146. data/examples/18_rails/.gitignore +0 -5
  147. data/examples/18_rails/Gemfile +0 -11
  148. data/examples/18_rails/README.md +0 -67
  149. data/examples/18_rails/Rakefile +0 -4
  150. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  151. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  152. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  153. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  154. data/examples/18_rails/app/models/application_record.rb +0 -5
  155. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  156. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  157. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  158. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  159. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  160. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  161. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  162. data/examples/18_rails/bin/dev +0 -7
  163. data/examples/18_rails/bin/rails +0 -6
  164. data/examples/18_rails/bin/setup +0 -15
  165. data/examples/18_rails/config/application.rb +0 -33
  166. data/examples/18_rails/config/cable.yml +0 -2
  167. data/examples/18_rails/config/database.yml +0 -5
  168. data/examples/18_rails/config/environment.rb +0 -4
  169. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  170. data/examples/18_rails/config/routes.rb +0 -6
  171. data/examples/18_rails/config.ru +0 -4
  172. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  173. data/examples/19_token_tracking.rb +0 -138
  174. data/examples/20_circuit_breaker.rb +0 -147
  175. data/examples/21_learning_loop.rb +0 -181
  176. data/examples/22_context_compression.rb +0 -172
  177. data/examples/23_convergence.rb +0 -133
  178. data/examples/24_structured_delegation.rb +0 -151
  179. data/examples/25_history_search/conversation.jsonl +0 -30
  180. data/examples/25_history_search.rb +0 -133
  181. data/examples/26_document_store/api_versioning_adr.md +0 -52
  182. data/examples/26_document_store/incident_postmortem.md +0 -46
  183. data/examples/26_document_store/postgres_runbook.md +0 -49
  184. data/examples/26_document_store/redis_caching_guide.md +0 -48
  185. data/examples/26_document_store/sidekiq_guide.md +0 -51
  186. data/examples/26_document_store.rb +0 -140
  187. data/examples/27_incident_response/README.md +0 -65
  188. data/examples/27_incident_response/incident_response.rb +0 -261
  189. data/examples/28_mcp_discovery.rb +0 -113
  190. data/examples/29_ractor_tools.rb +0 -240
  191. data/examples/30_ractor_network.rb +0 -252
  192. data/examples/31_launch_assessment.rb +0 -246
  193. data/examples/32_newsletter_reader.rb +0 -193
  194. data/examples/33_stock_generator.rb +0 -80
  195. data/examples/33_stock_predictor.rb +0 -327
  196. data/examples/34_agentskills.rb +0 -76
  197. data/examples/35_hooks.rb +0 -266
  198. data/examples/README.md +0 -550
  199. data/examples/common.rb +0 -180
  200. data/examples/prompts/assistant.md +0 -5
  201. data/examples/prompts/audit_trail.md +0 -8
  202. data/examples/prompts/billing.md +0 -10
  203. data/examples/prompts/classifier.md +0 -7
  204. data/examples/prompts/comedian.md +0 -6
  205. data/examples/prompts/comedy_critic.md +0 -10
  206. data/examples/prompts/configurable.md +0 -9
  207. data/examples/prompts/dispatcher.md +0 -12
  208. data/examples/prompts/entity_extractor.md +0 -14
  209. data/examples/prompts/escalation.md +0 -42
  210. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  211. data/examples/prompts/frontmatter_named_test.md +0 -5
  212. data/examples/prompts/frontmatter_tools_test.md +0 -6
  213. data/examples/prompts/general.md +0 -7
  214. data/examples/prompts/github_assistant.md +0 -14
  215. data/examples/prompts/helper.md +0 -4
  216. data/examples/prompts/incident_responder.md +0 -18
  217. data/examples/prompts/keyword_extractor.md +0 -11
  218. data/examples/prompts/llm_config_demo.md +0 -20
  219. data/examples/prompts/order_support.md +0 -35
  220. data/examples/prompts/os_advocate.md +0 -13
  221. data/examples/prompts/os_chief.md +0 -13
  222. data/examples/prompts/os_editor.md +0 -13
  223. data/examples/prompts/parameterized_main_test.md +0 -6
  224. data/examples/prompts/pii_redactor.md +0 -7
  225. data/examples/prompts/product_support.md +0 -37
  226. data/examples/prompts/runbook_protocol.md +0 -12
  227. data/examples/prompts/sentiment_analyzer.md +0 -12
  228. data/examples/prompts/skill_a_test.md +0 -4
  229. data/examples/prompts/skill_b_test.md +0 -4
  230. data/examples/prompts/skill_config_test.md +0 -5
  231. data/examples/prompts/skill_cycle_a_test.md +0 -6
  232. data/examples/prompts/skill_cycle_b_test.md +0 -6
  233. data/examples/prompts/skill_description_test.md +0 -4
  234. data/examples/prompts/skill_leaf_test.md +0 -4
  235. data/examples/prompts/skill_nested_test.md +0 -6
  236. data/examples/prompts/skill_refs_main_test.md +0 -6
  237. data/examples/prompts/skill_self_ref_test.md +0 -6
  238. data/examples/prompts/skill_with_mcp_test.md +0 -9
  239. data/examples/prompts/skill_with_params_test.md +0 -6
  240. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  241. data/examples/prompts/skill_with_tools_test.md +0 -6
  242. data/examples/prompts/sre_compliance.md +0 -10
  243. data/examples/prompts/structured_output.md +0 -7
  244. data/examples/prompts/synthesizer.md +0 -17
  245. data/examples/prompts/technical.md +0 -10
  246. data/examples/prompts/template_with_skills_test.md +0 -6
  247. data/examples/prompts/triage.md +0 -22
  248. data/examples/ruboruby.md +0 -423
  249. data/examples/run_all.rb +0 -60
  250. data/examples/xyzzy.rb +0 -97
  251. data/logfile +0 -8
  252. data/mkdocs.yml +0 -209
  253. data/robot_concurrency.md +0 -38
  254. data/simple_acp_review.md +0 -298
  255. data/temp.md +0 -6
  256. data/tool_manifest_plan.md +0 -155
@@ -1,66 +0,0 @@
1
- ---
2
- description: Screenwriter adapting source material into a TV movie pilot (scene-level)
3
- temperature: 0.7
4
- ---
5
- You are a screenwriter named <%= writer_name %> in a collaborative writers' room.
6
- You and the other writers share one goal: adapt source material into a 4-act
7
- made-for-TV movie screenplay — the pilot for a potential series.
8
-
9
- You work at the SCENE level. Each writer claims and writes individual scenes.
10
-
11
- You have tools to coordinate:
12
- - broadcast: Send a message to every writer in the room.
13
- - direct_message: Send a private message to one specific writer.
14
- - read_memory: Read from shared memory (source material, scenes, etc.).
15
- - write_memory: Store work in shared memory for everyone to see.
16
- - list_memory: See what keys exist in shared memory.
17
- - spawn_writer: Bring in a new writer to help with unclaimed scenes.
18
- - mark_complete: Signal that the screenplay is finished (all registered scenes written).
19
-
20
- SOURCE MATERIAL (read-only — do not overwrite these keys):
21
- - story_bible: characters, setting, themes, world rules from the original book
22
- - outline: the original chapter plan
23
- - chapter_1 through chapter_10: the original prose
24
-
25
- These keys contain the book you are adapting. Read them to understand the
26
- story, characters, and world before writing any screenplay content.
27
-
28
- Screenplay memory conventions:
29
- - screenplay_bible: adaptation notes — what to keep, cut, combine, reframe for screen
30
- - scene_outline: the scene-by-scene plan grouped into 4 acts with act breaks
31
- - scene_registry: comma-separated scene numbers (e.g. "1,2,3,4,5,6,7,8,9,10,11,12")
32
- This is the master list of scenes to write. Update it if scenes are dropped
33
- or reordered. mark_complete checks this list.
34
- - claims: who is writing which scene
35
- - scene_1, scene_2, ... scene_N: the actual screenplay content
36
- - screenplay_complete: set by mark_complete when all registered scenes are done
37
-
38
- FORMAT: Use standard screenplay format — scene headings (INT./EXT.), action
39
- lines, character names in caps before dialogue, parentheticals where needed.
40
- Structure the screenplay into 4 acts with natural act breaks (commercial
41
- breaks). Mark act boundaries clearly (e.g. "END OF ACT ONE") within the
42
- scene content where the break falls.
43
-
44
- How to work:
45
- - Read the source material first — story_bible, outline, and key chapters.
46
- - Check shared memory before doing anything — avoid duplicating work.
47
- - Build a screenplay_bible first to agree on adaptation choices.
48
- - Create a scene_outline with numbered scenes grouped into 4 acts.
49
- - Write the scene_registry once the outline is agreed on.
50
- - Claim a scene before writing it (update claims in memory).
51
- - After writing a scene, ALWAYS broadcast to announce it so others know
52
- the progress and can pick up the next scene.
53
- - Each scene should be substantial — full screenplay format with proper
54
- scene headings, action, and dialogue.
55
- - SPAWN MORE WRITERS when there are more unclaimed scenes than active
56
- writers. Don't let scenes sit unclaimed — recruit help.
57
- - Scenes may be dropped if they don't serve the story or the runtime is
58
- too long. Update scene_registry when dropping or reordering scenes.
59
- - When a ROOM STATUS message arrives, read memory to see what's missing,
60
- then claim and write an unclaimed scene.
61
- - When all registered scenes are written, use mark_complete.
62
- - You have no memory between messages — shared memory is your only
63
- persistence. Always read memory to understand the current state.
64
- - IMPORTANT: Always include a brief text response summarizing what
65
- you did or plan to do, even when using tools. Never respond with
66
- only tool calls.
@@ -1,37 +0,0 @@
1
- ---
2
- description: Fiction writer in a self-organizing writers' room
3
- temperature: 0.7
4
- ---
5
- You are a fiction writer named <%= writer_name %> in a collaborative writers' room.
6
- You and the other writers share one goal: produce a 10-chapter novella together.
7
-
8
- You have tools to coordinate:
9
- - broadcast: Send a message to every writer in the room.
10
- - direct_message: Send a private message to one specific writer.
11
- - read_memory: Read from shared memory (story bible, outline, chapters, etc.).
12
- - write_memory: Store work in shared memory for everyone to see.
13
- - list_memory: See what keys exist in shared memory.
14
- - spawn_writer: Bring in a new writer if the team needs more hands.
15
- - mark_complete: Signal that the book is finished (all 10 chapters written).
16
-
17
- Shared memory conventions (the team should converge on these naturally):
18
- - story_bible: characters, setting, themes, world rules
19
- - outline: the 10-chapter plan
20
- - claims: who is writing which chapter
21
- - chapter_1 through chapter_10: the actual prose
22
- - book_complete: set when all chapters are done
23
-
24
- How to work:
25
- - Check shared memory before doing anything — avoid duplicating work.
26
- - Claim a chapter before writing it.
27
- - After writing a chapter, ALWAYS broadcast to announce it so others know
28
- the progress and can pick up the next chapter.
29
- - Each chapter should be 3-5 paragraphs of vivid prose.
30
- - When a ROOM STATUS message arrives, read memory to see what's missing,
31
- then claim and write an unclaimed chapter.
32
- - When all 10 chapters are written, use mark_complete.
33
- - You have no memory between messages — shared memory is your only
34
- persistence. Always read memory to understand the current state.
35
- - IMPORTANT: Always include a brief text response summarizing what
36
- you did or plan to do, even when using tools. Never respond with
37
- only tool calls.
@@ -1,186 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "logger"
4
-
5
- # ── The Room ─────────────────────────────────────────────────
6
- #
7
- # Holds the bus, shared memory, and writer roster. Provides
8
- # spawn_writer so any writer's SpawnWriterTool can add new
9
- # members at runtime. That's the only "management" — the rest
10
- # is up to the group.
11
- #
12
- class Room
13
- attr_reader :bus, :memory, :writers, :display, :config, :logger, :mode
14
-
15
- def initialize(display:, mode:, config: nil, log_path: nil)
16
- @bus = TypedBus::MessageBus.new
17
- @memory = RobotLab::Memory.new(enable_cache: false)
18
- @display = display
19
- @mode = mode
20
- @config = config
21
- @writers = {}
22
-
23
- # Structured logger — always writes to output/room.log
24
- log_file = log_path || File.join(__dir__, "output", "room.log")
25
- @logger = Logger.new(log_file, progname: "room")
26
- @logger.formatter = proc do |severity, datetime, progname, msg|
27
- "#{datetime.strftime('%H:%M:%S.%L')} [#{severity}] #{msg}\n"
28
- end
29
- @logger.info("Room initialized")
30
-
31
- # Shared broadcast channel
32
- @bus.add_channel(:room, type: RobotLab::RobotMessage)
33
- @logger.info("Bus channel :room created")
34
- end
35
-
36
- # Add an initial writer to the room
37
- def add_writer(name)
38
- @logger.info("Adding writer '#{name}'")
39
- writer = Writer.new(
40
- name: name,
41
- bus: @bus,
42
- shared_memory: @memory,
43
- display: @display,
44
- room: self,
45
- config: @config
46
- )
47
- @writers[name] = writer
48
- @logger.info("Writer '#{name}' ready (tools: #{writer.local_tools.map(&:name).join(', ')})")
49
- writer
50
- end
51
-
52
- # Called by SpawnWriterTool — any writer can recruit
53
- def spawn_writer(name)
54
- raise "Writer '#{name}' already exists" if @writers.key?(name)
55
-
56
- @logger.info("Spawning writer '#{name}' (requested at runtime)")
57
- add_writer(name)
58
- end
59
-
60
- # Seed the room with the assignment
61
- def seed(assignment)
62
- first = @writers.values.first
63
- @logger.info("Seeding room via '#{first.name}' (#{assignment.length} chars)")
64
- first.send_message(to: :room, content: assignment)
65
- @logger.info("Seed message published to :room")
66
- end
67
-
68
- # Ordered list of expected unit numbers for the current mode.
69
- # Fixed modes return unit_range.to_a; dynamic modes parse the
70
- # registry key from memory (comma-separated integers).
71
- def expected_units
72
- if @mode[:unit_range] == :dynamic
73
- registry = @memory.get(@mode[:registry_key])
74
- return [] unless registry
75
-
76
- registry.to_s.split(",").map { |s| s.strip.to_i }.sort
77
- else
78
- @mode[:unit_range].to_a
79
- end
80
- end
81
-
82
- # Wait for the work to be marked complete.
83
- # Sends periodic heartbeat messages to :room so the feedback loop
84
- # doesn't starve — writers only act when messages arrive.
85
- def wait_for_completion(timeout: 600, poll_interval: 3, heartbeat_interval: 45)
86
- deadline = Time.now + timeout
87
- last_heartbeat = Time.now
88
- done_key = @mode[:completion_key]
89
- unit_name = @mode[:unit_name]
90
- @logger.info("Waiting for completion (timeout: #{timeout}s, heartbeat: #{heartbeat_interval}s)")
91
-
92
- loop do
93
- if @memory.key?(done_key)
94
- @logger.info("Work marked complete!")
95
- return true
96
- end
97
-
98
- if Time.now > deadline
99
- @logger.warn("Timeout reached (#{timeout}s) — work not completed")
100
- units = expected_units
101
- written = units.select { |n| @memory.key?(:"#{unit_name}_#{n}") }
102
- @logger.warn("#{unit_name.capitalize}s in memory: #{written.join(', ')}")
103
- @logger.warn("Memory keys: #{@memory.keys.join(', ')}")
104
- @display.info("Timeout reached (#{timeout}s) — work not completed.")
105
- return false
106
- end
107
-
108
- # Heartbeat: nudge the room with a progress summary
109
- if Time.now - last_heartbeat >= heartbeat_interval
110
- send_heartbeat
111
- last_heartbeat = Time.now
112
- end
113
-
114
- sleep poll_interval
115
- end
116
- end
117
-
118
- # Assemble the finished output from memory
119
- def assemble_output
120
- unit_name = @mode[:unit_name]
121
- bible_key = @mode[:bible_key]
122
- outline_key = @mode[:outline_key]
123
- units_list = expected_units
124
-
125
- @logger.info("Assembling #{@mode[:name]} from memory (#{units_list.size} #{unit_name}s)")
126
- units = units_list.map do |n|
127
- key = :"#{unit_name}_#{n}"
128
- content = @memory.get(key)
129
- if content
130
- @logger.info(" #{unit_name}_#{n}: #{content.to_s.length} chars")
131
- "## #{unit_name.capitalize} #{n}\n\n#{content}"
132
- else
133
- @logger.warn(" #{unit_name}_#{n}: MISSING")
134
- "## #{unit_name.capitalize} #{n}\n\n[Not written]"
135
- end
136
- end
137
-
138
- outline = @memory.get(outline_key)
139
- bible = @memory.get(bible_key)
140
-
141
- parts = []
142
- parts << "# #{bible_key.to_s.tr('_', ' ').split.map(&:capitalize).join(' ')}\n\n#{bible}\n" if bible
143
- parts << "# #{outline_key.to_s.tr('_', ' ').split.map(&:capitalize).join(' ')}\n\n#{outline}\n" if outline
144
- parts << "---\n"
145
- parts.concat(units)
146
-
147
- parts.join("\n\n")
148
- end
149
-
150
- private
151
-
152
- # Build and send a progress status message to :room
153
- def send_heartbeat
154
- unit_name = @mode[:unit_name]
155
- bible_key = @mode[:bible_key]
156
- outline_key = @mode[:outline_key]
157
- units_list = expected_units
158
- total = units_list.size
159
-
160
- written = units_list.select { |n| @memory.key?(:"#{unit_name}_#{n}") }
161
- missing = units_list.reject { |n| @memory.key?(:"#{unit_name}_#{n}") }
162
- has_bible = @memory.key?(bible_key)
163
- has_outline = @memory.key?(outline_key)
164
-
165
- if total.zero?
166
- status = "[ROOM STATUS] No #{unit_name}s registered yet."
167
- status += " Establish a #{@mode[:registry_key]} first." if @mode[:unit_range] == :dynamic
168
- else
169
- status = "[ROOM STATUS] Progress: #{written.size}/#{total} #{unit_name}s written."
170
- status += " Written: #{written.join(', ')}." if written.any?
171
- status += " Still needed: #{missing.join(', ')}." if missing.any?
172
- unclaimed = missing.size
173
- status += " Spawn more writers if #{unclaimed} unclaimed #{unit_name}s exceed active writers." if unclaimed > @writers.size
174
- end
175
- status += " #{bible_key.to_s.tr('_', ' ').capitalize}: #{has_bible ? 'yes' : 'NOT YET'}."
176
- status += " #{outline_key.to_s.tr('_', ' ').capitalize}: #{has_outline ? 'yes' : 'NOT YET'}."
177
- status += " Check shared memory, claim an unclaimed #{unit_name}, and write it."
178
-
179
- @logger.info("Heartbeat -> :room (#{written.size}/#{total} #{unit_name}s)")
180
- @display.info(status)
181
-
182
- # Pick a random writer to deliver the heartbeat through
183
- sender = @writers.values.sample
184
- sender.send_message(to: :room, content: status)
185
- end
186
- end
@@ -1,173 +0,0 @@
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
- parameter :message, type: "string",
27
- description: "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
- parameter :to, type: "string",
46
- description: "Name of the writer to message", required: true
47
- parameter :message, type: "string",
48
- description: "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
- parameter :key, type: "string",
67
- description: "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
- parameter :key, type: "string",
90
- description: "Memory key (e.g. story_bible, outline, claims, chapter_1)", required: true
91
- parameter :value, type: "string",
92
- description: "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
- parameter :name, type: "string",
128
- description: "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 work is finished — all units are written. " \
146
- "Only use this when you have verified all units exist in shared memory."
147
-
148
- def execute
149
- mode = robot.room.mode
150
- unit_name = mode[:unit_name]
151
- done_key = mode[:completion_key]
152
- units = robot.room.expected_units
153
-
154
- if units.empty?
155
- log&.warn("#{robot.name} TOOL mark_complete REJECTED — no #{unit_name}s registered")
156
- return "Cannot mark complete. No #{unit_name}s registered yet."
157
- end
158
-
159
- # Verify all registered units exist
160
- missing = units.reject { |n| robot.shared_memory.key?(:"#{unit_name}_#{n}") }
161
-
162
- if missing.any?
163
- labels = missing.map { |n| "#{unit_name}_#{n}" }.join(", ")
164
- log&.warn("#{robot.name} TOOL mark_complete REJECTED — missing: #{labels}")
165
- "Cannot mark complete. Missing: #{labels}"
166
- else
167
- log&.info("#{robot.name} TOOL mark_complete SUCCESS")
168
- robot.shared_memory.set(done_key, true)
169
- robot.display&.complete(robot.name)
170
- "Work marked as complete!"
171
- end
172
- end
173
- end
@@ -1,121 +0,0 @@
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. Several providers reject that on the next
24
- # call, permanently killing the writer.
25
- #
26
- # Fix: reset the conversation before each message via the public
27
- # Robot#clear_messages(keep_system: true). The writer doesn't need
28
- # persistent chat history — shared memory is the single source of
29
- # truth. Rebuilding @chat by hand instead would drop the tool
30
- # callbacks and chat params that Robot#initialize installed, so
31
- # clear_messages is both shorter and safer.
32
- #
33
- class Writer < RobotLab::Robot
34
- attr_accessor :shared_memory, :display, :room
35
- attr_reader :messages_processed
36
-
37
- def initialize(name:, bus:, shared_memory:, display:, room:, config: nil)
38
- @shared_memory = shared_memory
39
- @display = display
40
- @room = room
41
- @messages_processed = 0
42
-
43
- super(
44
- name: name,
45
- **llm_opts,
46
- template: room.mode[:template],
47
- context: { writer_name: name },
48
- bus: bus,
49
- config: config,
50
- local_tools: build_tools
51
- )
52
-
53
- setup_room_subscription
54
- setup_message_handler
55
- end
56
-
57
- private
58
-
59
- def log
60
- @room&.logger
61
- end
62
-
63
- def build_tools
64
- [
65
- BroadcastTool.new(robot: self),
66
- DirectMessageTool.new(robot: self),
67
- ReadMemoryTool.new(robot: self),
68
- WriteMemoryTool.new(robot: self),
69
- ListMemoryTool.new(robot: self),
70
- SpawnWriterTool.new(robot: self),
71
- MarkCompleteTool.new(robot: self),
72
- ]
73
- end
74
-
75
- # Room deliveries go to this robot's BusPoller, which serializes them
76
- # behind any run() already in flight before invoking on_message.
77
- def setup_room_subscription
78
- @bus.subscribe(:room) do |delivery|
79
- enqueue_delivery(delivery)
80
- end
81
- end
82
-
83
- # Drop the turn history, keep the rendered system prompt. Model,
84
- # temperature, tools and the tool callbacks all survive untouched.
85
- def reset_conversation!
86
- clear_messages(keep_system: true)
87
- end
88
-
89
- def setup_message_handler
90
- on_message do |message|
91
- # Don't respond to your own messages
92
- next if message.from == name
93
-
94
- @messages_processed += 1
95
- log&.info("#{name} <- [#{message.from}] msg ##{@messages_processed} (#{message.content.to_s[0..80]}...)")
96
- @display&.incoming(name, message.from, message.content)
97
-
98
- # Fresh conversation for each message — shared memory is our persistence
99
- reset_conversation!
100
-
101
- # Build prompt with current memory context
102
- memory_keys = shared_memory.keys
103
- prompt = "[#{message.from}]: #{message.content}"
104
- prompt += "\n\n[Memory keys: #{memory_keys.join(', ')}]" if memory_keys.any?
105
-
106
- log&.info("#{name} -> run() starting (prompt: #{prompt.length} chars)")
107
-
108
- begin
109
- # tools: :inherit — run() defaults to :none. Without it the writer
110
- # would have all seven tools attached and none of them offered to the
111
- # model, so nothing would ever be written to shared memory.
112
- result = run(prompt, tools: :inherit)
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