robot_lab 0.0.4 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/README.md +64 -6
  4. data/Rakefile +2 -1
  5. data/docs/api/core/index.md +41 -46
  6. data/docs/api/core/memory.md +200 -154
  7. data/docs/api/core/network.md +13 -3
  8. data/docs/api/core/robot.md +38 -26
  9. data/docs/api/core/state.md +55 -73
  10. data/docs/api/index.md +7 -28
  11. data/docs/api/messages/index.md +35 -20
  12. data/docs/api/messages/text-message.md +67 -21
  13. data/docs/api/messages/tool-call-message.md +80 -41
  14. data/docs/api/messages/tool-result-message.md +119 -50
  15. data/docs/api/messages/user-message.md +48 -24
  16. data/docs/architecture/core-concepts.md +10 -15
  17. data/docs/concepts.md +5 -7
  18. data/docs/examples/index.md +2 -2
  19. data/docs/getting-started/configuration.md +80 -0
  20. data/docs/guides/building-robots.md +10 -9
  21. data/docs/guides/creating-networks.md +49 -0
  22. data/docs/guides/index.md +0 -5
  23. data/docs/guides/rails-integration.md +244 -162
  24. data/docs/guides/streaming.md +118 -138
  25. data/docs/index.md +0 -8
  26. data/examples/03_network.rb +10 -7
  27. data/examples/08_llm_config.rb +40 -11
  28. data/examples/09_chaining.rb +45 -6
  29. data/examples/11_network_introspection.rb +30 -7
  30. data/examples/12_message_bus.rb +1 -1
  31. data/examples/14_rusty_circuit/heckler.rb +14 -8
  32. data/examples/14_rusty_circuit/open_mic.rb +5 -3
  33. data/examples/14_rusty_circuit/scout.rb +14 -31
  34. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +1 -1
  35. data/examples/16_writers_room/display.rb +158 -0
  36. data/examples/16_writers_room/output/.gitignore +4 -0
  37. data/examples/16_writers_room/output/README.md +69 -0
  38. data/examples/16_writers_room/output/opus_001.md +263 -0
  39. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  40. data/examples/16_writers_room/output/opus_002.md +245 -0
  41. data/examples/16_writers_room/output/opus_002_notes.log +546 -0
  42. data/examples/16_writers_room/output/opus_002_screenplay.md +7989 -0
  43. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +993 -0
  44. data/examples/16_writers_room/prompts/screenplay_writer.md +66 -0
  45. data/examples/16_writers_room/prompts/writer.md +37 -0
  46. data/examples/16_writers_room/room.rb +186 -0
  47. data/examples/16_writers_room/tools.rb +173 -0
  48. data/examples/16_writers_room/writer.rb +121 -0
  49. data/examples/16_writers_room/writers_room.rb +256 -0
  50. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  51. data/lib/robot_lab/memory.rb +8 -32
  52. data/lib/robot_lab/network.rb +13 -20
  53. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  54. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  55. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  56. data/lib/robot_lab/robot.rb +56 -420
  57. data/lib/robot_lab/run_config.rb +184 -0
  58. data/lib/robot_lab/state_proxy.rb +2 -12
  59. data/lib/robot_lab/task.rb +8 -1
  60. data/lib/robot_lab/utils.rb +39 -0
  61. data/lib/robot_lab/version.rb +1 -1
  62. data/lib/robot_lab.rb +29 -8
  63. data/mkdocs.yml +0 -11
  64. metadata +21 -20
  65. data/docs/api/adapters/anthropic.md +0 -121
  66. data/docs/api/adapters/gemini.md +0 -133
  67. data/docs/api/adapters/index.md +0 -104
  68. data/docs/api/adapters/openai.md +0 -134
  69. data/docs/api/history/active-record-adapter.md +0 -275
  70. data/docs/api/history/config.md +0 -284
  71. data/docs/api/history/index.md +0 -128
  72. data/docs/api/history/thread-manager.md +0 -194
  73. data/docs/guides/history.md +0 -359
  74. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  75. data/lib/robot_lab/adapters/base.rb +0 -85
  76. data/lib/robot_lab/adapters/gemini.rb +0 -193
  77. data/lib/robot_lab/adapters/openai.rb +0 -160
  78. data/lib/robot_lab/adapters/registry.rb +0 -81
  79. data/lib/robot_lab/errors.rb +0 -70
  80. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  81. data/lib/robot_lab/history/config.rb +0 -115
  82. data/lib/robot_lab/history/thread_manager.rb +0 -93
  83. data/lib/robot_lab/robotic_model.rb +0 -324
@@ -11,6 +11,8 @@
11
11
  # own jokes using the comedian as the punch line.
12
12
  #
13
13
  # Subscribes to the :room channel to hear performances.
14
+ # Room deliveries are routed through the core processing guard,
15
+ # which serializes run() calls to prevent Async fiber interleaving.
14
16
  # Sends feedback directly to the comic's personal channel.
15
17
  #
16
18
  class Heckler < RobotLab::Robot
@@ -23,10 +25,10 @@ class Heckler < RobotLab::Robot
23
25
 
24
26
  super(name: "heckler", template: :open_mic_heckler, bus: bus)
25
27
 
26
- # Listen to the room for the comic's performances
27
- @bus.subscribe(:room) do |delivery|
28
- delivery.ack!
29
- message = delivery.message
28
+ # Handle incoming messages the core processing guard
29
+ # serializes all deliveries, preventing concurrent run()
30
+ # calls from corrupting chat history.
31
+ on_message do |message|
30
32
  next unless message.from == "comic"
31
33
  next if @rounds >= MAX_ROUNDS
32
34
 
@@ -39,9 +41,7 @@ class Heckler < RobotLab::Robot
39
41
  ).reply.strip
40
42
 
41
43
  # The heckler chose silence — no output, no feedback
42
- if verdict.match?(/\[SILENCE\]/i)
43
- next
44
- end
44
+ next if verdict.match?(/\[SILENCE\]/i)
45
45
 
46
46
  @display.heckler("Heckler [Round #{@rounds}]", verdict)
47
47
 
@@ -51,7 +51,13 @@ class Heckler < RobotLab::Robot
51
51
  end
52
52
 
53
53
  # Send feedback to comic's personal channel until the set is done
54
- reply(message, verdict) if @rounds < MAX_ROUNDS
54
+ send_reply(to: message.from.to_sym, content: verdict, in_reply_to: message.key) if @rounds < MAX_ROUNDS
55
+ end
56
+
57
+ # Listen to the room for the comic's performances.
58
+ # Route through the core processing guard.
59
+ @bus.subscribe(:room) do |delivery|
60
+ handle_incoming_delivery(delivery)
55
61
  end
56
62
  end
57
63
  end
@@ -23,8 +23,10 @@
23
23
  # Communication uses a shared :room channel — the comic publishes
24
24
  # performances there, and both the heckler and scout subscribe.
25
25
  # The heckler sends feedback directly to the comic's personal channel.
26
- # The scout uses a processing guard to serialize observations,
27
- # preventing Async fiber interleaving from corrupting chat history.
26
+ # Room deliveries are routed through the core processing guard
27
+ # (BusMessaging#handle_incoming_delivery), which serializes all
28
+ # run() calls to prevent Async fiber interleaving from corrupting
29
+ # chat history.
28
30
  #
29
31
  # Style reinventions are injected into the next round's user prompt
30
32
  # rather than modifying the chat's system messages, avoiding message
@@ -96,7 +98,7 @@ display.comic("Comic [Opening]", opening)
96
98
  # channel, triggering the feedback loop:
97
99
  # room → heckler → comic → room → heckler → comic → ...
98
100
  # The loop terminates when the heckler stops replying (MAX_ROUNDS).
99
- # The scout observes each round via :room with serialized processing.
101
+ # The scout observes each round via :room, serialized by the core guard.
100
102
  comic.send_message(to: :room, content: "OPENING: #{opening}")
101
103
 
102
104
  display.separator
@@ -69,10 +69,10 @@ end
69
69
  # analysts when they see something worth examining closely.
70
70
  #
71
71
  # Subscribes to the :room channel to observe performances.
72
- # Uses a processing guard to serialize run() calls — TypedBus
73
- # delivers messages in concurrent Async fibers, and interleaved
74
- # run() calls on the same @chat would corrupt tool_use/tool_result
75
- # ordering in the Anthropic API.
72
+ # Room deliveries are routed through the core processing guard
73
+ # (BusMessaging#handle_incoming_delivery), which serializes all
74
+ # run() calls to prevent Async fiber interleaving from corrupting
75
+ # chat history.
76
76
  #
77
77
  class Scout < RobotLab::Robot
78
78
  attr_accessor :log, :analysts_spawned, :pending_criteria, :display
@@ -81,8 +81,6 @@ class Scout < RobotLab::Robot
81
81
  @log = []
82
82
  @analysts_spawned = 0
83
83
  @pending_criteria = nil
84
- @processing = false
85
- @observation_queue = []
86
84
  @display = display
87
85
 
88
86
  super(
@@ -95,20 +93,18 @@ class Scout < RobotLab::Robot
95
93
  ]
96
94
  )
97
95
 
98
- # Listen to the room for the comic's performances.
99
- # Processing guard prevents concurrent run() calls on @chat —
100
- # observations that arrive while processing are queued and
101
- # drained sequentially after the current one completes.
102
- @bus.subscribe(:room) do |delivery|
103
- delivery.ack!
104
- message = delivery.message
96
+ # Handle incoming messages the core processing guard
97
+ # serializes all deliveries, preventing concurrent run()
98
+ # calls from corrupting chat history.
99
+ on_message do |message|
105
100
  next unless message.from == "comic"
101
+ observe_and_note(message.content.to_s)
102
+ end
106
103
 
107
- if @processing
108
- @observation_queue << message.content.to_s
109
- else
110
- process_observation(message.content.to_s)
111
- end
104
+ # Listen to the room for the comic's performances.
105
+ # Route through the core processing guard.
106
+ @bus.subscribe(:room) do |delivery|
107
+ handle_incoming_delivery(delivery)
112
108
  end
113
109
  end
114
110
 
@@ -133,19 +129,6 @@ class Scout < RobotLab::Robot
133
129
 
134
130
  private
135
131
 
136
- def process_observation(content)
137
- @processing = true
138
-
139
- observe_and_note(content)
140
-
141
- # Drain any observations that arrived while we were processing
142
- while (queued = @observation_queue.shift)
143
- observe_and_note(queued)
144
- end
145
-
146
- @processing = false
147
- end
148
-
149
132
  def observe_and_note(content)
150
133
  @log << content
151
134
 
@@ -109,7 +109,7 @@ editor.on_message do |message|
109
109
  puts " Editor [revised]: #{revised[0..120]}..."
110
110
  puts " [editor] Revision written to #{path}"
111
111
 
112
- editor.reply(message, revised)
112
+ editor.send_reply(to: message.from.to_sym, content: revised, in_reply_to: message.key)
113
113
  end
114
114
 
115
115
  chief = EditorInChief.new(
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rainbow"
4
+ require "io/console"
5
+
6
+ # ── Display ─────────────────────────────────────────────────
7
+ #
8
+ # Terminal formatting for the Writers' Room demo.
9
+ #
10
+ # - Broadcasts: white, prefixed with speaker name
11
+ # - Direct messages: magenta, shows sender → recipient
12
+ # - Memory writes: dimmed annotation
13
+ # - Spawns: green annotation
14
+ # - Completion: bright green
15
+ # - Chapter content: cyan (when assembled)
16
+ #
17
+ class Display
18
+ WRITER_COLORS = %i[cyan yellow blue magenta white].freeze
19
+
20
+ def initialize(log_path: nil)
21
+ @term_width = (IO.console&.winsize&.last || 80)
22
+ @wrap_width = @term_width - 8
23
+ @log_file = log_path ? File.open(log_path, "w") : nil
24
+ @color_map = {}
25
+ @next_color = 0
26
+ end
27
+
28
+ # ── Room Messages ──────────────────────────────────────────
29
+
30
+ def broadcast(from, text)
31
+ color = color_for(from)
32
+ puts
33
+ puts Rainbow(" [#{from}] (broadcast):").send(color).bright
34
+ wrap(text, @wrap_width).each do |line|
35
+ puts Rainbow(" #{line}").send(color)
36
+ end
37
+
38
+ log("\n [#{from}] (broadcast):")
39
+ wrap(text, @wrap_width).each { |line| log(" #{line}") }
40
+ end
41
+
42
+ def direct_message(from, to, text)
43
+ puts Rainbow(" [#{from} -> #{to}]: #{text[0..100]}").magenta.faint
44
+ log(" [#{from} -> #{to}]: #{text[0..100]}")
45
+ end
46
+
47
+ # ── Memory ─────────────────────────────────────────────────
48
+
49
+ def memory_write(writer, key)
50
+ puts Rainbow(" [memory] #{writer} wrote :#{key}").darkgray
51
+ log(" [memory] #{writer} wrote :#{key}")
52
+ end
53
+
54
+ # ── Lifecycle ──────────────────────────────────────────────
55
+
56
+ def spawn(requester, name)
57
+ puts Rainbow(" [spawn] #{requester} recruited #{name}").green
58
+ log(" [spawn] #{requester} recruited #{name}")
59
+ end
60
+
61
+ def complete(writer)
62
+ puts
63
+ puts Rainbow(" [#{writer}] marked the work as COMPLETE").green.bright
64
+ log("\n [#{writer}] marked the work as COMPLETE")
65
+ end
66
+
67
+ # ── Incoming message (from bus, before LLM processes) ──────
68
+
69
+ def incoming(writer, from, content)
70
+ puts Rainbow(" [#{writer}] heard #{from}: #{content.to_s[0..80]}...").darkgray
71
+ log(" [#{writer}] heard #{from}: #{content.to_s[0..80]}...")
72
+ end
73
+
74
+ # ── Chrome ─────────────────────────────────────────────────
75
+
76
+ def banner(text)
77
+ puts
78
+ text.each_line { |line| puts Rainbow(line.chomp).bright }
79
+ puts
80
+
81
+ log("")
82
+ text.each_line { |line| log(line.chomp) }
83
+ end
84
+
85
+ def separator
86
+ puts Rainbow(" #{"─" * (@term_width - 4)}").darkgray
87
+ puts
88
+ log(" #{"─" * 56}")
89
+ log("")
90
+ end
91
+
92
+ def phase(text)
93
+ puts
94
+ puts Rainbow(" ▸ #{text}").bright
95
+ puts
96
+ log("\n ▸ #{text}\n")
97
+ end
98
+
99
+ def info(text)
100
+ puts Rainbow(" #{text}").darkgray
101
+ log(" #{text}")
102
+ end
103
+
104
+ def stats(text)
105
+ puts
106
+ text.each_line { |line| puts Rainbow(line.chomp).bright }
107
+ log("")
108
+ text.each_line { |line| log(line.chomp) }
109
+ end
110
+
111
+ def close
112
+ @log_file&.close
113
+ end
114
+
115
+ private
116
+
117
+ def color_for(name)
118
+ @color_map[name] ||= begin
119
+ c = WRITER_COLORS[@next_color % WRITER_COLORS.size]
120
+ @next_color += 1
121
+ c
122
+ end
123
+ end
124
+
125
+ def log(line)
126
+ return unless @log_file
127
+
128
+ @log_file.puts line
129
+ @log_file.flush
130
+ end
131
+
132
+ def wrap(text, max_width)
133
+ lines = []
134
+
135
+ text.to_s.each_line do |paragraph|
136
+ paragraph = paragraph.strip
137
+ next(lines << "") if paragraph.empty?
138
+
139
+ words = paragraph.split(/\s+/)
140
+ current = +""
141
+
142
+ words.each do |word|
143
+ if current.empty?
144
+ current = +word
145
+ elsif current.length + 1 + word.length <= max_width
146
+ current << " " << word
147
+ else
148
+ lines << current
149
+ current = +word
150
+ end
151
+ end
152
+
153
+ lines << current unless current.empty?
154
+ end
155
+
156
+ lines
157
+ end
158
+ end
@@ -0,0 +1,4 @@
1
+ book.md
2
+ room.log
3
+ memory.json
4
+ screenplay.md
@@ -0,0 +1,69 @@
1
+ # Writers' Room Output
2
+
3
+ This directory contains the creative works produced by the Writers' Room
4
+ self-organizing robot teams (Demo 16). No human wrote any of the prose,
5
+ outlines, or screenplay content — it all emerged from robots coordinating
6
+ through bus messages and shared memory.
7
+
8
+ ## The Works
9
+
10
+ ### opus_001.md — First Book
11
+
12
+ The first novella ever produced by the Writers' Room. A team of writer
13
+ robots self-organized to create a 10-chapter science fiction story. They
14
+ discussed the premise, built a story bible, outlined the plot, claimed
15
+ chapters, and wrote them — all without orchestration or assigned roles.
16
+
17
+ `opus_001_notes.log` contains the session log from that run.
18
+
19
+ ### opus_002.md — Second Book
20
+
21
+ The second novella produced by the Writers' Room. Same process, same
22
+ self-organizing pattern, different story.
23
+
24
+ `opus_002_notes.log` contains the session log.
25
+
26
+ ### opus_002_screenplay.md — First Screenplay
27
+
28
+ The first screenplay the robots ever created. After opus_002 was written,
29
+ the Writers' Room was extended with a screenplay mode that adapts a
30
+ finished book into a 4-act made-for-TV movie pilot.
31
+
32
+ The workflow:
33
+
34
+ 1. Book mode runs and produces a novella (opus_002.md)
35
+ 2. Book mode also dumps all creative artifacts (story bible, outline,
36
+ chapters) to `memory.json`
37
+ 3. Screenplay mode is launched with `--screenplay-from memory.json`,
38
+ which reloads that memory into the room before the screenwriters start
39
+ 4. The screenwriters read the source material, discuss adaptation choices,
40
+ build a scene outline, and write individual scenes in standard
41
+ screenplay format
42
+
43
+ ```bash
44
+ # Step 1: Write the book (memory.json is saved automatically)
45
+ bundle exec ruby examples/16_writers_room/writers_room.rb
46
+
47
+ # Step 2: Adapt it to a screenplay
48
+ bundle exec ruby examples/16_writers_room/writers_room.rb \
49
+ --screenplay-from examples/16_writers_room/output/memory.json
50
+ ```
51
+
52
+ Screenplay writers work at the scene level — each robot claims and writes
53
+ individual scenes rather than entire acts. They maintain a `scene_registry`
54
+ in shared memory listing all planned scenes, and can drop or reorder
55
+ scenes as the adaptation takes shape. When unclaimed scenes pile up, the
56
+ robots spawn additional writers to help.
57
+
58
+ `opus_002_screenplay_notes.md` is the log of the robots discussing the
59
+ screenplay adaptation process — debating what to keep, what to cut, how
60
+ to restructure the story for screen, and how to handle act breaks.
61
+
62
+ ## Working Files
63
+
64
+ These files are generated each run and git-ignored:
65
+
66
+ - `book.md` — latest book mode output
67
+ - `screenplay.md` — latest screenplay mode output
68
+ - `memory.json` — memory dump from the latest book mode run
69
+ - `room.log` — structured log from the Room (timestamps, tool calls, heartbeats)