swarm_memory 2.0.0

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 (189) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/lib/claude_swarm/base_executor.rb +133 -0
  4. data/lib/claude_swarm/claude_code_executor.rb +349 -0
  5. data/lib/claude_swarm/claude_mcp_server.rb +77 -0
  6. data/lib/claude_swarm/cli.rb +712 -0
  7. data/lib/claude_swarm/commands/ps.rb +216 -0
  8. data/lib/claude_swarm/commands/show.rb +139 -0
  9. data/lib/claude_swarm/configuration.rb +363 -0
  10. data/lib/claude_swarm/hooks/session_start_hook.rb +42 -0
  11. data/lib/claude_swarm/json_handler.rb +91 -0
  12. data/lib/claude_swarm/mcp_generator.rb +248 -0
  13. data/lib/claude_swarm/openai/chat_completion.rb +264 -0
  14. data/lib/claude_swarm/openai/executor.rb +254 -0
  15. data/lib/claude_swarm/openai/responses.rb +338 -0
  16. data/lib/claude_swarm/orchestrator.rb +879 -0
  17. data/lib/claude_swarm/process_tracker.rb +78 -0
  18. data/lib/claude_swarm/session_cost_calculator.rb +209 -0
  19. data/lib/claude_swarm/session_path.rb +42 -0
  20. data/lib/claude_swarm/settings_generator.rb +77 -0
  21. data/lib/claude_swarm/system_utils.rb +46 -0
  22. data/lib/claude_swarm/templates/generation_prompt.md.erb +230 -0
  23. data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
  24. data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
  25. data/lib/claude_swarm/tools/task_tool.rb +63 -0
  26. data/lib/claude_swarm/version.rb +5 -0
  27. data/lib/claude_swarm/worktree_manager.rb +475 -0
  28. data/lib/claude_swarm/yaml_loader.rb +22 -0
  29. data/lib/claude_swarm.rb +69 -0
  30. data/lib/swarm_cli/cli.rb +201 -0
  31. data/lib/swarm_cli/command_registry.rb +61 -0
  32. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  33. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  34. data/lib/swarm_cli/commands/migrate.rb +55 -0
  35. data/lib/swarm_cli/commands/run.rb +173 -0
  36. data/lib/swarm_cli/config_loader.rb +97 -0
  37. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  38. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  39. data/lib/swarm_cli/interactive_repl.rb +918 -0
  40. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  41. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  42. data/lib/swarm_cli/migrate_options.rb +54 -0
  43. data/lib/swarm_cli/migrator.rb +132 -0
  44. data/lib/swarm_cli/options.rb +151 -0
  45. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  46. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  47. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  48. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  49. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  50. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  51. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  52. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  53. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  54. data/lib/swarm_cli/ui/icons.rb +59 -0
  55. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  56. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  57. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  58. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  59. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  60. data/lib/swarm_cli/version.rb +5 -0
  61. data/lib/swarm_cli.rb +45 -0
  62. data/lib/swarm_memory/adapters/base.rb +140 -0
  63. data/lib/swarm_memory/adapters/filesystem_adapter.rb +789 -0
  64. data/lib/swarm_memory/chat_extension.rb +34 -0
  65. data/lib/swarm_memory/cli/commands.rb +306 -0
  66. data/lib/swarm_memory/core/entry.rb +37 -0
  67. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  68. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  69. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  70. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  71. data/lib/swarm_memory/core/storage.rb +286 -0
  72. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  73. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  74. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  75. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  76. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  77. data/lib/swarm_memory/errors.rb +21 -0
  78. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  79. data/lib/swarm_memory/integration/configuration.rb +43 -0
  80. data/lib/swarm_memory/integration/registration.rb +31 -0
  81. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  82. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  83. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  84. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  85. data/lib/swarm_memory/prompts/memory_assistant.md.erb +139 -0
  86. data/lib/swarm_memory/prompts/memory_researcher.md.erb +201 -0
  87. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +76 -0
  88. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  89. data/lib/swarm_memory/search/text_search.rb +40 -0
  90. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  91. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  92. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  93. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  94. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  95. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  96. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  97. data/lib/swarm_memory/tools/memory_glob.rb +145 -0
  98. data/lib/swarm_memory/tools/memory_grep.rb +209 -0
  99. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  100. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  101. data/lib/swarm_memory/tools/memory_write.rb +215 -0
  102. data/lib/swarm_memory/utils.rb +50 -0
  103. data/lib/swarm_memory/version.rb +5 -0
  104. data/lib/swarm_memory.rb +166 -0
  105. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  106. data/lib/swarm_sdk/agent/builder.rb +461 -0
  107. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  108. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  109. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  110. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  111. data/lib/swarm_sdk/agent/chat.rb +1144 -0
  112. data/lib/swarm_sdk/agent/context.rb +112 -0
  113. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  114. data/lib/swarm_sdk/agent/definition.rb +556 -0
  115. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  116. data/lib/swarm_sdk/configuration.rb +296 -0
  117. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  118. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  119. data/lib/swarm_sdk/context_compactor.rb +340 -0
  120. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  121. data/lib/swarm_sdk/hooks/context.rb +197 -0
  122. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  123. data/lib/swarm_sdk/hooks/error.rb +29 -0
  124. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  125. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  126. data/lib/swarm_sdk/hooks/result.rb +150 -0
  127. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  128. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  129. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  130. data/lib/swarm_sdk/log_collector.rb +51 -0
  131. data/lib/swarm_sdk/log_stream.rb +69 -0
  132. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  133. data/lib/swarm_sdk/model_aliases.json +5 -0
  134. data/lib/swarm_sdk/models.json +1 -0
  135. data/lib/swarm_sdk/models.rb +120 -0
  136. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  137. data/lib/swarm_sdk/node/builder.rb +439 -0
  138. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  139. data/lib/swarm_sdk/node_context.rb +170 -0
  140. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  141. data/lib/swarm_sdk/permissions/config.rb +239 -0
  142. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  143. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  144. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  145. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  146. data/lib/swarm_sdk/plugin.rb +147 -0
  147. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  148. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  149. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  150. data/lib/swarm_sdk/result.rb +97 -0
  151. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  152. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  153. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  154. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  155. data/lib/swarm_sdk/swarm/tool_configurator.rb +416 -0
  156. data/lib/swarm_sdk/swarm.rb +982 -0
  157. data/lib/swarm_sdk/tools/bash.rb +274 -0
  158. data/lib/swarm_sdk/tools/clock.rb +44 -0
  159. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  160. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  161. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  162. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  163. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  164. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  165. data/lib/swarm_sdk/tools/edit.rb +150 -0
  166. data/lib/swarm_sdk/tools/glob.rb +158 -0
  167. data/lib/swarm_sdk/tools/grep.rb +228 -0
  168. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  169. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  170. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  171. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  172. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  173. data/lib/swarm_sdk/tools/read.rb +251 -0
  174. data/lib/swarm_sdk/tools/registry.rb +93 -0
  175. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  176. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  177. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  178. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  179. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  180. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  181. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  182. data/lib/swarm_sdk/tools/think.rb +95 -0
  183. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  184. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  185. data/lib/swarm_sdk/tools/write.rb +117 -0
  186. data/lib/swarm_sdk/utils.rb +50 -0
  187. data/lib/swarm_sdk/version.rb +5 -0
  188. data/lib/swarm_sdk.rb +167 -0
  189. metadata +313 -0
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ class McpGenerator
5
+ def initialize(configuration, vibe: false, restore_session_path: nil)
6
+ @config = configuration
7
+ @vibe = vibe
8
+ @restore_session_path = restore_session_path
9
+ @session_path = nil # Will be set when needed
10
+ @instance_ids = {} # Store instance IDs for all instances
11
+ @restore_states = {} # Store loaded state data during restoration
12
+ end
13
+
14
+ def generate_all
15
+ ensure_swarm_directory
16
+
17
+ if @restore_session_path
18
+ # Load existing instance IDs and states from state files
19
+ load_instance_states
20
+ else
21
+ # Generate new instance IDs
22
+ @config.instances.each_key do |name|
23
+ @instance_ids[name] = "#{name}_#{SecureRandom.hex(4)}"
24
+ end
25
+ end
26
+
27
+ @config.instances.each do |name, instance|
28
+ generate_mcp_config(name, instance)
29
+ end
30
+ end
31
+
32
+ def mcp_config_path(instance_name)
33
+ File.join(session_path, "#{instance_name}.mcp.json")
34
+ end
35
+
36
+ private
37
+
38
+ def session_path
39
+ @session_path ||= SessionPath.from_env
40
+ end
41
+
42
+ def ensure_swarm_directory
43
+ # Session directory is already created by orchestrator
44
+ # Just ensure it exists
45
+ SessionPath.ensure_directory(session_path)
46
+ end
47
+
48
+ def generate_mcp_config(name, instance)
49
+ mcp_servers = {}
50
+
51
+ # Add configured MCP servers
52
+ instance[:mcps].each do |mcp|
53
+ mcp_servers[mcp["name"]] = build_mcp_server_config(mcp)
54
+ end
55
+
56
+ # Add connection MCPs for other instances
57
+ instance[:connections].each do |connection_name|
58
+ connected_instance = @config.instances[connection_name]
59
+ mcp_servers[connection_name] = build_instance_mcp_config(
60
+ connection_name,
61
+ connected_instance,
62
+ calling_instance: name,
63
+ calling_instance_id: @instance_ids[name],
64
+ )
65
+ end
66
+
67
+ # Add Claude tools MCP server for OpenAI instances
68
+ mcp_servers["claude_tools"] = build_claude_tools_mcp_config if instance[:provider] == "openai"
69
+
70
+ config = {
71
+ "instance_id" => @instance_ids[name],
72
+ "instance_name" => name,
73
+ "mcpServers" => mcp_servers,
74
+ }
75
+
76
+ JsonHandler.write_file!(mcp_config_path(name), config)
77
+ end
78
+
79
+ def build_mcp_server_config(mcp)
80
+ case mcp["type"]
81
+ when "stdio"
82
+ {
83
+ "type" => "stdio",
84
+ "command" => mcp["command"],
85
+ "args" => mcp["args"] || [],
86
+ }.tap do |config|
87
+ config["env"] = mcp["env"] if mcp["env"]
88
+ end
89
+ when "sse", "http"
90
+ {
91
+ "type" => mcp["type"],
92
+ "url" => mcp["url"],
93
+ }.tap do |config|
94
+ config["headers"] = mcp["headers"] if mcp["headers"]
95
+ end
96
+ end
97
+ end
98
+
99
+ def build_claude_tools_mcp_config
100
+ # Build environment for claude mcp serve by excluding Ruby/Bundler-specific variables
101
+ # This preserves all system variables while removing Ruby contamination
102
+ clean_env = ENV.to_h.reject do |key, _|
103
+ key.start_with?("BUNDLE_") ||
104
+ key.start_with?("RUBY") ||
105
+ key.start_with?("GEM_") ||
106
+ key == "RUBYOPT" ||
107
+ key == "RUBYLIB"
108
+ end
109
+
110
+ {
111
+ "type" => "stdio",
112
+ "command" => "claude",
113
+ "args" => ["mcp", "serve"],
114
+ "env" => clean_env,
115
+ }
116
+ end
117
+
118
+ def build_instance_mcp_config(name, instance, calling_instance:, calling_instance_id:)
119
+ # Get the path to the claude-swarm executable
120
+ exe_path = "claude-swarm"
121
+
122
+ # Build command-line arguments for Thor
123
+ args = [
124
+ "mcp-serve",
125
+ "--name",
126
+ name,
127
+ "--directory",
128
+ instance[:directory],
129
+ "--model",
130
+ instance[:model],
131
+ ]
132
+
133
+ # Add directories array if we have multiple directories
134
+ args.push("--directories", *instance[:directories]) if instance[:directories] && instance[:directories].size > 1
135
+
136
+ # Add optional arguments
137
+ # Handle prompt_file by reading the file contents
138
+ if instance[:prompt_file]
139
+ prompt_file_path = File.join(@config.root_directory, instance[:prompt_file])
140
+ if File.exist?(prompt_file_path)
141
+ prompt_content = File.read(prompt_file_path)
142
+ args.push("--prompt", prompt_content)
143
+ end
144
+ elsif instance[:prompt]
145
+ args.push("--prompt", instance[:prompt])
146
+ end
147
+
148
+ args.push("--description", instance[:description]) if instance[:description]
149
+
150
+ args.push("--allowed-tools", instance[:allowed_tools].join(",")) if instance[:allowed_tools] && !instance[:allowed_tools].empty?
151
+
152
+ args.push("--disallowed-tools", instance[:disallowed_tools].join(",")) if instance[:disallowed_tools] && !instance[:disallowed_tools].empty?
153
+
154
+ args.push("--connections", instance[:connections].join(",")) if instance[:connections] && !instance[:connections].empty?
155
+
156
+ args.push("--mcp-config-path", mcp_config_path(name))
157
+
158
+ args.push("--calling-instance", calling_instance) if calling_instance
159
+
160
+ args.push("--calling-instance-id", calling_instance_id) if calling_instance_id
161
+
162
+ args.push("--instance-id", @instance_ids[name]) if @instance_ids[name]
163
+
164
+ args.push("--vibe") if @vibe || instance[:vibe]
165
+
166
+ # Add provider-specific parameters
167
+ if instance[:provider]
168
+ args.push("--provider", instance[:provider])
169
+
170
+ # Add OpenAI-specific parameters
171
+ if instance[:provider] == "openai"
172
+ args.push("--reasoning-effort", instance[:reasoning_effort]) if instance[:reasoning_effort]
173
+ args.push("--temperature", instance[:temperature].to_s) if instance[:temperature]
174
+ args.push("--api-version", instance[:api_version]) if instance[:api_version]
175
+ args.push("--openai-token-env", instance[:openai_token_env]) if instance[:openai_token_env]
176
+ args.push("--base-url", instance[:base_url]) if instance[:base_url]
177
+ end
178
+ end
179
+
180
+ # Add claude session ID if restoring
181
+ if @restore_states[name.to_s]
182
+ claude_session_id = @restore_states[name.to_s]["claude_session_id"]
183
+ args.push("--claude-session-id", claude_session_id) if claude_session_id
184
+ end
185
+
186
+ # Capture environment variables needed for Ruby and Bundler to work properly
187
+ # This includes both BUNDLE_* variables and Ruby-specific variables
188
+ required_env = {}
189
+
190
+ # Bundle-specific variables
191
+ ENV.each do |k, v|
192
+ required_env[k] = v if k.start_with?("BUNDLE_")
193
+ end
194
+
195
+ # Claude Swarm-specific variables
196
+ ENV.each do |k, v|
197
+ required_env[k] = v if k.start_with?("CLAUDE_SWARM_")
198
+ end
199
+
200
+ # Ruby-specific variables that MCP servers need
201
+ [
202
+ "RUBY_ROOT",
203
+ "RUBY_ENGINE",
204
+ "RUBY_VERSION",
205
+ "GEM_ROOT",
206
+ "GEM_HOME",
207
+ "GEM_PATH",
208
+ "RUBYOPT",
209
+ "RUBYLIB",
210
+ "PATH",
211
+ ].each do |key|
212
+ required_env[key] = ENV[key] if ENV[key]
213
+ end
214
+
215
+ config = {
216
+ "type" => "stdio",
217
+ "command" => exe_path,
218
+ "args" => args,
219
+ }
220
+
221
+ # Add required environment variables if any exist
222
+ config["env"] = required_env unless required_env.empty?
223
+
224
+ config
225
+ end
226
+
227
+ def load_instance_states
228
+ state_dir = File.join(@restore_session_path, "state")
229
+ return unless Dir.exist?(state_dir)
230
+
231
+ Dir.glob(File.join(state_dir, "*.json")).each do |state_file|
232
+ data = JsonHandler.parse_file!(state_file)
233
+ instance_name = data["instance_name"]
234
+ instance_id = data["instance_id"]
235
+
236
+ # Check both string and symbol keys since config instances might have either
237
+ if instance_name && (@config.instances.key?(instance_name) || @config.instances.key?(instance_name.to_sym))
238
+ # Store with the same key type as in @config.instances
239
+ key = @config.instances.key?(instance_name) ? instance_name : instance_name.to_sym
240
+ @instance_ids[key] = instance_id
241
+ @restore_states[instance_name] = data
242
+ end
243
+ rescue StandardError
244
+ # Skip invalid state files
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ module OpenAI
5
+ class ChatCompletion
6
+ MAX_TURNS_WITH_TOOLS = 100_000 # virtually infinite
7
+
8
+ def initialize(openai_client:, mcp_client:, available_tools:, executor:, instance_name:, model:, temperature: nil, reasoning_effort: nil)
9
+ @openai_client = openai_client
10
+ @mcp_client = mcp_client
11
+ @available_tools = available_tools
12
+ @executor = executor
13
+ @instance_name = instance_name
14
+ @model = model
15
+ @temperature = temperature
16
+ @reasoning_effort = reasoning_effort
17
+ @conversation_messages = []
18
+ end
19
+
20
+ def execute(prompt, options = {})
21
+ # Build messages array
22
+ messages = build_messages(prompt, options)
23
+
24
+ # Process chat with recursive tool handling
25
+ result = process_chat_completion(messages)
26
+
27
+ # Update conversation state
28
+ @conversation_messages = messages
29
+
30
+ result
31
+ end
32
+
33
+ def reset_session
34
+ @conversation_messages = []
35
+ end
36
+
37
+ private
38
+
39
+ def build_messages(prompt, options)
40
+ messages = []
41
+
42
+ # Add system prompt if provided
43
+ system_prompt = options[:system_prompt]
44
+ if system_prompt && @conversation_messages.empty?
45
+ messages << { role: "system", content: system_prompt }
46
+ elsif !@conversation_messages.empty?
47
+ # Use existing conversation
48
+ messages = @conversation_messages.dup
49
+ end
50
+
51
+ # Add user message
52
+ messages << { role: "user", content: prompt }
53
+
54
+ messages
55
+ end
56
+
57
+ def process_chat_completion(messages, depth = 0)
58
+ # Prevent infinite recursion
59
+ if depth > MAX_TURNS_WITH_TOOLS
60
+ @executor.logger.error { "Maximum recursion depth reached in tool execution" }
61
+ return "Error: Maximum tool call depth exceeded"
62
+ end
63
+
64
+ # Build parameters
65
+ parameters = {
66
+ model: @model,
67
+ messages: messages,
68
+ }
69
+
70
+ # Only add temperature for non-o-series models
71
+ # O-series models don't support temperature parameter
72
+ if @temperature && !@model.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
73
+ parameters[:temperature] = @temperature
74
+ end
75
+
76
+ # Only add reasoning_effort for o-series models
77
+ # reasoning_effort is only supported by o-series models: o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, etc.
78
+ if @reasoning_effort && @model.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
79
+ parameters[:reasoning_effort] = @reasoning_effort
80
+ end
81
+
82
+ # Add tools if available
83
+ parameters[:tools] = @mcp_client.to_openai_tools if @available_tools&.any? && @mcp_client
84
+
85
+ # Log the request parameters
86
+ @executor.logger.info { "Chat API Request (depth=#{depth}): #{JsonHandler.pretty_generate!(parameters)}" }
87
+
88
+ # Append to session JSON
89
+ append_to_session_json({
90
+ type: "openai_request",
91
+ api: "chat",
92
+ depth: depth,
93
+ parameters: parameters,
94
+ })
95
+
96
+ # Make the API call without streaming
97
+ begin
98
+ response = @openai_client.chat(parameters: parameters)
99
+ rescue StandardError => e
100
+ @executor.logger.error { "Chat API error: #{e.class} - #{e.message}" }
101
+ @executor.logger.error { "Request parameters: #{JsonHandler.pretty_generate!(parameters)}" }
102
+
103
+ # Try to extract and log the response body for better debugging
104
+ if e.respond_to?(:response)
105
+ begin
106
+ error_body = e.response[:body]
107
+ @executor.logger.error { "Error response body: #{error_body}" }
108
+ rescue StandardError => parse_error
109
+ @executor.logger.error { "Could not parse error response: #{parse_error.message}" }
110
+ end
111
+ end
112
+
113
+ # Log error to session JSON
114
+ append_to_session_json({
115
+ type: "openai_error",
116
+ api: "chat",
117
+ depth: depth,
118
+ error: {
119
+ class: e.class.to_s,
120
+ message: e.message,
121
+ response_body: e.respond_to?(:response) ? e.response[:body] : nil,
122
+ backtrace: e.backtrace.first(5),
123
+ },
124
+ })
125
+
126
+ return "Error calling OpenAI chat API: #{e.message}"
127
+ end
128
+
129
+ # Log the response
130
+ @executor.logger.info { "Chat API Response (depth=#{depth}): #{JsonHandler.pretty_generate!(response)}" }
131
+
132
+ # Append to session JSON
133
+ append_to_session_json({
134
+ type: "openai_response",
135
+ api: "chat",
136
+ depth: depth,
137
+ response: response,
138
+ })
139
+
140
+ # Extract the message from the response
141
+ message = response.dig("choices", 0, "message")
142
+
143
+ if message.nil?
144
+ @executor.logger.error { "No message in response: #{response.inspect}" }
145
+ return "Error: No response from OpenAI"
146
+ end
147
+
148
+ # Check if there are tool calls
149
+ if message["tool_calls"]
150
+ # Add the assistant message with tool calls
151
+ messages << {
152
+ role: "assistant",
153
+ content: nil,
154
+ tool_calls: message["tool_calls"],
155
+ }
156
+
157
+ # Execute tools and collect results
158
+ execute_and_append_tool_results(message["tool_calls"], messages)
159
+
160
+ # Recursively process the next response
161
+ process_chat_completion(messages, depth + 1)
162
+ else
163
+ # Regular text response - this is the final response
164
+ response_text = message["content"] || ""
165
+ messages << { role: "assistant", content: response_text }
166
+ response_text
167
+ end
168
+ end
169
+
170
+ def execute_and_append_tool_results(tool_calls, messages)
171
+ # Log tool calls
172
+ @executor.logger.info { "Executing tool calls: #{JsonHandler.pretty_generate!(tool_calls)}" }
173
+
174
+ # Append to session JSON
175
+ append_to_session_json({
176
+ type: "tool_calls",
177
+ api: "chat",
178
+ tool_calls: tool_calls,
179
+ })
180
+
181
+ # Execute tool calls in parallel threads
182
+ threads = tool_calls.map do |tool_call|
183
+ Thread.new do
184
+ tool_name = tool_call.dig("function", "name")
185
+ tool_args_str = tool_call.dig("function", "arguments")
186
+
187
+ begin
188
+ # Parse arguments
189
+ tool_args = tool_args_str.is_a?(String) ? JsonHandler.parse!(tool_args_str) : tool_args_str
190
+
191
+ # Log tool execution
192
+ @executor.logger.info { "Executing tool: #{tool_name} with args: #{JsonHandler.pretty_generate!(tool_args)}" }
193
+
194
+ # Execute tool via MCP
195
+ result = @mcp_client.call_tool(tool_name, tool_args)
196
+
197
+ # Log result
198
+ @executor.logger.info { "Tool result for #{tool_name}: #{result}" }
199
+
200
+ # Append to session JSON
201
+ append_to_session_json({
202
+ type: "tool_execution",
203
+ tool_name: tool_name,
204
+ arguments: tool_args,
205
+ result: result.to_s,
206
+ })
207
+
208
+ # Return success result
209
+ {
210
+ success: true,
211
+ tool_call_id: tool_call["id"],
212
+ role: "tool",
213
+ name: tool_name,
214
+ content: result.to_s,
215
+ }
216
+ rescue StandardError => e
217
+ @executor.logger.error { "Tool execution failed for #{tool_name}: #{e.message}" }
218
+ @executor.logger.error { e.backtrace.join("\n") }
219
+
220
+ # Append error to session JSON
221
+ append_to_session_json({
222
+ type: "tool_error",
223
+ tool_name: tool_name,
224
+ arguments: tool_args,
225
+ error: {
226
+ class: e.class.to_s,
227
+ message: e.message,
228
+ backtrace: e.backtrace.first(5),
229
+ },
230
+ })
231
+
232
+ # Return error result
233
+ {
234
+ success: false,
235
+ tool_call_id: tool_call["id"],
236
+ role: "tool",
237
+ name: tool_name,
238
+ content: "Error: #{e.message}",
239
+ }
240
+ end
241
+ end
242
+ end
243
+
244
+ # Collect results from all threads
245
+ tool_results = threads.map(&:value)
246
+
247
+ # Add all tool results to messages
248
+ tool_results.each do |result|
249
+ messages << {
250
+ tool_call_id: result[:tool_call_id],
251
+ role: result[:role],
252
+ name: result[:name],
253
+ content: result[:content],
254
+ }
255
+ end
256
+ end
257
+
258
+ def append_to_session_json(event)
259
+ # Delegate to the executor's log method
260
+ @executor.log(event) if @executor.respond_to?(:log)
261
+ end
262
+ end
263
+ end
264
+ end