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,812 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'json'
5
+ require_relative 'errors'
6
+
7
+ module Legate
8
+ class AgentDefinition
9
+ extend Forwardable
10
+
11
+ # @return [Symbol] The unique name identifying this agent definition.
12
+ attr_reader :name
13
+ # @return [String] A description of the agent's purpose.
14
+ attr_reader :description
15
+ # @return [String] The core instructions given to the language model.
16
+ attr_reader :instruction
17
+ # @return [Set<Symbol>] A set of names of the tools available to this agent.
18
+ attr_reader :tool_names
19
+ # @return [String, nil] The specific model name to use (e.g., "gpt-4-turbo"). Overrides global default.
20
+ attr_reader :model_name
21
+ # @return [Float, nil] The temperature setting for the model. Overrides global default.
22
+ attr_reader :temperature
23
+ # @return [Boolean] Whether this agent can be triggered by webhooks. Defaults to false.
24
+ attr_reader :webhook_enabled
25
+ # @return [Symbol, Proc, nil] The validator (name or proc) for webhook requests.
26
+ attr_reader :webhook_validator
27
+ # @return [String, nil] The secret key for webhook validation.
28
+ attr_reader :webhook_secret
29
+ # @return [Proc, nil] The transformer proc for webhook payloads.
30
+ attr_reader :webhook_transformer
31
+ # @return [Proc, nil] The session extractor proc for webhook requests.
32
+ attr_reader :webhook_session_extractor
33
+ # @return [Symbol] The fallback mode (:error or :echo). Defaults to :error.
34
+ attr_reader :fallback_mode
35
+ # @return [Array<Hash>] Configuration for MCP servers. Defaults to [].
36
+ attr_reader :mcp_servers
37
+ # @return [Set<Symbol>] A set of names of sub-agent definitions to instantiate.
38
+ attr_reader :sub_agent_names
39
+ # @return [Symbol, nil] The key under which the agent's final output should be stored in the session state.
40
+ attr_reader :output_key
41
+ # @return [Symbol] The type of agent (:llm, :sequential, :parallel, :loop). Defaults to :llm.
42
+ attr_reader :agent_type
43
+ # @return [Symbol] Planning strategy for :llm agents — :plan (one upfront plan, default)
44
+ # or :react (agentic observe->think->act loop).
45
+ attr_reader :planning_strategy
46
+ # @return [Set<Symbol>] A set of names of sub-agents to execute in sequence (for SequentialAgent).
47
+ attr_reader :sequential_sub_agent_names
48
+ # @return [Set<Symbol>] A set of names of sub-agents to execute in parallel (for ParallelAgent).
49
+ attr_reader :parallel_sub_agent_names
50
+ # @return [Set<Symbol>] A set of names of sub-agents to execute in each loop iteration (for LoopAgent).
51
+ attr_reader :loop_sub_agent_names
52
+ # @return [Integer, nil] The maximum number of loop iterations (for LoopAgent).
53
+ attr_reader :loop_max_iterations
54
+ # @return [Integer, nil] Wall-clock timeout in seconds for the entire loop (for LoopAgent).
55
+ attr_reader :loop_timeout_seconds
56
+ # @return [Symbol, nil] The key in the session state to check for loop condition (for LoopAgent).
57
+ attr_reader :loop_condition_state_key
58
+ # @return [Object, nil] The expected value for loop condition (for LoopAgent).
59
+ attr_reader :loop_condition_expected_value
60
+ # @return [Integer, nil] Timeout in seconds for each parallel sub-agent (for ParallelAgent). Defaults to 120.
61
+ attr_reader :parallel_timeout_seconds
62
+ # @return [Set<Symbol>] A set of names of agents that this agent can delegate tasks to via LLM planning.
63
+ attr_reader :delegation_targets
64
+
65
+ # --- Authentication Attributes ---
66
+ # @return [Set<Symbol>] A set of credential names this agent can use.
67
+ attr_reader :auth_credential_names
68
+ # @return [Array<Hash>] URL pattern to scheme/credential mappings for this agent.
69
+ attr_reader :auth_url_mappings
70
+ # @return [Hash<Symbol, Symbol>] Service to scheme name assignments.
71
+ attr_reader :auth_scheme_assignments
72
+ # @return [Hash<Symbol, Symbol>] Service to credential name assignments.
73
+ attr_reader :auth_credential_assignments
74
+
75
+ # --- Callback Attributes ---
76
+ # @return [Proc, nil] Callback run before agent execution begins
77
+ attr_reader :before_agent_callback
78
+ # @return [Proc, nil] Callback run after agent execution completes
79
+ attr_reader :after_agent_callback
80
+ # @return [Proc, nil] Callback run before LLM model interaction
81
+ attr_reader :before_model_callback
82
+ # @return [Proc, nil] Callback run after LLM model interaction
83
+ attr_reader :after_model_callback
84
+ # @return [Proc, nil] Callback run before any tool execution
85
+ attr_reader :before_tool_callback
86
+ # @return [Proc, nil] Callback run after any tool execution
87
+ attr_reader :after_tool_callback
88
+
89
+ # --- End Callback Attributes ---
90
+
91
+ # Delegate common attributes to the definition proxy for easier access during definition
92
+ # Only delegate methods needed *within* the define block
93
+ def_delegators :@proxy, :use_tool
94
+
95
+ def initialize
96
+ @name = nil
97
+ @description = ''
98
+ @instruction = ''
99
+ @tool_names = Set.new
100
+ @model_name = nil
101
+ @temperature = nil
102
+ # --- Webhook Defaults ---
103
+ @webhook_enabled = false
104
+ @webhook_validator = nil
105
+ @webhook_secret = nil
106
+ @webhook_transformer = nil
107
+ @webhook_session_extractor = nil
108
+ @fallback_mode = :error # Default fallback mode
109
+ @mcp_servers = [] # Default MCP servers
110
+ @sub_agent_names = Set.new # MAS attribute for sub-agent definitions
111
+ @output_key = nil # MAS attribute for state management
112
+ # --- MAS Workflow Agent Attributes ---
113
+ @agent_type = :llm # Default agent type
114
+ @planning_strategy = :plan # :plan (upfront plan) or :react (agentic loop)
115
+ @sequential_sub_agent_names = Set.new # For SequentialAgent
116
+ @parallel_sub_agent_names = Set.new # For ParallelAgent
117
+ @loop_sub_agent_names = Set.new # For LoopAgent
118
+ @loop_max_iterations = nil # Maximum number of loop iterations
119
+ @loop_timeout_seconds = nil
120
+ @parallel_timeout_seconds = nil
121
+ @loop_condition_state_key = nil
122
+ @loop_condition_expected_value = nil # Expected value for loop condition
123
+ @delegation_targets = Set.new # Agent names that this agent can delegate to
124
+ # -----------------------
125
+
126
+ # --- Authentication Attributes ---
127
+ @auth_credential_names = Set.new # Credential names this agent can use
128
+ @auth_url_mappings = [] # URL pattern to scheme/credential mappings
129
+ @auth_scheme_assignments = {} # Service to scheme assignments
130
+ @auth_credential_assignments = {} # Service to credential assignments
131
+ # -----------------------
132
+
133
+ @proxy = DefinitionProxy.new(self)
134
+ end
135
+
136
+ # DSL method used within `Agent.define` block.
137
+ # @param block [Proc] The block containing the definition DSL calls.
138
+ def define(&block)
139
+ @proxy.instance_eval(&block)
140
+ validate!
141
+ self
142
+ end
143
+
144
+ # Validates that the definition has all required fields.
145
+ # @raise [ArgumentError] If validation fails.
146
+ def validate!
147
+ raise ArgumentError, 'Agent definition must have a name.' if @name.nil? || @name.to_s.strip.empty?
148
+ raise ArgumentError, 'Agent name must be a Symbol.' unless @name.is_a?(Symbol)
149
+
150
+ # Instruction is optional: a tool-only agent doesn't need a hand-written
151
+ # system prompt, so derive a sensible default from the name/description
152
+ # rather than failing the build.
153
+ if @instruction.nil? || @instruction.strip.empty?
154
+ description = @description.to_s.strip
155
+ @instruction = "You are the #{@name} agent.#{description.empty? ? '' : " #{description}"} " \
156
+ 'Use the available tools to help the user.'
157
+ end
158
+
159
+ # Explicitly check instance variable to bypass potential method resolution issues
160
+ return unless @webhook_enabled
161
+
162
+ # raise ArgumentError, "Agent '#{@name}' enabled for webhooks must define a webhook_transformer." unless @webhook_transformer.is_a?(Proc)
163
+ # raise ArgumentError, "Agent '#{@name}' enabled for webhooks must define a webhook_session_extractor." unless @webhook_session_extractor.is_a?(Proc)
164
+ Legate.logger.warn { "Agent '#{@name}' is webhook_enabled but lacks a valid :webhook_transformer Proc." } unless @webhook_transformer.is_a?(Proc)
165
+ return if @webhook_session_extractor.is_a?(Proc)
166
+
167
+ Legate.logger.warn { "Agent '#{@name}' is webhook_enabled but lacks a valid :webhook_session_extractor Proc." }
168
+ end
169
+
170
+ # Returns a hash representation suitable for logging or inspection.
171
+ # @return [Hash]
172
+ def to_h
173
+ {
174
+ name: @name,
175
+ description: @description,
176
+ instruction: @instruction,
177
+ tool_names: @tool_names.to_a,
178
+ model_name: @model_name,
179
+ temperature: @temperature,
180
+ webhook_enabled: @webhook_enabled,
181
+ webhook_validator: @webhook_validator.is_a?(Proc) ? '<Proc>' : @webhook_validator,
182
+ webhook_secret: @webhook_secret ? '<present>' : nil,
183
+ webhook_transformer: @webhook_transformer.is_a?(Proc) ? '<Proc>' : nil,
184
+ webhook_session_extractor: @webhook_session_extractor.is_a?(Proc) ? '<Proc>' : nil,
185
+ fallback_mode: @fallback_mode,
186
+ mcp_servers: @mcp_servers,
187
+ sub_agent_names: @sub_agent_names.to_a, # MAS attribute
188
+ output_key: @output_key, # MAS attribute
189
+ # Adding new MAS attributes for agent hierarchy and workflow
190
+ agent_type: @agent_type || :llm, # Default to :llm if not set
191
+ planning_strategy: @planning_strategy || :plan, # :plan (default) or :react
192
+ sequential_sub_agent_names: @sequential_sub_agent_names&.to_a || [],
193
+ parallel_sub_agent_names: @parallel_sub_agent_names&.to_a || [],
194
+ loop_sub_agent_names: @loop_sub_agent_names&.to_a || [],
195
+ loop_max_iterations: @loop_max_iterations,
196
+ loop_condition_state_key: @loop_condition_state_key,
197
+ loop_condition_expected_value: @loop_condition_expected_value,
198
+ delegation_targets: @delegation_targets&.to_a || [],
199
+ # --- Authentication fields ---
200
+ auth_credential_names: @auth_credential_names&.to_a || [],
201
+ auth_url_mappings: (@auth_url_mappings || []).map do |m|
202
+ {
203
+ pattern: m[:pattern].is_a?(Regexp) ? m[:pattern].source : m[:pattern],
204
+ pattern_type: m[:pattern].is_a?(Regexp) ? 'regexp' : 'string',
205
+ scheme_name: m[:scheme_name]&.to_s,
206
+ credential_name: m[:credential_name]&.to_s
207
+ }
208
+ end,
209
+ auth_scheme_assignments: (@auth_scheme_assignments || {}).transform_keys(&:to_s).transform_values(&:to_s),
210
+ auth_credential_assignments: (@auth_credential_assignments || {}).transform_keys(&:to_s).transform_values(&:to_s)
211
+ }
212
+ end
213
+
214
+ # Internal proxy class to provide a clean DSL within the `define` block.
215
+ class DefinitionProxy
216
+ def initialize(definition)
217
+ @definition = definition
218
+ end
219
+
220
+ # Sets the agent name.
221
+ # @param name [Symbol] Unique identifier for the agent.
222
+ def name(name)
223
+ raise ArgumentError, 'Agent name must be a Symbol.' unless name.is_a?(Symbol)
224
+
225
+ @definition.instance_variable_set(:@name, name)
226
+ end
227
+
228
+ # Sets the agent description.
229
+ # @param description [String]
230
+ def description(description)
231
+ @definition.instance_variable_set(:@description, description.to_s)
232
+ end
233
+
234
+ # Sets the agent's core instruction.
235
+ # @param instruction [String]
236
+ def instruction(instruction)
237
+ @definition.instance_variable_set(:@instruction, instruction.to_s)
238
+ end
239
+
240
+ # Selects a tool for the agent to use. Accepts the tool's registered name
241
+ # (Symbol or String, coerced to a Symbol) or a Legate::Tool subclass — the
242
+ # class is registered globally and selected by its metadata name in one
243
+ # step, so you don't have to call GlobalToolManager.register_tool yourself.
244
+ # @param tool [Symbol, String, Class<Legate::Tool>]
245
+ # @param options [Hash] Tool-specific options (currently unused).
246
+ def use_tool(tool, _options = {})
247
+ names = @definition.instance_variable_get(:@tool_names)
248
+ if tool.is_a?(Class) && tool < Legate::Tool
249
+ Legate::GlobalToolManager.register_tool(tool)
250
+ registered_name = tool.tool_metadata[:name]
251
+ raise ArgumentError, "Tool class #{tool} has no resolvable name to register." if registered_name.nil?
252
+
253
+ names << registered_name.to_sym
254
+ elsif tool.is_a?(Symbol) || tool.is_a?(String)
255
+ names << tool.to_sym
256
+ else
257
+ raise ArgumentError,
258
+ "use_tool expects a tool name (Symbol/String) or a Legate::Tool subclass, got #{tool.class}."
259
+ end
260
+ end
261
+
262
+ # Sets the specific model name for this agent.
263
+ # @param model_name [String, Symbol]
264
+ def model_name(model_name)
265
+ @definition.instance_variable_set(:@model_name, model_name.to_sym)
266
+ end
267
+
268
+ # Sets the temperature for this agent.
269
+ # @param temperature [Float]
270
+ def temperature(temperature)
271
+ @definition.instance_variable_set(:@temperature, temperature.to_f)
272
+ end
273
+
274
+ # --- Webhook Configuration DSL ---
275
+
276
+ # Enables or disables webhook triggering for this agent.
277
+ # @param enabled [Boolean]
278
+ def webhook_enabled(enabled)
279
+ @definition.instance_variable_set(:@webhook_enabled, !!enabled)
280
+ end
281
+
282
+ # Sets the validator for webhook requests.
283
+ # @param validator [Symbol, Proc, nil] The name of a registered validator, a Proc, or nil.
284
+ def webhook_validator(validator)
285
+ # Allow nil, Symbol, or Proc
286
+ raise ArgumentError, 'webhook_validator must be a Symbol, a Proc, or nil.' unless validator.nil? || validator.is_a?(Symbol) || validator.is_a?(Proc)
287
+
288
+ @definition.instance_variable_set(:@webhook_validator, validator)
289
+ end
290
+
291
+ # Sets the secret key for webhook validation.
292
+ # @param secret [String]
293
+ def webhook_secret(secret)
294
+ @definition.instance_variable_set(:@webhook_secret, secret)
295
+ end
296
+
297
+ # Sets the transformer proc for webhook payloads.
298
+ # @param transformer_proc [Proc, nil] The transformer proc or nil.
299
+ def webhook_transformer(transformer_proc)
300
+ # Allow nil or Proc
301
+ unless transformer_proc.nil? || transformer_proc.is_a?(Proc)
302
+ raise ArgumentError,
303
+ 'webhook_transformer must be a Proc or nil.'
304
+ end
305
+
306
+ @definition.instance_variable_set(:@webhook_transformer, transformer_proc)
307
+ end
308
+
309
+ # Sets the session extractor proc for webhook requests.
310
+ # @param extractor_proc [Proc, nil] The extractor proc or nil.
311
+ def webhook_session_extractor(extractor_proc)
312
+ # Allow nil or Proc
313
+ unless extractor_proc.nil? || extractor_proc.is_a?(Proc)
314
+ raise ArgumentError,
315
+ 'webhook_session_extractor must be a Proc or nil.'
316
+ end
317
+
318
+ @definition.instance_variable_set(:@webhook_session_extractor, extractor_proc)
319
+ end
320
+
321
+ # Sets the fallback mode for the agent.
322
+ # @param mode [Symbol] :error or :echo.
323
+ def fallback_mode(mode)
324
+ valid_modes = %i[error echo]
325
+ raise ArgumentError, "Invalid fallback_mode '#{mode}'. Must be one of: #{valid_modes.join(', ')}." unless valid_modes.include?(mode)
326
+
327
+ @definition.instance_variable_set(:@fallback_mode, mode)
328
+ end
329
+
330
+ # Configures MCP servers for the agent.
331
+ # @param server_configs [Hash, Array<Hash>] MCP server configuration(s).
332
+ def mcp_servers(*server_configs)
333
+ configs = Array(server_configs).flatten.compact
334
+ # Basic validation: Ensure it's an array of hashes?
335
+ raise ArgumentError, 'MCP server configurations must be provided as Hashes.' unless configs.all? { |c| c.is_a?(Hash) }
336
+
337
+ @definition.instance_variable_set(:@mcp_servers, configs)
338
+ end
339
+ # -----------------------------
340
+
341
+ # --- MAS Attributes DSL ---
342
+ # Defines the names of sub-agents that should be instantiated under this agent.
343
+ # @param names [Array<Symbol>] An array of sub-agent definition names.
344
+ def sub_agents_define(*names)
345
+ flat_names = names.flatten.map(&:to_sym)
346
+ invalid_names = flat_names.reject { |n| n.is_a?(Symbol) }
347
+ raise ArgumentError, "Sub-agent names must all be Symbols. Invalid names: #{invalid_names.join(', ')}" unless invalid_names.empty?
348
+
349
+ @definition.instance_variable_set(:@sub_agent_names, @definition.instance_variable_get(:@sub_agent_names).merge(flat_names))
350
+ end
351
+
352
+ # Sets the key under which the agent's final output should be stored in session state.
353
+ # @param key_name [Symbol] The key name.
354
+ def output_key(key_name)
355
+ raise ArgumentError, 'Output key must be a Symbol.' unless key_name.is_a?(Symbol)
356
+
357
+ @definition.instance_variable_set(:@output_key, key_name)
358
+ end
359
+
360
+ # --- MAS Workflow Agent Type ---
361
+ # Sets the agent type
362
+ # @param type [Symbol] The agent type (:llm, :sequential, :parallel, :loop)
363
+ def agent_type(type)
364
+ valid_types = %i[llm sequential parallel loop]
365
+ raise ArgumentError, "Agent type must be one of: #{valid_types.join(', ')}. Got: #{type}" unless valid_types.include?(type.to_sym)
366
+
367
+ @definition.instance_variable_set(:@agent_type, type.to_sym)
368
+ end
369
+
370
+ # Sets the planning strategy for an :llm agent.
371
+ # @param strategy [Symbol] :plan (one upfront plan, the default) or
372
+ # :react (agentic observe->think->act loop that reacts to tool results).
373
+ def planning_strategy(strategy)
374
+ valid = %i[plan react]
375
+ raise ArgumentError, "Planning strategy must be one of: #{valid.join(', ')}. Got: #{strategy}" unless valid.include?(strategy.to_sym)
376
+
377
+ @definition.instance_variable_set(:@planning_strategy, strategy.to_sym)
378
+ end
379
+
380
+ # --- SequentialAgent Configuration ---
381
+ # Define sequential sub-agent names in order of execution
382
+ # @param names [Array<Symbol>] Names of sub-agents to execute in sequence
383
+ def sequential_sub_agents(*names)
384
+ flat_names = names.flatten.map(&:to_sym)
385
+ # Log a warning if the array is empty but don't raise an error
386
+ Legate.logger.warn("Empty sequential sub-agents list for agent '#{@definition.name}'") if flat_names.empty?
387
+
388
+ @definition.instance_variable_set(:@sequential_sub_agent_names, Set.new(flat_names))
389
+ end
390
+
391
+ # --- ParallelAgent Configuration ---
392
+ # Define parallel sub-agent names to execute concurrently
393
+ # @param names [Array<Symbol>] Names of sub-agents to execute in parallel
394
+ def parallel_sub_agents(*names)
395
+ flat_names = names.flatten.map(&:to_sym)
396
+ # Log a warning if the array is empty but don't raise an error
397
+ Legate.logger.warn("Empty parallel sub-agents list for agent '#{@definition.name}'") if flat_names.empty?
398
+
399
+ @definition.instance_variable_set(:@parallel_sub_agent_names, Set.new(flat_names))
400
+ end
401
+
402
+ # --- LoopAgent Configuration ---
403
+ # Define loop sub-agent names in order of execution within each loop iteration
404
+ # @param names [Array<Symbol>] Names of sub-agents to execute in each loop iteration
405
+ def loop_sub_agents(*names)
406
+ flat_names = names.flatten.map(&:to_sym)
407
+ # Log a warning if the array is empty but don't raise an error
408
+ Legate.logger.warn("Empty loop sub-agents list for agent '#{@definition.name}'") if flat_names.empty?
409
+
410
+ @definition.instance_variable_set(:@loop_sub_agent_names, Set.new(flat_names))
411
+ end
412
+
413
+ # Set maximum number of loop iterations
414
+ # @param max [Integer] The maximum number of iterations
415
+ def loop_max_iterations(max)
416
+ raise ArgumentError, "Maximum iterations must be a positive integer. Got: #{max}" unless max.is_a?(Integer) && max > 0
417
+
418
+ @definition.instance_variable_set(:@loop_max_iterations, max)
419
+ end
420
+
421
+ # Set wall-clock timeout for the entire loop execution
422
+ # @param seconds [Integer] The timeout in seconds
423
+ def loop_timeout_seconds(seconds)
424
+ raise ArgumentError, "Loop timeout must be a positive integer (seconds). Got: #{seconds}" unless seconds.is_a?(Integer) && seconds > 0
425
+
426
+ @definition.instance_variable_set(:@loop_timeout_seconds, seconds)
427
+ end
428
+
429
+ # Set timeout for each parallel sub-agent
430
+ # @param seconds [Integer] The timeout in seconds (default: 120)
431
+ def parallel_timeout_seconds(seconds)
432
+ raise ArgumentError, "Parallel timeout must be a positive integer (seconds). Got: #{seconds}" unless seconds.is_a?(Integer) && seconds > 0
433
+
434
+ @definition.instance_variable_set(:@parallel_timeout_seconds, seconds)
435
+ end
436
+
437
+ # Set the loop condition state key and expected value
438
+ # @param key [Symbol] The key in the session state to check
439
+ # @param value [Object] The expected value that indicates loop completion
440
+ def loop_condition(key, value)
441
+ raise ArgumentError, 'Loop condition key must be a Symbol.' unless key.is_a?(Symbol)
442
+
443
+ @definition.instance_variable_set(:@loop_condition_state_key, key)
444
+ @definition.instance_variable_set(:@loop_condition_expected_value, value)
445
+ end
446
+
447
+ # --- Delegation Configuration ---
448
+ # Define agent names that this agent can delegate tasks to via LLM planning
449
+ # @param names [Array<Symbol>] Names of agents that can be delegation targets
450
+ def can_delegate_to(*names)
451
+ flat_names = names.flatten.map(&:to_sym)
452
+ # Log a warning if the array is empty but don't raise an error
453
+ Legate.logger.warn("Empty delegation targets list for agent '#{@definition.name}'") if flat_names.empty?
454
+
455
+ @definition.instance_variable_set(:@delegation_targets, Set.new(flat_names))
456
+ end
457
+ # --- End MAS Attributes DSL ---
458
+
459
+ # --- Authentication DSL Methods ---
460
+
461
+ # Associate a registered credential with this agent
462
+ # @param credential_name [Symbol] Name of a registered credential
463
+ # @example
464
+ # use_credential :google_maps_api
465
+ # use_credential :openai_key
466
+ def use_credential(credential_name)
467
+ raise ArgumentError, 'Credential name must be a Symbol.' unless credential_name.is_a?(Symbol)
468
+
469
+ @definition.instance_variable_get(:@auth_credential_names) << credential_name
470
+ end
471
+
472
+ # Map a URL pattern to an authentication scheme and credential
473
+ # @param url_pattern [String, Regexp] URL pattern to match
474
+ # @param scheme [Symbol] Scheme type or name to use
475
+ # @param credential [Symbol] Credential name to use
476
+ # @example
477
+ # auth_mapping 'https://maps.googleapis.com/*', scheme: :api_key, credential: :google_maps_api
478
+ # auth_mapping /api\.openai\.com/, scheme: :http_bearer, credential: :openai_key
479
+ def auth_mapping(url_pattern, scheme:, credential:)
480
+ raise ArgumentError, 'URL pattern must be a String or Regexp.' unless url_pattern.is_a?(String) || url_pattern.is_a?(Regexp)
481
+ raise ArgumentError, 'Scheme must be a Symbol.' unless scheme.is_a?(Symbol)
482
+ raise ArgumentError, 'Credential must be a Symbol.' unless credential.is_a?(Symbol)
483
+
484
+ @definition.instance_variable_get(:@auth_url_mappings) << {
485
+ pattern: url_pattern,
486
+ scheme_name: scheme,
487
+ credential_name: credential
488
+ }
489
+ end
490
+
491
+ # Assign a scheme for a named service
492
+ # @param service [Symbol] Service identifier (e.g., :google_maps, :openai)
493
+ # @param scheme [Symbol] Scheme name to use for this service
494
+ # @example
495
+ # auth_scheme :google_maps, :api_key
496
+ # auth_scheme :openai, :http_bearer
497
+ def auth_scheme(service, scheme)
498
+ raise ArgumentError, 'Service must be a Symbol.' unless service.is_a?(Symbol)
499
+ raise ArgumentError, 'Scheme must be a Symbol.' unless scheme.is_a?(Symbol)
500
+
501
+ @definition.instance_variable_get(:@auth_scheme_assignments)[service] = scheme
502
+ end
503
+
504
+ # Assign a credential for a named service
505
+ # @param service [Symbol] Service identifier (e.g., :google_maps, :openai)
506
+ # @param credential [Symbol] Credential name to use for this service
507
+ # @example
508
+ # auth_credential :google_maps, :google_maps_api
509
+ # auth_credential :openai, :openai_key
510
+ def auth_credential(service, credential)
511
+ raise ArgumentError, 'Service must be a Symbol.' unless service.is_a?(Symbol)
512
+ raise ArgumentError, 'Credential must be a Symbol.' unless credential.is_a?(Symbol)
513
+
514
+ @definition.instance_variable_get(:@auth_credential_assignments)[service] = credential
515
+ end
516
+
517
+ # --- End Authentication DSL Methods ---
518
+
519
+ # --- Callback DSL Methods ---
520
+
521
+ # Sets the callback to run before agent execution
522
+ # @param block [Proc] The callback code to run
523
+ # @yieldparam context [Legate::Callbacks::CallbackContext] Context for state management
524
+ # @yieldreturn [Hash, nil] Optional hash to override normal execution
525
+ def before_agent_callback(&block)
526
+ raise ArgumentError, 'Callback must be a Proc or lambda.' unless block.is_a?(Proc)
527
+
528
+ @definition.instance_variable_set(:@before_agent_callback, block)
529
+ end
530
+
531
+ # Sets the callback to run after agent execution
532
+ # @param block [Proc] The callback code to run
533
+ # @yieldparam context [Legate::Callbacks::CallbackContext] Context for state management
534
+ # @yieldparam result [Hash] The agent's result hash that can be modified
535
+ # @yieldreturn [Hash, nil] Optional hash to replace the agent's result
536
+ def after_agent_callback(&block)
537
+ raise ArgumentError, 'Callback must be a Proc or lambda.' unless block.is_a?(Proc)
538
+
539
+ @definition.instance_variable_set(:@after_agent_callback, block)
540
+ end
541
+
542
+ # Sets the callback to run before model interaction
543
+ # @param block [Proc] The callback code to run
544
+ # @yieldparam context [Legate::Callbacks::CallbackContext] Context for state management
545
+ # @yieldparam llm_request [Hash] The request parameters being sent to the model
546
+ # @yieldreturn [Hash, nil] Optional hash to override normal model execution
547
+ def before_model_callback(&block)
548
+ raise ArgumentError, 'Callback must be a Proc or lambda.' unless block.is_a?(Proc)
549
+
550
+ @definition.instance_variable_set(:@before_model_callback, block)
551
+ end
552
+
553
+ # Sets the callback to run after model interaction
554
+ # @param block [Proc] The callback code to run
555
+ # @yieldparam context [Legate::Callbacks::CallbackContext] Context for state management
556
+ # @yieldparam plan [Hash] The plan returned by the model that can be modified
557
+ # @yieldreturn [Hash, nil] Optional hash to replace the model's plan
558
+ def after_model_callback(&block)
559
+ raise ArgumentError, 'Callback must be a Proc or lambda.' unless block.is_a?(Proc)
560
+
561
+ @definition.instance_variable_set(:@after_model_callback, block)
562
+ end
563
+
564
+ # Sets the callback to run before tool execution
565
+ # @param block [Proc] The callback code to run
566
+ # @yieldparam tool [Legate::Tool] The tool instance being executed
567
+ # @yieldparam params [Hash] The parameters being passed to the tool
568
+ # @yieldparam context [Legate::ToolContext] Context for tool execution and state management
569
+ # @yieldreturn [Hash, nil] Optional hash to override normal tool execution
570
+ def before_tool_callback(&block)
571
+ raise ArgumentError, 'Callback must be a Proc or lambda.' unless block.is_a?(Proc)
572
+
573
+ @definition.instance_variable_set(:@before_tool_callback, block)
574
+ end
575
+
576
+ # Sets the callback to run after tool execution
577
+ # @param block [Proc] The callback code to run
578
+ # @yieldparam tool [Legate::Tool] The tool instance that was executed
579
+ # @yieldparam params [Hash] The parameters that were passed to the tool
580
+ # @yieldparam context [Legate::ToolContext] Context for tool execution and state management
581
+ # @yieldparam result [Hash] The tool's result hash that can be modified
582
+ # @yieldreturn [Hash, nil] Optional hash to replace the tool's result
583
+ def after_tool_callback(&block)
584
+ raise ArgumentError, 'Callback must be a Proc or lambda.' unless block.is_a?(Proc)
585
+
586
+ @definition.instance_variable_set(:@after_tool_callback, block)
587
+ end
588
+
589
+ # --- End Callback DSL Methods ---
590
+ end
591
+ private_constant :DefinitionProxy
592
+
593
+ # Class method to create an AgentDefinition instance from a hash.
594
+ # This is typically used when loading a definition from a persistent store.
595
+ # @param hash_data [Hash] The hash containing agent definition attributes.
596
+ # @return [Legate::AgentDefinition, nil] A new AgentDefinition instance or nil on error.
597
+ def self.from_hash(hash_data)
598
+ return nil unless hash_data.is_a?(Hash)
599
+
600
+ definition = new
601
+
602
+ # Helper method to convert array values to Sets if present in the source hash
603
+ convert_to_set = lambda do |key, default = nil|
604
+ if hash_data.key?(key)
605
+ val = hash_data[key]
606
+ if val.is_a?(Array)
607
+ Set.new(val.map(&:to_sym))
608
+ else
609
+ (val.nil? ? default : Set.new([val.to_sym]))
610
+ end
611
+ else
612
+ default || Set.new
613
+ end
614
+ end
615
+
616
+ # Map string or symbol keys to our keys
617
+ definition.instance_variable_set(:@name, hash_data[:name]&.to_sym || hash_data['name']&.to_sym)
618
+ definition.instance_variable_set(:@description, hash_data[:description]&.to_s || hash_data['description']&.to_s || '')
619
+ definition.instance_variable_set(:@instruction, hash_data[:instruction]&.to_s || hash_data['instruction']&.to_s || '')
620
+
621
+ # Handle tools/tool_names (expected to be an array of strings or symbols)
622
+ tool_names = nil
623
+ if hash_data.key?(:tool_names) || hash_data.key?('tool_names')
624
+ tool_names = hash_data[:tool_names] || hash_data['tool_names']
625
+ elsif hash_data.key?(:tools) || hash_data.key?('tools')
626
+ tool_names = hash_data[:tools] || hash_data['tools']
627
+ end
628
+
629
+ # Convert tool_names to a Set of symbols (always ensure it's a Set)
630
+ if tool_names.is_a?(Array)
631
+ definition.instance_variable_set(:@tool_names, Set.new(tool_names.map(&:to_sym)))
632
+ elsif tool_names.is_a?(String)
633
+ # Special case: if it's a JSON string, try to parse it
634
+ begin
635
+ parsed_tools = JSON.parse(tool_names)
636
+ if parsed_tools.is_a?(Array)
637
+ definition.instance_variable_set(:@tool_names, Set.new(parsed_tools.map(&:to_sym)))
638
+ else
639
+ definition.instance_variable_set(:@tool_names, Set.new)
640
+ end
641
+ rescue JSON::ParserError
642
+ Legate.logger.warn("AgentDefinition.from_hash: tool_names string is not valid JSON, ignoring: #{tool_names.inspect}")
643
+ definition.instance_variable_set(:@tool_names, Set.new)
644
+ end
645
+ else
646
+ # No valid tools provided, use empty set
647
+ definition.instance_variable_set(:@tool_names, Set.new)
648
+ end
649
+
650
+ # Process model_name (string or symbol)
651
+ model_name = hash_data[:model_name] || hash_data['model_name'] || hash_data[:model] || hash_data['model']
652
+ definition.instance_variable_set(:@model_name, model_name&.to_sym)
653
+
654
+ # Process temperature (float)
655
+ temp_value = hash_data[:temperature] || hash_data['temperature']
656
+ definition.instance_variable_set(:@temperature, temp_value&.to_f)
657
+
658
+ # --- Process webhook fields ---
659
+ # Boolean conversion helper for webhook_enabled
660
+ wb_enabled = hash_data[:webhook_enabled] || hash_data['webhook_enabled']
661
+ wb_enabled = wb_enabled.to_s.downcase == 'true' if wb_enabled.is_a?(String)
662
+ definition.instance_variable_set(:@webhook_enabled, !!wb_enabled) # Force to boolean
663
+
664
+ # webhook_validator can be symbol or nil
665
+ wb_validator = hash_data[:webhook_validator] || hash_data['webhook_validator']
666
+ definition.instance_variable_set(:@webhook_validator, wb_validator.is_a?(Symbol) ? wb_validator : nil)
667
+
668
+ # webhook_secret is a string or nil
669
+ wb_secret = hash_data[:webhook_secret] || hash_data['webhook_secret']
670
+ # Special case: '<present>' is a placeholder used in to_h when the secret exists
671
+ definition.instance_variable_set(:@webhook_secret, wb_secret == '<present>' ? wb_secret : wb_secret)
672
+
673
+ # webhook_transformer and webhook_session_extractor are Procs (can't be serialized)
674
+ # Always nil when recreated from a hash
675
+ definition.instance_variable_set(:@webhook_transformer, nil)
676
+ definition.instance_variable_set(:@webhook_session_extractor, nil)
677
+
678
+ # --- Process MCP servers ---
679
+ # MCP servers can be array of hashes, or JSON string
680
+ mcp_value = hash_data[:mcp_servers] || hash_data['mcp_servers'] || hash_data[:mcp_servers_json] || hash_data['mcp_servers_json']
681
+ if mcp_value.is_a?(String)
682
+ begin
683
+ parsed_mcp = JSON.parse(mcp_value)
684
+ definition.instance_variable_set(:@mcp_servers, parsed_mcp.is_a?(Array) ? parsed_mcp : [])
685
+ rescue JSON::ParserError
686
+ # Not valid JSON, use empty array
687
+ definition.instance_variable_set(:@mcp_servers, [])
688
+ end
689
+ elsif mcp_value.is_a?(Array)
690
+ definition.instance_variable_set(:@mcp_servers, mcp_value)
691
+ else
692
+ definition.instance_variable_set(:@mcp_servers, [])
693
+ end
694
+
695
+ # --- Process fallback_mode (convert to symbol) ---
696
+ fallback = hash_data[:fallback_mode] || hash_data['fallback_mode']
697
+ if fallback.is_a?(String) || fallback.is_a?(Symbol)
698
+ fb_sym = fallback.to_sym
699
+ definition.instance_variable_set(:@fallback_mode, fb_sym == :echo ? :echo : :error)
700
+ else
701
+ definition.instance_variable_set(:@fallback_mode, :error) # Default
702
+ end
703
+
704
+ # --- Process MAS attributes ---
705
+ # Sub-agent names (convert to Set of symbols)
706
+ definition.instance_variable_set(:@sub_agent_names, convert_to_set.call(:sub_agent_names))
707
+
708
+ # Output key (convert to symbol if present)
709
+ output_key = hash_data[:output_key] || hash_data['output_key']
710
+ definition.instance_variable_set(:@output_key, output_key&.to_sym)
711
+
712
+ # --- MAS Workflow Agent Attributes ---
713
+ # Agent type (convert to symbol, default to :llm)
714
+ agent_type = hash_data[:agent_type] || hash_data['agent_type'] || :llm
715
+ if agent_type.is_a?(String) || agent_type.is_a?(Symbol)
716
+ agent_type_sym = agent_type.to_sym
717
+ valid_types = %i[llm sequential parallel loop]
718
+ # If invalid type, use default :llm
719
+ agent_type_sym = :llm unless valid_types.include?(agent_type_sym)
720
+ definition.instance_variable_set(:@agent_type, agent_type_sym)
721
+ else
722
+ definition.instance_variable_set(:@agent_type, :llm) # Default
723
+ end
724
+
725
+ # Planning strategy (:plan default, :react opt-in)
726
+ strategy = (hash_data[:planning_strategy] || hash_data['planning_strategy'] || :plan).to_sym
727
+ strategy = :plan unless %i[plan react].include?(strategy)
728
+ definition.instance_variable_set(:@planning_strategy, strategy)
729
+
730
+ # Workflow-specific sub-agent lists
731
+ definition.instance_variable_set(:@sequential_sub_agent_names, convert_to_set.call(:sequential_sub_agent_names))
732
+ definition.instance_variable_set(:@parallel_sub_agent_names, convert_to_set.call(:parallel_sub_agent_names))
733
+ definition.instance_variable_set(:@loop_sub_agent_names, convert_to_set.call(:loop_sub_agent_names))
734
+
735
+ # Loop configuration
736
+ loop_max = hash_data[:loop_max_iterations] || hash_data['loop_max_iterations']
737
+ definition.instance_variable_set(:@loop_max_iterations, loop_max&.to_i)
738
+
739
+ loop_key = hash_data[:loop_condition_state_key] || hash_data['loop_condition_state_key']
740
+ definition.instance_variable_set(:@loop_condition_state_key, loop_key&.to_sym)
741
+
742
+ loop_value = hash_data[:loop_condition_expected_value] || hash_data['loop_condition_expected_value']
743
+ definition.instance_variable_set(:@loop_condition_expected_value, loop_value)
744
+
745
+ # Delegation targets
746
+ definition.instance_variable_set(:@delegation_targets, convert_to_set.call(:delegation_targets))
747
+
748
+ # --- Authentication fields ---
749
+ # Auth credential names (convert to Set of symbols)
750
+ definition.instance_variable_set(:@auth_credential_names, convert_to_set.call(:auth_credential_names))
751
+
752
+ # Auth URL mappings (array of hashes)
753
+ auth_mappings_raw = hash_data[:auth_url_mappings] || hash_data['auth_url_mappings'] || []
754
+ if auth_mappings_raw.is_a?(String)
755
+ begin
756
+ auth_mappings_raw = JSON.parse(auth_mappings_raw)
757
+ rescue JSON::ParserError
758
+ auth_mappings_raw = []
759
+ end
760
+ end
761
+ auth_url_mappings = (auth_mappings_raw || []).map do |m|
762
+ pattern = m['pattern'] || m[:pattern]
763
+ pattern_type = m['pattern_type'] || m[:pattern_type]
764
+ # Convert back to Regexp if it was serialized as such
765
+ if pattern_type == 'regexp' && pattern.is_a?(String)
766
+ if pattern.length > 500
767
+ Legate.logger.warn("AgentDefinition.from_h: Rejecting oversized regex pattern (#{pattern.length} chars)")
768
+ else
769
+ begin
770
+ pattern = Regexp.new(pattern, timeout: 1)
771
+ rescue RegexpError
772
+ # Keep as string if invalid regexp
773
+ end
774
+ end
775
+ end
776
+ {
777
+ pattern: pattern,
778
+ scheme_name: (m['scheme_name'] || m[:scheme_name])&.to_sym,
779
+ credential_name: (m['credential_name'] || m[:credential_name])&.to_sym
780
+ }
781
+ end
782
+ definition.instance_variable_set(:@auth_url_mappings, auth_url_mappings)
783
+
784
+ # Auth scheme assignments (hash of symbol -> symbol)
785
+ auth_scheme_raw = hash_data[:auth_scheme_assignments] || hash_data['auth_scheme_assignments'] || {}
786
+ if auth_scheme_raw.is_a?(String)
787
+ begin
788
+ auth_scheme_raw = JSON.parse(auth_scheme_raw)
789
+ rescue JSON::ParserError
790
+ auth_scheme_raw = {}
791
+ end
792
+ end
793
+ auth_scheme_assignments = (auth_scheme_raw || {}).transform_keys(&:to_sym).transform_values(&:to_sym)
794
+ definition.instance_variable_set(:@auth_scheme_assignments, auth_scheme_assignments)
795
+
796
+ # Auth credential assignments (hash of symbol -> symbol)
797
+ auth_cred_raw = hash_data[:auth_credential_assignments] || hash_data['auth_credential_assignments'] || {}
798
+ if auth_cred_raw.is_a?(String)
799
+ begin
800
+ auth_cred_raw = JSON.parse(auth_cred_raw)
801
+ rescue JSON::ParserError
802
+ auth_cred_raw = {}
803
+ end
804
+ end
805
+ auth_credential_assignments = (auth_cred_raw || {}).transform_keys(&:to_sym).transform_values(&:to_sym)
806
+ definition.instance_variable_set(:@auth_credential_assignments, auth_credential_assignments)
807
+
808
+ definition.validate!
809
+ definition
810
+ end
811
+ end
812
+ end