robot_lab 0.2.8 → 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 (262) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +16 -18
  4. data/lib/robot_lab/ask_user.rb +3 -3
  5. data/lib/robot_lab/config/defaults.yml +5 -5
  6. data/lib/robot_lab/config.rb +2 -2
  7. data/lib/robot_lab/mcp/transports/streamable_http.rb +2 -4
  8. data/lib/robot_lab/memory.rb +16 -7
  9. data/lib/robot_lab/robot/hooking.rb +26 -0
  10. data/lib/robot_lab/robot/result_building.rb +119 -0
  11. data/lib/robot_lab/robot.rb +37 -120
  12. data/lib/robot_lab/run_config.rb +52 -20
  13. data/lib/robot_lab/tool.rb +7 -12
  14. data/lib/robot_lab/version.rb +1 -1
  15. data/lib/robot_lab.rb +3 -3
  16. metadata +15 -274
  17. data/.architecture/AGENTS.md +0 -32
  18. data/.architecture/config.yml +0 -8
  19. data/.architecture/members.yml +0 -60
  20. data/.architecture/reviews/feature-free-will.md +0 -490
  21. data/.architecture/reviews/overall-codebase.md +0 -427
  22. data/.claude/settings.local.json +0 -57
  23. data/.codex/config.toml +0 -2
  24. data/.envrc +0 -6
  25. data/.github/workflows/deploy-github-pages.yml +0 -52
  26. data/.irbrc +0 -6
  27. data/.loki +0 -65
  28. data/.quality/reek_baseline.txt +0 -43
  29. data/.rubocop.yml +0 -10
  30. data/Archspec.rb +0 -44
  31. data/CLAUDE.md +0 -139
  32. data/COMMITS.md +0 -196
  33. data/Rakefile +0 -193
  34. data/_typos.toml +0 -21
  35. data/agent2agent_review.md +0 -192
  36. data/agentf_improvements.md +0 -253
  37. data/agents.md +0 -14
  38. data/docs/api/core/index.md +0 -147
  39. data/docs/api/core/memory.md +0 -578
  40. data/docs/api/core/network.md +0 -553
  41. data/docs/api/core/result.md +0 -211
  42. data/docs/api/core/robot.md +0 -1667
  43. data/docs/api/core/state.md +0 -145
  44. data/docs/api/core/tool.md +0 -520
  45. data/docs/api/errors.md +0 -323
  46. data/docs/api/hooks.md +0 -469
  47. data/docs/api/index.md +0 -137
  48. data/docs/api/mcp/client.md +0 -387
  49. data/docs/api/mcp/index.md +0 -267
  50. data/docs/api/mcp/server.md +0 -252
  51. data/docs/api/mcp/transports.md +0 -339
  52. data/docs/api/messages/index.md +0 -126
  53. data/docs/api/messages/text-message.md +0 -170
  54. data/docs/api/messages/tool-call-message.md +0 -206
  55. data/docs/api/messages/tool-result-message.md +0 -242
  56. data/docs/api/messages/user-message.md +0 -232
  57. data/docs/api/skills.md +0 -423
  58. data/docs/api/streaming/context.md +0 -280
  59. data/docs/api/streaming/events.md +0 -185
  60. data/docs/api/streaming/index.md +0 -156
  61. data/docs/api/support.md +0 -846
  62. data/docs/architecture/core-concepts.md +0 -575
  63. data/docs/architecture/index.md +0 -252
  64. data/docs/architecture/message-flow.md +0 -366
  65. data/docs/architecture/network-orchestration.md +0 -478
  66. data/docs/architecture/robot-execution.md +0 -396
  67. data/docs/architecture/state-management.md +0 -410
  68. data/docs/assets/css/custom.css +0 -56
  69. data/docs/assets/images/robot_lab.jpg +0 -0
  70. data/docs/concepts.md +0 -641
  71. data/docs/examples/basic-chat.md +0 -258
  72. data/docs/examples/index.md +0 -425
  73. data/docs/examples/mcp-server.md +0 -403
  74. data/docs/examples/multi-robot-network.md +0 -476
  75. data/docs/examples/tool-usage.md +0 -468
  76. data/docs/getting-started/configuration.md +0 -645
  77. data/docs/getting-started/index.md +0 -56
  78. data/docs/getting-started/installation.md +0 -200
  79. data/docs/getting-started/quick-start.md +0 -293
  80. data/docs/guides/building-robots.md +0 -1187
  81. data/docs/guides/creating-networks.md +0 -736
  82. data/docs/guides/hooks.md +0 -1206
  83. data/docs/guides/index.md +0 -78
  84. data/docs/guides/knowledge.md +0 -219
  85. data/docs/guides/mcp-integration.md +0 -740
  86. data/docs/guides/memory.md +0 -458
  87. data/docs/guides/observability.md +0 -733
  88. data/docs/guides/streaming.md +0 -323
  89. data/docs/guides/using-tools.md +0 -780
  90. data/docs/index.md +0 -280
  91. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  92. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  93. data/examples/.envrc +0 -1
  94. data/examples/01_simple_robot.rb +0 -31
  95. data/examples/02_tools.rb +0 -109
  96. data/examples/03_network.rb +0 -118
  97. data/examples/04_mcp.rb +0 -215
  98. data/examples/05_streaming.rb +0 -137
  99. data/examples/06_prompt_templates.rb +0 -325
  100. data/examples/07_network_memory.rb +0 -313
  101. data/examples/08_llm_config.rb +0 -162
  102. data/examples/09_chaining.rb +0 -251
  103. data/examples/10_memory.rb +0 -315
  104. data/examples/11_network_introspection.rb +0 -239
  105. data/examples/12_message_bus.rb +0 -67
  106. data/examples/13_spawn.rb +0 -85
  107. data/examples/14_rusty_circuit/.envrc +0 -1
  108. data/examples/14_rusty_circuit/.gitignore +0 -1
  109. data/examples/14_rusty_circuit/README.md +0 -1
  110. data/examples/14_rusty_circuit/comic.rb +0 -149
  111. data/examples/14_rusty_circuit/display.rb +0 -217
  112. data/examples/14_rusty_circuit/heckler.rb +0 -65
  113. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  114. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  115. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  116. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  117. data/examples/14_rusty_circuit/scout.rb +0 -165
  118. data/examples/14_rusty_circuit/show.log +0 -234
  119. data/examples/15_memory_network_and_bus/.envrc +0 -1
  120. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  121. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  122. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  123. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  124. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  125. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -13
  126. data/examples/15_memory_network_and_bus/output/final_article.md +0 -13
  127. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  128. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  129. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  130. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -18
  131. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -13
  132. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  133. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  134. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  135. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  136. data/examples/16_writers_room/.envrc +0 -1
  137. data/examples/16_writers_room/display.rb +0 -158
  138. data/examples/16_writers_room/output/.gitignore +0 -4
  139. data/examples/16_writers_room/output/README.md +0 -69
  140. data/examples/16_writers_room/output/opus_001.md +0 -263
  141. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  142. data/examples/16_writers_room/output/opus_002.md +0 -245
  143. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  144. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  145. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  146. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  147. data/examples/16_writers_room/prompts/writer.md +0 -37
  148. data/examples/16_writers_room/room.rb +0 -186
  149. data/examples/16_writers_room/tools.rb +0 -173
  150. data/examples/16_writers_room/writer.rb +0 -121
  151. data/examples/16_writers_room/writers_room.rb +0 -256
  152. data/examples/17_skills.rb +0 -234
  153. data/examples/18_rails/.envrc +0 -3
  154. data/examples/18_rails/.gitignore +0 -5
  155. data/examples/18_rails/Gemfile +0 -11
  156. data/examples/18_rails/README.md +0 -67
  157. data/examples/18_rails/Rakefile +0 -4
  158. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  159. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  160. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  161. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  162. data/examples/18_rails/app/models/application_record.rb +0 -5
  163. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  164. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  165. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  166. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  167. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  168. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  169. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  170. data/examples/18_rails/bin/dev +0 -7
  171. data/examples/18_rails/bin/rails +0 -6
  172. data/examples/18_rails/bin/setup +0 -15
  173. data/examples/18_rails/config/application.rb +0 -33
  174. data/examples/18_rails/config/cable.yml +0 -2
  175. data/examples/18_rails/config/database.yml +0 -5
  176. data/examples/18_rails/config/environment.rb +0 -4
  177. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  178. data/examples/18_rails/config/routes.rb +0 -6
  179. data/examples/18_rails/config.ru +0 -4
  180. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  181. data/examples/19_token_tracking.rb +0 -138
  182. data/examples/20_circuit_breaker.rb +0 -147
  183. data/examples/21_learning_loop.rb +0 -181
  184. data/examples/22_context_compression.rb +0 -172
  185. data/examples/23_convergence.rb +0 -133
  186. data/examples/24_structured_delegation.rb +0 -151
  187. data/examples/25_history_search/conversation.jsonl +0 -30
  188. data/examples/25_history_search.rb +0 -133
  189. data/examples/26_document_store/api_versioning_adr.md +0 -52
  190. data/examples/26_document_store/incident_postmortem.md +0 -46
  191. data/examples/26_document_store/postgres_runbook.md +0 -49
  192. data/examples/26_document_store/redis_caching_guide.md +0 -48
  193. data/examples/26_document_store/sidekiq_guide.md +0 -51
  194. data/examples/26_document_store.rb +0 -140
  195. data/examples/27_incident_response/incident_response.rb +0 -261
  196. data/examples/28_mcp_discovery.rb +0 -113
  197. data/examples/29_ractor_tools.rb +0 -240
  198. data/examples/30_ractor_network.rb +0 -252
  199. data/examples/31_launch_assessment.rb +0 -246
  200. data/examples/32_newsletter_reader.rb +0 -193
  201. data/examples/33_stock_generator.rb +0 -80
  202. data/examples/33_stock_predictor.rb +0 -327
  203. data/examples/34_agentskills.rb +0 -76
  204. data/examples/35_hooks.rb +0 -266
  205. data/examples/README.md +0 -533
  206. data/examples/common.rb +0 -148
  207. data/examples/prompts/assistant.md +0 -5
  208. data/examples/prompts/audit_trail.md +0 -8
  209. data/examples/prompts/billing.md +0 -10
  210. data/examples/prompts/classifier.md +0 -7
  211. data/examples/prompts/comedian.md +0 -6
  212. data/examples/prompts/comedy_critic.md +0 -10
  213. data/examples/prompts/configurable.md +0 -9
  214. data/examples/prompts/dispatcher.md +0 -12
  215. data/examples/prompts/entity_extractor.md +0 -14
  216. data/examples/prompts/escalation.md +0 -42
  217. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  218. data/examples/prompts/frontmatter_named_test.md +0 -5
  219. data/examples/prompts/frontmatter_tools_test.md +0 -6
  220. data/examples/prompts/general.md +0 -7
  221. data/examples/prompts/github_assistant.md +0 -14
  222. data/examples/prompts/helper.md +0 -4
  223. data/examples/prompts/incident_responder.md +0 -18
  224. data/examples/prompts/keyword_extractor.md +0 -11
  225. data/examples/prompts/llm_config_demo.md +0 -20
  226. data/examples/prompts/order_support.md +0 -35
  227. data/examples/prompts/os_advocate.md +0 -13
  228. data/examples/prompts/os_chief.md +0 -13
  229. data/examples/prompts/os_editor.md +0 -13
  230. data/examples/prompts/parameterized_main_test.md +0 -6
  231. data/examples/prompts/pii_redactor.md +0 -7
  232. data/examples/prompts/product_support.md +0 -37
  233. data/examples/prompts/runbook_protocol.md +0 -12
  234. data/examples/prompts/sentiment_analyzer.md +0 -12
  235. data/examples/prompts/skill_a_test.md +0 -4
  236. data/examples/prompts/skill_b_test.md +0 -4
  237. data/examples/prompts/skill_config_test.md +0 -5
  238. data/examples/prompts/skill_cycle_a_test.md +0 -6
  239. data/examples/prompts/skill_cycle_b_test.md +0 -6
  240. data/examples/prompts/skill_description_test.md +0 -4
  241. data/examples/prompts/skill_leaf_test.md +0 -4
  242. data/examples/prompts/skill_nested_test.md +0 -6
  243. data/examples/prompts/skill_refs_main_test.md +0 -6
  244. data/examples/prompts/skill_self_ref_test.md +0 -6
  245. data/examples/prompts/skill_with_mcp_test.md +0 -9
  246. data/examples/prompts/skill_with_params_test.md +0 -6
  247. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  248. data/examples/prompts/skill_with_tools_test.md +0 -6
  249. data/examples/prompts/sre_compliance.md +0 -10
  250. data/examples/prompts/structured_output.md +0 -7
  251. data/examples/prompts/synthesizer.md +0 -17
  252. data/examples/prompts/technical.md +0 -10
  253. data/examples/prompts/template_with_skills_test.md +0 -6
  254. data/examples/prompts/triage.md +0 -22
  255. data/examples/ruboruby.md +0 -423
  256. data/examples/xyzzy.rb +0 -97
  257. data/logfile +0 -8
  258. data/mkdocs.yml +0 -209
  259. data/robot_concurrency.md +0 -38
  260. data/simple_acp_review.md +0 -298
  261. data/temp.md +0 -6
  262. 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
- 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 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