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,959 @@
1
+ # File: lib/legate/agent.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'logger'
5
+ require 'concurrent'
6
+ require 'did_you_mean' # for "did you mean" suggestions on unknown tool names
7
+ require 'pathname' # Added for path manipulation
8
+ require_relative 'tool_context'
9
+ # NOTE: Requires are handled by lib/legate.rb
10
+ require_relative 'planner'
11
+ require_relative 'tool_registry'
12
+ require_relative 'agent_definition'
13
+ require_relative 'mcp/client'
14
+ require_relative 'mcp/tool_wrapper'
15
+ require 'forwardable'
16
+ require 'json'
17
+ require_relative 'global_definition_registry'
18
+ require_relative 'global_tool_manager' # Added
19
+ require_relative 'tool_loader'
20
+ require 'securerandom'
21
+
22
+ module Legate
23
+ # Represents the static definition of an Agent, including its name,
24
+ # description, instructions, tools, and model configuration.
25
+
26
+ # Agent class represents an AI agent that can perform tasks using tools and a planner.
27
+ # It operates within the context of a session managed by a SessionService.
28
+ class Agent
29
+ DEFAULT_MODEL = 'gemini-3.5-flash' # Default Gemini model (supports structured output)
30
+
31
+ attr_reader :name, :description, :planner, :logger, :model_name, :state, :tool_registry, :fallback_mode, :instruction, :definition, :session_service, :sub_agents # Added session_service to attr_reader
32
+ # MAS Attributes
33
+ attr_reader :parent_agent # The parent agent in a hierarchy, if any # A collection of sub-agents
34
+
35
+ # --- Callback Instance Variables ---
36
+ attr_reader :before_agent_callback, :after_agent_callback,
37
+ :before_model_callback, :after_model_callback,
38
+ :before_tool_callback, :after_tool_callback
39
+
40
+ # --- End Callback Instance Variables ---
41
+
42
+ # --- Authentication Instance Variables ---
43
+ attr_reader :auth_credential_names, :auth_url_mappings,
44
+ :auth_scheme_assignments, :auth_credential_assignments
45
+
46
+ # --- End Authentication Instance Variables ---
47
+
48
+ # --- Class Method for Configuration DSL ---
49
+ # Provides a block-based DSL for configuring and creating an Agent instance.
50
+ #
51
+ # The DSL is positional (method-call style), not assignment. The resulting
52
+ # definition is registered globally in {GlobalDefinitionRegistry} as a side
53
+ # effect, then returned.
54
+ #
55
+ # @example
56
+ # definition = Legate::Agent.define do |a|
57
+ # a.name :news_agent
58
+ # a.description 'Summarizes news articles.'
59
+ # a.instruction 'Summarize the article the user provides.'
60
+ # a.model_name 'gemini-3.5-flash'
61
+ # a.use_tool :echo
62
+ # a.fallback_mode :echo
63
+ # end
64
+ #
65
+ # @yieldparam a [Legate::AgentDefinition::DefinitionProxy] The proxy object to configure the definition.
66
+ # @return [Legate::AgentDefinition] The validated, globally-registered definition.
67
+ # @raise [ArgumentError] if the block is not provided or required attributes are missing.
68
+ def self.define(&block)
69
+ raise ArgumentError, 'Legate::Agent.define requires a block.' unless block_given?
70
+
71
+ # 1. Create a new AgentDefinition
72
+ definition = Legate::AgentDefinition.new
73
+
74
+ # 2. Evaluate the block within the definition's proxy DSL
75
+ # Use the definition instance's define method which takes the block
76
+ # This also handles internal validation via validate!
77
+ begin
78
+ definition.define(&block)
79
+ rescue ArgumentError => e
80
+ # Re-raise DSL validation errors immediately
81
+ raise e
82
+ end
83
+
84
+ # 3. Register the validated definition in the GlobalDefinitionRegistry
85
+ begin
86
+ GlobalDefinitionRegistry.register(definition)
87
+ agent_name = definition.instance_variable_get(:@name)
88
+ Legate.logger.info("Agent definition '#{agent_name}' registered in GlobalDefinitionRegistry.")
89
+ rescue ArgumentError => e
90
+ agent_name_for_log = definition.instance_variable_get(:@name) || 'unknown'
91
+ Legate.logger.error("Failed to register definition '#{agent_name_for_log}': #{e.class} - #{e.message}")
92
+ raise e
93
+ rescue StandardError => e
94
+ agent_name_for_log = definition.instance_variable_get(:@name) || 'unknown'
95
+ Legate.logger.error("Unexpected error registering definition '#{agent_name_for_log}': #{e.class} - #{e.message}")
96
+ raise Legate::StoreError, "Unexpected error registering definition '#{agent_name_for_log}': #{e.message}"
97
+ end
98
+
99
+ definition # Return the definition instance
100
+ end
101
+ # --- End Class Method ---
102
+
103
+ # Initializes a new agent instance.
104
+ # An agent MUST be initialized with a valid Legate::AgentDefinition object.
105
+ #
106
+ # @param definition [Legate::AgentDefinition] The agent definition object.
107
+ # @param session_service [Legate::SessionService::Base, nil] Optional: Pre-initialized session service.
108
+ # @param planner_override [Legate::Planner, nil] Optional: A specific planner instance to override the default.
109
+ # @param sub_agents [Array<Legate::Agent>, nil] Optional: An array of pre-initialized sub-agent instances. If provided, these will be used instead of instantiating from `definition.sub_agent_names`.
110
+ def initialize(definition:, session_service: nil, planner_override: nil, sub_agents: nil)
111
+ unless definition.is_a?(Legate::AgentDefinition)
112
+ raise ArgumentError,
113
+ "Agent must be initialized with an Legate::AgentDefinition object. Received: #{definition.class}"
114
+ end
115
+ # Perform a more thorough check if it looks like a definition
116
+ unless definition.respond_to?(:name) && definition.respond_to?(:description) &&
117
+ definition.respond_to?(:instruction) && definition.respond_to?(:tool_names) &&
118
+ definition.respond_to?(:model_name) && definition.respond_to?(:fallback_mode) &&
119
+ definition.respond_to?(:mcp_servers)
120
+ raise ArgumentError,
121
+ 'Provided definition object does not appear to be a valid Legate::AgentDefinition (missing required attributes/methods).'
122
+ end
123
+
124
+ @definition = definition
125
+ @name = definition.name
126
+
127
+ # --- Initialize Callbacks from Definition ---
128
+ @before_agent_callback = definition.before_agent_callback
129
+ @after_agent_callback = definition.after_agent_callback
130
+ @before_model_callback = definition.before_model_callback
131
+ @after_model_callback = definition.after_model_callback
132
+ @before_tool_callback = definition.before_tool_callback
133
+ @after_tool_callback = definition.after_tool_callback
134
+ # --- End Initialize Callbacks ---
135
+
136
+ # --- Initialize Authentication Config from Definition ---
137
+ @auth_credential_names = definition.auth_credential_names || Set.new
138
+ @auth_url_mappings = definition.auth_url_mappings || []
139
+ @auth_scheme_assignments = definition.auth_scheme_assignments || {}
140
+ @auth_credential_assignments = definition.auth_credential_assignments || {}
141
+ # --- End Initialize Authentication Config ---
142
+
143
+ # Check for direct self-references in the definition's sub_agent_names
144
+ raise Legate::ConfigurationError, "Circular dependency detected: Agent '#{@name}' cannot include itself as a sub-agent" if definition.respond_to?(:sub_agent_names) && definition.sub_agent_names&.any? && definition.sub_agent_names.include?(@name)
145
+
146
+ @description = definition.description
147
+ @instruction = definition.instruction
148
+ @model_name = definition.model_name || DEFAULT_MODEL
149
+ @fallback_mode = definition.fallback_mode # Assumes :error is default in AgentDefinition
150
+ @selected_tool_names = definition.tool_names.to_a # Tool names are directly from definition
151
+
152
+ # MAS Attributes Initialization
153
+ @parent_agent = nil # Will be set by parent if this is a sub-agent
154
+ @sub_agents = [] # Will be populated if this agent has sub-agents defined
155
+
156
+ @session_service = session_service || Legate.config.session_service
157
+ @state = :idle
158
+
159
+ Legate.logger.info("Initializing agent '#{@name}' from provided definition object...")
160
+
161
+ setup_tool_registry(definition)
162
+ setup_mcp_config(definition)
163
+
164
+ @selected_tool_names = @definition.tool_names.to_a
165
+ @mcp_manager = Legate::Mcp::ConnectionManager.new(
166
+ tool_registry: @tool_registry,
167
+ selected_tool_names: @selected_tool_names,
168
+ agent_name: @name
169
+ )
170
+ @plan_executor = Legate::PlanExecutor.new(self)
171
+
172
+ @planner = planner_override || Legate::Planner.new(agent: self, model_name: @model_name)
173
+
174
+ unless @session_service&.respond_to?(:get_session) && @session_service.respond_to?(:append_event)
175
+ raise ConfigurationError,
176
+ "Agent '#{@name}' requires a valid Session Service (must respond to :get_session, :append_event)."
177
+ end
178
+ raise ConfigurationError, "Agent '#{@name}' requires a valid Planner (must respond to :plan)." unless @planner&.respond_to?(:plan)
179
+
180
+ Legate.logger.debug {
181
+ "Agent '#{@name}' initialized with #{@tool_registry.tools.count} tools: [#{@tool_registry.tools.keys.join(', ')}]"
182
+ }
183
+
184
+ setup_sub_agents(definition, sub_agents)
185
+ end
186
+
187
+ # Adds a tool instance OR class to the agent's registry
188
+ # @param tool [Legate::Tool, Class<Legate::Tool>] The tool instance or class to add
189
+ # @return [Boolean] True if the tool was added, false otherwise
190
+ def add_tool(tool)
191
+ # Check if it's a valid tool instance or class
192
+ is_tool_instance = tool.is_a?(Legate::Tool)
193
+ is_tool_class = tool.is_a?(Class) && tool < Legate::Tool
194
+
195
+ unless is_tool_instance || is_tool_class
196
+ Legate.logger.error("Agent '#{name}' add_tool: Attempted to add invalid tool: #{tool.inspect}")
197
+ return false
198
+ end
199
+
200
+ # Determine the actual tool class
201
+ tool_class = is_tool_class ? tool : tool.class
202
+
203
+ # --- Determine Tool Name with Fallbacks --- #
204
+ tool_name = get_tool_name_from_class(tool_class) # Use the new helper
205
+ # --- End Determine Tool Name --- #
206
+
207
+ # Validate name was found
208
+ unless tool_name # The helper returns nil if no valid name is found
209
+ Legate.logger.error("Agent '#{name}' add_tool: Could not determine tool name for class #{tool_class}. Cannot add tool.")
210
+ return false # Explicitly return false
211
+ end
212
+
213
+ # Check for overwrite
214
+ Legate.logger.warn("Agent '#{name}': Tool '#{tool_name}' already added. Overwriting with class #{tool_class}.") if @tool_registry.find_class(tool_name)
215
+
216
+ # Register the class using the determined name
217
+ Legate.logger.debug("Agent '#{name}' add_tool: Registering tool_name=#{tool_name.inspect} with class=#{tool_class.inspect} in registry=#{@tool_registry.object_id}")
218
+ registration_result = @tool_registry.register(tool_name, tool_class)
219
+ Legate.logger.debug("Agent '#{name}' add_tool: Registry after registration for #{tool_name.inspect}: #{@tool_registry.tools.keys.inspect}")
220
+
221
+ # Explicitly return the boolean result from the registry
222
+ registration_result
223
+ end
224
+
225
+ # Returns the list of tools registered with this agent
226
+ # @return [Array<Legate::Tool>] Array of tool instances
227
+ def tools
228
+ @tool_registry.tools.values.map do |tool_class|
229
+ # Get name reliably using the new helper method
230
+ tool_name = get_tool_name_from_class(tool_class)
231
+ if tool_name
232
+ @tool_registry.create_instance(tool_name)
233
+ else
234
+ # This branch should ideally not be hit frequently if registration robustly requires a name.
235
+ Legate.logger.warn("Agent '#{name}': Skipping tool instance creation for class #{tool_class} as its name could not be determined post-registration.")
236
+ nil
237
+ end
238
+ end.compact
239
+ end
240
+
241
+ # Finds a tool instance by name
242
+ # @param tool_name [Symbol] The name of the tool to find
243
+ # @return [Legate::Tool, nil] The tool instance if found, nil otherwise
244
+ def find_tool(tool_name)
245
+ @tool_registry.create_instance(tool_name.to_sym)
246
+ end
247
+
248
+ # Registers a tool class with the agent's specific registry.
249
+ # @param tool_class [Class] The tool class to register (must inherit from Legate::Tool).
250
+ # @return [Boolean] True if registration was successful, false otherwise.
251
+ def register_tool_class(tool_class)
252
+ Legate.logger.debug("[register_tool_class] Registering class: #{tool_class.inspect} (Object ID: #{tool_class.object_id})")
253
+ # Basic validation
254
+ unless tool_class < Legate::Tool
255
+ Legate.logger.error("Agent '#{name}': Attempted to register invalid object (must inherit from Legate::Tool): #{tool_class.inspect}")
256
+ return false
257
+ end
258
+
259
+ # Get name via metadata method
260
+ tool_name = get_tool_name_from_class(tool_class) # Use the new helper
261
+ Legate.logger.debug("[register_tool_class] Determined tool name: #{tool_name.inspect} for class #{tool_class.inspect}")
262
+
263
+ unless tool_name # Helper returns nil if no valid name
264
+ # Use logger method, not direct access
265
+ Legate.logger.error("Agent '#{name}': Could not determine tool name for class #{tool_class}. Cannot register.") # Consistent error message
266
+ return false
267
+ end
268
+
269
+ Legate.logger.warn("Agent '#{name}': Tool '#{tool_name}' already registered. Overwriting.") if @tool_registry.find_class(tool_name)
270
+
271
+ # Register with the instance registry
272
+ @tool_registry.register(tool_name, tool_class)
273
+ true # Return true on success
274
+ end
275
+
276
+ # --- Runtime State Methods (unchanged) ---
277
+ def start
278
+ return if running? # Prevent starting multiple times
279
+
280
+ Legate.logger.info("Starting agent '#{name}' runtime...")
281
+ @state = :running
282
+
283
+ # Connect to MCP Servers and register tools
284
+ connect_mcp_servers
285
+
286
+ Legate.logger.info("Agent '#{name}' runtime started.")
287
+ end
288
+
289
+ def stop
290
+ return unless running?
291
+
292
+ Legate.logger.info("Stopping agent '#{name}' runtime...")
293
+ @state = :stopped
294
+
295
+ # Disconnect MCP Clients
296
+ disconnect_mcp_servers
297
+
298
+ Legate.logger.info("Agent '#{name}' runtime stopped.")
299
+ end
300
+
301
+ def running?
302
+ @state == :running
303
+ end
304
+
305
+ # Returns the list of available tool metadata (names, descriptions, parameters)
306
+ # from the agent's specific tool registry.
307
+ def available_tools_metadata
308
+ @tool_registry.list_tools
309
+ end
310
+
311
+ # Finds a tool class by name from the agent's specific tool registry.
312
+ # @param tool_name [Symbol]
313
+ # @return [Class<Legate::Tool>, nil]
314
+ def find_tool_class(tool_name)
315
+ @tool_registry.find_class(tool_name.to_sym)
316
+ end
317
+
318
+ # One-shot convenience runner: starts the agent if needed, creates (or
319
+ # reuses) a session on the agent's own session service, runs the task, and
320
+ # returns the final event. The friendly path over the explicit
321
+ # start/create_session/run_task/stop dance.
322
+ #
323
+ # answer = agent.ask('What is 2 + 2?').answer
324
+ # agent.ask('Search ruby') { |event| puts event.role } # live progress (R3)
325
+ #
326
+ # Lazy-starts but does NOT auto-stop — stopping tears down MCP connections
327
+ # that are costly to re-establish, and an agent typically answers many asks.
328
+ # Call #stop when done (or let process exit reclaim it).
329
+ #
330
+ # @param user_input [String] the user's request
331
+ # @param user_id [String] identity for the auto-created session
332
+ # @param session_id [String, nil] reuse an existing session to continue a conversation
333
+ # @yieldparam event [Legate::Event] optional live progress (forwarded to run_task's on_event)
334
+ # @return [Legate::Event] the final agent event (use #answer / #success?)
335
+ def ask(user_input, user_id: 'default', session_id: nil, &on_event)
336
+ start unless running?
337
+ session_id ||= @session_service.create_session(app_name: name.to_s, user_id: user_id).id
338
+ run_task(session_id: session_id, user_input: user_input,
339
+ session_service: @session_service, on_event: on_event)
340
+ end
341
+
342
+ # @param on_event [Proc, nil] optional callback invoked with each
343
+ # Legate::Event as it is appended during the run (user, tool_request,
344
+ # tool_result, final agent) — for streaming progress (R3). The final event
345
+ # is still returned; non-streaming callers pass nothing and are unaffected.
346
+ # @return [Legate::Event] The final agent event.
347
+ def run_task(session_id:, user_input:, session_service:, on_event: nil)
348
+ # --- Pre-execution Checks --- #
349
+ unless running?
350
+ err_msg = "Agent '#{name}' is not running. Call agent.start before run_task, " \
351
+ 'or use agent.ask (which starts automatically).'
352
+ Legate.logger.error(err_msg)
353
+ return Legate::Event.new(role: :agent, content: { status: :error, error_message: err_msg })
354
+ end
355
+
356
+ session = session_service.get_session(session_id: session_id)
357
+ unless session
358
+ err_msg = "Session not found: #{session_id}"
359
+ Legate.logger.error(err_msg)
360
+ # Even if session isn't found, return an event for consistency?
361
+ return Legate::Event.new(role: :agent, content: { status: :error, error_message: err_msg })
362
+ end
363
+ # ----------------- #
364
+
365
+ # Generate invocation_id for this run and create callback context
366
+ invocation_id = SecureRandom.uuid
367
+ callback_context = nil
368
+
369
+ # R3: stream lifecycle events to the optional on_event callback as they're
370
+ # appended. Torn down in the ensure below so the subscription can't leak.
371
+ event_subscription = subscribe_events(session_service, session_id, on_event)
372
+
373
+ begin
374
+ # Create callback context for callbacks to use
375
+ callback_context = Legate::Callbacks::CallbackContext.new(
376
+ agent_name: @name,
377
+ invocation_id: invocation_id,
378
+ session_id: session_id,
379
+ user_id: session.user_id,
380
+ app_name: session.app_name,
381
+ session_service: session_service
382
+ )
383
+
384
+ # Execute before_agent_callback if defined
385
+ if @definition.respond_to?(:before_agent_callback) && @definition.before_agent_callback
386
+ Legate.logger.debug { "Agent '#{@name}': Executing before_agent_callback." }
387
+
388
+ # Execute the callback and check if it returns a result
389
+ begin
390
+ override_result = @definition.before_agent_callback.call(callback_context)
391
+
392
+ # If the callback returns a result (not nil), use it instead of normal execution
393
+ if override_result
394
+ Legate.logger.info { "Agent '#{@name}': before_agent_callback provided an override result." }
395
+
396
+ # Apply any pending state changes from the callback
397
+ apply_pending_state(callback_context, session_id, session_service)
398
+
399
+ # Create an agent event with the override result
400
+ final_agent_event = Legate::Event.new(role: :agent, content: override_result)
401
+ session_service.append_event(session_id: session_id, event: final_agent_event)
402
+
403
+ # Store the output if configured
404
+ _store_output_in_session(final_agent_event, session_id, session_service)
405
+
406
+ return final_agent_event
407
+ end
408
+ rescue StandardError => e
409
+ Legate.logger.error { "Agent '#{@name}': Error in before_agent_callback: #{e.message}\n#{e.backtrace.join("\n")}" }
410
+ return record_error_event(session_id, session_service, "Error in before_agent_callback: #{e.message}")
411
+ end
412
+
413
+ # Apply any pending state changes from the callback if execution continues
414
+ apply_pending_state(callback_context, session_id, session_service, clear: true)
415
+ end
416
+
417
+ # --- Normal Execution Flow --- #
418
+ # Create a user-message event for this turn
419
+ user_message_event = Legate::Event.new(
420
+ role: :user,
421
+ content: user_input
422
+ )
423
+ session_service.append_event(session_id: session_id, event: user_message_event)
424
+
425
+ # Produce the result via the configured strategy. :plan (default) asks
426
+ # the planner for one upfront plan and runs it; :react drives an agentic
427
+ # observe->think->act loop. Both return the same { details:, last_result: }
428
+ # shape, so the final-event handling below is strategy-agnostic.
429
+ result_hash =
430
+ if react_strategy?
431
+ run_react_loop(user_input, session, session_service, invocation_id)
432
+ else
433
+ plan = @planner.plan(user_input, invocation_id)
434
+ execute_plan(plan, session, session_service, invocation_id)
435
+ end
436
+
437
+ # Create an agent event with the result
438
+ final_agent_event = Legate::Event.new(role: :agent, content: result_hash[:last_result] || result_hash)
439
+ session_service.append_event(session_id: session_id, event: final_agent_event)
440
+
441
+ # Execute after_agent_callback if defined
442
+ if @definition.respond_to?(:after_agent_callback) && @definition.after_agent_callback
443
+ Legate.logger.debug { "Agent '#{@name}': Executing after_agent_callback." }
444
+
445
+ begin
446
+ # Execute the callback and let it modify the result if needed
447
+ # Pass the actual result (last_result) to the callback, not the full hash with details
448
+ modified_result = @definition.after_agent_callback.call(callback_context, result_hash[:last_result] || result_hash)
449
+
450
+ # If the callback returned a modified result, use it
451
+ if modified_result && modified_result != (result_hash[:last_result] || result_hash)
452
+ Legate.logger.info { "Agent '#{@name}': after_agent_callback modified the result." }
453
+
454
+ # Create a new agent event with the modified result
455
+ final_agent_event = Legate::Event.new(role: :agent, content: modified_result)
456
+ session_service.append_event(session_id: session_id, event: final_agent_event)
457
+ end
458
+ rescue StandardError => e
459
+ Legate.logger.error { "Agent '#{@name}': Error in after_agent_callback: #{e.message}\n#{e.backtrace.join("\n")}" }
460
+ # Don't override the result completely on error, just log it
461
+ end
462
+
463
+ # Apply the callback's pending state changes exactly once (whether or
464
+ # not it modified the result).
465
+ apply_pending_state(callback_context, session_id, session_service)
466
+ end
467
+
468
+ # Store the output if configured
469
+ _store_output_in_session(final_agent_event, session_id, session_service)
470
+
471
+ # Return the final agent event
472
+ final_agent_event
473
+ rescue StandardError => e
474
+ # Handle any other errors during execution. Record the failure in the
475
+ # session so its history reflects what the caller saw (the success and
476
+ # callback paths already append their events).
477
+ Legate.logger.error { "Agent '#{@name}' runtime error: #{e.message}\n#{e.backtrace.join("\n")}" }
478
+ record_error_event(session_id, session_service, e.message)
479
+ ensure
480
+ session_service.unsubscribe(event_subscription) if event_subscription && session_service.respond_to?(:unsubscribe)
481
+ end
482
+ end
483
+
484
+ # Subscribes on_event (if given) to the session's appended events for the
485
+ # duration of a run. Returns a handle for #run_task's ensure to remove, or
486
+ # nil when there's nothing to stream / the service has no pub/sub.
487
+ def subscribe_events(session_service, session_id, on_event)
488
+ return nil unless on_event && session_service.respond_to?(:subscribe)
489
+
490
+ session_service.subscribe(session_id, &on_event)
491
+ end
492
+ private :subscribe_events
493
+
494
+ # Flushes a callback's accumulated state delta into the session via the
495
+ # session service. Optionally clears the delta afterward (when execution
496
+ # continues and the same context will be reused).
497
+ def apply_pending_state(callback_context, session_id, session_service, clear: false)
498
+ return if callback_context.pending_state_delta.empty?
499
+
500
+ callback_context.pending_state_delta.each do |key, value|
501
+ session_service.set_state(session_id: session_id, key: key, value: value)
502
+ end
503
+ callback_context.clear_pending_state_delta! if clear
504
+ end
505
+
506
+ # Builds an agent error event, records it in the session history (best-effort:
507
+ # a failed append must not mask the original error), and returns it.
508
+ # @return [Legate::Event] the error event
509
+ def record_error_event(session_id, session_service, message)
510
+ event = Legate::Event.new(role: :agent, content: { status: :error, error_message: message })
511
+ begin
512
+ session_service.append_event(session_id: session_id, event: event)
513
+ rescue StandardError => e
514
+ Legate.logger.error { "Agent '#{@name}': failed to record error event in session: #{e.message}" }
515
+ end
516
+ event
517
+ end
518
+
519
+ # Returns the root agent in the hierarchy (the topmost agent with no parent)
520
+ # @return [Legate::Agent] The root agent in the hierarchy
521
+ def root_agent
522
+ return self if @parent_agent.nil?
523
+
524
+ @parent_agent.root_agent
525
+ end
526
+
527
+ # Finds an agent with the given name in the hierarchy using DFS
528
+ # @param name_sym [Symbol] The name of the agent to find (as a symbol)
529
+ # @return [Legate::Agent, nil] The agent with the given name, or nil if not found
530
+ def find_agent(name_sym)
531
+ # Convert to symbol if string provided
532
+ name_sym = name_sym.to_sym if name_sym.is_a?(String)
533
+
534
+ # Check if this is the agent we're looking for
535
+ return self if @name.to_sym == name_sym
536
+
537
+ # Search sub-agents recursively
538
+ @sub_agents.each do |sub_agent|
539
+ found = sub_agent.find_agent(name_sym)
540
+ return found if found
541
+ end
542
+
543
+ # Not found in this branch
544
+ nil
545
+ end
546
+
547
+ # Finds a direct sub-agent with the given name
548
+ # @param name_sym [Symbol] The name of the sub-agent to find
549
+ # @return [Legate::Agent, nil] The sub-agent with the given name, or nil if not found
550
+ def find_sub_agent(name_sym)
551
+ # Convert to symbol if string provided
552
+ name_sym = name_sym.to_sym if name_sym.is_a?(String)
553
+
554
+ return nil unless @sub_agents.is_a?(Array)
555
+
556
+ @sub_agents.find { |sub_agent| sub_agent.name.to_sym == name_sym }
557
+ end
558
+
559
+ # Transfers control to another agent, executing a task with the same session context.
560
+ # This is a public version of the private transfer_to method
561
+ #
562
+ # @param target_agent_name [Symbol] The name of the target agent to delegate to
563
+ # @param task [String] The task to delegate to the target agent
564
+ # @param session_id [String] The current session ID
565
+ # @param session_service [Legate::SessionService::Base] The session service instance
566
+ # @return [Hash] A standard result hash { status: :success/:error, result/error_message: ... }
567
+ def transfer_to(target_agent_name, task, session_id, session_service)
568
+ # Verify the target agent is in the delegation_targets list if defined
569
+ if @definition.respond_to?(:delegation_targets) && @definition.delegation_targets&.any? && !@definition.delegation_targets.include?(target_agent_name)
570
+ error_msg = "Agent '#{target_agent_name}' is not in the delegation targets for '#{@name}'"
571
+ Legate.logger.error(error_msg)
572
+ return { status: :error, error_message: error_msg, error_class: 'InvalidDelegationTarget' }
573
+ end
574
+
575
+ # Find the target agent in the agent hierarchy, starting from the root
576
+ target_agent = root_agent.find_agent(target_agent_name)
577
+
578
+ # If not found in hierarchy, try to instantiate from definition store
579
+ unless target_agent
580
+ Legate.logger.info("Target agent '#{target_agent_name}' not found in hierarchy. Attempting to load from definition store.")
581
+
582
+ begin
583
+ # Try to find the definition in the global registry
584
+ target_def = Legate::GlobalDefinitionRegistry.find(target_agent_name)
585
+
586
+ unless target_def
587
+ error_msg = "Target agent definition '#{target_agent_name}' not found in registry"
588
+ Legate.logger.error(error_msg)
589
+ return { status: :error, error_message: error_msg, error_class: 'AgentDefinitionNotFound' }
590
+ end
591
+
592
+ # Create a new agent instance from the definition
593
+ target_agent = Legate::Agent.new(
594
+ definition: target_def,
595
+ session_service: session_service
596
+ )
597
+ rescue StandardError => e
598
+ error_msg = "Failed to instantiate target agent '#{target_agent_name}': #{e.message}"
599
+ Legate.logger.error("#{error_msg}\n#{e.backtrace.join("\n")}")
600
+ return { status: :error, error_message: error_msg, error_class: e.class.name }
601
+ end
602
+ end
603
+
604
+ # Verify the target agent exists
605
+ unless target_agent
606
+ error_msg = "Target agent '#{target_agent_name}' not found in hierarchy or definition store"
607
+ Legate.logger.error(error_msg)
608
+ return { status: :error, error_message: error_msg, error_class: 'AgentNotFound' }
609
+ end
610
+
611
+ # Start the target agent if it's not already running
612
+ target_agent.start unless target_agent.running?
613
+
614
+ # Execute the delegated task
615
+ begin
616
+ Legate.logger.info("Executing delegated task on agent '#{target_agent_name}': #{task}")
617
+
618
+ # Call run_task with the same session context
619
+ result_event = target_agent.run_task(
620
+ session_id: session_id,
621
+ user_input: task,
622
+ session_service: session_service
623
+ )
624
+
625
+ # Extract and format the result
626
+ result_content = result_event.respond_to?(:content) ? result_event.content : result_event
627
+
628
+ {
629
+ status: :success,
630
+ target_agent: target_agent_name.to_s,
631
+ result: result_content
632
+ }
633
+ rescue StandardError => e
634
+ error_msg = "Error executing task on target agent '#{target_agent_name}': #{e.message}"
635
+ Legate.logger.error("#{error_msg}\n#{e.backtrace.join("\n")}")
636
+ { status: :error, error_message: error_msg, error_class: e.class.name }
637
+ end
638
+ end
639
+
640
+ private
641
+
642
+ def setup_tool_registry(definition)
643
+ tool_classes_to_load = definition.tool_names.map { |tn| Legate::GlobalToolManager.find_class(tn) }.compact
644
+
645
+ if tool_classes_to_load.length != definition.tool_names.length
646
+ found_tool_names = tool_classes_to_load.map { |tc|
647
+ begin
648
+ tc.tool_metadata[:name].to_sym
649
+ rescue StandardError
650
+ nil
651
+ end
652
+ }.compact.to_set
653
+ missing_tool_names = definition.tool_names.to_set - found_tool_names
654
+ Legate.logger.warn(missing_tools_warning(missing_tool_names, definition)) if missing_tool_names.any?
655
+ end
656
+
657
+ @tool_registry = Legate::ToolRegistry.new
658
+ Legate.logger.debug("Agent '#{@name}' created its ToolRegistry instance: #{@tool_registry.object_id}")
659
+
660
+ tool_classes_to_load.each do |tool_class|
661
+ Legate.logger.debug("[Agent Init '#{@name}'] Processing class from builder: #{tool_class.inspect} (Object ID: #{tool_class.object_id})")
662
+ register_tool_class(tool_class)
663
+ end
664
+
665
+ return if @tool_registry.find_class(:check_job_status)
666
+
667
+ begin
668
+ require_relative 'tools/check_job_status_tool'
669
+ register_tool_class(Legate::Tools::CheckJobStatusTool)
670
+ Legate.logger.info("Automatically registered CheckJobStatusTool for agent '#{@name}'.")
671
+ rescue LoadError => e
672
+ Legate.logger.error("Failed to load CheckJobStatusTool: #{e.message}")
673
+ end
674
+ end
675
+
676
+ # Builds an actionable warning for selected tools with no registered class:
677
+ # a did-you-mean suggestion per name + the available tools. If the agent has
678
+ # MCP servers configured, the names may be MCP tools (registered at connect
679
+ # time), so the message softens rather than crying wolf.
680
+ def missing_tools_warning(missing_tool_names, definition)
681
+ available = Legate::GlobalToolManager.registered_tool_names.map(&:to_s).sort
682
+ checker = DidYouMean::SpellChecker.new(dictionary: available)
683
+ described = missing_tool_names.map do |name|
684
+ suggestions = checker.correct(name.to_s)
685
+ suggestions.empty? ? name.to_s : "#{name} (did you mean: #{suggestions.join(', ')}?)"
686
+ end
687
+
688
+ msg = "Agent '#{@name}': no registered tool for #{described.join('; ')}. " \
689
+ "Available tools: #{available.join(', ')}."
690
+ has_mcp = definition.respond_to?(:mcp_servers) && Array(definition.mcp_servers).any?
691
+ msg + (has_mcp ? ' (MCP tools register when the agent connects, so this may be expected.)' : ' These tools will be unavailable.')
692
+ end
693
+
694
+ def setup_mcp_config(definition)
695
+ mcp_servers_config_str = definition.mcp_servers || []
696
+ if mcp_servers_config_str.is_a?(String) && !mcp_servers_config_str.strip.empty?
697
+ # String-based MCP config parsing handled by existing logic
698
+ elsif mcp_servers_config_str.is_a?(Array)
699
+ @mcp_servers_config = mcp_servers_config_str
700
+ else
701
+ Legate.logger.debug("Agent '#{@name}': No valid MCP server config provided. Defaulting to empty array.")
702
+ @mcp_servers_config = []
703
+ end
704
+ end
705
+
706
+ def setup_sub_agents(definition, sub_agents)
707
+ if sub_agents && !sub_agents.empty?
708
+ link_provided_sub_agents(sub_agents)
709
+ elsif definition.respond_to?(:sub_agent_names) && definition.sub_agent_names&.any?
710
+ instantiate_sub_agents_from_definition(definition)
711
+ end
712
+ end
713
+
714
+ # Sets this agent as the sub-agent's parent, or returns false if the sub-agent
715
+ # already belongs to a different parent (the caller should then skip it).
716
+ # Idempotent when the parent is already this agent.
717
+ # @return [Boolean] true if linked (or already linked to self), false to skip
718
+ def link_parent_or_skip(sub_agent)
719
+ if sub_agent.parent_agent.nil?
720
+ sub_agent.instance_variable_set(:@parent_agent, self)
721
+ true
722
+ elsif sub_agent.parent_agent == self
723
+ true
724
+ else
725
+ Legate.logger.error("Agent '#{@name}': sub-agent '#{sub_agent.name}' already has a different parent: '#{sub_agent.parent_agent.name}'. Skipping.")
726
+ false
727
+ end
728
+ end
729
+
730
+ def link_provided_sub_agents(sub_agents)
731
+ Legate.logger.info("Agent '#{@name}': Initializing with programmatically provided sub-agents (#{sub_agents.length} agents).")
732
+ sub_agents.each do |sub_agent|
733
+ unless sub_agent.is_a?(Legate::Agent)
734
+ Legate.logger.warn("Agent '#{@name}': Item in provided sub_agents list is not an Legate::Agent. Skipping: #{sub_agent.inspect}")
735
+ next
736
+ end
737
+
738
+ begin
739
+ _check_circular_dependency(sub_agent.name)
740
+ rescue Legate::ConfigurationError => e
741
+ Legate.logger.error("Agent '#{@name}': #{e.message}")
742
+ next
743
+ end
744
+
745
+ next unless link_parent_or_skip(sub_agent)
746
+
747
+ if sub_agent.instance_variable_get(:@session_service).nil? && @session_service
748
+ Legate.logger.debug("Agent '#{@name}': Setting session_service for programmatic sub-agent '#{sub_agent.name}' to match parent.")
749
+ sub_agent.instance_variable_set(:@session_service, @session_service)
750
+ elsif sub_agent.instance_variable_get(:@session_service) != @session_service && @session_service
751
+ Legate.logger.warn("Agent '#{@name}': Programmatic sub-agent '#{sub_agent.name}' has a different session_service than parent.")
752
+ end
753
+ @sub_agents << sub_agent
754
+ Legate.logger.info("Agent '#{@name}': Successfully instantiated and linked sub-agent '#{sub_agent.name}'.")
755
+ end
756
+ Legate.logger.info("Agent '#{@name}' finished linking programmatic sub-agents. Total sub-agents: #{@sub_agents.length}")
757
+ end
758
+
759
+ def instantiate_sub_agents_from_definition(definition)
760
+ Legate.logger.info("Agent '#{@name}' attempting to instantiate sub-agents from definition: #{definition.sub_agent_names.to_a.inspect}")
761
+ definition.sub_agent_names.each do |sub_agent_name|
762
+ _check_circular_dependency(sub_agent_name)
763
+
764
+ sub_agent_definition = Legate::GlobalDefinitionRegistry.find(sub_agent_name)
765
+ unless sub_agent_definition
766
+ Legate.logger.error("Agent '#{@name}': Could not find definition for sub-agent '#{sub_agent_name}' in GlobalDefinitionRegistry. Skipping.")
767
+ next
768
+ end
769
+
770
+ Legate.logger.debug("Agent '#{@name}': Instantiating sub-agent '#{sub_agent_name}'...")
771
+ sub_agent = Legate::Agent.new(definition: sub_agent_definition, session_service: @session_service)
772
+ next unless link_parent_or_skip(sub_agent)
773
+
774
+ @sub_agents << sub_agent
775
+ Legate.logger.info("Agent '#{@name}': Successfully instantiated and linked sub-agent '#{sub_agent.name}'.")
776
+ rescue ArgumentError => e
777
+ Legate.logger.error("Agent '#{@name}': ArgumentError instantiating sub-agent '#{sub_agent_name}': #{e.message}")
778
+ rescue StandardError => e
779
+ Legate.logger.error("Agent '#{@name}': Unexpected error instantiating sub-agent '#{sub_agent_name}': #{e.class} - #{e.message}\n#{e.backtrace.first(5).join("\n")}")
780
+ end
781
+ Legate.logger.info("Agent '#{@name}' finished sub-agent instantiation. Total sub-agents: #{@sub_agents.length}")
782
+ end
783
+
784
+ # Build the agent-specific authentication configuration hash for ToolContext
785
+ # @return [Hash, nil] The auth config hash or nil if no auth configured
786
+ def build_agent_auth_config
787
+ return nil if @auth_credential_names.empty? &&
788
+ @auth_url_mappings.empty? &&
789
+ @auth_scheme_assignments.empty? &&
790
+ @auth_credential_assignments.empty?
791
+
792
+ {
793
+ credential_names: @auth_credential_names,
794
+ url_mappings: @auth_url_mappings,
795
+ scheme_assignments: @auth_scheme_assignments,
796
+ credential_assignments: @auth_credential_assignments
797
+ }
798
+ end
799
+
800
+ # Helper method to consistently determine the tool name from a tool class.
801
+ # Uses metadata, then deprecated @tool_name, then inferred_name.
802
+ def get_tool_name_from_class(tool_class)
803
+ return nil unless tool_class.is_a?(Class) && tool_class < Legate::Tool
804
+
805
+ begin
806
+ metadata = tool_class.tool_metadata
807
+ rescue StandardError => e
808
+ Legate.logger.error("Error calling tool_metadata on #{tool_class}: #{e.class} - #{e.message} - Backtrace: #{e.backtrace.first(3).join(' | ')}")
809
+ metadata = {} # Default to empty hash if metadata call fails, for diagnosis
810
+ end
811
+ name = metadata[:name]&.to_sym
812
+
813
+ if name.nil? || name == :''
814
+ # Check deprecated @tool_name (instance variable on the class itself)
815
+ if tool_class.instance_variable_defined?(:@tool_name)
816
+ name = tool_class.instance_variable_get(:@tool_name)&.to_sym
817
+ # Legate.logger.debug { "get_tool_name_from_class: Using name from deprecated @tool_name for #{tool_class}: #{name.inspect}" } if name
818
+ end
819
+
820
+ # If still no name, try inferred_name as a primary fallback if metadata[:name] is missing
821
+ if (name.nil? || name == '') && tool_class.respond_to?(:inferred_name)
822
+ name = tool_class.inferred_name
823
+ # Legate.logger.debug { "get_tool_name_from_class: Using inferred_name for #{tool_class}: #{name.inspect}" } if name
824
+ end
825
+ end
826
+
827
+ name && name != :'' ? name : nil
828
+ end
829
+
830
+ # --- REFACTORED: execute_plan now returns hash { details: [...], last_result: original_hash } ---
831
+ # Executes a plan, logging tool request/result events via the session service.
832
+ # @param plan [Hash, Array] The plan from the planner, either as a hash with :thought_process and :steps, or as an array of steps.
833
+ # @param session [Legate::Session] The current session object.
834
+ # @param session_service [Object] The session service instance.
835
+ # @return [Hash] { details: Array<Hash>, last_result: Hash } or { details: Hash, last_result: nil } on planning errors.
836
+ # Executes a planner-produced plan. Delegates to the agent's PlanExecutor;
837
+ # kept here as the entry point called by #run_task.
838
+ def execute_plan(plan, session, session_service, invocation_id)
839
+ @plan_executor.execute_plan(plan, session, session_service, invocation_id)
840
+ end
841
+
842
+ # True when this agent should use the agentic ReAct loop instead of the
843
+ # default plan-then-execute strategy.
844
+ def react_strategy?
845
+ @definition.respond_to?(:planning_strategy) && @definition.planning_strategy == :react
846
+ end
847
+
848
+ # Drives the agentic observe->think->act loop. Reuses the agent's existing
849
+ # planner and PlanExecutor, so tool execution, event logging, and state
850
+ # deltas behave identically to the default strategy.
851
+ def run_react_loop(user_input, session, session_service, invocation_id)
852
+ Legate::Agentic::Loop.new(
853
+ planner: @planner,
854
+ executor: @plan_executor,
855
+ logger: Legate.logger
856
+ ).run(
857
+ user_input: user_input,
858
+ session: session,
859
+ session_service: session_service,
860
+ invocation_id: invocation_id
861
+ )
862
+ end
863
+
864
+ # --- REFACTORED: execute_step uses session context and passes it to tools ---
865
+ # Executes a single step, logging :tool_request and :tool_result events via session service.
866
+ # @param step [Hash] A hash like { tool: :symbol, params: {...} }.
867
+ # @param session [Legate::Session] The current session object.
868
+ # @param session_service [Object] The session service instance.
869
+ # @param invocation_id [String] The ID of the current agent invocation.
870
+ # @return [Hash] A standard result hash { status: ..., result/error_message/job_id: ... }.
871
+ # Executes a single plan step. Delegates to the agent's PlanExecutor; kept
872
+ # here as a private method because specs and the test-only custom_agent_patch
873
+ # drive it via `send(:execute_step, ...)`.
874
+ def execute_step(step, session, session_service, invocation_id = nil)
875
+ @plan_executor.execute_step(step, session, session_service, invocation_id)
876
+ end
877
+
878
+ # Connects to all configured MCP servers.
879
+ # Connects the agent's configured MCP servers and registers their tools.
880
+ # Delegates to the agent's McpConnectionManager (kept as a lifecycle hook
881
+ # called from #start and exercised directly in specs).
882
+ def connect_mcp_servers
883
+ @mcp_manager.connect(@mcp_servers_config)
884
+ end
885
+
886
+ # Disconnects all active MCP clients.
887
+ def disconnect_mcp_servers
888
+ @mcp_manager.disconnect
889
+ end
890
+
891
+ # Helper method to check for circular dependencies in the agent hierarchy
892
+ # @param new_sub_agent_name [Symbol] The name of the new sub-agent to check for cycles
893
+ # @raise [Legate::ConfigurationError] If a circular dependency is detected
894
+ private def _check_circular_dependency(new_sub_agent_name)
895
+ # Direct self-reference check
896
+ raise Legate::ConfigurationError, "Circular dependency detected: Agent '#{@name}' cannot include itself as a sub-agent" if new_sub_agent_name == @name
897
+
898
+ # Check if the sub-agent would create an indirect circular reference
899
+ # by traversing up the parent chain (backwards check)
900
+ current_agent = self
901
+ ancestry_path = [@name]
902
+
903
+ while (parent = current_agent.parent_agent)
904
+ # If any parent has the same name as the new sub-agent, it's a circular reference
905
+ if parent.name == new_sub_agent_name
906
+ circular_path = [new_sub_agent_name] + ancestry_path
907
+ raise Legate::ConfigurationError, "Circular dependency detected: #{circular_path.join(' → ')}"
908
+ end
909
+
910
+ ancestry_path.unshift(parent.name)
911
+ current_agent = parent
912
+ end
913
+ end
914
+
915
+ # --- MAS: Store result in session state if output_key is defined --- #
916
+ def _store_output_in_session(event, session_id, session_service)
917
+ return unless @definition.respond_to?(:output_key) && @definition.output_key && event
918
+
919
+ # Get the content, which now should be the last_result only
920
+ output_value = event.content
921
+
922
+ # For Hash results, ensure a plan_details key exists (back-compat). Non-Hash
923
+ # results (a tool/callback returning a bare string, number, array, …) are
924
+ # stored as-is — calling #key? on them would raise NoMethodError.
925
+ needs_plan_details = output_value.is_a?(Hash) &&
926
+ !output_value.key?(:plan_details) && !output_value.key?('plan_details')
927
+ output_value = output_value.merge(plan_details: []) if needs_plan_details
928
+
929
+ serialized_value = begin
930
+ # If the value is a Hash or Array, deep transform keys and symbolized values
931
+ if output_value.is_a?(Hash) || output_value.is_a?(Array)
932
+ # Convert to JSON and back to remove symbols
933
+ JSON.parse(output_value.to_json)
934
+ else
935
+ # For other values, just pass through
936
+ output_value
937
+ end
938
+ rescue StandardError => e
939
+ # If serialization fails, log and return the original
940
+ Legate.logger.warn("Agent '#{@name}': Failed to serialize output value: #{e.message}. Using original value.")
941
+ output_value
942
+ end
943
+
944
+ Legate.logger.info("Agent '#{@name}' storing output to session state with key '#{@definition.output_key}' for session '#{session_id}'.")
945
+
946
+ begin
947
+ # Ensure session_service has set_state. Add if missing for base/inmemory.
948
+ if session_service.respond_to?(:set_state)
949
+ session_service.set_state(session_id: session_id, key: @definition.output_key, value: serialized_value)
950
+ else
951
+ Legate.logger.warn("Agent '#{@name}': Session service does not support :set_state. Cannot store output for key '#{@definition.output_key}'.")
952
+ end
953
+ rescue StandardError => e
954
+ Legate.logger.error("Agent '#{@name}': Failed to set state for key '#{@definition.output_key}' in session '#{session_id}': #{e.class} - #{e.message}")
955
+ end
956
+ end
957
+ # --- End MAS State Management ---
958
+ end # End Agent class
959
+ end # End Legate module