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,134 @@
1
+ # File: lib/legate/mcp/util/schema_converter.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'dry-types' # Ensure dry-types is available for coercion
5
+
6
+ module Legate
7
+ module Mcp
8
+ module Util
9
+ # Utility class for converting between MCP JSON Schema, Legate Tool parameters,
10
+ # and Dry::Schema definitions.
11
+ class SchemaConverter
12
+ # Converts MCP JSON Schema properties and required array into Legate parameters hash.
13
+ # Handles basic types: string, integer, number, boolean.
14
+ # Logs warnings for unsupported types.
15
+ #
16
+ # @param json_schema_properties [Hash] The 'properties' hash from MCP inputSchema.
17
+ # @param json_schema_required_array [Array<String>] The 'required' array from MCP inputSchema.
18
+ # @return [Hash] Legate parameters hash { name: { type:, required:, description: } }.
19
+ def self.json_to_legate(json_schema_properties, json_schema_required_array = [])
20
+ # Return empty hash if input is invalid or not a Hash
21
+ return {} unless json_schema_properties.is_a?(Hash)
22
+
23
+ legate_params = {} # Reverted: Store a hash of param hashes
24
+ required_set = Set.new((json_schema_required_array || []).map(&:to_s))
25
+
26
+ json_schema_properties.each do |name, schema|
27
+ # ---> MODIFIED Check: Allow string or symbol key for type <---
28
+ is_valid_schema = schema.is_a?(Hash) && (schema.key?('type') || schema.key?(:type))
29
+ unless is_valid_schema
30
+ Legate.logger.warn("Skipping MCP property '#{name}': Invalid schema format or missing type. Schema: #{schema.inspect}")
31
+ next
32
+ end
33
+
34
+ param_name = name.to_sym
35
+ # Determine the type using either key
36
+ schema_type = schema['type'] || schema[:type]
37
+ # Build the inner parameter definition hash
38
+ legate_param_def = {
39
+ # ---> FIX: Check string name in required_set <---
40
+ required: required_set.include?(name.to_s),
41
+ # Use string or symbol key for description
42
+ description: schema['description'] || schema[:description] || ''
43
+ }
44
+
45
+ # Determine and add the type to the inner hash based on schema_type
46
+ case schema_type
47
+ when 'string'
48
+ legate_param_def[:type] = :string
49
+ when 'integer'
50
+ legate_param_def[:type] = :integer
51
+ when 'number'
52
+ legate_param_def[:type] = :numeric
53
+ when 'boolean'
54
+ legate_param_def[:type] = :boolean
55
+ when 'array'
56
+ legate_param_def[:type] = :array
57
+ else
58
+ Legate.logger.warn("MCP property '#{name}': Unsupported JSON Schema type '#{schema_type}'. Skipping.")
59
+ next
60
+ end
61
+
62
+ # Add the inner hash to the main hash, keyed by param_name
63
+ legate_params[param_name] = legate_param_def
64
+ end
65
+
66
+ legate_params # Return the hash of parameter hashes
67
+ end
68
+
69
+ # Converts Legate parameters hash into a Proc suitable for Dry::Schema's definition block.
70
+ # Handles basic types: :string, :integer, :numeric, :boolean.
71
+ # Logs warnings for unsupported types.
72
+ #
73
+ # @param legate_parameters_hash [Hash] The Legate parameters hash { name: { type:, required:, description: } }.
74
+ # @return [Proc] A Proc containing the Dry::Schema definition.
75
+ def self.legate_to_dry_schema(legate_parameters_hash)
76
+ Legate.logger.debug("Converting Legate params to Dry::Schema: #{legate_parameters_hash.inspect}")
77
+ return proc {} unless legate_parameters_hash.is_a?(Hash)
78
+
79
+ schema_lines = []
80
+
81
+ legate_parameters_hash.each do |name, definition|
82
+ unless definition.is_a?(Hash) && definition[:type]
83
+ Legate.logger.warn("Skipping Legate parameter '#{name}': Invalid definition format or missing type.")
84
+ next
85
+ end
86
+
87
+ required_or_optional = definition[:required] ? 'required' : 'optional'
88
+ dry_type_method = nil
89
+ type_spec = nil
90
+
91
+ case definition[:type]
92
+ when :string
93
+ dry_type_method = 'filled'
94
+ type_spec = ':string'
95
+ when :integer
96
+ dry_type_method = 'filled'
97
+ type_spec = ':integer'
98
+ when :numeric
99
+ # Use coercible float type to handle string inputs that represent numbers
100
+ dry_type_method = 'filled'
101
+ type_spec = 'Dry::Types[\'coercible.float\']'
102
+ when :boolean
103
+ dry_type_method = 'filled'
104
+ type_spec = ':bool'
105
+ when :array
106
+ Legate.logger.warn("Legate parameter '#{name}': Type :array basic mapping to Dry::Schema. Item types/validation not processed in V1.")
107
+ dry_type_method = 'value'
108
+ type_spec = ':array'
109
+ when :hash, :object
110
+ Legate.logger.warn("Legate parameter '#{name}': Type :#{definition[:type]} basic mapping to Dry::Schema :hash. Nested schema not processed in V1.")
111
+ dry_type_method = 'value'
112
+ type_spec = ':hash'
113
+ else
114
+ Legate.logger.warn("Legate parameter '#{name}': Unsupported Legate type '#{definition[:type]}'. Skipping.")
115
+ next
116
+ end
117
+
118
+ # Build the line
119
+ line = " #{required_or_optional}(:#{name})"
120
+ line += ".#{dry_type_method}" if dry_type_method
121
+ line += "(#{type_spec})" # Add the type specifier
122
+
123
+ schema_lines << line
124
+ end
125
+
126
+ schema_definition_string = schema_lines.join("\n")
127
+ Legate.logger.debug("Generated Dry::Schema definition string:\n#{schema_definition_string}")
128
+
129
+ proc { instance_eval(schema_definition_string) }
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
data/lib/legate/mcp.rb ADDED
@@ -0,0 +1,23 @@
1
+ # File: lib/legate/mcp.rb
2
+ # frozen_string_literal: true
3
+
4
+ # MCP errors are defined in legate/errors.rb (loaded by lib/legate.rb)
5
+ require_relative 'mcp/util/schema_converter'
6
+ require_relative 'mcp/connection/stdio'
7
+ require_relative 'mcp/client'
8
+ require_relative 'mcp/tool_wrapper'
9
+ require_relative 'mcp/connection_manager'
10
+ require_relative 'mcp/server/legate_tool_adapter'
11
+ require_relative 'mcp/server/legate_agent_adapter'
12
+
13
+ module Legate
14
+ # Module for Model Context Protocol (MCP) integration.
15
+ module Mcp
16
+ # Central point for MCP-related logging.
17
+ def self.logger
18
+ Legate.logger
19
+ end
20
+
21
+ logger.info('Legate::Mcp module loaded.')
22
+ end
23
+ end
@@ -0,0 +1,375 @@
1
+ # File: lib/legate/plan_executor.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+ require_relative 'event'
6
+ require_relative 'tool_context'
7
+
8
+ module Legate
9
+ # Executes a planner-produced plan for an Agent: iterates the steps, injects
10
+ # prior-step results into parameters, runs each tool (with before/after-tool
11
+ # callbacks and delegation interception), and logs the tool_request/tool_result
12
+ # events. Extracted from Legate::Agent, which keeps thin execute_plan/execute_step
13
+ # delegators (the lifecycle entry points exercised directly by specs).
14
+ class PlanExecutor
15
+ # @param agent [Legate::Agent] the owning agent; the executor reads its
16
+ # tool registry, fallback mode, tool callbacks, and auth config.
17
+ def initialize(agent)
18
+ @agent = agent
19
+ end
20
+
21
+ # Executes a plan and returns { details: [...], last_result: <hash> }.
22
+ def execute_plan(plan, session, session_service, invocation_id)
23
+ session_id = session.id
24
+
25
+ # A planning failure returns a direct_result (a terminal result, no steps);
26
+ # surface it as-is so run_task builds a clean error Event.
27
+ return { details: [], last_result: plan[:direct_result] } if plan.is_a?(Hash) && plan[:direct_result]
28
+
29
+ # Extract steps based on the plan format
30
+ steps = nil
31
+ thought_process = nil
32
+
33
+ # Handle new plan structure with thought_process and steps
34
+ if plan.is_a?(Hash) && plan[:steps].is_a?(Array)
35
+ steps = plan[:steps]
36
+ thought_process = plan[:thought_process]
37
+ Legate.logger.info("Plan thought process: #{thought_process}") if thought_process
38
+ elsif plan.is_a?(Array)
39
+ # For backward compatibility with old format
40
+ steps = plan
41
+ else
42
+ msg = 'Invalid plan received from planner (not an Array or properly structured Hash).'
43
+ Legate.logger.error("#{msg} Plan: #{plan.inspect}")
44
+ return { details: [], last_result: { status: :error, error_message: msg } }
45
+ end
46
+
47
+ # --- Continue with original logic, using 'steps' variable ---
48
+ unless steps.is_a?(Array)
49
+ msg = 'Invalid steps structure in plan (not an Array).'
50
+ Legate.logger.error("#{msg} Steps: #{steps.inspect}")
51
+ return { details: [], last_result: { status: :error, error_message: msg } }
52
+ end
53
+
54
+ # --- Handle Empty Plan based on Fallback Mode ---
55
+ if steps.empty?
56
+ if @agent.fallback_mode == :echo
57
+ if @agent.tool_registry.find_class(:echo)
58
+ Legate.logger.warn("Plan is empty. Falling back to echo mode for session '#{session_id}'.")
59
+ # Reconstruct the plan to be a single echo step
60
+ # We need the original user input for this - fetch it from the session
61
+ # Find the *last* user event in case of corrections/multiple turns
62
+ original_user_input = session.events.reverse.find { |e|
63
+ e.role == :user
64
+ }&.content || '[Original input not found]'
65
+ steps = [{ tool: :echo, params: { message: original_user_input } }]
66
+ Legate.logger.debug("Reconstructed plan for echo fallback: #{steps.inspect}")
67
+ # Now continue execution with the modified plan
68
+ else
69
+ # Echo tool not available, default to error mode
70
+ msg = 'Planning failed and Echo fallback tool is not available to this agent.'
71
+ Legate.logger.warn(msg)
72
+ return { details: [], last_result: { status: :error, error_message: msg } }
73
+ end
74
+ else # Default or :error mode
75
+ msg = 'I cannot fulfill this request with the available tools (empty plan).'
76
+ Legate.logger.warn(msg)
77
+ return { details: [], last_result: { status: :error, error_message: msg } }
78
+ end
79
+ end
80
+ # --- End Handle Empty Plan ---
81
+
82
+ Legate.logger.debug("Executing plan with #{steps.length} step(s) for session '#{session_id}': #{steps.inspect}")
83
+ previous_step_result_hash = nil
84
+ plan_execution_details = []
85
+ last_successful_or_pending_result = nil # <-- Store the original last hash
86
+
87
+ steps.each_with_index do |step, index|
88
+ # Log the step type for clarity
89
+ step_type_desc = if step[:step_type] == :sequential_sub_agent
90
+ "sequential sub-agent '#{step[:sub_agent_name]}'"
91
+ else
92
+ "tool '#{step[:tool]}'"
93
+ end
94
+ Legate.logger.debug("Executing step #{index + 1}/#{steps.length}: #{step_type_desc}")
95
+ Legate.logger.debug(" Step details: #{step.inspect}")
96
+ Legate.logger.debug(" Input (result hash from previous step): #{previous_step_result_hash.inspect}")
97
+
98
+ # --- Input Injection Logic (Updated for job_id) ---
99
+ current_params = JSON.parse(JSON.generate(step[:params]), symbolize_names: true)
100
+ current_params.transform_values! do |value|
101
+ injection_value = nil
102
+ if value.is_a?(String) && value.match?(/\[Result from step \d+\]|\[Result from previous step\]/i)
103
+ if previous_step_result_hash.is_a?(Hash) && %i[success pending].include?(previous_step_result_hash[:status])
104
+ # Prioritize :result, then :job_id (was workflow_id), then :message
105
+ if previous_step_result_hash.key?(:result)
106
+ prev_result = previous_step_result_hash[:result]
107
+ if prev_result.is_a?(Hash) && prev_result.key?(:status) && prev_result.key?(:result) # AgentTool nested result
108
+ injection_value = prev_result[:result]
109
+ Legate.logger.debug('Injecting nested result...')
110
+ else
111
+ injection_value = prev_result
112
+ Legate.logger.debug('Injecting direct result...')
113
+ end
114
+ elsif previous_step_result_hash.key?(:job_id) # <-- CHANGED from workflow_id
115
+ injection_value = previous_step_result_hash[:job_id]
116
+ Legate.logger.debug('Injecting job_id from previous step...')
117
+ elsif previous_step_result_hash.key?(:message)
118
+ injection_value = previous_step_result_hash[:message]
119
+ Legate.logger.debug('Injecting message from previous step...')
120
+ else
121
+ Legate.logger.warn("Cannot inject: Previous successful/pending step missing usable key (:result, :job_id, :message). Prev Hash: #{previous_step_result_hash.inspect}")
122
+ value
123
+ end
124
+ else
125
+ Legate.logger.warn("Cannot inject: Previous step failed or absent. Prev Hash: #{previous_step_result_hash.inspect}")
126
+ value
127
+ end
128
+ injection_value || value # Use injection if found, otherwise keep original
129
+ else
130
+ value # Not a placeholder string, keep original value
131
+ end
132
+ end
133
+ step_with_injected_params = step.merge(params: current_params)
134
+ Legate.logger.debug(" Params after potential injection: #{current_params.inspect}")
135
+ # --- End Input Injection Logic ---
136
+
137
+ # --- Execute Step --- #
138
+ current_result_hash = execute_step(step_with_injected_params, session, session_service, invocation_id)
139
+
140
+ # --- Sanitize for plan_details --- #
141
+ sanitized_result_for_plan = {}
142
+ if current_result_hash.is_a?(Hash)
143
+ sanitized_result_for_plan[:status] = current_result_hash[:status]
144
+ # Always include error keys, defaulting to nil if not present
145
+ sanitized_result_for_plan[:error_message] = current_result_hash[:error_message] # Defaults to nil if key missing
146
+ sanitized_result_for_plan[:error_class] = current_result_hash[:error_class] # Defaults to nil if key missing
147
+ # Include other relevant keys if present
148
+ sanitized_result_for_plan[:job_id] = current_result_hash[:job_id] if current_result_hash.key?(:job_id)
149
+ sanitized_result_for_plan[:message] = current_result_hash[:message] if current_result_hash.key?(:message)
150
+ # Only include :result value if it's simple
151
+ result_val = current_result_hash[:result]
152
+ if result_val.is_a?(String) || result_val.is_a?(Numeric) || [true, false, nil].include?(result_val)
153
+ sanitized_result_for_plan[:result] = result_val
154
+ elsif current_result_hash.key?(:result) # It exists but is complex
155
+ sanitized_result_for_plan[:result] = '[Complex Result Structure]'
156
+ end
157
+ else # Should not happen based on execute_step validation, but handle defensively
158
+ sanitized_result_for_plan[:status] = :error
159
+ sanitized_result_for_plan[:error_message] = "Invalid format from execute_step: #{current_result_hash.inspect}"
160
+ end
161
+ # --- END Sanitization ---
162
+
163
+ # --- Store SANITIZED step detail --- #
164
+ plan_execution_details << {
165
+ tool_name: step[:tool],
166
+ params: current_params,
167
+ result: sanitized_result_for_plan
168
+ }
169
+
170
+ # --- Store ORIGINAL result and check for errors --- #
171
+ if current_result_hash[:status] == :error
172
+ Legate.logger.warn("Step #{index + 1} failed, stopping plan execution: #{current_result_hash[:error_message]}")
173
+ last_successful_or_pending_result = current_result_hash # Store the error hash as last result
174
+ break # Exit the loop
175
+ else
176
+ # Store successful or pending hash for potential injection AND final result
177
+ previous_step_result_hash = current_result_hash
178
+ last_successful_or_pending_result = current_result_hash
179
+ end
180
+ # --- End Stop on first error / Store last result --- #
181
+ end
182
+
183
+ Legate.logger.debug("Plan execution finished. Structured details collected: #{plan_execution_details.inspect}")
184
+ Legate.logger.debug("Plan execution finished. Original last result: #{last_successful_or_pending_result.inspect}")
185
+
186
+ # --- Return BOTH sanitized details AND original last result --- #
187
+ { details: plan_execution_details, last_result: last_successful_or_pending_result }
188
+ end
189
+
190
+ # Executes a single step, logging :tool_request and :tool_result events via
191
+ # the session service.
192
+ # @return [Hash] A standard result hash { status:, result/error_message/job_id: }.
193
+ def execute_step(step, session, session_service, invocation_id = nil)
194
+ session_id = session.id
195
+
196
+ # --- Basic validation ---
197
+ unless step.is_a?(Hash) && step[:tool] && step[:params].is_a?(Hash)
198
+ error_msg = 'Invalid step format. Expected { tool: :symbol, params: {...} }'
199
+ Legate.logger.error(error_msg)
200
+ return { status: :error, error_message: error_msg }
201
+ end
202
+
203
+ raw_tool_name = step[:tool].to_s
204
+
205
+ # Validate tool name against known tools before converting to Symbol
206
+ # to prevent Symbol table exhaustion from untrusted input
207
+ known_names = @agent.tool_registry.available_tool_names.map(&:to_s)
208
+ is_delegation = raw_tool_name.start_with?('agent_transfer_to_')
209
+ unless known_names.include?(raw_tool_name) || is_delegation
210
+ error_msg = "Unknown tool '#{raw_tool_name}' — not in agent's tool registry"
211
+ Legate.logger.error(error_msg)
212
+ return { status: :error, error_message: error_msg }
213
+ end
214
+
215
+ tool_name = raw_tool_name.to_sym
216
+ params = step[:params].to_h
217
+
218
+ # --- Intercept Delegation Tools (MAS) ---
219
+ # If the model outputs "agent_transfer_to_xyz", map it to "delegate_task"
220
+ if tool_name.to_s.start_with?('agent_transfer_to_')
221
+ target_agent_name = tool_name.to_s.sub('agent_transfer_to_', '')
222
+ Legate.logger.info("Intercepted delegation tool '#{tool_name}'. Mapping to 'delegate_task' for target '#{target_agent_name}'.")
223
+
224
+ # Remap tool name
225
+ tool_name = :delegate_task
226
+
227
+ # Remap params: ensure target_agent_name is set
228
+ params[:target_agent_name] = target_agent_name
229
+
230
+ # Ensure 'task' param exists (model should provide it, but handle aliasing/defaults if needed)
231
+ # The prompt says: - task (string, required)
232
+ unless params.key?(:task)
233
+ # Fallback: if model used a different key like 'message' or 'input', map it to 'task'
234
+ if params.key?(:message)
235
+ params[:task] = params.delete(:message)
236
+ elsif params.key?(:input)
237
+ params[:task] = params.delete(:input)
238
+ end
239
+ end
240
+ end
241
+ # --- End Delegation Interception ---
242
+
243
+ # --- Get the tool from our registry ---
244
+ tool = @agent.tool_registry.create_instance(tool_name)
245
+ unless tool
246
+ error_msg = "Tool '#{tool_name}' not found in available tools."
247
+ Legate.logger.error(error_msg)
248
+ return { status: :error, error_message: error_msg }
249
+ end
250
+
251
+ # --- Prepare tool context with invocation_id and auth config ---
252
+ tool_context = Legate::ToolContext.new(
253
+ session_id: session.id,
254
+ user_id: session.user_id,
255
+ app_name: session.app_name,
256
+ session_service: session_service,
257
+ tool_registry: @agent.tool_registry,
258
+ invocation_id: invocation_id,
259
+ agent_auth_config: @agent.send(:build_agent_auth_config)
260
+ )
261
+
262
+ # --- Log the tool request event ---
263
+ tool_request_event = Legate::Event.new(
264
+ role: :tool_request,
265
+ tool_name: tool_name,
266
+ content: params
267
+ )
268
+ session_service.append_event(session_id: session_id, event: tool_request_event)
269
+
270
+ # --- Execute before_tool_callback if defined ---
271
+ if @agent.before_tool_callback.is_a?(Proc)
272
+ Legate.logger.debug { "Agent '#{@agent.name}': Executing before_tool_callback for tool '#{tool_name}'." }
273
+
274
+ begin
275
+ # Execute the callback and check if it returns a result
276
+ override_result = @agent.before_tool_callback.call(tool, params.dup, tool_context)
277
+
278
+ # If the callback returns a result (not nil), use it instead of normal tool execution
279
+ if override_result
280
+ Legate.logger.info { "Agent '#{@agent.name}': before_tool_callback provided an override result for tool '#{tool_name}'." }
281
+
282
+ # Create a tool result event with the override result and any state changes
283
+ tool_result_event = Legate::Event.new(
284
+ role: :tool_result,
285
+ tool_name: tool_name,
286
+ content: override_result,
287
+ state_delta: tool_context.pending_state_delta
288
+ )
289
+ session_service.append_event(session_id: session_id, event: tool_result_event)
290
+
291
+ return override_result
292
+ end
293
+ rescue StandardError => e
294
+ Legate.logger.error { "Agent '#{@agent.name}': Error in before_tool_callback for tool '#{tool_name}': #{e.message}\n#{e.backtrace.join("\n")}" }
295
+
296
+ error_result = {
297
+ status: :error,
298
+ error_message: "Error in before_tool_callback: #{e.message}",
299
+ error_class: e.class.name
300
+ }
301
+
302
+ # Create a tool result event with the error
303
+ tool_result_event = Legate::Event.new(
304
+ role: :tool_result,
305
+ tool_name: tool_name,
306
+ content: error_result,
307
+ state_delta: tool_context.pending_state_delta
308
+ )
309
+ session_service.append_event(session_id: session_id, event: tool_result_event)
310
+
311
+ return error_result
312
+ end
313
+ end
314
+
315
+ # --- Execute the tool ---
316
+ begin
317
+ Legate.logger.debug { "Executing tool '#{tool_name}' with params #{params.inspect}" }
318
+ final_tool_name_to_execute = tool_name
319
+
320
+ # For delegate_task tool, capture the delegate to show in logs
321
+ final_tool_name_to_execute = "#{tool_name} -> #{params[:target_agent_name]}" if tool_name == :delegate_task && params[:target_agent_name]
322
+
323
+ result = tool.execute(params, tool_context)
324
+
325
+ # --- Execute after_tool_callback if defined ---
326
+ if @agent.after_tool_callback.is_a?(Proc)
327
+ Legate.logger.debug { "Agent '#{@agent.name}': Executing after_tool_callback for tool '#{final_tool_name_to_execute}'." }
328
+
329
+ begin
330
+ # Execute the callback and let it modify the result if needed
331
+ modified_result = @agent.after_tool_callback.call(tool, params.dup, tool_context, result.dup)
332
+
333
+ # If the callback returned a modified result, use it
334
+ if modified_result && modified_result != result
335
+ Legate.logger.info { "Agent '#{@agent.name}': after_tool_callback modified the result for tool '#{final_tool_name_to_execute}'." }
336
+ result = modified_result
337
+ end
338
+ rescue StandardError => e
339
+ Legate.logger.error { "Agent '#{@agent.name}': Error in after_tool_callback for tool '#{final_tool_name_to_execute}': #{e.message}\n#{e.backtrace.join("\n")}" }
340
+ # Don't override the result completely on error, just log it
341
+ end
342
+ end
343
+
344
+ # --- Log the tool result event ---
345
+ tool_result_event = Legate::Event.new(
346
+ role: :tool_result,
347
+ tool_name: tool_name,
348
+ content: result,
349
+ state_delta: tool_context.pending_state_delta
350
+ )
351
+ session_service.append_event(session_id: session_id, event: tool_result_event)
352
+
353
+ result
354
+ rescue StandardError => e
355
+ Legate.logger.error { "Error executing tool '#{tool_name}': #{e.message}\n#{e.backtrace.join("\n")}" }
356
+
357
+ error_result = {
358
+ status: :error,
359
+ error_message: "Tool '#{tool_name}' execution error: #{e.message}",
360
+ error_class: e.class.name # consistent with every other error hash + the plan-detail sanitizer
361
+ }
362
+
363
+ # Create a tool result event with the error
364
+ tool_result_event = Legate::Event.new(
365
+ role: :tool_result,
366
+ tool_name: tool_name,
367
+ content: error_result
368
+ )
369
+ session_service.append_event(session_id: session_id, event: tool_result_event)
370
+
371
+ error_result
372
+ end
373
+ end
374
+ end
375
+ end