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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +345 -0
- data/bin/legate +13 -0
- data/examples/00_quickstart.rb +51 -0
- data/examples/01_simple_agent.rb +105 -0
- data/examples/02_multi_tool_agent.rb +140 -0
- data/examples/03_custom_tool.rb +93 -0
- data/examples/04_agent_instructions.rb +84 -0
- data/examples/05_state_and_sessions.rb +91 -0
- data/examples/06_callbacks.rb +186 -0
- data/examples/07_async_jobs.rb +112 -0
- data/examples/08_loop_agent.rb +197 -0
- data/examples/09_sequential_workflow.rb +40 -0
- data/examples/10_parallel_workflow.rb +34 -0
- data/examples/11_agent_delegation.rb +24 -0
- data/examples/12_http_client_tool.rb +156 -0
- data/examples/13_authentication.rb +220 -0
- data/examples/14_mcp_client.rb +154 -0
- data/examples/15_mcp_server.rb +79 -0
- data/examples/16_webhooks.rb +91 -0
- data/examples/README_sequential_agents.md +164 -0
- data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
- data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
- data/examples/advanced/auth/excon_middleware.rb +317 -0
- data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
- data/examples/advanced/auth/fiber_auth_example.rb +281 -0
- data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
- data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
- data/examples/advanced/auth/oauth2_auth.rb +419 -0
- data/examples/advanced/auth/oidc_auth.rb +514 -0
- data/examples/advanced/auth/openweather_api.rb +251 -0
- data/examples/advanced/auth/openweather_tool.rb +153 -0
- data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
- data/examples/advanced/auth/service_account.rb +135 -0
- data/examples/advanced/auth/test_with_httpbin.rb +202 -0
- data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
- data/examples/advanced/callback_monitoring.rb +679 -0
- data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
- data/examples/advanced/mas/loop_workflow.rb +28 -0
- data/examples/advanced/mas/mock_planner.rb +77 -0
- data/examples/advanced/mas/proper_delegation_example.rb +276 -0
- data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
- data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
- data/examples/advanced/mcp/mcp_server_async.rb +76 -0
- data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
- data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
- data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
- data/examples/advanced/random_calculator.rb +104 -0
- data/examples/advanced/sleep_agent.rb +153 -0
- data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
- data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
- data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
- data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
- data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
- data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
- data/examples/tools/oauth2_example.rb +136 -0
- data/examples/tools/sleepy_tool.rb +42 -0
- data/lib/legate/activity_log.rb +71 -0
- data/lib/legate/agent.rb +959 -0
- data/lib/legate/agent_code_generator.rb +185 -0
- data/lib/legate/agent_definition.rb +812 -0
- data/lib/legate/agentic/decision.rb +49 -0
- data/lib/legate/agentic/loop.rb +134 -0
- data/lib/legate/agentic.rb +5 -0
- data/lib/legate/agents/loop_agent.rb +248 -0
- data/lib/legate/agents/parallel_agent.rb +163 -0
- data/lib/legate/agents/sequential_agent.rb +190 -0
- data/lib/legate/agents.rb +14 -0
- data/lib/legate/auth/config.rb +148 -0
- data/lib/legate/auth/coordinator.rb +218 -0
- data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
- data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
- data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
- data/lib/legate/auth/credential.rb +157 -0
- data/lib/legate/auth/encryption.rb +108 -0
- data/lib/legate/auth/error.rb +94 -0
- data/lib/legate/auth/exchanged_credential.rb +180 -0
- data/lib/legate/auth/excon_middleware.rb +285 -0
- data/lib/legate/auth/http_client_utils.rb +364 -0
- data/lib/legate/auth/manager.rb +531 -0
- data/lib/legate/auth/manager_store.rb +394 -0
- data/lib/legate/auth/middleware_factory.rb +290 -0
- data/lib/legate/auth/runner.rb +279 -0
- data/lib/legate/auth/scheme.rb +125 -0
- data/lib/legate/auth/schemes/api_key.rb +212 -0
- data/lib/legate/auth/schemes/google_service_account.rb +108 -0
- data/lib/legate/auth/schemes/http_bearer.rb +98 -0
- data/lib/legate/auth/schemes/oauth2.rb +396 -0
- data/lib/legate/auth/schemes/openid_connect.rb +346 -0
- data/lib/legate/auth/schemes/service_account.rb +388 -0
- data/lib/legate/auth/schemes.rb +40 -0
- data/lib/legate/auth/token_manager.rb +362 -0
- data/lib/legate/auth/token_store.rb +86 -0
- data/lib/legate/auth/tool_context_extension.rb +97 -0
- data/lib/legate/auth/tool_integration.rb +188 -0
- data/lib/legate/auth/url_guard.rb +81 -0
- data/lib/legate/auth.rb +453 -0
- data/lib/legate/callbacks/callback_context.rb +71 -0
- data/lib/legate/cli/agent_commands.rb +950 -0
- data/lib/legate/cli/auth_commands.rb +520 -0
- data/lib/legate/cli/base_command.rb +24 -0
- data/lib/legate/cli/deployment_commands.rb +934 -0
- data/lib/legate/cli/output_helper.rb +108 -0
- data/lib/legate/cli/session_commands.rb +138 -0
- data/lib/legate/cli/skaffold_commands.rb +223 -0
- data/lib/legate/cli/tool_commands.rb +261 -0
- data/lib/legate/cli/web_commands.rb +182 -0
- data/lib/legate/cli.rb +40 -0
- data/lib/legate/configuration/webhooks.rb +113 -0
- data/lib/legate/configuration.rb +39 -0
- data/lib/legate/definition_store.rb +23 -0
- data/lib/legate/errors.rb +118 -0
- data/lib/legate/event.rb +161 -0
- data/lib/legate/gemini_ai_beta_patch.rb +39 -0
- data/lib/legate/generators/agent_generator.rb +412 -0
- data/lib/legate/generators/code_validator.rb +48 -0
- data/lib/legate/generators/legate/install_generator.rb +35 -0
- data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
- data/lib/legate/generators/legate/templates/initializer.rb +18 -0
- data/lib/legate/generators/runtime_tool_loader.rb +76 -0
- data/lib/legate/generators/tool_generator.rb +408 -0
- data/lib/legate/generators.rb +11 -0
- data/lib/legate/global_definition_registry.rb +506 -0
- data/lib/legate/global_tool_manager.rb +135 -0
- data/lib/legate/llm/adapter.rb +69 -0
- data/lib/legate/llm/gemini.rb +172 -0
- data/lib/legate/llm/ollama.rb +80 -0
- data/lib/legate/llm.rb +34 -0
- data/lib/legate/mcp/client.rb +320 -0
- data/lib/legate/mcp/connection/sse.rb +292 -0
- data/lib/legate/mcp/connection/stdio.rb +273 -0
- data/lib/legate/mcp/connection_manager.rb +103 -0
- data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
- data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
- data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
- data/lib/legate/mcp/tool_wrapper.rb +138 -0
- data/lib/legate/mcp/util/schema_converter.rb +134 -0
- data/lib/legate/mcp.rb +23 -0
- data/lib/legate/plan_executor.rb +375 -0
- data/lib/legate/planner.rb +839 -0
- data/lib/legate/rails/railtie.rb +43 -0
- data/lib/legate/rails.rb +9 -0
- data/lib/legate/redaction.rb +32 -0
- data/lib/legate/session.rb +299 -0
- data/lib/legate/session_service/active_record.rb +300 -0
- data/lib/legate/session_service/base.rb +68 -0
- data/lib/legate/session_service/event_broadcast.rb +74 -0
- data/lib/legate/session_service/in_memory.rb +188 -0
- data/lib/legate/tool/metadata_dsl.rb +122 -0
- data/lib/legate/tool.rb +276 -0
- data/lib/legate/tool_code_generator.rb +103 -0
- data/lib/legate/tool_context.rb +350 -0
- data/lib/legate/tool_loader.rb +39 -0
- data/lib/legate/tool_registry.rb +73 -0
- data/lib/legate/tool_result.rb +61 -0
- data/lib/legate/tools/agent_tool.rb +187 -0
- data/lib/legate/tools/base/http_client.rb +319 -0
- data/lib/legate/tools/base/safe_url.rb +56 -0
- data/lib/legate/tools/base_async_job_tool.rb +91 -0
- data/lib/legate/tools/calculator.rb +89 -0
- data/lib/legate/tools/cat_facts.rb +81 -0
- data/lib/legate/tools/check_job_status_tool.rb +48 -0
- data/lib/legate/tools/current_time_tool.rb +64 -0
- data/lib/legate/tools/echo.rb +43 -0
- data/lib/legate/tools/http_request_tool.rb +105 -0
- data/lib/legate/tools/random_number_tool.rb +64 -0
- data/lib/legate/tools/read_webpage_tool.rb +92 -0
- data/lib/legate/tools/sleepy_tool.rb +74 -0
- data/lib/legate/tools/webhook_tool.rb +146 -0
- data/lib/legate/version.rb +5 -0
- data/lib/legate/web/app.rb +984 -0
- data/lib/legate/web/public/css/main.css +4980 -0
- data/lib/legate/web/public/images/favicon-256.png +0 -0
- data/lib/legate/web/public/images/favicon-32.png +0 -0
- data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
- data/lib/legate/web/public/images/legate-logo-light.png +0 -0
- data/lib/legate/web/public/js/legate.js +616 -0
- data/lib/legate/web/public/styles/main.scss +4402 -0
- data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
- data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
- data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
- data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
- data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
- data/lib/legate/web/routes/api_routes.rb +56 -0
- data/lib/legate/web/routes/authentication_routes.rb +1541 -0
- data/lib/legate/web/routes/core_routes.rb +111 -0
- data/lib/legate/web/routes/documentation_routes.rb +220 -0
- data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
- data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
- data/lib/legate/web/sass_compiler.rb +73 -0
- data/lib/legate/web/views/_active_session_info.slim +25 -0
- data/lib/legate/web/views/_activity_list.slim +55 -0
- data/lib/legate/web/views/_agent_card.slim +56 -0
- data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
- data/lib/legate/web/views/_agent_status_controls.slim +71 -0
- data/lib/legate/web/views/_agent_tool_table.slim +74 -0
- data/lib/legate/web/views/_chat_message.slim +95 -0
- data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
- data/lib/legate/web/views/_display_agent_description.slim +11 -0
- data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
- data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
- data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
- data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
- data/lib/legate/web/views/_display_agent_model.slim +17 -0
- data/lib/legate/web/views/_display_agent_name.slim +42 -0
- data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
- data/lib/legate/web/views/_display_agent_type.slim +65 -0
- data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
- data/lib/legate/web/views/_edit_agent_description.slim +16 -0
- data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
- data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
- data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
- data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
- data/lib/legate/web/views/_edit_agent_model.slim +23 -0
- data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
- data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
- data/lib/legate/web/views/_edit_agent_type.slim +67 -0
- data/lib/legate/web/views/_session_error.slim +4 -0
- data/lib/legate/web/views/_skeleton.slim +69 -0
- data/lib/legate/web/views/_tool_card.slim +9 -0
- data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
- data/lib/legate/web/views/agent.slim +436 -0
- data/lib/legate/web/views/agent_auth.slim +562 -0
- data/lib/legate/web/views/agents.slim +369 -0
- data/lib/legate/web/views/auth.slim +112 -0
- data/lib/legate/web/views/auth_credential_detail.slim +327 -0
- data/lib/legate/web/views/auth_credentials.slim +261 -0
- data/lib/legate/web/views/auth_debug.slim +94 -0
- data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
- data/lib/legate/web/views/auth_mapping_new.slim +123 -0
- data/lib/legate/web/views/auth_mappings.slim +120 -0
- data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
- data/lib/legate/web/views/auth_schemes.slim +259 -0
- data/lib/legate/web/views/auth_test.slim +418 -0
- data/lib/legate/web/views/chat.slim +192 -0
- data/lib/legate/web/views/docs_index.slim +105 -0
- data/lib/legate/web/views/docs_show.slim +105 -0
- data/lib/legate/web/views/error_404.slim +5 -0
- data/lib/legate/web/views/index.slim +148 -0
- data/lib/legate/web/views/layout.slim +144 -0
- data/lib/legate/web/views/tool_detail.slim +87 -0
- data/lib/legate/web/views/tools.slim +50 -0
- data/lib/legate/web/webhook_listener.rb +367 -0
- data/lib/legate/web.rb +9 -0
- data/lib/legate.rb +220 -0
- data/public/docs/advanced/callbacks.md +828 -0
- data/public/docs/advanced/mcp_schema_conversion.md +59 -0
- data/public/docs/authentication/api_reference/config.md +210 -0
- data/public/docs/authentication/api_reference/credential.md +246 -0
- data/public/docs/authentication/api_reference/encryption.md +218 -0
- data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
- data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
- data/public/docs/authentication/api_reference/index.md +30 -0
- data/public/docs/authentication/api_reference/scheme.md +250 -0
- data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
- data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
- data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
- data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
- data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
- data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
- data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
- data/public/docs/authentication/api_reference/token_manager.md +221 -0
- data/public/docs/authentication/api_reference/token_store.md +146 -0
- data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
- data/public/docs/authentication/guides/api_key.md +190 -0
- data/public/docs/authentication/guides/bearer.md +172 -0
- data/public/docs/authentication/guides/configuration.md +255 -0
- data/public/docs/authentication/guides/custom_flow.md +523 -0
- data/public/docs/authentication/guides/index.md +24 -0
- data/public/docs/authentication/guides/migration.md +435 -0
- data/public/docs/authentication/guides/oauth2.md +252 -0
- data/public/docs/authentication/guides/oidc.md +241 -0
- data/public/docs/authentication/guides/overview.md +155 -0
- data/public/docs/authentication/guides/secure_storage.md +301 -0
- data/public/docs/authentication/guides/service_account.md +228 -0
- data/public/docs/authentication/guides/token_lifecycle.md +295 -0
- data/public/docs/authentication/guides/web_ui_integration.md +504 -0
- data/public/docs/authentication/index.md +58 -0
- data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
- data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
- data/public/docs/authentication/troubleshooting/index.md +11 -0
- data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
- data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
- data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
- data/public/docs/cli/legate_cli_usage.md +363 -0
- data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
- data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
- data/public/docs/core_concepts/legate_configuration.md +116 -0
- data/public/docs/core_concepts/legate_definition_store.md +102 -0
- data/public/docs/core_concepts/legate_planner.md +94 -0
- data/public/docs/core_concepts/legate_session_service.md +104 -0
- data/public/docs/error_handling/legate_error_handling.md +122 -0
- data/public/docs/examples.md +199 -0
- data/public/docs/getting_started.md +111 -0
- data/public/docs/guides/agentic_agents.md +137 -0
- data/public/docs/guides/ai_code_generators.md +437 -0
- data/public/docs/guides/auto_loading.md +326 -0
- data/public/docs/guides/configuring_agent_webhooks.md +219 -0
- data/public/docs/guides/http_client_usage.md +264 -0
- data/public/docs/guides/llm_providers.md +137 -0
- data/public/docs/guides/mcp_client_integration.md +232 -0
- data/public/docs/guides/mcp_server_exposure.md +206 -0
- data/public/docs/guides/rails_integration.md +128 -0
- data/public/docs/guides/sending_outbound_webhooks.md +227 -0
- data/public/docs/guides/streaming.md +112 -0
- data/public/docs/guides/webhooks.md +288 -0
- data/public/docs/introduction.md +51 -0
- data/public/docs/multi_agent_systems/advanced_features.md +57 -0
- data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
- data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
- data/public/docs/multi_agent_systems/state_management.md +47 -0
- data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
- data/public/docs/tools/legate_built_in_tools.md +332 -0
- data/public/docs/tools/legate_tools_and_registry.md +263 -0
- data/public/docs/web_ui/legate_web_ui.md +137 -0
- 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,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
|