legate 0.1.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 (317) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +345 -0
  4. data/bin/legate +13 -0
  5. data/examples/00_quickstart.rb +51 -0
  6. data/examples/01_simple_agent.rb +105 -0
  7. data/examples/02_multi_tool_agent.rb +140 -0
  8. data/examples/03_custom_tool.rb +93 -0
  9. data/examples/04_agent_instructions.rb +84 -0
  10. data/examples/05_state_and_sessions.rb +91 -0
  11. data/examples/06_callbacks.rb +186 -0
  12. data/examples/07_async_jobs.rb +112 -0
  13. data/examples/08_loop_agent.rb +197 -0
  14. data/examples/09_sequential_workflow.rb +40 -0
  15. data/examples/10_parallel_workflow.rb +34 -0
  16. data/examples/11_agent_delegation.rb +24 -0
  17. data/examples/12_http_client_tool.rb +156 -0
  18. data/examples/13_authentication.rb +220 -0
  19. data/examples/14_mcp_client.rb +154 -0
  20. data/examples/15_mcp_server.rb +79 -0
  21. data/examples/16_webhooks.rb +91 -0
  22. data/examples/README_sequential_agents.md +164 -0
  23. data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
  24. data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
  25. data/examples/advanced/auth/excon_middleware.rb +317 -0
  26. data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
  27. data/examples/advanced/auth/fiber_auth_example.rb +281 -0
  28. data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
  29. data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
  30. data/examples/advanced/auth/oauth2_auth.rb +419 -0
  31. data/examples/advanced/auth/oidc_auth.rb +514 -0
  32. data/examples/advanced/auth/openweather_api.rb +251 -0
  33. data/examples/advanced/auth/openweather_tool.rb +153 -0
  34. data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
  35. data/examples/advanced/auth/service_account.rb +135 -0
  36. data/examples/advanced/auth/test_with_httpbin.rb +202 -0
  37. data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
  38. data/examples/advanced/callback_monitoring.rb +679 -0
  39. data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
  40. data/examples/advanced/mas/loop_workflow.rb +28 -0
  41. data/examples/advanced/mas/mock_planner.rb +77 -0
  42. data/examples/advanced/mas/proper_delegation_example.rb +276 -0
  43. data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
  44. data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
  45. data/examples/advanced/mcp/mcp_server_async.rb +76 -0
  46. data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
  47. data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
  48. data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
  49. data/examples/advanced/random_calculator.rb +104 -0
  50. data/examples/advanced/sleep_agent.rb +153 -0
  51. data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
  52. data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
  53. data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
  54. data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
  55. data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
  56. data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
  57. data/examples/tools/oauth2_example.rb +136 -0
  58. data/examples/tools/sleepy_tool.rb +42 -0
  59. data/lib/legate/activity_log.rb +71 -0
  60. data/lib/legate/agent.rb +959 -0
  61. data/lib/legate/agent_code_generator.rb +185 -0
  62. data/lib/legate/agent_definition.rb +812 -0
  63. data/lib/legate/agentic/decision.rb +49 -0
  64. data/lib/legate/agentic/loop.rb +134 -0
  65. data/lib/legate/agentic.rb +5 -0
  66. data/lib/legate/agents/loop_agent.rb +248 -0
  67. data/lib/legate/agents/parallel_agent.rb +163 -0
  68. data/lib/legate/agents/sequential_agent.rb +190 -0
  69. data/lib/legate/agents.rb +14 -0
  70. data/lib/legate/auth/config.rb +148 -0
  71. data/lib/legate/auth/coordinator.rb +218 -0
  72. data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
  73. data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
  74. data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
  75. data/lib/legate/auth/credential.rb +157 -0
  76. data/lib/legate/auth/encryption.rb +108 -0
  77. data/lib/legate/auth/error.rb +94 -0
  78. data/lib/legate/auth/exchanged_credential.rb +180 -0
  79. data/lib/legate/auth/excon_middleware.rb +285 -0
  80. data/lib/legate/auth/http_client_utils.rb +364 -0
  81. data/lib/legate/auth/manager.rb +531 -0
  82. data/lib/legate/auth/manager_store.rb +394 -0
  83. data/lib/legate/auth/middleware_factory.rb +290 -0
  84. data/lib/legate/auth/runner.rb +279 -0
  85. data/lib/legate/auth/scheme.rb +125 -0
  86. data/lib/legate/auth/schemes/api_key.rb +212 -0
  87. data/lib/legate/auth/schemes/google_service_account.rb +108 -0
  88. data/lib/legate/auth/schemes/http_bearer.rb +98 -0
  89. data/lib/legate/auth/schemes/oauth2.rb +396 -0
  90. data/lib/legate/auth/schemes/openid_connect.rb +346 -0
  91. data/lib/legate/auth/schemes/service_account.rb +388 -0
  92. data/lib/legate/auth/schemes.rb +40 -0
  93. data/lib/legate/auth/token_manager.rb +362 -0
  94. data/lib/legate/auth/token_store.rb +86 -0
  95. data/lib/legate/auth/tool_context_extension.rb +97 -0
  96. data/lib/legate/auth/tool_integration.rb +188 -0
  97. data/lib/legate/auth/url_guard.rb +81 -0
  98. data/lib/legate/auth.rb +453 -0
  99. data/lib/legate/callbacks/callback_context.rb +71 -0
  100. data/lib/legate/cli/agent_commands.rb +950 -0
  101. data/lib/legate/cli/auth_commands.rb +520 -0
  102. data/lib/legate/cli/base_command.rb +24 -0
  103. data/lib/legate/cli/deployment_commands.rb +934 -0
  104. data/lib/legate/cli/output_helper.rb +108 -0
  105. data/lib/legate/cli/session_commands.rb +138 -0
  106. data/lib/legate/cli/skaffold_commands.rb +223 -0
  107. data/lib/legate/cli/tool_commands.rb +261 -0
  108. data/lib/legate/cli/web_commands.rb +182 -0
  109. data/lib/legate/cli.rb +40 -0
  110. data/lib/legate/configuration/webhooks.rb +113 -0
  111. data/lib/legate/configuration.rb +39 -0
  112. data/lib/legate/definition_store.rb +23 -0
  113. data/lib/legate/errors.rb +118 -0
  114. data/lib/legate/event.rb +161 -0
  115. data/lib/legate/gemini_ai_beta_patch.rb +39 -0
  116. data/lib/legate/generators/agent_generator.rb +412 -0
  117. data/lib/legate/generators/code_validator.rb +48 -0
  118. data/lib/legate/generators/legate/install_generator.rb +35 -0
  119. data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
  120. data/lib/legate/generators/legate/templates/initializer.rb +18 -0
  121. data/lib/legate/generators/runtime_tool_loader.rb +76 -0
  122. data/lib/legate/generators/tool_generator.rb +408 -0
  123. data/lib/legate/generators.rb +11 -0
  124. data/lib/legate/global_definition_registry.rb +506 -0
  125. data/lib/legate/global_tool_manager.rb +135 -0
  126. data/lib/legate/llm/adapter.rb +69 -0
  127. data/lib/legate/llm/gemini.rb +172 -0
  128. data/lib/legate/llm/ollama.rb +80 -0
  129. data/lib/legate/llm.rb +34 -0
  130. data/lib/legate/mcp/client.rb +320 -0
  131. data/lib/legate/mcp/connection/sse.rb +292 -0
  132. data/lib/legate/mcp/connection/stdio.rb +273 -0
  133. data/lib/legate/mcp/connection_manager.rb +103 -0
  134. data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
  135. data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
  136. data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
  137. data/lib/legate/mcp/tool_wrapper.rb +138 -0
  138. data/lib/legate/mcp/util/schema_converter.rb +134 -0
  139. data/lib/legate/mcp.rb +23 -0
  140. data/lib/legate/plan_executor.rb +375 -0
  141. data/lib/legate/planner.rb +839 -0
  142. data/lib/legate/rails/railtie.rb +43 -0
  143. data/lib/legate/rails.rb +9 -0
  144. data/lib/legate/redaction.rb +32 -0
  145. data/lib/legate/session.rb +299 -0
  146. data/lib/legate/session_service/active_record.rb +300 -0
  147. data/lib/legate/session_service/base.rb +68 -0
  148. data/lib/legate/session_service/event_broadcast.rb +74 -0
  149. data/lib/legate/session_service/in_memory.rb +188 -0
  150. data/lib/legate/tool/metadata_dsl.rb +122 -0
  151. data/lib/legate/tool.rb +276 -0
  152. data/lib/legate/tool_code_generator.rb +103 -0
  153. data/lib/legate/tool_context.rb +350 -0
  154. data/lib/legate/tool_loader.rb +39 -0
  155. data/lib/legate/tool_registry.rb +73 -0
  156. data/lib/legate/tool_result.rb +61 -0
  157. data/lib/legate/tools/agent_tool.rb +187 -0
  158. data/lib/legate/tools/base/http_client.rb +319 -0
  159. data/lib/legate/tools/base/safe_url.rb +56 -0
  160. data/lib/legate/tools/base_async_job_tool.rb +91 -0
  161. data/lib/legate/tools/calculator.rb +89 -0
  162. data/lib/legate/tools/cat_facts.rb +81 -0
  163. data/lib/legate/tools/check_job_status_tool.rb +48 -0
  164. data/lib/legate/tools/current_time_tool.rb +64 -0
  165. data/lib/legate/tools/echo.rb +43 -0
  166. data/lib/legate/tools/http_request_tool.rb +105 -0
  167. data/lib/legate/tools/random_number_tool.rb +64 -0
  168. data/lib/legate/tools/read_webpage_tool.rb +92 -0
  169. data/lib/legate/tools/sleepy_tool.rb +74 -0
  170. data/lib/legate/tools/webhook_tool.rb +146 -0
  171. data/lib/legate/version.rb +5 -0
  172. data/lib/legate/web/app.rb +984 -0
  173. data/lib/legate/web/public/css/main.css +4980 -0
  174. data/lib/legate/web/public/images/favicon-256.png +0 -0
  175. data/lib/legate/web/public/images/favicon-32.png +0 -0
  176. data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
  177. data/lib/legate/web/public/images/legate-logo-light.png +0 -0
  178. data/lib/legate/web/public/js/legate.js +616 -0
  179. data/lib/legate/web/public/styles/main.scss +4402 -0
  180. data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
  181. data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
  182. data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
  183. data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
  184. data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
  185. data/lib/legate/web/routes/api_routes.rb +56 -0
  186. data/lib/legate/web/routes/authentication_routes.rb +1541 -0
  187. data/lib/legate/web/routes/core_routes.rb +111 -0
  188. data/lib/legate/web/routes/documentation_routes.rb +220 -0
  189. data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
  190. data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
  191. data/lib/legate/web/sass_compiler.rb +73 -0
  192. data/lib/legate/web/views/_active_session_info.slim +25 -0
  193. data/lib/legate/web/views/_activity_list.slim +55 -0
  194. data/lib/legate/web/views/_agent_card.slim +56 -0
  195. data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
  196. data/lib/legate/web/views/_agent_status_controls.slim +71 -0
  197. data/lib/legate/web/views/_agent_tool_table.slim +74 -0
  198. data/lib/legate/web/views/_chat_message.slim +95 -0
  199. data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
  200. data/lib/legate/web/views/_display_agent_description.slim +11 -0
  201. data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
  202. data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
  203. data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
  204. data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
  205. data/lib/legate/web/views/_display_agent_model.slim +17 -0
  206. data/lib/legate/web/views/_display_agent_name.slim +42 -0
  207. data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
  208. data/lib/legate/web/views/_display_agent_type.slim +65 -0
  209. data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
  210. data/lib/legate/web/views/_edit_agent_description.slim +16 -0
  211. data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
  212. data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
  213. data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
  214. data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
  215. data/lib/legate/web/views/_edit_agent_model.slim +23 -0
  216. data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
  217. data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
  218. data/lib/legate/web/views/_edit_agent_type.slim +67 -0
  219. data/lib/legate/web/views/_session_error.slim +4 -0
  220. data/lib/legate/web/views/_skeleton.slim +69 -0
  221. data/lib/legate/web/views/_tool_card.slim +9 -0
  222. data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
  223. data/lib/legate/web/views/agent.slim +436 -0
  224. data/lib/legate/web/views/agent_auth.slim +562 -0
  225. data/lib/legate/web/views/agents.slim +369 -0
  226. data/lib/legate/web/views/auth.slim +112 -0
  227. data/lib/legate/web/views/auth_credential_detail.slim +327 -0
  228. data/lib/legate/web/views/auth_credentials.slim +261 -0
  229. data/lib/legate/web/views/auth_debug.slim +94 -0
  230. data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
  231. data/lib/legate/web/views/auth_mapping_new.slim +123 -0
  232. data/lib/legate/web/views/auth_mappings.slim +120 -0
  233. data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
  234. data/lib/legate/web/views/auth_schemes.slim +259 -0
  235. data/lib/legate/web/views/auth_test.slim +418 -0
  236. data/lib/legate/web/views/chat.slim +192 -0
  237. data/lib/legate/web/views/docs_index.slim +105 -0
  238. data/lib/legate/web/views/docs_show.slim +105 -0
  239. data/lib/legate/web/views/error_404.slim +5 -0
  240. data/lib/legate/web/views/index.slim +148 -0
  241. data/lib/legate/web/views/layout.slim +144 -0
  242. data/lib/legate/web/views/tool_detail.slim +87 -0
  243. data/lib/legate/web/views/tools.slim +50 -0
  244. data/lib/legate/web/webhook_listener.rb +367 -0
  245. data/lib/legate/web.rb +9 -0
  246. data/lib/legate.rb +220 -0
  247. data/public/docs/advanced/callbacks.md +828 -0
  248. data/public/docs/advanced/mcp_schema_conversion.md +59 -0
  249. data/public/docs/authentication/api_reference/config.md +210 -0
  250. data/public/docs/authentication/api_reference/credential.md +246 -0
  251. data/public/docs/authentication/api_reference/encryption.md +218 -0
  252. data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
  253. data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
  254. data/public/docs/authentication/api_reference/index.md +30 -0
  255. data/public/docs/authentication/api_reference/scheme.md +250 -0
  256. data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
  257. data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
  258. data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
  259. data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
  260. data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
  261. data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
  262. data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
  263. data/public/docs/authentication/api_reference/token_manager.md +221 -0
  264. data/public/docs/authentication/api_reference/token_store.md +146 -0
  265. data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
  266. data/public/docs/authentication/guides/api_key.md +190 -0
  267. data/public/docs/authentication/guides/bearer.md +172 -0
  268. data/public/docs/authentication/guides/configuration.md +255 -0
  269. data/public/docs/authentication/guides/custom_flow.md +523 -0
  270. data/public/docs/authentication/guides/index.md +24 -0
  271. data/public/docs/authentication/guides/migration.md +435 -0
  272. data/public/docs/authentication/guides/oauth2.md +252 -0
  273. data/public/docs/authentication/guides/oidc.md +241 -0
  274. data/public/docs/authentication/guides/overview.md +155 -0
  275. data/public/docs/authentication/guides/secure_storage.md +301 -0
  276. data/public/docs/authentication/guides/service_account.md +228 -0
  277. data/public/docs/authentication/guides/token_lifecycle.md +295 -0
  278. data/public/docs/authentication/guides/web_ui_integration.md +504 -0
  279. data/public/docs/authentication/index.md +58 -0
  280. data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
  281. data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
  282. data/public/docs/authentication/troubleshooting/index.md +11 -0
  283. data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
  284. data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
  285. data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
  286. data/public/docs/cli/legate_cli_usage.md +363 -0
  287. data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
  288. data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
  289. data/public/docs/core_concepts/legate_configuration.md +116 -0
  290. data/public/docs/core_concepts/legate_definition_store.md +102 -0
  291. data/public/docs/core_concepts/legate_planner.md +94 -0
  292. data/public/docs/core_concepts/legate_session_service.md +104 -0
  293. data/public/docs/error_handling/legate_error_handling.md +122 -0
  294. data/public/docs/examples.md +199 -0
  295. data/public/docs/getting_started.md +111 -0
  296. data/public/docs/guides/agentic_agents.md +137 -0
  297. data/public/docs/guides/ai_code_generators.md +437 -0
  298. data/public/docs/guides/auto_loading.md +326 -0
  299. data/public/docs/guides/configuring_agent_webhooks.md +219 -0
  300. data/public/docs/guides/http_client_usage.md +264 -0
  301. data/public/docs/guides/llm_providers.md +137 -0
  302. data/public/docs/guides/mcp_client_integration.md +232 -0
  303. data/public/docs/guides/mcp_server_exposure.md +206 -0
  304. data/public/docs/guides/rails_integration.md +128 -0
  305. data/public/docs/guides/sending_outbound_webhooks.md +227 -0
  306. data/public/docs/guides/streaming.md +112 -0
  307. data/public/docs/guides/webhooks.md +288 -0
  308. data/public/docs/introduction.md +51 -0
  309. data/public/docs/multi_agent_systems/advanced_features.md +57 -0
  310. data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
  311. data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
  312. data/public/docs/multi_agent_systems/state_management.md +47 -0
  313. data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
  314. data/public/docs/tools/legate_built_in_tools.md +332 -0
  315. data/public/docs/tools/legate_tools_and_registry.md +263 -0
  316. data/public/docs/web_ui/legate_web_ui.md +137 -0
  317. metadata +823 -0
@@ -0,0 +1,506 @@
1
+ # File: lib/legate/global_definition_registry.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+
6
+ module Legate
7
+ # In-memory registry for AgentDefinition instances.
8
+ # Serves as both the runtime definition registry (used by agents) and as a
9
+ # drop-in replacement for the Redis-backed DefinitionStore used by the Web UI.
10
+ #
11
+ # Internal structure:
12
+ # @registry = { name_symbol => { definition: AgentDefinition, metadata: {} } }
13
+ #
14
+ # The `register`, `find`, `all`, and `clear!` methods maintain backward
15
+ # compatibility with the original API. The new methods (`get_definition`,
16
+ # `save_definition`, `update_definition`, `delete_definition`,
17
+ # `list_definitions`, `check_connection`, `definition_exists?`) provide
18
+ # the DefinitionStore interface the Web UI routes expect.
19
+ module GlobalDefinitionRegistry
20
+ @registry = {}
21
+ @mutex = Mutex.new
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Original API (backward-compatible)
25
+ # ---------------------------------------------------------------------------
26
+
27
+ # Registers an AgentDefinition instance.
28
+ # @param definition [Legate::AgentDefinition] The definition object to register.
29
+ # @return [Boolean] true if registered successfully, false otherwise.
30
+ def self.register(definition)
31
+ unless definition.is_a?(Legate::AgentDefinition) && definition.name.is_a?(Symbol)
32
+ Legate.logger.error("GlobalDefinitionRegistry: Invalid object passed to register: #{definition.inspect}")
33
+ return false
34
+ end
35
+
36
+ name = definition.name
37
+ @mutex.synchronize do
38
+ if @registry.key?(name)
39
+ Legate.logger.warn("GlobalDefinitionRegistry: Overwriting existing definition for agent :#{name}")
40
+ # Preserve existing metadata when re-registering
41
+ existing_metadata = @registry[name][:metadata] || {}
42
+ @registry[name] = { definition: definition, metadata: existing_metadata }
43
+ else
44
+ @registry[name] = { definition: definition, metadata: {} }
45
+ end
46
+ end
47
+ Legate.logger.debug("GlobalDefinitionRegistry: Registered definition for :#{name}")
48
+ true
49
+ end
50
+
51
+ # Finds an AgentDefinition instance by name.
52
+ # @param name [Symbol] The name of the agent definition.
53
+ # @return [Legate::AgentDefinition, nil] The definition object or nil if not found.
54
+ def self.find(name)
55
+ unless name.is_a?(Symbol)
56
+ Legate.logger.warn("GlobalDefinitionRegistry: Find called with non-symbol key: #{name.inspect}")
57
+ return nil
58
+ end
59
+ entry = @mutex.synchronize { @registry[name] }
60
+ entry&.[](:definition)
61
+ end
62
+
63
+ # Clears the registry (primarily for testing).
64
+ def self.clear!
65
+ @mutex.synchronize { @registry = {} }
66
+ Legate.logger.debug('GlobalDefinitionRegistry: Cleared.')
67
+ end
68
+
69
+ # Returns the current registry hash mapping names to AgentDefinition objects.
70
+ # @return [Hash{Symbol => Legate::AgentDefinition}]
71
+ def self.all
72
+ @mutex.synchronize do
73
+ @registry.transform_values { |entry| entry[:definition] }.dup
74
+ end
75
+ end
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # DefinitionStore-compatible API (used by Web UI routes)
79
+ # ---------------------------------------------------------------------------
80
+
81
+ # Retrieves a single agent definition as a hash with Web UI field names.
82
+ #
83
+ # Field name mapping from AgentDefinition#to_h:
84
+ # :tool_names -> :tools (Array of Symbols)
85
+ # :model_name -> :model (Symbol or nil)
86
+ # :mcp_servers -> :mcp_servers_json (JSON String)
87
+ #
88
+ # Metadata fields (e.g. :persistent_status, :last_run_at) are merged in.
89
+ #
90
+ # @param name [String, Symbol] The agent name.
91
+ # @return [Hash, nil] A hash with symbol keys in Web UI format, or nil if not found.
92
+ def self.get_definition(name)
93
+ sym_name = normalize_name(name)
94
+ return nil unless sym_name
95
+
96
+ entry = @mutex.synchronize { @registry[sym_name] }
97
+ return nil unless entry
98
+
99
+ build_web_hash(entry)
100
+ end
101
+
102
+ # Saves a new agent definition. Supports two call signatures:
103
+ #
104
+ # 1. Keyword splat (from the create form):
105
+ # save_definition(name:, description:, tools:, model:, ...)
106
+ #
107
+ # 2. Two positional args (from the duplicate route):
108
+ # save_definition(new_name, definition_hash)
109
+ #
110
+ # @return [Boolean] true on success.
111
+ def self.save_definition(*args, **kwargs)
112
+ if args.length == 2
113
+ # Positional form: save_definition(new_name, definition_hash)
114
+ new_name = args[0]
115
+ definition_hash = args[1]
116
+ _save_from_hash(new_name, definition_hash)
117
+ elsif args.empty? && !kwargs.empty?
118
+ # Keyword form: save_definition(name:, description:, tools:, model:, ...)
119
+ _save_from_keywords(**kwargs)
120
+ else
121
+ raise ArgumentError, 'save_definition expects either (name, hash) or keyword arguments'
122
+ end
123
+ end
124
+
125
+ # Updates specific fields of an existing agent definition's metadata.
126
+ # This is used for things like persistent_status, last_run_at, and also
127
+ # for updating definition fields via the Web UI edit forms.
128
+ #
129
+ # @param name [String, Symbol] The agent name.
130
+ # @param updates [Hash] A hash of field names to new values.
131
+ # @return [Boolean] true if the agent was found and updated, false otherwise.
132
+ def self.update_definition(name, updates)
133
+ sym_name = normalize_name(name)
134
+ return false unless sym_name
135
+
136
+ @mutex.synchronize do
137
+ entry = @registry[sym_name]
138
+ return false unless entry
139
+
140
+ definition = entry[:definition]
141
+ # Snapshot for atomic rollback: a web edit that left the definition in a
142
+ # state the constructor would reject (e.g. cleared instruction) used to
143
+ # persist silently. We apply the batch, then validate! once and restore
144
+ # the prior state on failure. (update_definition_field reassigns ivars
145
+ # rather than mutating in place, so a shallow snapshot is a faithful
146
+ # rollback.)
147
+ ivar_snapshot = definition&.instance_variables&.to_h { |iv| [iv, definition.instance_variable_get(iv)] }
148
+ metadata_snapshot = entry[:metadata].dup
149
+
150
+ updates.each do |key, value|
151
+ key_sym = key.to_sym
152
+ # Check if this is a field that should update the AgentDefinition itself
153
+ update_definition_field(definition, key_sym, value) if definition_field?(key_sym) && definition
154
+ # Always store in metadata as well (for fields like persistent_status,
155
+ # last_run_at, and as a cache for definition field overrides)
156
+ entry[:metadata][key_sym] = value
157
+ end
158
+
159
+ if definition
160
+ begin
161
+ definition.validate!
162
+ rescue StandardError => e
163
+ ivar_snapshot.each { |iv, val| definition.instance_variable_set(iv, val) }
164
+ entry[:metadata].replace(metadata_snapshot)
165
+ Legate.logger.error("GlobalDefinitionRegistry: Rejected update for :#{sym_name} (would leave the definition invalid): #{e.message}")
166
+ return false
167
+ end
168
+ end
169
+ end
170
+
171
+ Legate.logger.debug("GlobalDefinitionRegistry: Updated definition for :#{sym_name} with keys: #{updates.keys.join(', ')}")
172
+ true
173
+ end
174
+
175
+ # Deletes an agent definition from the registry.
176
+ # @param name [String, Symbol] The agent name.
177
+ # @return [Boolean] true if deleted (or didn't exist), false on error.
178
+ def self.delete_definition(name)
179
+ sym_name = normalize_name(name)
180
+ return true unless sym_name # Nothing to delete
181
+
182
+ @mutex.synchronize { @registry.delete(sym_name) }
183
+ Legate.logger.info("GlobalDefinitionRegistry: Deleted definition for :#{sym_name}")
184
+ true
185
+ end
186
+
187
+ # Returns an array of hashes, each in the same format as get_definition output.
188
+ # @return [Array<Hash>]
189
+ def self.list_definitions
190
+ entries = @mutex.synchronize { @registry.dup }
191
+ entries.map { |_name, entry| build_web_hash(entry) }
192
+ .compact
193
+ .sort_by { |d| d[:name].to_s }
194
+ end
195
+
196
+ # Always returns true for the in-memory store (no external connection to check).
197
+ # @return [Boolean] true
198
+ def self.check_connection
199
+ true
200
+ end
201
+
202
+ # Checks if an agent definition with the given name exists.
203
+ # @param name [String, Symbol] The agent name.
204
+ # @return [Boolean]
205
+ def self.definition_exists?(name)
206
+ sym_name = normalize_name(name)
207
+ return false unless sym_name
208
+
209
+ @mutex.synchronize { @registry.key?(sym_name) }
210
+ end
211
+
212
+ # ---------------------------------------------------------------------------
213
+ # Private helpers
214
+ # ---------------------------------------------------------------------------
215
+
216
+ # Normalizes a name (String or Symbol) to a Symbol for internal use.
217
+ # @param name [String, Symbol, nil]
218
+ # @return [Symbol, nil]
219
+ def self.normalize_name(name)
220
+ return nil if name.nil?
221
+
222
+ str = name.to_s.strip
223
+ return nil if str.empty?
224
+
225
+ str.to_sym
226
+ end
227
+ private_class_method :normalize_name
228
+
229
+ # Builds a Web UI compatible hash from an internal registry entry.
230
+ # Maps AgentDefinition field names to Web UI field names and merges metadata.
231
+ def self.build_web_hash(entry)
232
+ definition = entry[:definition]
233
+ metadata = entry[:metadata] || {}
234
+
235
+ if definition
236
+ h = definition.to_h.dup
237
+
238
+ # Map :tool_names -> :tools (array of symbols)
239
+ h[:tools] = h.delete(:tool_names) || []
240
+ h[:tools] = h[:tools].map(&:to_sym) if h[:tools].is_a?(Array)
241
+
242
+ # Map :model_name -> :model
243
+ h[:model] = h.delete(:model_name)
244
+
245
+ # Convert :mcp_servers (Array) -> :mcp_servers_json (JSON String)
246
+ mcp_array = h.delete(:mcp_servers) || []
247
+ h[:mcp_servers_json] = mcp_array.is_a?(Array) ? mcp_array.to_json : '[]'
248
+
249
+ # Ensure persistent_status defaults to 'stopped'
250
+ h[:persistent_status] = metadata[:persistent_status] || 'stopped'
251
+
252
+ # Merge all metadata fields (last_run_at, etc.)
253
+ metadata.each do |key, value|
254
+ # Metadata overrides definition fields for display purposes
255
+ h[key] = value
256
+ end
257
+
258
+ # Ensure required defaults
259
+ h[:fallback_mode] = h[:fallback_mode] || :error
260
+ h[:instruction] ||= ''
261
+ h[:agent_type] = h[:agent_type]&.to_sym || :llm
262
+ h[:planning_strategy] = h[:planning_strategy]&.to_sym || :plan
263
+ h[:sub_agent_names] ||= []
264
+ h[:delegation_targets] ||= []
265
+ else
266
+ # Definition-less entry (created from hash via duplicate route)
267
+ h = metadata.dup
268
+ h[:persistent_status] ||= 'stopped'
269
+ h[:fallback_mode] ||= :error
270
+ h[:instruction] ||= ''
271
+ h[:agent_type] = h[:agent_type]&.to_sym || :llm
272
+ h[:planning_strategy] = h[:planning_strategy]&.to_sym || :plan
273
+ h[:tools] ||= []
274
+ h[:mcp_servers_json] ||= '[]'
275
+ h[:sub_agent_names] ||= []
276
+ h[:delegation_targets] ||= []
277
+ end
278
+
279
+ h
280
+ end
281
+ private_class_method :build_web_hash
282
+
283
+ # Saves a definition from keyword arguments (create form).
284
+ def self._save_from_keywords(name:, description: '', tools: [], model: nil,
285
+ fallback_mode: :error, mcp_servers_json: '[]',
286
+ instruction: '', webhook_enabled: false,
287
+ webhook_secret: nil, agent_type: :llm,
288
+ planning_strategy: :plan,
289
+ sub_agent_names: [], sequential_sub_agent_names: [],
290
+ parallel_sub_agent_names: [], loop_sub_agent_names: [],
291
+ output_key: nil, delegation_targets: [],
292
+ loop_max_iterations: nil, loop_condition_state_key: nil,
293
+ loop_condition_expected_value: nil,
294
+ auth_scheme_assignments: {}, auth_credential_assignments: {},
295
+ auth_url_mappings: [])
296
+ raise ArgumentError, 'Agent name cannot be empty.' if name.nil? || name.to_s.strip.empty?
297
+
298
+ sym_name = name.to_s.strip.to_sym
299
+
300
+ # Parse MCP servers JSON to array for AgentDefinition
301
+ mcp_array = parse_mcp_json(mcp_servers_json)
302
+
303
+ # Build a hash suitable for AgentDefinition.from_hash
304
+ definition_data = {
305
+ name: sym_name,
306
+ description: description || '',
307
+ instruction: instruction || '',
308
+ tool_names: normalize_tools(tools),
309
+ model_name: model,
310
+ temperature: nil,
311
+ fallback_mode: fallback_mode&.to_sym || :error,
312
+ mcp_servers: mcp_array,
313
+ webhook_enabled: !!webhook_enabled,
314
+ webhook_secret: webhook_secret,
315
+ agent_type: agent_type&.to_sym || :llm,
316
+ planning_strategy: planning_strategy&.to_sym || :plan,
317
+ sub_agent_names: Array(sub_agent_names).map(&:to_sym),
318
+ output_key: output_key&.to_sym,
319
+ sequential_sub_agent_names: Array(sequential_sub_agent_names).map(&:to_sym),
320
+ parallel_sub_agent_names: Array(parallel_sub_agent_names).map(&:to_sym),
321
+ loop_sub_agent_names: Array(loop_sub_agent_names).map(&:to_sym),
322
+ delegation_targets: Array(delegation_targets).map(&:to_sym),
323
+ loop_max_iterations: loop_max_iterations&.to_i,
324
+ loop_condition_state_key: loop_condition_state_key&.to_sym,
325
+ loop_condition_expected_value: loop_condition_expected_value,
326
+ auth_scheme_assignments: auth_scheme_assignments || {},
327
+ auth_credential_assignments: auth_credential_assignments || {},
328
+ auth_url_mappings: auth_url_mappings || []
329
+ }
330
+
331
+ definition = Legate::AgentDefinition.from_hash(definition_data)
332
+
333
+ @mutex.synchronize do
334
+ @registry[sym_name] = {
335
+ definition: definition,
336
+ metadata: { persistent_status: 'stopped' }
337
+ }
338
+ end
339
+
340
+ Legate.logger.info("GlobalDefinitionRegistry: Saved definition for :#{sym_name}")
341
+ true
342
+ end
343
+ private_class_method :_save_from_keywords
344
+
345
+ # Saves a definition from a name and hash (duplicate route).
346
+ def self._save_from_hash(new_name, definition_hash)
347
+ raise ArgumentError, 'Agent name cannot be empty.' if new_name.nil? || new_name.to_s.strip.empty?
348
+
349
+ sym_name = new_name.to_s.strip.to_sym
350
+ hash_data = definition_hash.is_a?(Hash) ? definition_hash.dup : {}
351
+
352
+ # Normalize field names for AgentDefinition.from_hash compatibility
353
+ hash_data[:name] = sym_name
354
+
355
+ # Map Web UI field names back to AgentDefinition field names
356
+ hash_data[:tool_names] = hash_data.delete(:tools) if hash_data.key?(:tools) && !hash_data.key?(:tool_names)
357
+ hash_data[:model_name] = hash_data.delete(:model) if hash_data.key?(:model) && !hash_data.key?(:model_name)
358
+ if hash_data.key?(:mcp_servers_json) && !hash_data.key?(:mcp_servers)
359
+ mcp_json = hash_data.delete(:mcp_servers_json)
360
+ hash_data[:mcp_servers] = parse_mcp_json(mcp_json)
361
+ end
362
+
363
+ # Normalize tools to symbols
364
+ hash_data[:tool_names] = hash_data[:tool_names].map(&:to_sym) if hash_data[:tool_names].is_a?(Array)
365
+
366
+ # Try to create an AgentDefinition from the hash
367
+ definition = begin
368
+ Legate::AgentDefinition.from_hash(hash_data)
369
+ rescue StandardError => e
370
+ Legate.logger.warn("GlobalDefinitionRegistry: Could not create AgentDefinition from hash for :#{sym_name}: #{e.message}")
371
+ nil
372
+ end
373
+
374
+ # Extract metadata fields that are not part of AgentDefinition
375
+ metadata = {}
376
+ metadata_keys = %i[persistent_status last_run_at]
377
+ metadata_keys.each do |mk|
378
+ metadata[mk] = hash_data[mk] if hash_data.key?(mk)
379
+ end
380
+ metadata[:persistent_status] ||= 'stopped'
381
+
382
+ @mutex.synchronize do
383
+ @registry[sym_name] = {
384
+ definition: definition,
385
+ metadata: metadata
386
+ }
387
+ end
388
+
389
+ Legate.logger.info("GlobalDefinitionRegistry: Saved definition for :#{sym_name} (from hash)")
390
+ true
391
+ end
392
+ private_class_method :_save_from_hash
393
+
394
+ # Parses an MCP servers JSON string into an array.
395
+ def self.parse_mcp_json(mcp_json)
396
+ return [] if mcp_json.nil? || mcp_json.to_s.strip.empty? || mcp_json.to_s.strip == '[]'
397
+
398
+ if mcp_json.is_a?(String)
399
+ begin
400
+ parsed = JSON.parse(mcp_json)
401
+ parsed.is_a?(Array) ? parsed : []
402
+ rescue JSON::ParserError
403
+ []
404
+ end
405
+ elsif mcp_json.is_a?(Array)
406
+ mcp_json
407
+ else
408
+ []
409
+ end
410
+ end
411
+ private_class_method :parse_mcp_json
412
+
413
+ # Normalizes a tools value (could be array of strings, symbols, or JSON string).
414
+ def self.normalize_tools(tools)
415
+ if tools.is_a?(Array)
416
+ tools.map(&:to_sym)
417
+ elsif tools.is_a?(String)
418
+ begin
419
+ parsed = JSON.parse(tools)
420
+ parsed.is_a?(Array) ? parsed.map(&:to_sym) : []
421
+ rescue JSON::ParserError
422
+ tools.strip.empty? ? [] : [tools.to_sym]
423
+ end
424
+ else
425
+ []
426
+ end
427
+ end
428
+ private_class_method :normalize_tools
429
+
430
+ # Checks if a key corresponds to a field on AgentDefinition.
431
+ DEFINITION_FIELDS = %i[
432
+ description instruction tool_names model_name temperature
433
+ fallback_mode mcp_servers webhook_enabled webhook_secret
434
+ agent_type planning_strategy sub_agent_names output_key
435
+ sequential_sub_agent_names parallel_sub_agent_names loop_sub_agent_names
436
+ delegation_targets loop_max_iterations loop_condition_state_key
437
+ loop_condition_expected_value auth_credential_names auth_url_mappings
438
+ auth_scheme_assignments auth_credential_assignments
439
+ ].freeze
440
+
441
+ # Web UI uses different field names; map them to definition ivars.
442
+ WEB_TO_DEFINITION_MAP = {
443
+ tools: :tool_names,
444
+ model: :model_name,
445
+ mcp_servers_json: :mcp_servers
446
+ }.freeze
447
+
448
+ def self.definition_field?(key_sym)
449
+ DEFINITION_FIELDS.include?(key_sym) || WEB_TO_DEFINITION_MAP.key?(key_sym)
450
+ end
451
+ private_class_method :definition_field?
452
+
453
+ # Updates a field on an AgentDefinition instance via instance_variable_set.
454
+ def self.update_definition_field(definition, key_sym, value)
455
+ # Map web field names to definition ivar names
456
+ ivar_name = WEB_TO_DEFINITION_MAP[key_sym] || key_sym
457
+
458
+ case ivar_name
459
+ when :tool_names
460
+ tools = normalize_tools(value)
461
+ definition.instance_variable_set(:@tool_names, Set.new(tools))
462
+ when :model_name
463
+ definition.instance_variable_set(:@model_name, value&.to_sym)
464
+ when :mcp_servers
465
+ if value.is_a?(String)
466
+ definition.instance_variable_set(:@mcp_servers, parse_mcp_json(value))
467
+ elsif value.is_a?(Array)
468
+ definition.instance_variable_set(:@mcp_servers, value)
469
+ end
470
+ when :fallback_mode
471
+ val_sym = value&.to_sym
472
+ definition.instance_variable_set(:@fallback_mode, %i[error echo].include?(val_sym) ? val_sym : :error)
473
+ when :agent_type
474
+ val_sym = value&.to_sym
475
+ valid = %i[llm sequential parallel loop]
476
+ definition.instance_variable_set(:@agent_type, valid.include?(val_sym) ? val_sym : :llm)
477
+ when :planning_strategy
478
+ val_sym = value&.to_sym
479
+ definition.instance_variable_set(:@planning_strategy, %i[plan react].include?(val_sym) ? val_sym : :plan)
480
+ when :sub_agent_names, :sequential_sub_agent_names, :parallel_sub_agent_names,
481
+ :loop_sub_agent_names, :delegation_targets, :auth_credential_names
482
+ arr = value.is_a?(Array) ? value.map(&:to_sym) : []
483
+ definition.instance_variable_set(:"@#{ivar_name}", Set.new(arr))
484
+ when :output_key, :loop_condition_state_key
485
+ definition.instance_variable_set(:"@#{ivar_name}", value&.to_sym)
486
+ when :loop_max_iterations
487
+ definition.instance_variable_set(:@loop_max_iterations, value&.to_i)
488
+ when :temperature
489
+ definition.instance_variable_set(:@temperature, value&.to_f)
490
+ when :webhook_enabled
491
+ definition.instance_variable_set(:@webhook_enabled, !!value)
492
+ when :description, :instruction, :webhook_secret
493
+ definition.instance_variable_set(:"@#{ivar_name}", value&.to_s)
494
+ when :loop_condition_expected_value
495
+ definition.instance_variable_set(:@loop_condition_expected_value, value)
496
+ when :auth_url_mappings
497
+ definition.instance_variable_set(:@auth_url_mappings, value.is_a?(Array) ? value : [])
498
+ when :auth_scheme_assignments, :auth_credential_assignments
499
+ definition.instance_variable_set(:"@#{ivar_name}", value.is_a?(Hash) ? value : {})
500
+ end
501
+ rescue StandardError => e
502
+ Legate.logger.warn("GlobalDefinitionRegistry: Failed to update definition field :#{ivar_name}: #{e.message}")
503
+ end
504
+ private_class_method :update_definition_field
505
+ end
506
+ end
@@ -0,0 +1,135 @@
1
+ # lib/legate/global_tool_manager.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'logger'
5
+ require_relative 'tool' # Need Tool class for instance checks/metadata
6
+ require_relative 'tool/metadata_dsl'
7
+
8
+ module Legate
9
+ # Manages the central registration and discovery of all defined Legate::Tool subclasses.
10
+ # This provides a way to list all available tools without needing a specific
11
+ # ToolRegistry instance (which is tied to an Agent).
12
+ module GlobalToolManager
13
+ # Store tool classes keyed by their symbolic name
14
+ @defined_tools = {} # { :tool_symbol => ToolClass }
15
+ # Guards @defined_tools: tools can be registered at runtime (e.g. custom-tool
16
+ # autoload) while requests read it concurrently under Puma.
17
+ @mutex = Mutex.new
18
+
19
+ # Register a tool class globally. Called automatically via Legate::Tool.inherited
20
+ # @param tool_class [Class] The tool class to register.
21
+ def self.register_tool(tool_class)
22
+ unless tool_class < Legate::Tool
23
+ Legate.logger.warn("GlobalToolManager: Attempted to register non-tool class: #{tool_class.inspect}")
24
+ return
25
+ end
26
+
27
+ metadata = tool_class.tool_metadata
28
+ tool_name = metadata[:name]&.to_sym
29
+
30
+ # --- Attempt name inference if not found in metadata ---
31
+ # This handles cases where the new DSL isn't used (e.g., old define_metadata)
32
+ # or if the DSL itself couldn't determine a name (e.g., anonymous class)
33
+ if tool_name.nil? || tool_name == :''
34
+ # First, check for the instance variable set by the DEPRECATED define_metadata
35
+ if tool_class.instance_variable_defined?(:@tool_name)
36
+ tool_name = tool_class.instance_variable_get(:@tool_name)
37
+ Legate.logger.debug("GlobalToolManager: Tool class #{tool_class} using name from deprecated @tool_name: #{tool_name.inspect}")
38
+ else
39
+ # If not found via deprecated method, try inference via DSL
40
+ begin
41
+ # Check if the class responds to inferred_name (from MetadataDsl)
42
+ if tool_class.respond_to?(:inferred_name)
43
+ inferred = tool_class.inferred_name
44
+ if inferred
45
+ Legate.logger.debug("GlobalToolManager: Tool class #{tool_class} had no explicit name, using inferred name: #{inferred.inspect}")
46
+ tool_name = inferred
47
+ else
48
+ Legate.logger.warn("GlobalToolManager: Tool class #{tool_class} has no explicit name and inference failed (maybe anonymous?). Skipping registration.")
49
+ return
50
+ end
51
+ else
52
+ # Fallback if MetadataDsl isn't included or something is wrong
53
+ Legate.logger.warn("GlobalToolManager: Tool class #{tool_class} has no name defined via tool_metadata or @tool_name, and does not support inferred_name. Skipping registration.")
54
+ return
55
+ end
56
+ rescue StandardError => e
57
+ Legate.logger.error("GlobalToolManager: Error during name inference for #{tool_class}: #{e.message}")
58
+ return # Don't register if inference itself fails
59
+ end
60
+ end
61
+ end
62
+ # --- End Name Inference Attempt ---
63
+
64
+ # Ensure tool_name is a symbol before proceeding
65
+ tool_name = tool_name&.to_sym
66
+ if tool_name.nil? || tool_name == :''
67
+ Legate.logger.error("GlobalToolManager: Could not determine a valid tool name for #{tool_class}. Skipping registration.")
68
+ return
69
+ end
70
+
71
+ @mutex.synchronize do
72
+ if @defined_tools.key?(tool_name) && @defined_tools[tool_name] != tool_class
73
+ Legate.logger.warn("GlobalToolManager: Tool name '#{tool_name}' is already registered with class #{@defined_tools[tool_name]}. Overwriting with #{tool_class}.")
74
+ elsif !@defined_tools.key?(tool_name)
75
+ Legate.logger.debug("GlobalToolManager: Registered tool '#{tool_name}' with class #{tool_class}.")
76
+ end
77
+ @defined_tools[tool_name] = tool_class
78
+ end
79
+ end
80
+
81
+ # Get a list of all globally registered tools with basic info.
82
+ # @return [Array<Hash>] An array of hashes, each with :name and :description.
83
+ def self.list_all_tools
84
+ snapshot = @mutex.synchronize { @defined_tools.dup }
85
+ snapshot.map do |name_sym, klass|
86
+ metadata = klass.tool_metadata
87
+ {
88
+ name: metadata[:name] || name_sym, # Fallback, though name should always be present if registered
89
+ description: metadata[:description] || '[No description provided]',
90
+ parameters: metadata[:parameters] || []
91
+ }
92
+ end.sort_by { |t| t[:name].to_s }
93
+ end
94
+
95
+ # Find a registered tool class by its name symbol.
96
+ # @param name_symbol [Symbol] The symbolic name of the tool.
97
+ # @return [Class, nil] The tool class or nil if not found.
98
+ def self.find_class(name_symbol)
99
+ @mutex.synchronize { @defined_tools[name_symbol.to_sym] }
100
+ end
101
+
102
+ # Get the names (symbols) of all registered tools.
103
+ # @return [Array<Symbol>] An array of tool name symbols.
104
+ def self.registered_tool_names
105
+ @mutex.synchronize { @defined_tools.keys }
106
+ end
107
+
108
+ # Create an instance of a tool by its name symbol using the globally registered class.
109
+ # @param name_symbol [Symbol] The symbolic name of the tool.
110
+ # @return [Legate::Tool, nil] An instance of the tool or nil if instantiation fails or class not found.
111
+ def self.create_instance(name_symbol)
112
+ klass = find_class(name_symbol.to_sym)
113
+
114
+ if klass
115
+ begin
116
+ instance = klass.new
117
+ Legate.logger.debug("GlobalToolManager: Successfully instantiated tool '#{name_symbol}'.")
118
+ instance
119
+ rescue StandardError => e
120
+ Legate.logger.error("GlobalToolManager: Failed to instantiate tool '#{name_symbol}' (Class: #{klass}): #{e.class} - #{e.message}")
121
+ Legate.logger.error(e.backtrace.first(5).join("\n"))
122
+ nil
123
+ end
124
+ else
125
+ Legate.logger.warn("GlobalToolManager: Attempted to create instance of tool '#{name_symbol}' which is not globally registered.")
126
+ nil
127
+ end
128
+ end
129
+
130
+ # Clears all registered tools. Primarily for testing.
131
+ def self.reset!
132
+ @mutex.synchronize { @defined_tools = {} }
133
+ end
134
+ end # End GlobalToolManager module
135
+ end # End Legate module