robot_lab 0.0.4 → 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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -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 +2 -0
  37. data/examples/16_writers_room/output/opus_001.md +263 -0
  38. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  39. data/examples/16_writers_room/prompts/writer.md +37 -0
  40. data/examples/16_writers_room/room.rb +150 -0
  41. data/examples/16_writers_room/tools.rb +162 -0
  42. data/examples/16_writers_room/writer.rb +121 -0
  43. data/examples/16_writers_room/writers_room.rb +162 -0
  44. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  45. data/lib/robot_lab/memory.rb +8 -32
  46. data/lib/robot_lab/network.rb +13 -20
  47. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  48. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  49. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  50. data/lib/robot_lab/robot.rb +56 -420
  51. data/lib/robot_lab/run_config.rb +184 -0
  52. data/lib/robot_lab/state_proxy.rb +2 -12
  53. data/lib/robot_lab/task.rb +8 -1
  54. data/lib/robot_lab/utils.rb +39 -0
  55. data/lib/robot_lab/version.rb +1 -1
  56. data/lib/robot_lab.rb +29 -8
  57. data/mkdocs.yml +0 -11
  58. metadata +15 -20
  59. data/docs/api/adapters/anthropic.md +0 -121
  60. data/docs/api/adapters/gemini.md +0 -133
  61. data/docs/api/adapters/index.md +0 -104
  62. data/docs/api/adapters/openai.md +0 -134
  63. data/docs/api/history/active-record-adapter.md +0 -275
  64. data/docs/api/history/config.md +0 -284
  65. data/docs/api/history/index.md +0 -128
  66. data/docs/api/history/thread-manager.md +0 -194
  67. data/docs/guides/history.md +0 -359
  68. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  69. data/lib/robot_lab/adapters/base.rb +0 -85
  70. data/lib/robot_lab/adapters/gemini.rb +0 -193
  71. data/lib/robot_lab/adapters/openai.rb +0 -160
  72. data/lib/robot_lab/adapters/registry.rb +0 -81
  73. data/lib/robot_lab/errors.rb +0 -70
  74. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  75. data/lib/robot_lab/history/config.rb +0 -115
  76. data/lib/robot_lab/history/thread_manager.rb +0 -93
  77. 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
@@ -26,16 +26,3 @@ end
26
26
  # config.openai_api_key = ENV["OPENAI_API_KEY"]
27
27
  # config.gemini_api_key = ENV["GEMINI_API_KEY"]
28
28
  # end
29
-
30
- # History Persistence (optional)
31
- #
32
- # Uncomment to enable conversation history storage:
33
- #
34
- # Rails.application.config.after_initialize do
35
- # adapter = RobotLab::History::ActiveRecordAdapter.new(
36
- # thread_model: RobotLabThread,
37
- # result_model: RobotLabResult
38
- # )
39
- #
40
- # RobotLab.config.history = adapter.to_config
41
- # end
@@ -59,6 +59,8 @@ module RobotLab
59
59
  # memory.cache # => RubyLLM::SemanticCache instance
60
60
  #
61
61
  class Memory
62
+ include Utils
63
+
62
64
  # Reserved keys that have special behavior
63
65
  RESERVED_KEYS = %i[data results messages session_id cache].freeze
64
66
 
@@ -553,8 +555,8 @@ module RobotLab
553
555
  def clone
554
556
  cloned = Memory.new(
555
557
  data: deep_dup(data.to_h),
556
- results: results.dup,
557
- messages: messages.dup,
558
+ results: results,
559
+ messages: messages,
558
560
  session_id: session_id,
559
561
  backend: @backend.is_a?(Hash) ? :hash : :auto,
560
562
  enable_cache: @enable_cache,
@@ -685,25 +687,15 @@ module RobotLab
685
687
  ->(result) { result.output + result.tool_calls }
686
688
  end
687
689
 
688
- def deep_dup(obj)
689
- case obj
690
- when Hash
691
- obj.transform_values { |v| deep_dup(v) }
692
- when Array
693
- obj.map { |v| deep_dup(v) }
694
- else
695
- obj.dup rescue obj
696
- end
697
- end
698
-
699
690
  # =========================================================================
700
691
  # Reactive Memory Helpers
701
692
  # =========================================================================
702
693
 
703
694
  def get_single(key, wait:)
704
695
  # Try immediate read
705
- value = @mutex.synchronize { @backend[key] }
706
- return value unless value.nil? && wait
696
+ @mutex.synchronize { return @backend[key] if @backend.key?(key) }
697
+
698
+ return nil unless wait
707
699
 
708
700
  # Need to wait
709
701
  timeout = wait == true ? nil : wait
@@ -740,8 +732,7 @@ module RobotLab
740
732
 
741
733
  @waiter_mutex.synchronize do
742
734
  # Double-check - value might have arrived while setting up
743
- value = @mutex.synchronize { @backend[key] }
744
- return value unless value.nil?
735
+ @mutex.synchronize { return @backend[key] if @backend.key?(key) }
745
736
 
746
737
  @waiters[key] << waiter
747
738
  end
@@ -795,21 +786,6 @@ module RobotLab
795
786
  end
796
787
  end
797
788
 
798
- def dispatch_async(&block)
799
- # Use Async if available (preferred for fiber-based concurrency)
800
- if defined?(Async) && Async::Task.current?
801
- Async { block.call }
802
- else
803
- # Fall back to Thread for basic async dispatch
804
- Thread.new do
805
- block.call
806
- rescue StandardError => e
807
- # Log but don't crash the notification system
808
- warn "Memory subscription callback error: #{e.message}"
809
- end
810
- end
811
- end
812
-
813
789
  def generate_subscription_id
814
790
  SecureRandom.uuid
815
791
  end
@@ -58,6 +58,8 @@ module RobotLab
58
58
  # network.broadcast(event: :pause, reason: "rate limit")
59
59
  #
60
60
  class Network
61
+ include Utils
62
+
61
63
  # Reserved key for broadcast messages in memory
62
64
  BROADCAST_KEY = :_network_broadcast
63
65
 
@@ -69,7 +71,7 @@ module RobotLab
69
71
  # @return [Hash<String, Robot>] robots in this network, keyed by name
70
72
  # @!attribute [r] memory
71
73
  # @return [Memory] shared memory for all robots in the network
72
- attr_reader :name, :pipeline, :robots, :memory
74
+ attr_reader :name, :pipeline, :robots, :memory, :config
73
75
 
74
76
  # Creates a new Network instance.
75
77
  #
@@ -84,12 +86,13 @@ module RobotLab
84
86
  # task :billing, billing_robot, context: { dept: "billing" }, depends_on: :optional
85
87
  # end
86
88
  #
87
- def initialize(name:, concurrency: :auto, memory: nil, &block)
89
+ def initialize(name:, concurrency: :auto, memory: nil, config: nil, &block)
88
90
  @name = name.to_s
89
91
  @robots = {}
90
92
  @tasks = {}
91
93
  @pipeline = SimpleFlow::Pipeline.new(concurrency: concurrency)
92
94
  @memory = memory || Memory.new(network_name: @name)
95
+ @config = config || RunConfig.new
93
96
  @broadcast_handlers = []
94
97
 
95
98
  instance_eval(&block) if block_given?
@@ -118,14 +121,15 @@ module RobotLab
118
121
  # @example Task with dependencies
119
122
  # task :writer, writer_robot, depends_on: [:analyst]
120
123
  #
121
- def task(name, robot, context: {}, mcp: :none, tools: :none, memory: nil, depends_on: :none)
124
+ def task(name, robot, context: {}, mcp: :none, tools: :none, memory: nil, config: nil, depends_on: :none)
122
125
  task_wrapper = Task.new(
123
126
  name: name,
124
127
  robot: robot,
125
128
  context: context,
126
129
  mcp: mcp,
127
130
  tools: tools,
128
- memory: memory
131
+ memory: memory,
132
+ config: config
129
133
  )
130
134
 
131
135
  @robots[name.to_s] = robot
@@ -170,6 +174,9 @@ module RobotLab
170
174
  # Include shared memory in run params so robots can access it
171
175
  run_context[:network_memory] = @memory
172
176
 
177
+ # Pass network's config so robots can inherit it
178
+ run_context[:network_config] = @config unless @config.empty?
179
+
173
180
  initial_result = SimpleFlow::Result.new(
174
181
  run_context,
175
182
  context: { run_params: run_context }
@@ -327,24 +334,10 @@ module RobotLab
327
334
  name: name,
328
335
  robots: @robots.keys,
329
336
  tasks: @tasks.keys,
330
- optional_tasks: @pipeline.optional_steps.to_a
337
+ optional_tasks: @pipeline.optional_steps.to_a,
338
+ config: (@config.empty? ? nil : @config.to_json_hash)
331
339
  }.compact
332
340
  end
333
341
 
334
- private
335
-
336
- def dispatch_async(&block)
337
- # Use Async if available (preferred for fiber-based concurrency)
338
- if defined?(Async) && Async::Task.current?
339
- Async { block.call }
340
- else
341
- # Fall back to Thread for basic async dispatch
342
- Thread.new do
343
- block.call
344
- rescue StandardError => e
345
- warn "Network broadcast handler error: #{e.message}"
346
- end
347
- end
348
- end
349
342
  end
350
343
  end