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,206 @@
1
+ # Exposing Legate Components via MCP
2
+
3
+ This guide explains how to make your `Legate::Tool`s (and experimentally, `Legate::Agent`s) available to external MCP clients. This is achieved by using provided adapters with the [`fast-mcp`](https://github.com/yjacquin/fast-mcp) Ruby gem, which handles the MCP server implementation.
4
+
5
+ **Prerequisite**: You must have the `fast-mcp` gem included in your project's Gemfile and installed (`bundle add fast_mcp`).
6
+
7
+ ## Architecture Overview: Legate as MCP Service Provider
8
+
9
+ The following diagram illustrates how Legate components are wrapped and exposed via `fast-mcp`:
10
+
11
+ ```mermaid
12
+ graph LR
13
+ subgraph Your Ruby Application / Script
14
+ LegateTool["Your Legate::Tool Class"] -- Wrapped by --> Adapter["Legate::Mcp::Server::LegateToolAdapter"]
15
+ LegateAgent["Your Legate::Agent Definition/Instance"] -- Wrapped by --> AgentAdapter["Legate::Mcp::Server::LegateAgentAdapter"]
16
+ Adapter -- Registers with --> FastMcpServer["fast-mcp Server Instance"]
17
+ AgentAdapter -- Registers with --> FastMcpServer
18
+ FastMcpServer -- Manages --> Transport["fast-mcp Transport (STDIO/Rack)"]
19
+ end
20
+
21
+ subgraph External MCP Client
22
+ MCP_Client["External MCP Client"]
23
+ end
24
+
25
+ Transport -- JSON-RPC --> MCP_Client
26
+
27
+ style LegateTool fill:#ccf,stroke:#333,stroke-width:2px
28
+ style LegateAgent fill:#ccf,stroke:#333,stroke-width:2px
29
+ style Adapter fill:#cff,stroke:#333,stroke-width:2px
30
+ style AgentAdapter fill:#cff,stroke:#333,stroke-width:2px
31
+ style FastMcpServer fill:#fcf,stroke:#333,stroke-width:2px
32
+ style MCP_Client fill:#f9f,stroke:#333,stroke-width:2px
33
+ ```
34
+
35
+ **Key Components:**
36
+
37
+ * **`Legate::Tool` / `Legate::Agent`**: Your existing Legate components.
38
+ * **`Legate::Mcp::Server::LegateToolAdapter`**: Wraps an `Legate::Tool` class to make it compatible with `fast-mcp`.
39
+ * **`Legate::Mcp::Server::LegateAgentAdapter` / `LegateDirectAgentAdapter`**: Wraps an `Legate::Agent` (either a registry definition or a direct instance) to expose it as a single MCP tool.
40
+ * **`fast-mcp Server Instance`**: The MCP server provided by the `fast-mcp` gem.
41
+ * **`fast-mcp Transport`**: How the `fast-mcp` server communicates (e.g., STDIO, Rack middleware for HTTP/SSE).
42
+
43
+ ## 1. Exposing Legate Tools
44
+
45
+ Use the `Legate::Mcp::Server::LegateToolAdapter.wrap` method to create a `fast-mcp` compatible tool class from your existing `Legate::Tool` subclass.
46
+
47
+ ### 1.1. Wrapping a Tool
48
+
49
+ ```ruby
50
+ require 'legate'
51
+ require 'fast_mcp'
52
+ require 'legate/mcp/server/legate_tool_adapter'
53
+ require 'legate/tools/calculator' # Example Legate tool
54
+
55
+ # Configure Legate logger if needed
56
+ # Legate.configure { |c| c.log_level = Logger::INFO }
57
+
58
+ # 1. Wrap your Legate::Tool class
59
+ AdaptedCalculatorTool = Legate::Mcp::Server::LegateToolAdapter.wrap(Legate::Tools::Calculator)
60
+
61
+ # 2. Create and configure the fast-mcp server
62
+ mcp_server = FastMcp::Server.new(
63
+ name: 'legate-tool-server',
64
+ version: Legate::VERSION,
65
+ logger: Legate.logger # Optional: Use Legate's logger
66
+ )
67
+
68
+ # 3. Register the adapted tool with the fast-mcp server
69
+ mcp_server.register_tool(AdaptedCalculatorTool)
70
+
71
+ # 4. Start the server (e.g., using STDIO transport)
72
+ Legate.logger.info("Starting Legate MCP Tool Server via STDIO...")
73
+ mcp_server.start # This will block and listen on STDIN/STDOUT
74
+ ```
75
+
76
+ ### 1.2. How it Works (Tool Adapter)
77
+
78
+ * `LegateToolAdapter.wrap(YourLegateToolClass)`:
79
+ * Retrieves metadata (name, description, parameters) from `YourLegateToolClass`.
80
+ * Uses `Legate::Mcp::Util::SchemaConverter.legate_to_dry_schema` to convert Legate parameter definitions into a `Dry::Schema` block for `fast-mcp` argument validation. (See [Schema Conversion Details](../advanced/mcp_schema_conversion))
81
+ * Dynamically creates a new class that inherits from `FastMcp::Tool`.
82
+ * The `call` method of this new class will:
83
+ * Instantiate `YourLegateToolClass`.
84
+ * Create a dummy `Legate::ToolContext` (as there's no Legate session in this server context).
85
+ * Execute `your_legate_tool_instance.execute(params, dummy_context)`.
86
+ * Translate the Legate result hash (`{status: :success, result: ...}` or `{status: :error, ...}`) into a format `fast-mcp` expects (either direct result or raises an error).
87
+
88
+ ### 1.3. Handling Asynchronous Legate Tools
89
+
90
+ If your Legate tool is asynchronous (returns `{status: :pending, job_id: ...}`), the `LegateToolAdapter` will return a corresponding MCP-friendly pending response (e.g., `{'status': 'pending', 'job_id': '...'}`).
91
+
92
+ To allow clients to check the status of these jobs, you **must** also wrap and expose the `Legate::Tools::CheckJobStatusTool`:
93
+
94
+ ```ruby
95
+ require 'legate/tools/base_async_job_tool' # If defining your own async tool
96
+ require 'legate/tools/sleepy_tool' # Example Legate async tool
97
+ require 'legate/tools/check_job_status_tool'
98
+
99
+ # ... (fast_mcp server setup) ...
100
+
101
+ AdaptedSleepyTool = Legate::Mcp::Server::LegateToolAdapter.wrap(Legate::Tools::SleepyTool)
102
+ AdaptedCheckJobStatusTool = Legate::Mcp::Server::LegateToolAdapter.wrap(Legate::Tools::CheckJobStatusTool)
103
+
104
+ mcp_server.register_tool(AdaptedSleepyTool)
105
+ mcp_server.register_tool(AdaptedCheckJobStatusTool) # Crucial for async tools!
106
+
107
+ # ... (start server) ...
108
+ ```
109
+ Clients will then call your async tool, get a `job_id`, and use the exposed `check_job_status` tool to poll for completion.
110
+
111
+ ## 2. Exposing Legate Agents (Experimental)
112
+
113
+ You can wrap an entire Legate Agent and expose its `run_task` functionality as a single MCP tool. This is useful for providing a simple, prompt-based interface to your agent for external MCP clients.
114
+
115
+ Two adapters are available:
116
+
117
+ * **`Legate::Mcp::Server::LegateAgentAdapter`**: For agents defined and stored in the **`GlobalDefinitionRegistry`** (e.g., created via `legate agent save`).
118
+ * **`Legate::Mcp::Server::LegateDirectAgentAdapter`**: For `Legate::Agent` instances created directly in your Ruby code.
119
+
120
+ ### 2.1. Using `LegateAgentAdapter` (Registry Definition)
121
+
122
+ **Prerequisites:**
123
+ * Your agent definition must exist in the `GlobalDefinitionRegistry`.
124
+ * An `Legate::SessionService` instance is needed by the adapter for temporary session management.
125
+
126
+ ```ruby
127
+ require 'legate/mcp/server/legate_agent_adapter'
128
+ require 'legate/session_service/in_memory'
129
+
130
+ AGENT_NAME_IN_REGISTRY = 'my_chat_agent' # The name of your agent in the registry
131
+ session_service = Legate::SessionService::InMemory.new
132
+
133
+ # 1. Wrap the agent definition from the registry
134
+ AdaptedRegistryAgent = Legate::Mcp::Server::LegateAgentAdapter.wrap(AGENT_NAME_IN_REGISTRY, session_service)
135
+
136
+ # 2. Setup fast-mcp server and register AdaptedRegistryAgent
137
+ # ... (as shown in Tool Adapter example) ...
138
+ mcp_server.register_tool(AdaptedRegistryAgent)
139
+ # ... (start server) ...
140
+ ```
141
+ This will expose a tool named something like `run_agent_my_chat_agent` that accepts a `prompt` argument.
142
+
143
+ ### 2.2. Using `LegateDirectAgentAdapter` (Ruby Instance)
144
+
145
+ **Prerequisites:**
146
+ * An `Legate::Agent` instance created in your Ruby code.
147
+ * An `Legate::SessionService` instance.
148
+
149
+ ```ruby
150
+ require 'legate/mcp/server/legate_direct_agent_adapter'
151
+ require 'legate/session_service/in_memory'
152
+ require 'legate/tools/echo' # Example tool for the agent
153
+
154
+ # 1. Create your Legate Agent instance from a definition.
155
+ # Tools are selected on the definition via use_tool; ensure the tool
156
+ # class is registered globally so the agent can find it.
157
+ Legate::GlobalToolManager.register_tool(Legate::Tools::Echo)
158
+
159
+ agent_definition = Legate::AgentDefinition.new.define do |a|
160
+ a.name :my_code_defined_agent
161
+ a.instruction 'Echo the user input.'
162
+ a.use_tool :echo
163
+ end
164
+
165
+ my_agent_instance = Legate::Agent.new(definition: agent_definition)
166
+ # Do NOT call my_agent_instance.start() here if only using for MCP wrapping.
167
+
168
+ session_service = Legate::SessionService::InMemory.new
169
+
170
+ # 2. Wrap the agent *instance*
171
+ AdaptedDirectAgent = Legate::Mcp::Server::LegateDirectAgentAdapter.wrap(my_agent_instance, session_service)
172
+
173
+ # 3. Setup fast-mcp server and register AdaptedDirectAgent
174
+ # ... (as shown in Tool Adapter example) ...
175
+ mcp_server.register_tool(AdaptedDirectAgent)
176
+ # ... (start server) ...
177
+ ```
178
+ This will expose a tool named `run_agent_my_code_defined_agent`.
179
+
180
+ ### 2.3. Agent Adapter Limitations (Current Version)
181
+
182
+ * **Stateless Execution**: Each call to the wrapped agent tool via MCP is treated as a new, isolated interaction. A temporary Legate session is created for the `run_task` call and then deleted. There is no persistent conversation history between MCP calls to the agent tool.
183
+ * **Tool Availability**: For the `LegateAgentAdapter` (registry-based), any tools specified in the agent's definition must be available in the global `Legate::ToolRegistry` of the Ruby process running the MCP server.
184
+
185
+ ## 3. Server Setup with `fast-mcp`
186
+
187
+ `fast-mcp` offers different ways to run the MCP server:
188
+
189
+ * **STDIO Server (`FastMcp::Server.new` or `FastMcp::Server::Stdio.new`)**:
190
+ * Listens for JSON-RPC messages on standard input and writes responses to standard output.
191
+ * Ideal for local development, integration with tools that manage child processes (like `mcp-inspector`), or simple standalone tool/agent servers.
192
+ * Start by calling `mcp_server.start`.
193
+ * See `examples/15_mcp_server.rb`.
194
+ * **Rack Middleware (`FastMcp.rack_middleware`)**:
195
+ * Integrates the MCP server into any Rack-based web application (e.g., Sinatra, Rails).
196
+ * Adds MCP endpoints (typically `/mcp/messages` for POST and `/mcp/sse` for Server-Sent Events) to your existing app.
197
+ * Allows clients to connect via HTTP/SSE.
198
+ * See `examples/advanced/mcp/mcp_server_rack.rb`.
199
+
200
+ Refer to the [`fast-mcp` documentation](https://github.com/yjacquin/fast-mcp) for more details on server setup, authentication, and other advanced features.
201
+
202
+ ## 4. Security Considerations
203
+
204
+ * When exposing tools and agents via MCP, you are making their functionality available to any client that can connect to your `fast-mcp` server.
205
+ * If using the Rack middleware for an HTTP-accessible server, consider appropriate network security (firewalls, private networks) and authentication mechanisms if needed. `fast-mcp` provides options for token-based authentication (`FastMcp.authenticated_rack_middleware`) and origin checks.
206
+ * Be mindful of the capabilities of the Legate tools and agents you expose. Avoid exposing components that could lead to unintended actions or data access if called by unauthorized clients.
@@ -0,0 +1,128 @@
1
+ # Rails integration
2
+
3
+ Legate runs in any Ruby program, but it ships first-class glue for Rails: a
4
+ durable ActiveRecord session store, a Railtie, and an install generator. None of
5
+ it is loaded unless you ask for it — `require 'legate'` never touches Rails or
6
+ ActiveRecord.
7
+
8
+ ## Install
9
+
10
+ Add the gem and require the Rails integration:
11
+
12
+ ```ruby
13
+ # Gemfile
14
+ gem 'legate', require: 'legate/rails'
15
+ ```
16
+
17
+ Then run the generator and migrate:
18
+
19
+ ```sh
20
+ bin/rails generate legate:install
21
+ bin/rails db:migrate
22
+ ```
23
+
24
+ That creates two files:
25
+
26
+ - **`db/migrate/…_create_legate_tables.rb`** — the schema for the session store
27
+ (`legate_sessions`, `legate_events`, `legate_scoped_states`).
28
+ - **`config/initializers/legate.rb`** — points Legate at the ActiveRecord store
29
+ and maps `GEMINI_API_KEY` → `GOOGLE_API_KEY` for the planner.
30
+
31
+ The initializer wires the store explicitly, so you stay in control:
32
+
33
+ ```ruby
34
+ require 'legate/session_service/active_record'
35
+
36
+ Legate.configure do |config|
37
+ config.session_service = Legate::SessionService::ActiveRecord.new
38
+ end
39
+ ```
40
+
41
+ (Prefer Rails encrypted credentials for the API key in production rather than a
42
+ plain env var.)
43
+
44
+ ## Durable sessions
45
+
46
+ With the ActiveRecord store configured, every conversation — its events and its
47
+ state — is persisted to your database and survives restarts. The store behaves
48
+ exactly like the in-memory one within a single run (the agent sees one live
49
+ `Session` object), but writes through on every change, so another process or a
50
+ later request re-hydrates the committed history:
51
+
52
+ ```ruby
53
+ service = Legate.config.session_service # the AR store
54
+ session = service.create_session(app_name: 'support', user_id: current_user.id)
55
+
56
+ agent.run_task(session_id: session.id, user_input: params[:message], session_service: service)
57
+
58
+ # …later, another request / process:
59
+ restored = service.get_session(session_id: session.id)
60
+ restored.events # the full persisted history
61
+ restored.get_state(:some_key)
62
+ ```
63
+
64
+ `persistent?` returns `true`, and scoped state (`user:` / `app:` / `temp:` keys)
65
+ is persisted too.
66
+
67
+ ## Running agents in the background (ActiveJob)
68
+
69
+ Agent runs can take many seconds — do them off the request thread. Because the
70
+ store is durable, the controller enqueues a job and reads the result back from
71
+ the persisted session when it's done (poll, or push via Turbo/ActionCable):
72
+
73
+ ```ruby
74
+ class LegateRunJob < ApplicationJob
75
+ queue_as :default
76
+
77
+ def perform(agent_name:, session_id:, message:)
78
+ agent = MyAgents.build(agent_name) # your factory: definition -> started Agent
79
+ agent.run_task(
80
+ session_id: session_id,
81
+ user_input: message,
82
+ session_service: Legate.config.session_service,
83
+ )
84
+ # The final answer + full history are now persisted on the session.
85
+ end
86
+ end
87
+ ```
88
+
89
+ ```ruby
90
+ # app/controllers/messages_controller.rb
91
+ session = Legate.config.session_service.create_session(app_name: 'support', user_id: current_user.id)
92
+ LegateRunJob.perform_later(agent_name: 'support', session_id: session.id, message: params[:message])
93
+ render json: { session_id: session.id }
94
+ ```
95
+
96
+ Pair this with [event streaming](streaming): pass an `on_event:` callback to
97
+ `run_task` inside the job to broadcast progress over ActionCable as each tool
98
+ runs.
99
+
100
+ ## Without Rails
101
+
102
+ The store is plain ActiveRecord — you can use it in any Ruby program. Establish a
103
+ connection and create the tables yourself:
104
+
105
+ ```ruby
106
+ require 'active_record'
107
+ require 'legate/session_service/active_record'
108
+
109
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: 'legate.db')
110
+ Legate::SessionService::ActiveRecord.create_tables! # idempotent
111
+
112
+ Legate.configure { |c| c.session_service = Legate::SessionService::ActiveRecord.new }
113
+ ```
114
+
115
+ The host application owns the connection and pool; Legate doesn't manage it.
116
+
117
+ ## Notes
118
+
119
+ - Event content and state are stored as JSON. Resumed (historical) event content
120
+ comes back with string keys — the live run uses in-memory objects with their
121
+ original keys, so this only affects how you read prior turns.
122
+ - The schema is portable across SQLite, PostgreSQL, and MySQL (JSON stored as
123
+ text); the generated migration matches `ActiveRecord.create_tables!`.
124
+
125
+ ## Related
126
+
127
+ - [Streaming agent events](streaming) — progress over ActionCable from a job.
128
+ - [LLM Providers](llm_providers) — configuring the model in the initializer.
@@ -0,0 +1,227 @@
1
+ # Sending Outbound Webhooks from Legate Agents
2
+
3
+ The Legate Agents often need to notify external systems upon completing a task or when a specific event occurs. This can be achieved by sending outbound HTTP requests, commonly known as webhooks.
4
+
5
+ ## Architecture Overview: Sending Outbound Webhooks
6
+
7
+ The following diagram illustrates the two primary ways an Legate Agent can send outbound webhooks:
8
+
9
+ ```mermaid
10
+ graph TD
11
+ subgraph Legate Agent Process
12
+ Agent[Legate::Agent]
13
+ Planner[Legate::Planner]
14
+ WebhookTool[Legate::Tools::WebhookTool]
15
+ CustomTool[Your Custom Tool with HttpClient]
16
+ HttpClient[Legate::Tools::Base::HttpClient]
17
+
18
+ Agent -- Task Prompt --> Planner
19
+ Planner -- Selects Tool --> WebhookTool
20
+ Planner -- Selects Tool --> CustomTool
21
+ CustomTool -- Uses --> HttpClient
22
+ end
23
+
24
+ subgraph External Systems
25
+ ExternalService1[External Service A]
26
+ ExternalService2[External Service B]
27
+ end
28
+
29
+ WebhookTool -- HTTP POST --> ExternalService1
30
+ HttpClient -- HTTP Request (GET/HEAD/POST/PUT etc.) --> ExternalService2
31
+
32
+ style Agent fill:#ccf,stroke:#333,stroke-width:2px
33
+ style Planner fill:#ccf,stroke:#333,stroke-width:2px
34
+ style WebhookTool fill:#cff,stroke:#333,stroke-width:2px
35
+ style CustomTool fill:#cff,stroke:#333,stroke-width:2px
36
+ style HttpClient fill:#cff,stroke:#333,stroke-width:2px
37
+ style ExternalService1 fill:#f9f,stroke:#333,stroke-width:2px
38
+ style ExternalService2 fill:#f9f,stroke:#333,stroke-width:2px
39
+ ```
40
+
41
+ **Key Scenarios Depicted:**
42
+
43
+ 1. **Using `WebhookTool`:** The agent, guided by the planner, uses the built-in `WebhookTool` to send a (typically POST) request to an external service.
44
+ 2. **Using a Custom Tool with `HttpClient`:** The agent, guided by the planner, invokes a custom-developed tool. This custom tool then utilizes the `Legate::Tools::Base::HttpClient` module to make more complex or varied HTTP requests to another (or the same) external service.
45
+
46
+ There are two primary ways to send outbound webhooks from within the Legate framework:
47
+
48
+ 1. **Using the built-in `WebhookTool`:** A convenient tool specifically designed for sending standard HTTP POST requests with JSON payloads, including optional HMAC-SHA256 signing. This is the recommended approach for common webhook integrations.
49
+ 2. **Using the `HttpClient` module within a Custom Tool:** For more complex scenarios requiring different HTTP methods (GET, PUT, etc.), custom headers, non-JSON bodies, or more intricate logic before/after the request, you can create a custom tool that utilizes the `Legate::Tools::Base::HttpClient` mixin.
50
+
51
+ ## 1. Using the `WebhookTool`
52
+
53
+ The `Legate::Tools::WebhookTool` provides a simple interface for sending POST requests.
54
+
55
+ ### Adding the Tool to your Agent
56
+
57
+ Ensure the tool is available to your agent by including it in the agent definition:
58
+
59
+ ```ruby
60
+ # my_notifying_agent.rb
61
+ Legate::Agent.define do |a|
62
+ a.name :my_notifying_agent
63
+ a.description "Performs a task and notifies an external system."
64
+ # ... other config ...
65
+
66
+ a.use_tool :webhook_tool # Make the tool available
67
+ end
68
+ ```
69
+
70
+ ### Instructing the Agent
71
+
72
+ You instruct the agent to use the tool within its task prompt, providing the necessary parameters.
73
+
74
+ **`WebhookTool` Parameters:**
75
+
76
+ * `url` (String, required): The target URL to send the POST request to.
77
+ * `payload` (Hash | String, required): The data to send.
78
+ * If a `Hash` is provided, it will be automatically encoded as JSON, and the `Content-Type` header will be set to `application/json; charset=utf-8`.
79
+ * If a `String` is provided, it will be sent as the raw request body. You might need to specify a `Content-Type` via the `headers` parameter if the receiving system requires it.
80
+ * `secret` (String, optional): If provided, the tool will calculate an HMAC-SHA256 signature of the request body using this secret. The signature will be added to the request headers as `X-Hub-Signature-256: sha256=<calculated_signature>`. **Note:** Passing secrets directly in agent prompts can have security implications. Consider if a custom tool with configured secrets is more appropriate for sensitive integrations.
81
+ * `headers` (Hash, optional): Additional custom headers to include in the request (e.g., `{'Authorization': 'Bearer ...'}`). These are merged with default headers like `User-Agent` and the automatically added `Content-Type` (for Hash payloads) or `X-Hub-Signature-256` (if `secret` is used).
82
+
83
+ **Example Agent Prompt:**
84
+
85
+ ```
86
+ "Analyze the provided data file. Once the analysis is complete and saved, use the `webhook_tool` to notify the monitoring system. Send a POST request with the following parameters:
87
+ - url: 'https://monitor.example.com/api/v1/updates'
88
+ - payload: A JSON object like {'task_id': '12345', 'status': 'completed', 'result_summary': 'Analysis finished successfully.'}
89
+ - secret: 'my-monitoring-api-secret'
90
+ - headers: {'X-Source-System': 'Legate-Agent'}"
91
+ ```
92
+
93
+ ### Result
94
+
95
+ The `webhook_tool` will return a result indicating success or failure:
96
+
97
+ * **Success:** `{ status: :success, result: { response_status: <Integer>, response_body: <String> } }`
98
+ * **Failure:** Raises an `Legate::ToolError` (or a subclass like `Legate::ToolHttpError`, `Legate::ToolTimeoutError`) which the agent should handle or report.
99
+
100
+ ## 2. Using `HttpClient` in a Custom Tool
101
+
102
+ For more control over the HTTP request (e.g., using methods other than POST, sending non-JSON data, complex authentication headers, custom retry logic), create a dedicated custom tool that includes the `Legate::Tools::Base::HttpClient` module.
103
+
104
+ **Important:** The agent itself *cannot* directly call `http_get`, `http_post`, etc. It must invoke a *custom tool* that encapsulates this logic.
105
+
106
+ ### Creating the Custom Tool
107
+
108
+ ```ruby
109
+ # lib/my_app/tools/notification_tool.rb
110
+ require 'legate/tool'
111
+ require 'legate/tools/base/http_client'
112
+ require 'json'
113
+ require 'openssl' # If manual signing is needed
114
+
115
+ module MyApp
116
+ module Tools
117
+ # The tool name :notification_tool is inferred from the class name.
118
+ # (To override, use `self.explicit_tool_name = :some_name`.)
119
+ class NotificationTool < Legate::Tool
120
+ include Legate::Tools::Base::HttpClient
121
+
122
+ tool_description "Sends notifications to a specific internal system."
123
+
124
+ parameter :endpoint, type: :string, required: true, description: "The API endpoint path (e.g., '/events')."
125
+ parameter :event_data, type: :hash, required: true, description: "Data for the notification payload."
126
+ # NOTE: `default:` is not applied by the framework; defaults are handled in
127
+ # perform_execution below (e.g. `params[:http_method] || 'POST'`).
128
+ parameter :http_method, type: :string, required: false, description: "HTTP method (POST, PUT, etc.). Defaults to POST."
129
+
130
+ def initialize(**options)
131
+ super(**options)
132
+ # Setup HttpClient - read base URL/secret from ENV or config
133
+ # IMPORTANT: Avoid hardcoding secrets!
134
+ @api_base_url = ENV.fetch('NOTIFICATION_API_URL', 'https://internal.example.com/api')
135
+ @api_secret = ENV['NOTIFICATION_API_SECRET'] # Optional secret
136
+
137
+ setup_http_client(
138
+ base_url: @api_base_url,
139
+ headers: { 'Accept' => 'application/json' },
140
+ options: { read_timeout: 10 }
141
+ )
142
+ end
143
+
144
+ private
145
+
146
+ def perform_execution(params, context)
147
+ endpoint = params[:endpoint]
148
+ event_data = params[:event_data]
149
+ # Handle the default in code; the `default:` parameter option is not applied.
150
+ http_method = (params[:http_method] || 'POST').downcase.to_sym
151
+
152
+ request_headers = {}
153
+ request_body = JSON.generate(event_data) rescue event_data.to_s # Ensure JSON encoding
154
+
155
+ # Example: Manual HMAC Signing (if not using WebhookTool's secret param)
156
+ if @api_secret
157
+ signature = OpenSSL::HMAC.hexdigest('sha256', @api_secret, request_body)
158
+ request_headers['X-Internal-Signature-256'] = "sha256=#{signature}"
159
+ end
160
+ # Always set Content-Type for JSON body when using HttpClient directly
161
+ request_headers['Content-Type'] = 'application/json; charset=utf-8'
162
+
163
+ begin
164
+ response = make_request(
165
+ http_method,
166
+ endpoint, # Path relative to base_url
167
+ body: request_body,
168
+ headers: request_headers
169
+ )
170
+ Legate.logger.info "NotificationTool: Successfully sent #{http_method.upcase} to #{endpoint}. Status: #{response.status}"
171
+ # Return a success indicator
172
+ { status: :success, response_code: response.status }
173
+ rescue Legate::ToolHttpError => e
174
+ Legate.logger.error "NotificationTool: HTTP Error sending to #{endpoint}. Status: #{e.response&.status}. Body: #{e.response&.body}"
175
+ # Re-raise or return structured error
176
+ raise Legate::ToolError, "Failed to send notification (HTTP #{e.response&.status})"
177
+ rescue Legate::ToolError => e # Includes timeouts, network errors etc.
178
+ Legate.logger.error "NotificationTool: Error sending to #{endpoint}: #{e.message}"
179
+ # Re-raise or return structured error
180
+ raise Legate::ToolError, "Failed to send notification: #{e.message}"
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ ```
187
+
188
+ *(See [`http_client_usage`](./http_client_usage) for full details on using the `HttpClient` module).*
189
+
190
+ ### Adding the Custom Tool to your Agent
191
+
192
+ ```ruby
193
+ # my_complex_agent.rb
194
+ # require 'my_app/tools/notification_tool' # Make sure tool class is loaded
195
+
196
+ Legate::Agent.define do |a|
197
+ a.name :my_complex_agent
198
+ # ...
199
+
200
+ a.use_tool :notification_tool # Use the custom tool
201
+ end
202
+ ```
203
+
204
+ ### Instructing the Agent (to use the Custom Tool)
205
+
206
+ ```
207
+ "After processing the order (ID: #{order_id}), use the `notification_tool` to inform the fulfillment system. Use the endpoint '/order_updates' and send the following event_data as JSON: {'orderId': '#{order_id}', 'status': 'processed', 'items': [...]}. Use the default POST method."
208
+ ```
209
+
210
+ ## Choosing the Right Approach
211
+
212
+ * Use **`WebhookTool`** if:
213
+ * You need to send a simple HTTP POST request.
214
+ * Your payload is typically JSON (Hash).
215
+ * You need standard HMAC-SHA256 signing (`X-Hub-Signature-256`).
216
+ * You are comfortable with the agent prompt potentially containing the signing secret (use with caution).
217
+ * Use **`HttpClient` in a Custom Tool** if:
218
+ * You need to use other HTTP methods (GET, HEAD, PUT, DELETE, etc.).
219
+ * You need to send non-JSON request bodies (e.g., XML, form data) and manage `Content-Type` manually.
220
+ * You require complex custom headers or authentication schemes beyond simple HMAC.
221
+ * You need to perform logic before or after the HTTP request within the tool itself.
222
+ * You want to manage API secrets within the tool's configuration (e.g., via ENV variables) rather than passing them via agent parameters.
223
+
224
+ ## Security Considerations
225
+
226
+ * **Secrets Management:** Avoid hardcoding secrets. Use environment variables or a dedicated configuration management system to provide secrets (like API keys or webhook signing keys) to your custom tools or potentially to the `WebhookTool`'s `secret` parameter (though configuring them in the tool is generally safer than passing via LLM prompt).
227
+ * **URL Validation:** Ensure the URLs being targeted by your webhooks are intended and trusted. Be cautious if URLs are dynamically generated based on user input.
@@ -0,0 +1,112 @@
1
+ # Streaming agent events
2
+
3
+ An agent task can take many seconds — it may call several tools before it
4
+ answers. Rather than block until the final result, you can **stream the agent's
5
+ lifecycle events as they happen**: the user message, each tool request, each tool
6
+ result, and the final answer. This is how you build a responsive "thinking…
7
+ calling search… answering" experience instead of a frozen spinner.
8
+
9
+ ## `on_event` — the streaming callback
10
+
11
+ `Agent#run_task` accepts an optional `on_event:` proc. It's called with each
12
+ `Legate::Event` the moment it's appended to the session, while the task runs:
13
+
14
+ ```ruby
15
+ agent.run_task(
16
+ session_id: session.id,
17
+ user_input: 'Find the population of France and double it',
18
+ session_service: session_service,
19
+ on_event: ->(event) do
20
+ case event.role
21
+ when :user then puts "▶ #{event.content}"
22
+ when :tool_request then puts " → calling #{event.tool_name}"
23
+ when :tool_result then puts " ← #{event.content[:result]}"
24
+ when :agent then puts "✓ #{event.content[:result]}"
25
+ end
26
+ end
27
+ )
28
+ ```
29
+
30
+ The events you'll see, in order:
31
+
32
+ | `event.role` | When | `event.content` |
33
+ |----------------|------|-----------------|
34
+ | `:user` | task starts | the user input string |
35
+ | `:tool_request`| before each tool runs | the params; `event.tool_name` is the tool |
36
+ | `:tool_result` | after each tool runs | `{ status:, result: / error_message: }` |
37
+ | `:agent` | task finishes | the final result hash |
38
+
39
+ `on_event` is **purely additive**: the method still returns the final `Event`, and
40
+ omitting `on_event` leaves behavior exactly as before. It works for both the
41
+ default plan-then-execute strategy and the [agentic `:react` loop](agentic_agents)
42
+ — every event flows through the same session funnel.
43
+
44
+ ### How it works
45
+
46
+ Every event an agent produces is persisted through `SessionService#append_event`.
47
+ The service broadcasts each appended event to subscribers of that session
48
+ (`EventBroadcast`), and `run_task` subscribes your `on_event` for the duration of
49
+ the run, tearing the subscription down afterward. Delivery is synchronous and
50
+ in order on the running thread, so a streaming HTTP response can write each frame
51
+ as it arrives.
52
+
53
+ ## Server-Sent Events over HTTP
54
+
55
+ The web app exposes the stream as SSE:
56
+
57
+ ```
58
+ POST /agents/:name/stream (form field: message=…, plus the CSRF token)
59
+ Content-Type: text/event-stream
60
+ ```
61
+
62
+ Each appended event is sent as an `event: message` frame; the run ends with an
63
+ `event: done` frame carrying the final result (or `event: error`):
64
+
65
+ ```
66
+ event: message
67
+ data: {"role":"user","content":"double the population of France",…}
68
+
69
+ event: message
70
+ data: {"role":"tool_request","tool_name":"search","content":{…},…}
71
+
72
+ event: message
73
+ data: {"role":"tool_result","content":{"status":"success","result":"67 million"},…}
74
+
75
+ event: done
76
+ data: {"role":"agent","content":{"status":"success","result":"134 million"},…}
77
+ ```
78
+
79
+ The frame payloads are `Event#to_h` (JSON, ISO-8601 timestamps). Because the
80
+ endpoint is CSRF-protected it can't be consumed by a bare `EventSource` (which is
81
+ GET-only and can't send the token); use `fetch` with a streaming reader:
82
+
83
+ ```js
84
+ const res = await fetch(`/agents/${name}/stream`, {
85
+ method: 'POST',
86
+ headers: { 'X-CSRF-Token': csrfToken, 'Content-Type': 'application/x-www-form-urlencoded' },
87
+ body: new URLSearchParams({ message }),
88
+ });
89
+ const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
90
+ for (;;) {
91
+ const { value, done } = await reader.read();
92
+ if (done) break;
93
+ // parse SSE frames out of `value`…
94
+ }
95
+ ```
96
+
97
+ Each request runs as a fresh one-shot session.
98
+
99
+ ## Scope: steps, not tokens
100
+
101
+ Streaming here is **step/event** level — you get each tool call and result as it
102
+ happens. Legate agents always plan and call tools rather than emitting free-form
103
+ model prose, and in the agentic loop the final answer arrives whole inside one
104
+ decision, so there's no separate token stream to forward. Token-level streaming
105
+ is a possible future addition (an adapter `supports_streaming?` capability); it
106
+ isn't needed to solve the "no feedback during a long run" problem, which event
107
+ streaming already does.
108
+
109
+ ## Related
110
+
111
+ - [Agentic Agents](agentic_agents) — multi-step runs where streaming shines.
112
+ - [Legate Planner](../core_concepts/legate_planner) — what produces the steps.