robot_lab 0.0.1 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +9 -9
  3. data/.irbrc +6 -0
  4. data/CHANGELOG.md +140 -0
  5. data/README.md +263 -48
  6. data/Rakefile +71 -1
  7. data/docs/api/core/index.md +53 -46
  8. data/docs/api/core/memory.md +200 -154
  9. data/docs/api/core/network.md +13 -3
  10. data/docs/api/core/robot.md +490 -130
  11. data/docs/api/core/state.md +55 -73
  12. data/docs/api/core/tool.md +205 -209
  13. data/docs/api/index.md +7 -28
  14. data/docs/api/mcp/client.md +119 -48
  15. data/docs/api/mcp/index.md +75 -60
  16. data/docs/api/mcp/server.md +120 -136
  17. data/docs/api/mcp/transports.md +172 -184
  18. data/docs/api/messages/index.md +35 -20
  19. data/docs/api/messages/text-message.md +67 -21
  20. data/docs/api/messages/tool-call-message.md +80 -41
  21. data/docs/api/messages/tool-result-message.md +119 -50
  22. data/docs/api/messages/user-message.md +48 -24
  23. data/docs/api/streaming/context.md +157 -74
  24. data/docs/api/streaming/events.md +114 -166
  25. data/docs/api/streaming/index.md +74 -72
  26. data/docs/architecture/core-concepts.md +360 -116
  27. data/docs/architecture/index.md +97 -59
  28. data/docs/architecture/message-flow.md +138 -129
  29. data/docs/architecture/network-orchestration.md +197 -50
  30. data/docs/architecture/robot-execution.md +199 -146
  31. data/docs/architecture/state-management.md +255 -187
  32. data/docs/concepts.md +311 -49
  33. data/docs/examples/basic-chat.md +89 -77
  34. data/docs/examples/index.md +222 -47
  35. data/docs/examples/mcp-server.md +207 -203
  36. data/docs/examples/multi-robot-network.md +129 -35
  37. data/docs/examples/rails-application.md +159 -160
  38. data/docs/examples/tool-usage.md +295 -204
  39. data/docs/getting-started/configuration.md +347 -154
  40. data/docs/getting-started/index.md +1 -1
  41. data/docs/getting-started/installation.md +22 -13
  42. data/docs/getting-started/quick-start.md +166 -121
  43. data/docs/guides/building-robots.md +418 -212
  44. data/docs/guides/creating-networks.md +143 -24
  45. data/docs/guides/index.md +0 -5
  46. data/docs/guides/mcp-integration.md +152 -113
  47. data/docs/guides/memory.md +220 -164
  48. data/docs/guides/rails-integration.md +244 -162
  49. data/docs/guides/streaming.md +137 -187
  50. data/docs/guides/using-tools.md +259 -212
  51. data/docs/index.md +46 -41
  52. data/examples/01_simple_robot.rb +6 -9
  53. data/examples/02_tools.rb +6 -9
  54. data/examples/03_network.rb +19 -17
  55. data/examples/04_mcp.rb +5 -8
  56. data/examples/05_streaming.rb +5 -8
  57. data/examples/06_prompt_templates.rb +42 -37
  58. data/examples/07_network_memory.rb +13 -14
  59. data/examples/08_llm_config.rb +169 -0
  60. data/examples/09_chaining.rb +262 -0
  61. data/examples/10_memory.rb +331 -0
  62. data/examples/11_network_introspection.rb +253 -0
  63. data/examples/12_message_bus.rb +74 -0
  64. data/examples/13_spawn.rb +90 -0
  65. data/examples/14_rusty_circuit/comic.rb +143 -0
  66. data/examples/14_rusty_circuit/display.rb +203 -0
  67. data/examples/14_rusty_circuit/heckler.rb +63 -0
  68. data/examples/14_rusty_circuit/open_mic.rb +123 -0
  69. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
  70. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
  71. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
  72. data/examples/14_rusty_circuit/scout.rb +156 -0
  73. data/examples/14_rusty_circuit/scout_notes.md +89 -0
  74. data/examples/14_rusty_circuit/show.log +234 -0
  75. data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
  76. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
  77. data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
  78. data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
  79. data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
  80. data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
  81. data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
  82. data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
  83. data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
  84. data/examples/15_memory_network_and_bus/output/memory.json +13 -0
  85. data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
  86. data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
  87. data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
  88. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
  89. data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
  90. data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
  91. data/examples/16_writers_room/display.rb +158 -0
  92. data/examples/16_writers_room/output/.gitignore +2 -0
  93. data/examples/16_writers_room/output/opus_001.md +263 -0
  94. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  95. data/examples/16_writers_room/prompts/writer.md +37 -0
  96. data/examples/16_writers_room/room.rb +150 -0
  97. data/examples/16_writers_room/tools.rb +162 -0
  98. data/examples/16_writers_room/writer.rb +121 -0
  99. data/examples/16_writers_room/writers_room.rb +162 -0
  100. data/examples/README.md +197 -0
  101. data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
  102. data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
  103. data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
  104. data/examples/prompts/comedian.md +6 -0
  105. data/examples/prompts/comedy_critic.md +10 -0
  106. data/examples/prompts/configurable.md +9 -0
  107. data/examples/prompts/dispatcher.md +12 -0
  108. data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
  109. data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
  110. data/examples/prompts/frontmatter_mcp_test.md +9 -0
  111. data/examples/prompts/frontmatter_named_test.md +5 -0
  112. data/examples/prompts/frontmatter_tools_test.md +6 -0
  113. data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
  114. data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
  115. data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
  116. data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
  117. data/examples/prompts/llm_config_demo.md +20 -0
  118. data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
  119. data/examples/prompts/os_advocate.md +13 -0
  120. data/examples/prompts/os_chief.md +13 -0
  121. data/examples/prompts/os_editor.md +13 -0
  122. data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
  123. data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
  124. data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
  125. data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
  126. data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
  127. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  128. data/lib/robot_lab/ask_user.rb +75 -0
  129. data/lib/robot_lab/config/defaults.yml +121 -0
  130. data/lib/robot_lab/config.rb +183 -0
  131. data/lib/robot_lab/error.rb +6 -0
  132. data/lib/robot_lab/mcp/client.rb +1 -1
  133. data/lib/robot_lab/memory.rb +10 -34
  134. data/lib/robot_lab/network.rb +13 -20
  135. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  136. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  137. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  138. data/lib/robot_lab/robot.rb +240 -330
  139. data/lib/robot_lab/robot_message.rb +44 -0
  140. data/lib/robot_lab/robot_result.rb +1 -0
  141. data/lib/robot_lab/run_config.rb +184 -0
  142. data/lib/robot_lab/state_proxy.rb +2 -12
  143. data/lib/robot_lab/streaming/context.rb +1 -1
  144. data/lib/robot_lab/task.rb +8 -1
  145. data/lib/robot_lab/tool.rb +108 -172
  146. data/lib/robot_lab/tool_config.rb +1 -1
  147. data/lib/robot_lab/tool_manifest.rb +2 -18
  148. data/lib/robot_lab/utils.rb +39 -0
  149. data/lib/robot_lab/version.rb +1 -1
  150. data/lib/robot_lab.rb +89 -57
  151. data/mkdocs.yml +0 -11
  152. metadata +121 -135
  153. data/docs/api/adapters/anthropic.md +0 -121
  154. data/docs/api/adapters/gemini.md +0 -133
  155. data/docs/api/adapters/index.md +0 -104
  156. data/docs/api/adapters/openai.md +0 -134
  157. data/docs/api/history/active-record-adapter.md +0 -195
  158. data/docs/api/history/config.md +0 -191
  159. data/docs/api/history/index.md +0 -132
  160. data/docs/api/history/thread-manager.md +0 -144
  161. data/docs/guides/history.md +0 -359
  162. data/examples/prompts/assistant/user.txt.erb +0 -1
  163. data/examples/prompts/billing/user.txt.erb +0 -1
  164. data/examples/prompts/classifier/user.txt.erb +0 -1
  165. data/examples/prompts/entity_extractor/user.txt.erb +0 -3
  166. data/examples/prompts/escalation/user.txt.erb +0 -34
  167. data/examples/prompts/general/user.txt.erb +0 -1
  168. data/examples/prompts/github_assistant/user.txt.erb +0 -1
  169. data/examples/prompts/helper/user.txt.erb +0 -1
  170. data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
  171. data/examples/prompts/order_support/user.txt.erb +0 -22
  172. data/examples/prompts/product_support/user.txt.erb +0 -32
  173. data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
  174. data/examples/prompts/synthesizer/user.txt.erb +0 -15
  175. data/examples/prompts/technical/user.txt.erb +0 -1
  176. data/examples/prompts/triage/user.txt.erb +0 -17
  177. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  178. data/lib/robot_lab/adapters/base.rb +0 -85
  179. data/lib/robot_lab/adapters/gemini.rb +0 -193
  180. data/lib/robot_lab/adapters/openai.rb +0 -159
  181. data/lib/robot_lab/adapters/registry.rb +0 -81
  182. data/lib/robot_lab/configuration.rb +0 -143
  183. data/lib/robot_lab/errors.rb +0 -70
  184. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  185. data/lib/robot_lab/history/config.rb +0 -115
  186. data/lib/robot_lab/history/thread_manager.rb +0 -93
  187. data/lib/robot_lab/robotic_model.rb +0 -324
@@ -1,76 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'robot/template_rendering'
4
+ require_relative 'robot/mcp_management'
5
+ require_relative 'robot/bus_messaging'
6
+
3
7
  module RobotLab
4
- # LLM-powered robot using ruby_llm-template for prompts
8
+ # LLM-powered robot built on RubyLLM::Agent
5
9
  #
6
- # Robot is a thin wrapper around RubyLLM.chat that provides:
7
- # - Template-based prompts via ruby_llm-template
8
- # - Build-time context (static robot configuration)
9
- # - Run-time context (per-request dynamic data)
10
- # - Tool integration via RubyLLM::Tool
11
- # - Hierarchical MCP and tools configuration
10
+ # Robot is a subclass of RubyLLM::Agent that adds:
11
+ # - Template-based prompts via prompt_manager
12
+ # - Shared memory (standalone or network)
13
+ # - Tool integration with hierarchical MCP configuration
14
+ # - SimpleFlow pipeline integration
12
15
  #
13
16
  # == Memory Behavior
14
17
  #
15
- # Robots have two memory contexts depending on how they're used:
16
- #
17
18
  # *Standalone*: Robot uses its own inherent memory (`robot.memory`).
18
- # Use `robot.reset_memory` to clear it.
19
- #
20
- # *In a Network*: Robot uses the network's shared memory (`network.memory`).
21
- # The robot's inherent memory is ignored. Use `network.reset_memory` to clear it.
22
- #
23
- # This allows the same robot instance to work both standalone and as part
24
- # of a network, with appropriate memory isolation in each context.
19
+ # *In a Network*: Robot uses the network's shared memory.
25
20
  #
26
21
  # @example Simple robot with template
27
- # robot = Robot.new(
28
- # name: "helper",
29
- # template: :helper,
30
- # context: { company_name: "Acme Corp" }
31
- # )
32
- # result = robot.run(message: "Hello!", user_name: "Alice")
22
+ # robot = Robot.new(name: "helper", template: :helper)
23
+ # result = robot.run("Hello!")
33
24
  #
34
- # @example Robot with inline system prompt (no template file needed)
35
- # robot = Robot.new(
36
- # name: "quick_bot",
37
- # system_prompt: "You are a helpful assistant. Be concise."
38
- # )
25
+ # @example Robot with inline system prompt
26
+ # robot = Robot.new(name: "bot", system_prompt: "You are helpful.")
27
+ # result = robot.run("What is 2+2?")
39
28
  #
40
- # @example Robot with template and additional system prompt
41
- # robot = Robot.new(
42
- # name: "support",
43
- # template: :support_agent,
44
- # system_prompt: "Today is #{Date.today}. Current promotion: 20% off."
45
- # )
29
+ # @example Bare robot configured via chaining
30
+ # robot = Robot.new(name: "bot")
31
+ # robot.with_instructions("Be concise.").run("Hello")
46
32
  #
47
33
  # @example Robot with tools
48
34
  # robot = Robot.new(
49
35
  # name: "support",
50
36
  # template: :support,
51
- # context: { policies: POLICIES },
52
- # tools: [OrderLookup, RefundProcessor]
53
- # )
54
- #
55
- # @example Robot with hierarchical MCP/tools config
56
- # robot = Robot.new(
57
- # name: "assistant",
58
- # template: :assistant,
59
- # mcp: :inherit, # Inherit from network/config
60
- # tools: %w[search_code] # Only allow search_code tool
37
+ # local_tools: [OrderLookup, RefundProcessor]
61
38
  # )
62
39
  #
63
- class Robot
40
+ class Robot < RubyLLM::Agent
41
+ include Robot::TemplateRendering
42
+ include Robot::MCPManagement
43
+ include Robot::BusMessaging
44
+
64
45
  # @!attribute [r] name
65
46
  # @return [String] the unique identifier for the robot
66
47
  # @!attribute [r] description
67
48
  # @return [String, nil] an optional description of the robot's purpose
68
49
  # @!attribute [r] template
69
- # @return [Symbol, nil] the ERB template for the robot's prompt
50
+ # @return [Symbol, nil] the prompt_manager template for the robot's prompt
70
51
  # @!attribute [r] system_prompt
71
52
  # @return [String, nil] inline system prompt (used alone or appended to template)
72
- # @!attribute [r] model
73
- # @return [String, Object] the LLM model identifier or model object
74
53
  # @!attribute [r] local_tools
75
54
  # @return [Array] the locally defined tools for this robot
76
55
  # @!attribute [r] mcp_clients
@@ -79,7 +58,15 @@ module RobotLab
79
58
  # @return [Array<Tool>] tools discovered from MCP servers
80
59
  # @!attribute [r] memory
81
60
  # @return [Memory] the robot's inherent memory (used when standalone, not in network)
82
- attr_reader :name, :description, :template, :system_prompt, :model, :local_tools, :mcp_clients, :mcp_tools, :memory
61
+ # @!attribute [rw] input
62
+ # @return [IO] input stream for user interaction (default: $stdin)
63
+ # @!attribute [rw] output
64
+ # @return [IO] output stream for user interaction (default: $stdout)
65
+ attr_accessor :input, :output
66
+
67
+ attr_reader :name, :description, :template, :system_prompt,
68
+ :local_tools, :mcp_clients, :mcp_tools, :memory,
69
+ :bus, :outbox, :config
83
70
 
84
71
  # @!attribute [r] mcp_config
85
72
  # @return [Symbol, Array] build-time MCP configuration (raw, unresolved)
@@ -90,37 +77,27 @@ module RobotLab
90
77
  # Creates a new Robot instance.
91
78
  #
92
79
  # @param name [String] the unique identifier for the robot
93
- # @param template [Symbol, nil] the ERB template for the robot's prompt
94
- # @param system_prompt [String, nil] inline system prompt (can be used alone or with template)
95
- # @param context [Hash, Proc] variables to pass to the template at build time
96
- # @param description [String, nil] an optional description of the robot's purpose
97
- # @param local_tools [Array] tools defined locally for this robot
98
- # @param model [String, nil] the LLM model to use (defaults to config.default_model)
80
+ # @param template [Symbol, nil] the prompt_manager template
81
+ # @param system_prompt [String, nil] inline system prompt
82
+ # @param context [Hash, Proc] variables to pass to the template
83
+ # @param description [String, nil] optional description
84
+ # @param local_tools [Array] tools defined locally
85
+ # @param model [String, nil] the LLM model to use
99
86
  # @param mcp_servers [Array] legacy parameter for MCP server configurations
100
- # @param mcp [Symbol, Array] hierarchical MCP config (:none, :inherit, or array of servers)
101
- # @param tools [Symbol, Array] hierarchical tools config (:none, :inherit, or array of tool names)
87
+ # @param mcp [Symbol, Array] hierarchical MCP config
88
+ # @param tools [Symbol, Array] hierarchical tools config
102
89
  # @param on_tool_call [Proc, nil] callback invoked when a tool is called
103
90
  # @param on_tool_result [Proc, nil] callback invoked when a tool returns a result
104
- # @param enable_cache [Boolean] whether to enable semantic caching (default: true)
105
- #
106
- # @example Basic robot with template
107
- # Robot.new(name: "helper", template: :helper)
108
- #
109
- # @example Robot with inline system prompt
110
- # Robot.new(name: "bot", system_prompt: "You are helpful.")
111
- #
112
- # @example Robot with template and additional system prompt
113
- # Robot.new(name: "bot", template: :base, system_prompt: "Extra context here.")
114
- #
115
- # @example Robot with tools and callbacks
116
- # Robot.new(
117
- # name: "support",
118
- # template: :support,
119
- # local_tools: [OrderLookup],
120
- # on_tool_call: ->(call) { puts "Calling #{call.name}" }
121
- # )
122
- #
123
- # @raise [ArgumentError] if neither template nor system_prompt is provided
91
+ # @param enable_cache [Boolean] whether to enable semantic caching
92
+ # @param bus [TypedBus::MessageBus, nil] optional message bus for inter-robot communication
93
+ # @param temperature [Float, nil] controls randomness
94
+ # @param top_p [Float, nil] nucleus sampling threshold
95
+ # @param top_k [Integer, nil] top-k sampling
96
+ # @param max_tokens [Integer, nil] maximum tokens in response
97
+ # @param presence_penalty [Float, nil] penalize based on presence
98
+ # @param frequency_penalty [Float, nil] penalize based on frequency
99
+ # @param stop [String, Array, nil] stop sequences
100
+ # @param config [RunConfig, nil] shared configuration (merged with explicit kwargs)
124
101
  def initialize(
125
102
  name:,
126
103
  template: nil,
@@ -134,71 +111,138 @@ module RobotLab
134
111
  tools: :none,
135
112
  on_tool_call: nil,
136
113
  on_tool_result: nil,
137
- enable_cache: true
114
+ enable_cache: true,
115
+ bus: nil,
116
+ temperature: nil,
117
+ top_p: nil,
118
+ top_k: nil,
119
+ max_tokens: nil,
120
+ presence_penalty: nil,
121
+ frequency_penalty: nil,
122
+ stop: nil,
123
+ config: nil
138
124
  )
139
- unless template || system_prompt
140
- raise ArgumentError, "Must provide either template or system_prompt"
141
- end
142
-
143
125
  @name = name.to_s
126
+ @name_from_constructor = (name.to_s != "robot")
144
127
  @template = template
145
128
  @system_prompt = system_prompt
146
129
  @build_context = context
147
130
  @description = description
148
131
  @local_tools = Array(local_tools)
149
- @model = model || RobotLab.configuration.default_model
150
- @on_tool_call = on_tool_call
151
- @on_tool_result = on_tool_result
132
+
133
+ # Build RunConfig from explicit kwargs, merged on top of passed-in config.
134
+ # Explicit constructor kwargs always override the shared config.
135
+ explicit_fields = {
136
+ model: model, temperature: temperature, top_p: top_p, top_k: top_k,
137
+ max_tokens: max_tokens, presence_penalty: presence_penalty,
138
+ frequency_penalty: frequency_penalty, stop: stop,
139
+ on_tool_call: on_tool_call, on_tool_result: on_tool_result,
140
+ bus: bus, enable_cache: enable_cache
141
+ }.compact
142
+
143
+ # Only include mcp/tools if explicitly set (not the default :none sentinel)
144
+ resolved_mcp = mcp_servers.any? ? mcp_servers : mcp
145
+ explicit_fields[:mcp] = resolved_mcp unless ToolConfig.none_value?(resolved_mcp)
146
+ explicit_fields[:tools] = tools unless ToolConfig.none_value?(tools)
147
+
148
+ explicit_config = RunConfig.new(**explicit_fields)
149
+ @config = config ? config.merge(explicit_config) : explicit_config
150
+
151
+ # Extract values from effective config for backward compatibility
152
+ @on_tool_call = @config.on_tool_call
153
+ @on_tool_result = @config.on_tool_result
152
154
 
153
155
  # Store raw config values for hierarchical resolution
154
- # mcp_servers is legacy parameter, mcp is the new hierarchical one
155
- @mcp_config = mcp_servers.any? ? mcp_servers : mcp
156
- @tools_config = tools
156
+ @mcp_config = @config.mcp || :none
157
+ @tools_config = @config.tools || :none
157
158
 
158
159
  # MCP state
159
160
  @mcp_clients = {}
160
161
  @mcp_tools = []
161
162
  @mcp_initialized = false
162
163
 
164
+ # Bus state (optional inter-robot communication)
165
+ @bus = @config.bus
166
+ @message_counter = 0
167
+ @outbox = {}
168
+ @message_handler = nil
169
+ @bus_processing = false
170
+ @bus_queue = []
171
+
163
172
  # Inherent memory (used when standalone, not in a network)
164
- @memory = Memory.new(enable_cache: enable_cache)
173
+ cache_enabled = @config.key?(:enable_cache) ? @config.enable_cache : true
174
+ @memory = Memory.new(enable_cache: cache_enabled)
175
+
176
+ # Ensure config is loaded (triggers PM setup, RubyLLM config, etc.)
177
+ config = RobotLab.config
178
+
179
+ # Build chat kwargs for Agent's super
180
+ resolved_model = @config.model || config.ruby_llm.model
181
+ chat_kwargs = { model: resolved_model }
182
+
183
+ # Create the persistent chat via Agent's initialize
184
+ super(chat: nil, **chat_kwargs)
185
+
186
+ # Apply template first (includes front matter config like model, temperature)
187
+ # then constructor params override — constructor is more specific than template.
188
+ apply_template_to_chat(context) if @template
189
+ @chat.with_instructions(@system_prompt) if @system_prompt
190
+
191
+ # Constructor params override template front matter (use config values)
192
+ apply_chat_option(:with_temperature, @config.temperature)
193
+ apply_chat_option(:with_top_p, @config.top_p)
194
+ apply_chat_option(:with_top_k, @config.top_k)
195
+ apply_chat_option(:with_max_tokens, @config.max_tokens)
196
+ apply_chat_option(:with_presence_penalty, @config.presence_penalty)
197
+ apply_chat_option(:with_frequency_penalty, @config.frequency_penalty)
198
+ apply_chat_option(:with_stop, @config.stop)
199
+
200
+ # Apply callbacks
201
+ @chat.on_tool_call(&@on_tool_call) if @on_tool_call
202
+ @chat.on_tool_result(&@on_tool_result) if @on_tool_result
203
+
204
+ # Set up bus channel if a bus was provided
205
+ setup_bus_channel if @bus
165
206
  end
166
207
 
167
- # Returns the robot's local tools (alias for local_tools).
168
- #
169
- # Provided for backward compatibility with earlier API versions.
208
+
209
+ # Returns the model identifier
170
210
  #
171
- # @return [Array] the locally defined tools
172
- def tools
173
- @local_tools
211
+ # @return [String, nil] the LLM model ID string
212
+ def model
213
+ return nil unless @chat.respond_to?(:model)
214
+
215
+ m = @chat.model
216
+ m.respond_to?(:id) ? m.id : m.to_s
174
217
  end
175
218
 
176
- # Run the robot with the given context
219
+ # Forward with_* methods to the persistent chat, returning self for chaining
220
+ %i[
221
+ with_model with_temperature with_top_p with_top_k with_max_tokens
222
+ with_presence_penalty with_frequency_penalty with_stop
223
+ with_instructions with_tool with_tools with_params
224
+ with_headers with_schema with_context with_thinking
225
+ ].each do |method|
226
+ define_method(method) do |*args, **kwargs, &block|
227
+ @chat.public_send(method, *args, **kwargs, &block)
228
+ self
229
+ end
230
+ end
231
+
232
+
233
+ # Send a message and get a response, with Robot's extended capabilities
177
234
  #
178
- # @param network [NetworkRun, nil] Network context if running in network (legacy)
179
- # @param network_memory [Memory, nil] Shared network memory (preferred)
180
- # @param memory [Memory, Hash, nil] Runtime memory to merge
181
- # @param mcp [Symbol, Array, nil] Runtime MCP override (:inherit, :none, nil, [], or array of servers)
182
- # @param tools [Symbol, Array, nil] Runtime tools override (:inherit, :none, nil, [], or array of tool names)
183
- # @param run_context [Hash] Context for rendering user template
235
+ # @param message [String] the user message
236
+ # @param network [NetworkRun, nil] network context (legacy)
237
+ # @param network_memory [Memory, nil] shared network memory
238
+ # @param memory [Memory, Hash, nil] runtime memory to merge
239
+ # @param mcp [Symbol, Array, nil] runtime MCP override
240
+ # @param tools [Symbol, Array, nil] runtime tools override
184
241
  # @return [RobotResult]
185
- #
186
- # @example Standalone robot with inherent memory
187
- # robot.run(message: "My name is Alice")
188
- # robot.run(message: "What's my name?") # Memory persists
189
- #
190
- # @example Runtime memory injection
191
- # robot.run(message: "Hello", memory: { user_id: 123, session: "abc" })
192
- #
193
- # @example With network shared memory
194
- # robot.run(message: "Analyze this", network_memory: network.memory)
195
- #
196
- def run(network: nil, network_memory: nil, memory: nil, mcp: :none, tools: :none, **run_context)
197
- # Determine which memory to use (priority order):
198
- # 1. Explicit network_memory parameter
199
- # 2. Network object's memory (legacy)
200
- # 3. Robot's inherent memory
201
- run_memory = network_memory || network&.memory || @memory
242
+ def run(message = nil, network: nil, network_memory: nil, network_config: nil,
243
+ memory: nil, mcp: :none, tools: :none, **kwargs)
244
+ # Determine which memory to use
245
+ run_memory = resolve_active_memory(network: network, network_memory: network_memory)
202
246
 
203
247
  # Merge runtime memory if provided
204
248
  case memory
@@ -214,90 +258,101 @@ module RobotLab
214
258
 
215
259
  begin
216
260
  # Resolve hierarchical MCP and tools configuration
217
- resolved_mcp = resolve_mcp_hierarchy(mcp, network: network)
218
- resolved_tools = resolve_tools_hierarchy(tools, network: network)
261
+ resolved_mcp = resolve_mcp_hierarchy(mcp, network: network, network_config: network_config)
262
+ resolved_tools = resolve_tools_hierarchy(tools, network: network, network_config: network_config)
219
263
 
220
264
  # Initialize or update MCP clients based on resolved config
221
265
  ensure_mcp_clients(resolved_mcp)
222
266
 
223
- # Merge build context + run context
224
- full_context = resolve_context(@build_context, network: network)
225
- .merge(run_context)
267
+ # Apply filtered tools to the persistent chat
268
+ filtered = filtered_tools(resolved_tools)
269
+ @chat.with_tools(*filtered) if filtered.any?
226
270
 
227
- # Build chat with template, filtered tools, and semantic cache
228
- chat = build_chat(full_context, allowed_tools: resolved_tools, memory: run_memory)
271
+ # Re-render template with run-time context merged into build-time context.
272
+ # Template parameters (e.g. customer: null) may require values that are
273
+ # only available at run time — the robot gathers them before rendering.
274
+ run_context = kwargs.except(:with)
275
+ rerender_template(run_context) if @template && run_context.any?
229
276
 
230
- # Execute and return result
231
- response = chat.complete
277
+ # Delegate to Agent's ask (which calls @chat.ask)
278
+ ask_kwargs = kwargs.slice(:with)
279
+ response = ask(message, **ask_kwargs)
232
280
 
233
281
  build_result(response, run_memory)
234
282
  ensure
235
- # Restore previous writer
236
283
  run_memory.current_writer = previous_writer
237
284
  end
238
285
  end
239
286
 
240
- # SimpleFlow step interface
287
+
288
+ # Reconfigure the robot for a new context
241
289
  #
242
- # Allows Robot to be used directly as a step in a SimpleFlow::Pipeline.
243
- # The robot receives a SimpleFlow::Result, executes, and returns a new
244
- # SimpleFlow::Result with the robot's output.
290
+ # @param template [Symbol, nil] new template to apply
291
+ # @param context [Hash, nil] new context for the template
292
+ # @param system_prompt [String, nil] new system prompt
293
+ # @param model [String, nil] new model
294
+ # @param temperature [Float, nil] new temperature
295
+ # @return [self]
296
+ def update(template: nil, context: nil, system_prompt: nil, model: nil, temperature: nil, **kwargs)
297
+ if template
298
+ @template = template
299
+ ctx = context || @build_context
300
+ apply_template_to_chat(ctx)
301
+ end
302
+
303
+ @chat.with_instructions(system_prompt) if system_prompt
304
+ @chat.with_model(model) if model
305
+ apply_chat_option(:with_temperature, temperature)
306
+
307
+ kwargs.each do |key, value|
308
+ method = :"with_#{key}"
309
+ @chat.public_send(method, value) if value && @chat.respond_to?(method)
310
+ end
311
+
312
+ self
313
+ end
314
+
315
+
316
+ # SimpleFlow step interface
245
317
  #
246
318
  # @param result [SimpleFlow::Result] incoming result from previous step
247
319
  # @return [SimpleFlow::Result] result with robot output
248
- #
249
- # @example Using a robot as a pipeline step
250
- # pipeline = SimpleFlow::Pipeline.new do
251
- # step :classifier, classifier_robot, depends_on: :none
252
- # step :billing, billing_robot, depends_on: :optional
253
- # end
254
- #
255
320
  def call(result)
256
- robot_result = run(**extract_run_context(result))
321
+ run_context = extract_run_context(result)
322
+
323
+ # Extract the message from run context
324
+ message = run_context.delete(:message)
325
+
326
+ robot_result = run(message, **run_context)
257
327
 
258
328
  result
259
329
  .with_context(@name.to_sym, robot_result)
260
330
  .continue(robot_result)
261
331
  end
262
332
 
333
+
263
334
  # Reset the robot's inherent memory
264
335
  #
265
- # NOTE: This only affects the robot's standalone memory. When a robot runs
266
- # as part of a network, it uses the network's shared memory instead.
267
- # To reset memory for network execution, use `network.reset_memory`.
268
- #
269
336
  # @return [self]
270
- #
271
- # @example Standalone robot
272
- # robot.run(message: "My name is Alice")
273
- # robot.reset_memory # Clears the conversation
274
- # robot.run(message: "What's my name?") # Won't remember Alice
275
- #
276
- # @example Robot in network (reset_memory has no effect on network runs)
277
- # network.run(message: "Hello")
278
- # robot.reset_memory # Does NOT affect network memory
279
- # network.run(message: "Hi") # Network memory still intact
280
- # network.reset_memory # Use this to reset network memory
281
- #
282
337
  def reset_memory
283
338
  @memory.reset
284
339
  self
285
340
  end
286
341
 
287
- # Disconnect all MCP clients
288
- #
289
- # Call this method when done using the robot to clean up MCP connections.
342
+
343
+ # Disconnect all MCP clients and bus channel.
290
344
  #
291
345
  # @return [self]
292
- #
293
346
  def disconnect
294
347
  @mcp_clients.each_value(&:disconnect)
348
+ teardown_bus_channel if @bus
295
349
  self
296
350
  end
297
351
 
298
- # Converts the robot to a hash representation.
352
+
353
+ # Converts the robot to a hash representation
299
354
  #
300
- # @return [Hash] a hash containing the robot's configuration
355
+ # @return [Hash]
301
356
  def to_h
302
357
  {
303
358
  name: name,
@@ -309,28 +364,36 @@ module RobotLab
309
364
  mcp_config: @mcp_config,
310
365
  tools_config: @tools_config,
311
366
  mcp_servers: @mcp_clients.keys,
312
- model: model.respond_to?(:model_id) ? model.model_id : model
367
+ model: model,
368
+ config: (@config.empty? ? nil : @config.to_json_hash),
369
+ bus: @bus ? true : nil
313
370
  }.compact
314
371
  end
315
372
 
316
373
  private
317
374
 
375
+ # Apply a chat option if the value is non-nil
376
+ def apply_chat_option(method, value)
377
+ @chat.public_send(method, value) if value
378
+ end
379
+
380
+
381
+ # Determine which memory to use
382
+ def resolve_active_memory(network: nil, network_memory: nil)
383
+ network_memory || network&.memory || @memory
384
+ end
385
+
386
+
318
387
  # Extract run context from SimpleFlow::Result
319
- #
320
- # Merges original run params (preserved in context) with current value.
321
- # Extracts special parameters (mcp, tools, memory, network_memory) for Robot#run.
322
- #
323
- # @param result [SimpleFlow::Result] the incoming result
324
- # @return [Hash] context for run method including mcp/tools config
325
- #
326
388
  def extract_run_context(result)
327
389
  run_params = result.context[:run_params] || {}
328
390
 
329
- # Extract robot-specific params that should be passed to run()
391
+ # Extract robot-specific params
330
392
  mcp = run_params.delete(:mcp) || :none
331
393
  tools = run_params.delete(:tools) || :none
332
394
  memory = run_params.delete(:memory)
333
395
  network_memory = run_params.delete(:network_memory)
396
+ network_config = run_params.delete(:network_config)
334
397
 
335
398
  # Build base context from remaining run params
336
399
  base = run_params.dup
@@ -347,58 +410,20 @@ module RobotLab
347
410
  base.merge(message: result.value.to_s)
348
411
  end
349
412
 
350
- # Add back the special params for run()
413
+ # Add back the special params
351
414
  merged[:mcp] = mcp
352
415
  merged[:tools] = tools
353
416
  merged[:memory] = memory if memory
354
417
  merged[:network_memory] = network_memory if network_memory
418
+ merged[:network_config] = network_config if network_config
355
419
 
356
420
  merged
357
421
  end
358
422
 
359
- def resolve_context(context, network:)
360
- case context
361
- when Proc then context.call(network: network)
362
- when Hash then context
363
- else {}
364
- end
365
- end
366
-
367
- def build_chat(context, allowed_tools:, memory:)
368
- model_id = @model.respond_to?(:model_id) ? @model.model_id : @model.to_s
369
-
370
- chat = RubyLLM.chat(model: model_id)
371
-
372
- # Apply template and/or system_prompt
373
- # - Template only: use with_template
374
- # - system_prompt only: use with_instructions
375
- # - Both: use with_template, then append with_instructions
376
- if @template
377
- chat = chat.with_template(@template, **context)
378
- chat = chat.with_instructions(@system_prompt) if @system_prompt
379
- else
380
- chat = chat.with_instructions(@system_prompt)
381
- end
382
-
383
- # Get filtered tools based on whitelist
384
- filtered = filtered_tools(allowed_tools)
385
- chat = chat.with_tools(*filtered) if filtered.any?
386
-
387
- # Add callbacks if provided
388
- chat = chat.on_tool_call(&@on_tool_call) if @on_tool_call
389
- chat = chat.on_tool_result(&@on_tool_result) if @on_tool_result
390
-
391
- # NOTE: Semantic cache wrapping is disabled because the SemanticCache::Middleware
392
- # only supports `ask` method, not `complete`. The caching feature needs to be
393
- # re-designed to use the `ask` interface or the `fetch` pattern.
394
- # See: https://github.com/ruby-llm/ruby_llm-semantic_cache
395
-
396
- chat
397
- end
398
423
 
399
424
  def build_result(response, _memory)
400
425
  output = if response.respond_to?(:content) && response.content
401
- [TextMessage.new(role: "assistant", content: response.content)]
426
+ [TextMessage.new(role: 'assistant', content: response.content)]
402
427
  else
403
428
  []
404
429
  end
@@ -413,6 +438,7 @@ module RobotLab
413
438
  )
414
439
  end
415
440
 
441
+
416
442
  def normalize_tool_calls(tool_calls)
417
443
  return [] unless tool_calls
418
444
 
@@ -420,7 +446,7 @@ module RobotLab
420
446
  if tc.is_a?(Hash)
421
447
  ToolResultMessage.new(
422
448
  tool: tc,
423
- content: tc[:result] || tc["result"]
449
+ content: tc[:result] || tc['result']
424
450
  )
425
451
  else
426
452
  tc
@@ -428,128 +454,12 @@ module RobotLab
428
454
  end
429
455
  end
430
456
 
431
- # Resolve MCP hierarchy: runtime -> robot build -> network -> config
432
- #
433
- # @param runtime_value [Symbol, Array, nil] Runtime MCP override
434
- # @param network [NetworkRun, nil] Network context
435
- # @return [Array] Resolved MCP server configurations
436
- #
437
- def resolve_mcp_hierarchy(runtime_value, network:)
438
- # Get parent value (network or config)
439
- parent_value = network&.network&.mcp || RobotLab.configuration.mcp
440
-
441
- # Resolve robot build config against parent
442
- build_resolved = ToolConfig.resolve_mcp(@mcp_config, parent_value: parent_value)
443
-
444
- # Resolve runtime against build
445
- ToolConfig.resolve_mcp(runtime_value, parent_value: build_resolved)
446
- end
447
-
448
- # Resolve tools hierarchy: runtime -> robot build -> network -> config
449
- #
450
- # @param runtime_value [Symbol, Array, nil] Runtime tools override
451
- # @param network [NetworkRun, nil] Network context
452
- # @return [Array<String>] Resolved tool names whitelist
453
- #
454
- def resolve_tools_hierarchy(runtime_value, network:)
455
- # Get parent value (network or config)
456
- parent_value = network&.network&.tools || RobotLab.configuration.tools
457
-
458
- # Resolve robot build config against parent
459
- build_resolved = ToolConfig.resolve_tools(@tools_config, parent_value: parent_value)
460
-
461
- # Resolve runtime against build
462
- ToolConfig.resolve_tools(runtime_value, parent_value: build_resolved)
463
- end
464
-
465
- # Ensure MCP clients are initialized for the given server configs
466
- #
467
- # @param mcp_servers [Array] MCP server configurations
468
- #
469
- def ensure_mcp_clients(mcp_servers)
470
- return if mcp_servers.empty?
471
-
472
- # Get server names from configs
473
- needed_servers = mcp_servers.map { |s| s.is_a?(Hash) ? s[:name] : s.to_s }.compact
474
-
475
- # Skip if already initialized with same servers
476
- return if @mcp_initialized && (@mcp_clients.keys.sort == needed_servers.sort)
477
-
478
- # Disconnect existing clients if config changed
479
- disconnect if @mcp_initialized
480
-
481
- # Initialize new clients
482
- @mcp_clients = {}
483
- @mcp_tools = []
484
-
485
- mcp_servers.each do |server_config|
486
- init_mcp_client(server_config)
487
- end
488
-
489
- @mcp_initialized = true
490
- end
491
-
492
- # Initialize a single MCP client
493
- #
494
- # @param server_config [Hash] MCP server configuration
495
- #
496
- def init_mcp_client(server_config)
497
- client = MCP::Client.new(server_config)
498
- client.connect
499
-
500
- if client.connected?
501
- server_name = client.server.name
502
- @mcp_clients[server_name] = client
503
- discover_mcp_tools(client, server_name)
504
- else
505
- RobotLab.configuration.logger.warn(
506
- "Robot '#{@name}' failed to connect to MCP server: #{server_config[:name] || server_config}"
507
- )
508
- end
509
- end
510
-
511
- # Discover tools from an MCP server and add them to @mcp_tools
512
- #
513
- # @param client [MCP::Client] Connected MCP client
514
- # @param server_name [String] Name of the MCP server
515
- #
516
- def discover_mcp_tools(client, server_name)
517
- tools = client.list_tools
518
-
519
- tools.each do |tool_def|
520
- tool_name = tool_def[:name]
521
- mcp_client = client
522
-
523
- # Create a Tool that delegates to the MCP client
524
- tool = Tool.new(
525
- name: tool_name,
526
- description: tool_def[:description],
527
- parameters: tool_def[:inputSchema],
528
- mcp: server_name,
529
- handler: ->(input, **_opts) { mcp_client.call_tool(tool_name, input) }
530
- )
531
-
532
- @mcp_tools << tool
533
- end
534
-
535
- RobotLab.configuration.logger.info(
536
- "Robot '#{@name}' discovered #{tools.size} tools from MCP server '#{server_name}'"
537
- )
538
- end
539
457
 
540
- # Get all tools (local + MCP)
541
- #
542
- # @return [Array] Combined array of local and MCP tools
543
- #
544
458
  def all_tools
545
459
  @local_tools + @mcp_tools
546
460
  end
547
461
 
548
- # Filter tools based on allowed tool names whitelist
549
- #
550
- # @param allowed_names [Array<String>] Whitelist of tool names (empty = all allowed)
551
- # @return [Array] Filtered tools
552
- #
462
+
553
463
  def filtered_tools(allowed_names)
554
464
  available = all_tools
555
465
  return available if allowed_names.empty?