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,49 @@
1
+ # File: lib/legate/agentic/decision.rb
2
+ # frozen_string_literal: true
3
+
4
+ module Legate
5
+ # The agentic (observe -> think -> act) execution strategy.
6
+ module Agentic
7
+ # One step of an agentic loop: the model either calls a tool or gives a
8
+ # final answer. Immutable.
9
+ #
10
+ # @!attribute action [Symbol] :tool or :final
11
+ # @!attribute thought [String, nil] the model's reasoning
12
+ # @!attribute tool [Symbol, nil] the tool to call (when action == :tool)
13
+ # @!attribute params [Hash] tool arguments (when action == :tool)
14
+ # @!attribute answer [String, nil] the final answer (when action == :final)
15
+ Decision = Data.define(:action, :thought, :tool, :params, :answer) do
16
+ def self.tool(tool:, params:, thought: nil)
17
+ new(action: :tool, thought: thought, tool: tool.to_sym, params: params || {}, answer: nil)
18
+ end
19
+
20
+ def self.final(answer:, thought: nil)
21
+ new(action: :final, thought: thought, tool: nil, params: {}, answer: answer)
22
+ end
23
+
24
+ # An unusable decision (the model returned neither a valid tool call nor a
25
+ # final answer).
26
+ def self.invalid(thought: nil)
27
+ new(action: :invalid, thought: thought, tool: nil, params: {}, answer: nil)
28
+ end
29
+
30
+ def final?
31
+ action == :final
32
+ end
33
+
34
+ def tool?
35
+ action == :tool && !tool.nil? && !tool.to_s.empty?
36
+ end
37
+
38
+ def invalid?
39
+ !final? && !tool?
40
+ end
41
+
42
+ # The plan step hash that PlanExecutor#execute_step expects.
43
+ # @return [Hash]
44
+ def to_step
45
+ { tool: tool, params: params }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,134 @@
1
+ # File: lib/legate/agentic/loop.rb
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'decision'
5
+
6
+ module Legate
7
+ module Agentic
8
+ # Drives the observe -> think -> act loop: ask the planner for the next
9
+ # single action, run it via the executor, feed the result back as an
10
+ # observation, and repeat until the model gives a final answer or the
11
+ # iteration cap is hit.
12
+ #
13
+ # Returns the same { details:, last_result: } shape as
14
+ # PlanExecutor#execute_plan, so Agent#run_task builds the final event
15
+ # identically for both execution strategies.
16
+ class Loop
17
+ DEFAULT_MAX_ITERATIONS = 8
18
+ # Long string tool results are truncated to this many characters before
19
+ # being fed back, so one big output doesn't dominate the prompt each turn.
20
+ MAX_OBSERVATION_CHARS = 2_000
21
+
22
+ # @param planner [#reason_next_action] returns a Decision for the next step
23
+ # @param executor [#execute_step] runs one tool step (e.g. a PlanExecutor)
24
+ # @param logger [Logger, nil]
25
+ # @param max_iterations [Integer]
26
+ def initialize(planner:, executor:, logger: nil, max_iterations: DEFAULT_MAX_ITERATIONS)
27
+ @planner = planner
28
+ @executor = executor
29
+ @logger = logger || Legate.logger
30
+ @max_iterations = max_iterations
31
+ end
32
+
33
+ # @return [Hash] { details: <observations>, last_result: <result hash> }
34
+ def run(user_input:, session:, session_service:, invocation_id: nil)
35
+ observations = []
36
+
37
+ @max_iterations.times do |i|
38
+ decision = @planner.reason_next_action(user_input, observations, invocation_id)
39
+
40
+ return success(decision.answer, observations) if decision.final?
41
+
42
+ if decision.invalid?
43
+ @logger.warn("Agentic loop: model returned an unusable decision at step #{i + 1}; stopping.")
44
+ return error('The agent could not decide on a valid next action.', observations)
45
+ end
46
+
47
+ result = execute(decision, session, session_service, invocation_id)
48
+ observation = { tool: decision.tool, params: decision.params, result: sanitize(result) }
49
+ spinning = observation == observations.last
50
+ observations << observation
51
+
52
+ # Loop-breaker: the model just repeated the exact same action and got
53
+ # the exact same result — re-running won't make progress, so stop and
54
+ # summarize rather than burn the rest of the iteration budget.
55
+ next unless spinning
56
+
57
+ @logger.warn("Agentic loop: repeated action '#{decision.tool}' with no change; stopping to avoid spinning.")
58
+ return finish_without_final(user_input, observations, invocation_id,
59
+ fallback: "Stopped after repeating the same action ('#{decision.tool}') without progress.")
60
+ end
61
+
62
+ @logger.warn("Agentic loop: reached the #{@max_iterations}-iteration cap without a final answer.")
63
+ finish_without_final(user_input, observations, invocation_id,
64
+ fallback: "Stopped after #{@max_iterations} steps without a final answer.")
65
+ end
66
+
67
+ private
68
+
69
+ # The loop stopped without the model giving a final answer. Try one
70
+ # best-effort summary of the observations; fall back to an error result if
71
+ # the planner can't summarize (no adapter, failure, or empty answer).
72
+ def finish_without_final(user_input, observations, invocation_id, fallback:)
73
+ summary = summarize(user_input, observations, invocation_id)
74
+ return success(summary, observations) if summary
75
+
76
+ error(fallback, observations)
77
+ end
78
+
79
+ def summarize(user_input, observations, invocation_id)
80
+ return nil unless @planner.respond_to?(:summarize_final)
81
+
82
+ answer = @planner.summarize_final(user_input, observations, invocation_id)
83
+ answer if answer.is_a?(String) && !answer.empty?
84
+ rescue StandardError => e
85
+ @logger.error("Agentic loop: summary failed: #{e.class}: #{e.message}")
86
+ nil
87
+ end
88
+
89
+ def execute(decision, session, session_service, invocation_id)
90
+ @executor.execute_step(decision.to_step, session, session_service, invocation_id)
91
+ rescue StandardError => e
92
+ # A hard tool failure becomes an observation the model can react to on
93
+ # the next turn — it does not abort the loop.
94
+ @logger.error("Agentic loop: tool '#{decision.tool}' raised: #{e.class}: #{e.message}")
95
+ { status: :error, error_message: "Tool '#{decision.tool}' raised: #{e.message}" }
96
+ end
97
+
98
+ def success(answer, observations)
99
+ { details: observations, last_result: { status: :success, result: answer } }
100
+ end
101
+
102
+ def error(message, observations)
103
+ { details: observations, last_result: { status: :error, error_message: message } }
104
+ end
105
+
106
+ # Keep large tool outputs from blowing the context fed back to the model
107
+ # (mirrors PlanExecutor#execute_plan's per-step sanitization).
108
+ def sanitize(result)
109
+ return result unless result.is_a?(Hash)
110
+
111
+ out = { status: result[:status] }
112
+ out[:error_message] = result[:error_message] if result.key?(:error_message)
113
+ out[:job_id] = result[:job_id] if result.key?(:job_id)
114
+ out[:message] = result[:message] if result.key?(:message)
115
+
116
+ val = result[:result]
117
+ if val.is_a?(String)
118
+ out[:result] = truncate(val)
119
+ elsif val.is_a?(Numeric) || [true, false, nil].include?(val)
120
+ out[:result] = val
121
+ elsif result.key?(:result)
122
+ out[:result] = '[Complex Result Structure]'
123
+ end
124
+ out
125
+ end
126
+
127
+ def truncate(str)
128
+ return str if str.length <= MAX_OBSERVATION_CHARS
129
+
130
+ "#{str[0, MAX_OBSERVATION_CHARS]}… [truncated #{str.length - MAX_OBSERVATION_CHARS} chars]"
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,5 @@
1
+ # File: lib/legate/agentic.rb
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'agentic/decision'
5
+ require_relative 'agentic/loop'
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ # File: lib/legate/agents/loop_agent.rb
4
+ require_relative '../agent'
5
+
6
+ module Legate
7
+ module Agents
8
+ # LoopAgent executes a set of sub-agents repeatedly until a condition is met or
9
+ # maximum iterations are reached.
10
+ class LoopAgent < Legate::Agent
11
+ DEFAULT_MAX_ITERATIONS = 100
12
+ DEFAULT_TIMEOUT_SECONDS = 3600 # 1 hour
13
+ MAX_STORED_ITERATIONS = 50
14
+ # Override run_task to execute sub-agents in a loop
15
+ # @param session_id [String] The session ID
16
+ # @param user_input [String] User input to process
17
+ # @param session_service [Legate::SessionService::Base] Session service for persistence
18
+ # @return [Legate::Event] The final agent event
19
+ def run_task(session_id:, user_input:, session_service:)
20
+ # Verify we have loop sub-agents defined
21
+ unless @definition.loop_sub_agent_names&.any?
22
+ err_msg = "LoopAgent '#{name}' has no loop_sub_agent_names defined."
23
+ Legate.logger.error(err_msg)
24
+ return Legate::Event.new(role: :agent, content: {
25
+ status: :error,
26
+ error_message: err_msg,
27
+ error_class: 'ConfigurationError'
28
+ })
29
+ end
30
+
31
+ # Verify we have either loop_max_iterations or a condition
32
+ unless @definition.loop_max_iterations || (@definition.loop_condition_state_key && !@definition.loop_condition_expected_value.nil?)
33
+ err_msg = "LoopAgent '#{name}' must define either loop_max_iterations or loop_condition (state key + expected value)."
34
+ Legate.logger.error(err_msg)
35
+ return Legate::Event.new(role: :agent, content: {
36
+ status: :error,
37
+ error_message: err_msg,
38
+ error_class: 'ConfigurationError'
39
+ })
40
+ end
41
+
42
+ # --- Pre-execution Checks --- #
43
+ unless running?
44
+ err_msg = "Agent '#{name}' is not running. Call agent.start before run_task, " \
45
+ 'or use agent.ask (which starts automatically).'
46
+ Legate.logger.error(err_msg)
47
+ return Legate::Event.new(role: :agent, content: { status: :error, error_message: err_msg })
48
+ end
49
+
50
+ session = session_service.get_session(session_id: session_id)
51
+ unless session
52
+ err_msg = "Session not found: #{session_id}"
53
+ Legate.logger.error(err_msg)
54
+ return Legate::Event.new(role: :agent, content: { status: :error, error_message: err_msg })
55
+ end
56
+ # --------------------------- #
57
+
58
+ # Log user input to the LoopAgent itself
59
+ user_event = Legate::Event.new(role: :user, content: user_input)
60
+ session_service.append_event(session_id: session_id, event: user_event)
61
+
62
+ # Determine loop parameters
63
+ max_iterations = @definition.loop_max_iterations || DEFAULT_MAX_ITERATIONS
64
+ timeout_seconds = @definition.respond_to?(:loop_timeout_seconds) && @definition.loop_timeout_seconds || DEFAULT_TIMEOUT_SECONDS
65
+ condition_key = @definition.loop_condition_state_key
66
+ expected_value = @definition.loop_condition_expected_value
67
+
68
+ Legate.logger.info("LoopAgent '#{name}' starting execution with max #{max_iterations} iterations, #{timeout_seconds}s timeout" +
69
+ (condition_key ? " or until #{condition_key} equals #{expected_value.inspect}" : ''))
70
+
71
+ # Track loop iterations and results
72
+ iteration = 0
73
+ all_iterations = []
74
+ final_result = nil
75
+ loop_condition_met = false
76
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
77
+
78
+ # Execute the loop
79
+ while iteration < max_iterations
80
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
81
+ if elapsed >= timeout_seconds
82
+ Legate.logger.warn("LoopAgent '#{name}' timed out after #{elapsed.round(1)}s (limit: #{timeout_seconds}s)")
83
+ final_result = {
84
+ status: :error,
85
+ error_message: "Loop timed out after #{elapsed.round(1)} seconds (#{iteration} iterations completed)",
86
+ error_class: 'TimeoutError',
87
+ iterations_completed: iteration,
88
+ max_iterations: max_iterations,
89
+ loop_condition_met: false,
90
+ iterations: all_iterations
91
+ }
92
+ break
93
+ end
94
+ iteration += 1
95
+ Legate.logger.info("LoopAgent '#{name}' starting iteration #{iteration}/#{max_iterations == Float::INFINITY ? '∞' : max_iterations}")
96
+
97
+ # Check condition (if defined) before executing the iteration
98
+ if condition_key && session_service.respond_to?(:get_state)
99
+ current_value = session_service.get_state(session_id: session_id, key: condition_key)
100
+ if current_value == expected_value
101
+ Legate.logger.info("LoopAgent '#{name}' condition met: #{condition_key} = #{expected_value.inspect}. Exiting loop.")
102
+ loop_condition_met = true
103
+ break
104
+ end
105
+ end
106
+
107
+ # Execute one iteration (all sub-agents in sequence)
108
+ iteration_results = []
109
+ iteration_error = nil
110
+
111
+ # Execute each sub-agent in order (sequential execution within each iteration)
112
+ @definition.loop_sub_agent_names.each_with_index do |sub_agent_name, index|
113
+ sub_agent = find_sub_agent(sub_agent_name)
114
+ unless sub_agent
115
+ err_msg = "Sub-agent '#{sub_agent_name}' not found for LoopAgent '#{name}'."
116
+ Legate.logger.error(err_msg)
117
+ iteration_error = {
118
+ status: :error,
119
+ error_message: err_msg,
120
+ error_class: 'MissingSubAgentError',
121
+ step: index + 1,
122
+ total_steps: @definition.loop_sub_agent_names.size,
123
+ previous_results: iteration_results.map.with_index { |r, i| { agent: @definition.loop_sub_agent_names.to_a[i], result: r } }
124
+ }
125
+ break # Stop this iteration's execution
126
+ end
127
+
128
+ # Start the sub-agent if it's not already running
129
+ sub_agent.start unless sub_agent.running?
130
+
131
+ # Execute the sub-agent with the same session and input
132
+ begin
133
+ Legate.logger.info("LoopAgent '#{name}' executing sub-agent '#{sub_agent_name}' (iteration #{iteration}, step #{index + 1}/#{@definition.loop_sub_agent_names.size}).")
134
+ sub_result = sub_agent.run_task(
135
+ session_id: session_id,
136
+ user_input: user_input,
137
+ session_service: session_service
138
+ )
139
+
140
+ # Record the result
141
+ iteration_results << { agent: sub_agent_name, result: sub_result.content }
142
+
143
+ # Check for error to break sequence
144
+ if sub_result.content[:status] == :error
145
+ Legate.logger.warn("Sub-agent '#{sub_agent_name}' returned error, breaking iteration: #{sub_result.content[:error_message]}")
146
+ iteration_error = {
147
+ status: :error,
148
+ error_message: "Error in sub-agent '#{sub_agent_name}': #{sub_result.content[:error_message]}",
149
+ error_class: sub_result.content[:error_class] || 'SubAgentError',
150
+ step: index + 1,
151
+ total_steps: @definition.loop_sub_agent_names.size,
152
+ sub_agent: sub_agent_name.to_s,
153
+ sub_result: sub_result.content
154
+ }
155
+ break # Stop this iteration's execution on error
156
+ end
157
+ rescue StandardError => e
158
+ Legate.logger.error("Error executing sub-agent '#{sub_agent_name}': #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}")
159
+ iteration_error = {
160
+ status: :error,
161
+ error_message: "Exception in sub-agent '#{sub_agent_name}': #{e.message}",
162
+ error_class: e.class.name,
163
+ step: index + 1,
164
+ total_steps: @definition.loop_sub_agent_names.size,
165
+ sub_agent: sub_agent_name.to_s
166
+ }
167
+ break # Stop this iteration's execution on error
168
+ end
169
+ end
170
+
171
+ # Record this iteration's results, capping stored history
172
+ all_iterations << {
173
+ iteration: iteration,
174
+ results: iteration_results,
175
+ error: iteration_error
176
+ }
177
+ all_iterations.shift if all_iterations.size > MAX_STORED_ITERATIONS
178
+
179
+ # If there was an error in this iteration, we may need to break the loop
180
+ if iteration_error
181
+ # An error occurred during execution - decide whether to break the loop
182
+ Legate.logger.warn("LoopAgent '#{name}' iteration #{iteration} encountered an error. Exiting loop.")
183
+ final_result = {
184
+ status: :error,
185
+ error_message: "Loop terminated due to error in iteration #{iteration}: #{iteration_error[:error_message]}",
186
+ error_class: iteration_error[:error_class],
187
+ iterations_completed: iteration,
188
+ max_iterations: max_iterations,
189
+ loop_condition_met: false,
190
+ iterations: all_iterations
191
+ }
192
+ break # Exit the loop on error
193
+ end
194
+
195
+ # Check condition (if defined) after executing the iteration
196
+ next unless condition_key && session_service.respond_to?(:get_state)
197
+
198
+ current_value = session_service.get_state(session_id: session_id, key: condition_key)
199
+ next unless current_value == expected_value
200
+
201
+ Legate.logger.info("LoopAgent '#{name}' condition met: #{condition_key} = #{expected_value.inspect}. Exiting loop.")
202
+ loop_condition_met = true
203
+ break
204
+ end
205
+
206
+ # If we didn't set a final_result due to an error, create a success result
207
+ if final_result.nil?
208
+ completion_reason = if loop_condition_met
209
+ "condition met (#{condition_key} = #{expected_value.inspect})"
210
+ elsif iteration >= max_iterations
211
+ "maximum iterations (#{max_iterations}) reached"
212
+ else
213
+ 'unknown reason'
214
+ end
215
+
216
+ final_result = {
217
+ status: :success,
218
+ result: "Completed #{iteration} iteration(s) of #{@definition.loop_sub_agent_names.size} sub-agent(s) - #{completion_reason}",
219
+ iterations_completed: iteration,
220
+ max_iterations: max_iterations,
221
+ loop_condition_met: loop_condition_met,
222
+ iterations: all_iterations
223
+ }
224
+ end
225
+
226
+ # Create the final event
227
+ final_agent_event = Legate::Event.new(role: :agent, content: final_result)
228
+
229
+ # Log the final event to the session
230
+ session_service.append_event(session_id: session_id, event: final_agent_event)
231
+
232
+ # --- MAS: Store result in session state if output_key is defined --- #
233
+ if @definition.respond_to?(:output_key) && @definition.output_key && final_agent_event
234
+ output_value = final_agent_event.content # Store the entire content hash
235
+ Legate.logger.info("LoopAgent '#{@name}' storing output to session state with key '#{@definition.output_key}' for session '#{session_id}'.")
236
+ if session_service.respond_to?(:set_state)
237
+ session_service.set_state(session_id: session_id, key: @definition.output_key, value: output_value)
238
+ else
239
+ Legate.logger.warn("LoopAgent '#{@name}': Session service does not support :set_state. Cannot store output for key '#{@definition.output_key}'.")
240
+ end
241
+ end
242
+ # --- End MAS State Management --- #
243
+
244
+ final_agent_event
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ # File: lib/legate/agents/parallel_agent.rb
4
+ require_relative '../agent'
5
+ require 'concurrent'
6
+
7
+ module Legate
8
+ module Agents
9
+ # ParallelAgent executes a set of sub-agents concurrently.
10
+ # All sub-agents are started simultaneously and the agent waits for all to complete.
11
+ class ParallelAgent < Legate::Agent
12
+ DEFAULT_PARALLEL_TIMEOUT = 120
13
+
14
+ # Override run_task to execute sub-agents in parallel
15
+ # @param session_id [String] The session ID
16
+ # @param user_input [String] User input to process
17
+ # @param session_service [Legate::SessionService::Base] Session service for persistence
18
+ # @return [Legate::Event] The final agent event
19
+ def run_task(session_id:, user_input:, session_service:)
20
+ # Verify we have parallel sub-agents defined
21
+ unless @definition.parallel_sub_agent_names&.any?
22
+ err_msg = "ParallelAgent '#{name}' has no parallel_sub_agent_names defined."
23
+ Legate.logger.error(err_msg)
24
+ return Legate::Event.new(role: :agent, content: {
25
+ status: :error,
26
+ error_message: err_msg,
27
+ error_class: 'ConfigurationError'
28
+ })
29
+ end
30
+
31
+ # --- Pre-execution Checks --- #
32
+ unless running?
33
+ err_msg = "Agent '#{name}' is not running. Call agent.start before run_task, " \
34
+ 'or use agent.ask (which starts automatically).'
35
+ Legate.logger.error(err_msg)
36
+ return Legate::Event.new(role: :agent, content: { status: :error, error_message: err_msg })
37
+ end
38
+
39
+ session = session_service.get_session(session_id: session_id)
40
+ unless session
41
+ err_msg = "Session not found: #{session_id}"
42
+ Legate.logger.error(err_msg)
43
+ return Legate::Event.new(role: :agent, content: { status: :error, error_message: err_msg })
44
+ end
45
+ # --------------------------- #
46
+
47
+ # Log user input to the ParallelAgent itself
48
+ user_event = Legate::Event.new(role: :user, content: user_input)
49
+ session_service.append_event(session_id: session_id, event: user_event)
50
+
51
+ # Log the execution start
52
+ Legate.logger.info("ParallelAgent '#{name}' starting parallel execution of #{@definition.parallel_sub_agent_names.size} sub-agents.")
53
+
54
+ # Get the sub-agents to run in parallel
55
+ sub_agents_to_run = []
56
+ missing_agents = []
57
+
58
+ @definition.parallel_sub_agent_names.each do |sub_agent_name|
59
+ sub_agent = find_sub_agent(sub_agent_name)
60
+ if sub_agent
61
+ # Start the sub-agent if it's not already running
62
+ sub_agent.start unless sub_agent.running?
63
+ sub_agents_to_run << { name: sub_agent_name, agent: sub_agent }
64
+ else
65
+ missing_agents << sub_agent_name
66
+ end
67
+ end
68
+
69
+ # Check if any agents are missing
70
+ unless missing_agents.empty?
71
+ err_msg = "The following sub-agents were not found for ParallelAgent '#{name}': #{missing_agents.join(', ')}."
72
+ Legate.logger.error(err_msg)
73
+ return Legate::Event.new(role: :agent, content: {
74
+ status: :error,
75
+ error_message: err_msg,
76
+ error_class: 'MissingSubAgentError'
77
+ })
78
+ end
79
+
80
+ # Prepare futures for parallel execution
81
+ futures = {}
82
+ sub_agents_to_run.each do |agent_info|
83
+ futures[agent_info[:name]] = Concurrent::Promises.future do
84
+ Legate.logger.info("ParallelAgent '#{name}' executing sub-agent '#{agent_info[:name]}' in parallel.")
85
+ agent_info[:agent].run_task(
86
+ session_id: session_id,
87
+ user_input: user_input,
88
+ session_service: session_service
89
+ )
90
+ rescue StandardError => e
91
+ Legate.logger.error("Error executing sub-agent '#{agent_info[:name]}': #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}")
92
+ # Return an error event
93
+ Legate::Event.new(role: :agent, content: {
94
+ status: :error,
95
+ error_message: "Exception in sub-agent '#{agent_info[:name]}': #{e.message}",
96
+ error_class: e.class.name
97
+ })
98
+ end
99
+ end
100
+
101
+ # Wait for all futures to complete
102
+ all_results = {}
103
+ has_errors = false
104
+ timeout = @definition.respond_to?(:parallel_timeout_seconds) && @definition.parallel_timeout_seconds || DEFAULT_PARALLEL_TIMEOUT
105
+
106
+ futures.each do |agent_name, future|
107
+ result = future.value(timeout)
108
+ all_results[agent_name] = result.content
109
+ has_errors = true if result.content[:status] == :error
110
+ rescue Concurrent::TimeoutError
111
+ Legate.logger.error("Timeout waiting for sub-agent '#{agent_name}' to complete.")
112
+ all_results[agent_name] = {
113
+ status: :error,
114
+ error_message: 'Timeout waiting for sub-agent to complete',
115
+ error_class: 'TimeoutError'
116
+ }
117
+ has_errors = true
118
+ rescue StandardError => e
119
+ Legate.logger.error("Error processing sub-agent '#{agent_name}' result: #{e.class} - #{e.message}")
120
+ all_results[agent_name] = {
121
+ status: :error,
122
+ error_message: "Error processing result: #{e.message}",
123
+ error_class: e.class.name
124
+ }
125
+ has_errors = true
126
+ end
127
+
128
+ # Create the final result
129
+ final_result = {
130
+ status: has_errors ? :partial_success : :success,
131
+ result: if has_errors
132
+ 'Completed parallel execution with some errors'
133
+ else
134
+ "Successfully completed parallel execution of #{@definition.parallel_sub_agent_names.size} sub-agents"
135
+ end,
136
+ sub_results: all_results,
137
+ agents_completed: all_results.keys.map(&:to_sym),
138
+ all_successful: !has_errors
139
+ }
140
+
141
+ # Create the final event
142
+ final_agent_event = Legate::Event.new(role: :agent, content: final_result)
143
+
144
+ # Log the final event to the session
145
+ session_service.append_event(session_id: session_id, event: final_agent_event)
146
+
147
+ # --- MAS: Store result in session state if output_key is defined --- #
148
+ if @definition.respond_to?(:output_key) && @definition.output_key && final_agent_event
149
+ output_value = final_agent_event.content # Store the entire content hash
150
+ Legate.logger.info("ParallelAgent '#{@name}' storing output to session state with key '#{@definition.output_key}' for session '#{session_id}'.")
151
+ if session_service.respond_to?(:set_state)
152
+ session_service.set_state(session_id: session_id, key: @definition.output_key, value: output_value)
153
+ else
154
+ Legate.logger.warn("ParallelAgent '#{@name}': Session service does not support :set_state. Cannot store output for key '#{@definition.output_key}'.")
155
+ end
156
+ end
157
+ # --- End MAS State Management --- #
158
+
159
+ final_agent_event
160
+ end
161
+ end
162
+ end
163
+ end