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
data/lib/legate/agent.rb
ADDED
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
# File: lib/legate/agent.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'logger'
|
|
5
|
+
require 'concurrent'
|
|
6
|
+
require 'did_you_mean' # for "did you mean" suggestions on unknown tool names
|
|
7
|
+
require 'pathname' # Added for path manipulation
|
|
8
|
+
require_relative 'tool_context'
|
|
9
|
+
# NOTE: Requires are handled by lib/legate.rb
|
|
10
|
+
require_relative 'planner'
|
|
11
|
+
require_relative 'tool_registry'
|
|
12
|
+
require_relative 'agent_definition'
|
|
13
|
+
require_relative 'mcp/client'
|
|
14
|
+
require_relative 'mcp/tool_wrapper'
|
|
15
|
+
require 'forwardable'
|
|
16
|
+
require 'json'
|
|
17
|
+
require_relative 'global_definition_registry'
|
|
18
|
+
require_relative 'global_tool_manager' # Added
|
|
19
|
+
require_relative 'tool_loader'
|
|
20
|
+
require 'securerandom'
|
|
21
|
+
|
|
22
|
+
module Legate
|
|
23
|
+
# Represents the static definition of an Agent, including its name,
|
|
24
|
+
# description, instructions, tools, and model configuration.
|
|
25
|
+
|
|
26
|
+
# Agent class represents an AI agent that can perform tasks using tools and a planner.
|
|
27
|
+
# It operates within the context of a session managed by a SessionService.
|
|
28
|
+
class Agent
|
|
29
|
+
DEFAULT_MODEL = 'gemini-3.5-flash' # Default Gemini model (supports structured output)
|
|
30
|
+
|
|
31
|
+
attr_reader :name, :description, :planner, :logger, :model_name, :state, :tool_registry, :fallback_mode, :instruction, :definition, :session_service, :sub_agents # Added session_service to attr_reader
|
|
32
|
+
# MAS Attributes
|
|
33
|
+
attr_reader :parent_agent # The parent agent in a hierarchy, if any # A collection of sub-agents
|
|
34
|
+
|
|
35
|
+
# --- Callback Instance Variables ---
|
|
36
|
+
attr_reader :before_agent_callback, :after_agent_callback,
|
|
37
|
+
:before_model_callback, :after_model_callback,
|
|
38
|
+
:before_tool_callback, :after_tool_callback
|
|
39
|
+
|
|
40
|
+
# --- End Callback Instance Variables ---
|
|
41
|
+
|
|
42
|
+
# --- Authentication Instance Variables ---
|
|
43
|
+
attr_reader :auth_credential_names, :auth_url_mappings,
|
|
44
|
+
:auth_scheme_assignments, :auth_credential_assignments
|
|
45
|
+
|
|
46
|
+
# --- End Authentication Instance Variables ---
|
|
47
|
+
|
|
48
|
+
# --- Class Method for Configuration DSL ---
|
|
49
|
+
# Provides a block-based DSL for configuring and creating an Agent instance.
|
|
50
|
+
#
|
|
51
|
+
# The DSL is positional (method-call style), not assignment. The resulting
|
|
52
|
+
# definition is registered globally in {GlobalDefinitionRegistry} as a side
|
|
53
|
+
# effect, then returned.
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# definition = Legate::Agent.define do |a|
|
|
57
|
+
# a.name :news_agent
|
|
58
|
+
# a.description 'Summarizes news articles.'
|
|
59
|
+
# a.instruction 'Summarize the article the user provides.'
|
|
60
|
+
# a.model_name 'gemini-3.5-flash'
|
|
61
|
+
# a.use_tool :echo
|
|
62
|
+
# a.fallback_mode :echo
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
# @yieldparam a [Legate::AgentDefinition::DefinitionProxy] The proxy object to configure the definition.
|
|
66
|
+
# @return [Legate::AgentDefinition] The validated, globally-registered definition.
|
|
67
|
+
# @raise [ArgumentError] if the block is not provided or required attributes are missing.
|
|
68
|
+
def self.define(&block)
|
|
69
|
+
raise ArgumentError, 'Legate::Agent.define requires a block.' unless block_given?
|
|
70
|
+
|
|
71
|
+
# 1. Create a new AgentDefinition
|
|
72
|
+
definition = Legate::AgentDefinition.new
|
|
73
|
+
|
|
74
|
+
# 2. Evaluate the block within the definition's proxy DSL
|
|
75
|
+
# Use the definition instance's define method which takes the block
|
|
76
|
+
# This also handles internal validation via validate!
|
|
77
|
+
begin
|
|
78
|
+
definition.define(&block)
|
|
79
|
+
rescue ArgumentError => e
|
|
80
|
+
# Re-raise DSL validation errors immediately
|
|
81
|
+
raise e
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# 3. Register the validated definition in the GlobalDefinitionRegistry
|
|
85
|
+
begin
|
|
86
|
+
GlobalDefinitionRegistry.register(definition)
|
|
87
|
+
agent_name = definition.instance_variable_get(:@name)
|
|
88
|
+
Legate.logger.info("Agent definition '#{agent_name}' registered in GlobalDefinitionRegistry.")
|
|
89
|
+
rescue ArgumentError => e
|
|
90
|
+
agent_name_for_log = definition.instance_variable_get(:@name) || 'unknown'
|
|
91
|
+
Legate.logger.error("Failed to register definition '#{agent_name_for_log}': #{e.class} - #{e.message}")
|
|
92
|
+
raise e
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
agent_name_for_log = definition.instance_variable_get(:@name) || 'unknown'
|
|
95
|
+
Legate.logger.error("Unexpected error registering definition '#{agent_name_for_log}': #{e.class} - #{e.message}")
|
|
96
|
+
raise Legate::StoreError, "Unexpected error registering definition '#{agent_name_for_log}': #{e.message}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
definition # Return the definition instance
|
|
100
|
+
end
|
|
101
|
+
# --- End Class Method ---
|
|
102
|
+
|
|
103
|
+
# Initializes a new agent instance.
|
|
104
|
+
# An agent MUST be initialized with a valid Legate::AgentDefinition object.
|
|
105
|
+
#
|
|
106
|
+
# @param definition [Legate::AgentDefinition] The agent definition object.
|
|
107
|
+
# @param session_service [Legate::SessionService::Base, nil] Optional: Pre-initialized session service.
|
|
108
|
+
# @param planner_override [Legate::Planner, nil] Optional: A specific planner instance to override the default.
|
|
109
|
+
# @param sub_agents [Array<Legate::Agent>, nil] Optional: An array of pre-initialized sub-agent instances. If provided, these will be used instead of instantiating from `definition.sub_agent_names`.
|
|
110
|
+
def initialize(definition:, session_service: nil, planner_override: nil, sub_agents: nil)
|
|
111
|
+
unless definition.is_a?(Legate::AgentDefinition)
|
|
112
|
+
raise ArgumentError,
|
|
113
|
+
"Agent must be initialized with an Legate::AgentDefinition object. Received: #{definition.class}"
|
|
114
|
+
end
|
|
115
|
+
# Perform a more thorough check if it looks like a definition
|
|
116
|
+
unless definition.respond_to?(:name) && definition.respond_to?(:description) &&
|
|
117
|
+
definition.respond_to?(:instruction) && definition.respond_to?(:tool_names) &&
|
|
118
|
+
definition.respond_to?(:model_name) && definition.respond_to?(:fallback_mode) &&
|
|
119
|
+
definition.respond_to?(:mcp_servers)
|
|
120
|
+
raise ArgumentError,
|
|
121
|
+
'Provided definition object does not appear to be a valid Legate::AgentDefinition (missing required attributes/methods).'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
@definition = definition
|
|
125
|
+
@name = definition.name
|
|
126
|
+
|
|
127
|
+
# --- Initialize Callbacks from Definition ---
|
|
128
|
+
@before_agent_callback = definition.before_agent_callback
|
|
129
|
+
@after_agent_callback = definition.after_agent_callback
|
|
130
|
+
@before_model_callback = definition.before_model_callback
|
|
131
|
+
@after_model_callback = definition.after_model_callback
|
|
132
|
+
@before_tool_callback = definition.before_tool_callback
|
|
133
|
+
@after_tool_callback = definition.after_tool_callback
|
|
134
|
+
# --- End Initialize Callbacks ---
|
|
135
|
+
|
|
136
|
+
# --- Initialize Authentication Config from Definition ---
|
|
137
|
+
@auth_credential_names = definition.auth_credential_names || Set.new
|
|
138
|
+
@auth_url_mappings = definition.auth_url_mappings || []
|
|
139
|
+
@auth_scheme_assignments = definition.auth_scheme_assignments || {}
|
|
140
|
+
@auth_credential_assignments = definition.auth_credential_assignments || {}
|
|
141
|
+
# --- End Initialize Authentication Config ---
|
|
142
|
+
|
|
143
|
+
# Check for direct self-references in the definition's sub_agent_names
|
|
144
|
+
raise Legate::ConfigurationError, "Circular dependency detected: Agent '#{@name}' cannot include itself as a sub-agent" if definition.respond_to?(:sub_agent_names) && definition.sub_agent_names&.any? && definition.sub_agent_names.include?(@name)
|
|
145
|
+
|
|
146
|
+
@description = definition.description
|
|
147
|
+
@instruction = definition.instruction
|
|
148
|
+
@model_name = definition.model_name || DEFAULT_MODEL
|
|
149
|
+
@fallback_mode = definition.fallback_mode # Assumes :error is default in AgentDefinition
|
|
150
|
+
@selected_tool_names = definition.tool_names.to_a # Tool names are directly from definition
|
|
151
|
+
|
|
152
|
+
# MAS Attributes Initialization
|
|
153
|
+
@parent_agent = nil # Will be set by parent if this is a sub-agent
|
|
154
|
+
@sub_agents = [] # Will be populated if this agent has sub-agents defined
|
|
155
|
+
|
|
156
|
+
@session_service = session_service || Legate.config.session_service
|
|
157
|
+
@state = :idle
|
|
158
|
+
|
|
159
|
+
Legate.logger.info("Initializing agent '#{@name}' from provided definition object...")
|
|
160
|
+
|
|
161
|
+
setup_tool_registry(definition)
|
|
162
|
+
setup_mcp_config(definition)
|
|
163
|
+
|
|
164
|
+
@selected_tool_names = @definition.tool_names.to_a
|
|
165
|
+
@mcp_manager = Legate::Mcp::ConnectionManager.new(
|
|
166
|
+
tool_registry: @tool_registry,
|
|
167
|
+
selected_tool_names: @selected_tool_names,
|
|
168
|
+
agent_name: @name
|
|
169
|
+
)
|
|
170
|
+
@plan_executor = Legate::PlanExecutor.new(self)
|
|
171
|
+
|
|
172
|
+
@planner = planner_override || Legate::Planner.new(agent: self, model_name: @model_name)
|
|
173
|
+
|
|
174
|
+
unless @session_service&.respond_to?(:get_session) && @session_service.respond_to?(:append_event)
|
|
175
|
+
raise ConfigurationError,
|
|
176
|
+
"Agent '#{@name}' requires a valid Session Service (must respond to :get_session, :append_event)."
|
|
177
|
+
end
|
|
178
|
+
raise ConfigurationError, "Agent '#{@name}' requires a valid Planner (must respond to :plan)." unless @planner&.respond_to?(:plan)
|
|
179
|
+
|
|
180
|
+
Legate.logger.debug {
|
|
181
|
+
"Agent '#{@name}' initialized with #{@tool_registry.tools.count} tools: [#{@tool_registry.tools.keys.join(', ')}]"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
setup_sub_agents(definition, sub_agents)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Adds a tool instance OR class to the agent's registry
|
|
188
|
+
# @param tool [Legate::Tool, Class<Legate::Tool>] The tool instance or class to add
|
|
189
|
+
# @return [Boolean] True if the tool was added, false otherwise
|
|
190
|
+
def add_tool(tool)
|
|
191
|
+
# Check if it's a valid tool instance or class
|
|
192
|
+
is_tool_instance = tool.is_a?(Legate::Tool)
|
|
193
|
+
is_tool_class = tool.is_a?(Class) && tool < Legate::Tool
|
|
194
|
+
|
|
195
|
+
unless is_tool_instance || is_tool_class
|
|
196
|
+
Legate.logger.error("Agent '#{name}' add_tool: Attempted to add invalid tool: #{tool.inspect}")
|
|
197
|
+
return false
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Determine the actual tool class
|
|
201
|
+
tool_class = is_tool_class ? tool : tool.class
|
|
202
|
+
|
|
203
|
+
# --- Determine Tool Name with Fallbacks --- #
|
|
204
|
+
tool_name = get_tool_name_from_class(tool_class) # Use the new helper
|
|
205
|
+
# --- End Determine Tool Name --- #
|
|
206
|
+
|
|
207
|
+
# Validate name was found
|
|
208
|
+
unless tool_name # The helper returns nil if no valid name is found
|
|
209
|
+
Legate.logger.error("Agent '#{name}' add_tool: Could not determine tool name for class #{tool_class}. Cannot add tool.")
|
|
210
|
+
return false # Explicitly return false
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Check for overwrite
|
|
214
|
+
Legate.logger.warn("Agent '#{name}': Tool '#{tool_name}' already added. Overwriting with class #{tool_class}.") if @tool_registry.find_class(tool_name)
|
|
215
|
+
|
|
216
|
+
# Register the class using the determined name
|
|
217
|
+
Legate.logger.debug("Agent '#{name}' add_tool: Registering tool_name=#{tool_name.inspect} with class=#{tool_class.inspect} in registry=#{@tool_registry.object_id}")
|
|
218
|
+
registration_result = @tool_registry.register(tool_name, tool_class)
|
|
219
|
+
Legate.logger.debug("Agent '#{name}' add_tool: Registry after registration for #{tool_name.inspect}: #{@tool_registry.tools.keys.inspect}")
|
|
220
|
+
|
|
221
|
+
# Explicitly return the boolean result from the registry
|
|
222
|
+
registration_result
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Returns the list of tools registered with this agent
|
|
226
|
+
# @return [Array<Legate::Tool>] Array of tool instances
|
|
227
|
+
def tools
|
|
228
|
+
@tool_registry.tools.values.map do |tool_class|
|
|
229
|
+
# Get name reliably using the new helper method
|
|
230
|
+
tool_name = get_tool_name_from_class(tool_class)
|
|
231
|
+
if tool_name
|
|
232
|
+
@tool_registry.create_instance(tool_name)
|
|
233
|
+
else
|
|
234
|
+
# This branch should ideally not be hit frequently if registration robustly requires a name.
|
|
235
|
+
Legate.logger.warn("Agent '#{name}': Skipping tool instance creation for class #{tool_class} as its name could not be determined post-registration.")
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
end.compact
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Finds a tool instance by name
|
|
242
|
+
# @param tool_name [Symbol] The name of the tool to find
|
|
243
|
+
# @return [Legate::Tool, nil] The tool instance if found, nil otherwise
|
|
244
|
+
def find_tool(tool_name)
|
|
245
|
+
@tool_registry.create_instance(tool_name.to_sym)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Registers a tool class with the agent's specific registry.
|
|
249
|
+
# @param tool_class [Class] The tool class to register (must inherit from Legate::Tool).
|
|
250
|
+
# @return [Boolean] True if registration was successful, false otherwise.
|
|
251
|
+
def register_tool_class(tool_class)
|
|
252
|
+
Legate.logger.debug("[register_tool_class] Registering class: #{tool_class.inspect} (Object ID: #{tool_class.object_id})")
|
|
253
|
+
# Basic validation
|
|
254
|
+
unless tool_class < Legate::Tool
|
|
255
|
+
Legate.logger.error("Agent '#{name}': Attempted to register invalid object (must inherit from Legate::Tool): #{tool_class.inspect}")
|
|
256
|
+
return false
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Get name via metadata method
|
|
260
|
+
tool_name = get_tool_name_from_class(tool_class) # Use the new helper
|
|
261
|
+
Legate.logger.debug("[register_tool_class] Determined tool name: #{tool_name.inspect} for class #{tool_class.inspect}")
|
|
262
|
+
|
|
263
|
+
unless tool_name # Helper returns nil if no valid name
|
|
264
|
+
# Use logger method, not direct access
|
|
265
|
+
Legate.logger.error("Agent '#{name}': Could not determine tool name for class #{tool_class}. Cannot register.") # Consistent error message
|
|
266
|
+
return false
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
Legate.logger.warn("Agent '#{name}': Tool '#{tool_name}' already registered. Overwriting.") if @tool_registry.find_class(tool_name)
|
|
270
|
+
|
|
271
|
+
# Register with the instance registry
|
|
272
|
+
@tool_registry.register(tool_name, tool_class)
|
|
273
|
+
true # Return true on success
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# --- Runtime State Methods (unchanged) ---
|
|
277
|
+
def start
|
|
278
|
+
return if running? # Prevent starting multiple times
|
|
279
|
+
|
|
280
|
+
Legate.logger.info("Starting agent '#{name}' runtime...")
|
|
281
|
+
@state = :running
|
|
282
|
+
|
|
283
|
+
# Connect to MCP Servers and register tools
|
|
284
|
+
connect_mcp_servers
|
|
285
|
+
|
|
286
|
+
Legate.logger.info("Agent '#{name}' runtime started.")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def stop
|
|
290
|
+
return unless running?
|
|
291
|
+
|
|
292
|
+
Legate.logger.info("Stopping agent '#{name}' runtime...")
|
|
293
|
+
@state = :stopped
|
|
294
|
+
|
|
295
|
+
# Disconnect MCP Clients
|
|
296
|
+
disconnect_mcp_servers
|
|
297
|
+
|
|
298
|
+
Legate.logger.info("Agent '#{name}' runtime stopped.")
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def running?
|
|
302
|
+
@state == :running
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Returns the list of available tool metadata (names, descriptions, parameters)
|
|
306
|
+
# from the agent's specific tool registry.
|
|
307
|
+
def available_tools_metadata
|
|
308
|
+
@tool_registry.list_tools
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Finds a tool class by name from the agent's specific tool registry.
|
|
312
|
+
# @param tool_name [Symbol]
|
|
313
|
+
# @return [Class<Legate::Tool>, nil]
|
|
314
|
+
def find_tool_class(tool_name)
|
|
315
|
+
@tool_registry.find_class(tool_name.to_sym)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# One-shot convenience runner: starts the agent if needed, creates (or
|
|
319
|
+
# reuses) a session on the agent's own session service, runs the task, and
|
|
320
|
+
# returns the final event. The friendly path over the explicit
|
|
321
|
+
# start/create_session/run_task/stop dance.
|
|
322
|
+
#
|
|
323
|
+
# answer = agent.ask('What is 2 + 2?').answer
|
|
324
|
+
# agent.ask('Search ruby') { |event| puts event.role } # live progress (R3)
|
|
325
|
+
#
|
|
326
|
+
# Lazy-starts but does NOT auto-stop — stopping tears down MCP connections
|
|
327
|
+
# that are costly to re-establish, and an agent typically answers many asks.
|
|
328
|
+
# Call #stop when done (or let process exit reclaim it).
|
|
329
|
+
#
|
|
330
|
+
# @param user_input [String] the user's request
|
|
331
|
+
# @param user_id [String] identity for the auto-created session
|
|
332
|
+
# @param session_id [String, nil] reuse an existing session to continue a conversation
|
|
333
|
+
# @yieldparam event [Legate::Event] optional live progress (forwarded to run_task's on_event)
|
|
334
|
+
# @return [Legate::Event] the final agent event (use #answer / #success?)
|
|
335
|
+
def ask(user_input, user_id: 'default', session_id: nil, &on_event)
|
|
336
|
+
start unless running?
|
|
337
|
+
session_id ||= @session_service.create_session(app_name: name.to_s, user_id: user_id).id
|
|
338
|
+
run_task(session_id: session_id, user_input: user_input,
|
|
339
|
+
session_service: @session_service, on_event: on_event)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# @param on_event [Proc, nil] optional callback invoked with each
|
|
343
|
+
# Legate::Event as it is appended during the run (user, tool_request,
|
|
344
|
+
# tool_result, final agent) — for streaming progress (R3). The final event
|
|
345
|
+
# is still returned; non-streaming callers pass nothing and are unaffected.
|
|
346
|
+
# @return [Legate::Event] The final agent event.
|
|
347
|
+
def run_task(session_id:, user_input:, session_service:, on_event: nil)
|
|
348
|
+
# --- Pre-execution Checks --- #
|
|
349
|
+
unless running?
|
|
350
|
+
err_msg = "Agent '#{name}' is not running. Call agent.start before run_task, " \
|
|
351
|
+
'or use agent.ask (which starts automatically).'
|
|
352
|
+
Legate.logger.error(err_msg)
|
|
353
|
+
return Legate::Event.new(role: :agent, content: { status: :error, error_message: err_msg })
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
session = session_service.get_session(session_id: session_id)
|
|
357
|
+
unless session
|
|
358
|
+
err_msg = "Session not found: #{session_id}"
|
|
359
|
+
Legate.logger.error(err_msg)
|
|
360
|
+
# Even if session isn't found, return an event for consistency?
|
|
361
|
+
return Legate::Event.new(role: :agent, content: { status: :error, error_message: err_msg })
|
|
362
|
+
end
|
|
363
|
+
# ----------------- #
|
|
364
|
+
|
|
365
|
+
# Generate invocation_id for this run and create callback context
|
|
366
|
+
invocation_id = SecureRandom.uuid
|
|
367
|
+
callback_context = nil
|
|
368
|
+
|
|
369
|
+
# R3: stream lifecycle events to the optional on_event callback as they're
|
|
370
|
+
# appended. Torn down in the ensure below so the subscription can't leak.
|
|
371
|
+
event_subscription = subscribe_events(session_service, session_id, on_event)
|
|
372
|
+
|
|
373
|
+
begin
|
|
374
|
+
# Create callback context for callbacks to use
|
|
375
|
+
callback_context = Legate::Callbacks::CallbackContext.new(
|
|
376
|
+
agent_name: @name,
|
|
377
|
+
invocation_id: invocation_id,
|
|
378
|
+
session_id: session_id,
|
|
379
|
+
user_id: session.user_id,
|
|
380
|
+
app_name: session.app_name,
|
|
381
|
+
session_service: session_service
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Execute before_agent_callback if defined
|
|
385
|
+
if @definition.respond_to?(:before_agent_callback) && @definition.before_agent_callback
|
|
386
|
+
Legate.logger.debug { "Agent '#{@name}': Executing before_agent_callback." }
|
|
387
|
+
|
|
388
|
+
# Execute the callback and check if it returns a result
|
|
389
|
+
begin
|
|
390
|
+
override_result = @definition.before_agent_callback.call(callback_context)
|
|
391
|
+
|
|
392
|
+
# If the callback returns a result (not nil), use it instead of normal execution
|
|
393
|
+
if override_result
|
|
394
|
+
Legate.logger.info { "Agent '#{@name}': before_agent_callback provided an override result." }
|
|
395
|
+
|
|
396
|
+
# Apply any pending state changes from the callback
|
|
397
|
+
apply_pending_state(callback_context, session_id, session_service)
|
|
398
|
+
|
|
399
|
+
# Create an agent event with the override result
|
|
400
|
+
final_agent_event = Legate::Event.new(role: :agent, content: override_result)
|
|
401
|
+
session_service.append_event(session_id: session_id, event: final_agent_event)
|
|
402
|
+
|
|
403
|
+
# Store the output if configured
|
|
404
|
+
_store_output_in_session(final_agent_event, session_id, session_service)
|
|
405
|
+
|
|
406
|
+
return final_agent_event
|
|
407
|
+
end
|
|
408
|
+
rescue StandardError => e
|
|
409
|
+
Legate.logger.error { "Agent '#{@name}': Error in before_agent_callback: #{e.message}\n#{e.backtrace.join("\n")}" }
|
|
410
|
+
return record_error_event(session_id, session_service, "Error in before_agent_callback: #{e.message}")
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Apply any pending state changes from the callback if execution continues
|
|
414
|
+
apply_pending_state(callback_context, session_id, session_service, clear: true)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# --- Normal Execution Flow --- #
|
|
418
|
+
# Create a user-message event for this turn
|
|
419
|
+
user_message_event = Legate::Event.new(
|
|
420
|
+
role: :user,
|
|
421
|
+
content: user_input
|
|
422
|
+
)
|
|
423
|
+
session_service.append_event(session_id: session_id, event: user_message_event)
|
|
424
|
+
|
|
425
|
+
# Produce the result via the configured strategy. :plan (default) asks
|
|
426
|
+
# the planner for one upfront plan and runs it; :react drives an agentic
|
|
427
|
+
# observe->think->act loop. Both return the same { details:, last_result: }
|
|
428
|
+
# shape, so the final-event handling below is strategy-agnostic.
|
|
429
|
+
result_hash =
|
|
430
|
+
if react_strategy?
|
|
431
|
+
run_react_loop(user_input, session, session_service, invocation_id)
|
|
432
|
+
else
|
|
433
|
+
plan = @planner.plan(user_input, invocation_id)
|
|
434
|
+
execute_plan(plan, session, session_service, invocation_id)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Create an agent event with the result
|
|
438
|
+
final_agent_event = Legate::Event.new(role: :agent, content: result_hash[:last_result] || result_hash)
|
|
439
|
+
session_service.append_event(session_id: session_id, event: final_agent_event)
|
|
440
|
+
|
|
441
|
+
# Execute after_agent_callback if defined
|
|
442
|
+
if @definition.respond_to?(:after_agent_callback) && @definition.after_agent_callback
|
|
443
|
+
Legate.logger.debug { "Agent '#{@name}': Executing after_agent_callback." }
|
|
444
|
+
|
|
445
|
+
begin
|
|
446
|
+
# Execute the callback and let it modify the result if needed
|
|
447
|
+
# Pass the actual result (last_result) to the callback, not the full hash with details
|
|
448
|
+
modified_result = @definition.after_agent_callback.call(callback_context, result_hash[:last_result] || result_hash)
|
|
449
|
+
|
|
450
|
+
# If the callback returned a modified result, use it
|
|
451
|
+
if modified_result && modified_result != (result_hash[:last_result] || result_hash)
|
|
452
|
+
Legate.logger.info { "Agent '#{@name}': after_agent_callback modified the result." }
|
|
453
|
+
|
|
454
|
+
# Create a new agent event with the modified result
|
|
455
|
+
final_agent_event = Legate::Event.new(role: :agent, content: modified_result)
|
|
456
|
+
session_service.append_event(session_id: session_id, event: final_agent_event)
|
|
457
|
+
end
|
|
458
|
+
rescue StandardError => e
|
|
459
|
+
Legate.logger.error { "Agent '#{@name}': Error in after_agent_callback: #{e.message}\n#{e.backtrace.join("\n")}" }
|
|
460
|
+
# Don't override the result completely on error, just log it
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Apply the callback's pending state changes exactly once (whether or
|
|
464
|
+
# not it modified the result).
|
|
465
|
+
apply_pending_state(callback_context, session_id, session_service)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Store the output if configured
|
|
469
|
+
_store_output_in_session(final_agent_event, session_id, session_service)
|
|
470
|
+
|
|
471
|
+
# Return the final agent event
|
|
472
|
+
final_agent_event
|
|
473
|
+
rescue StandardError => e
|
|
474
|
+
# Handle any other errors during execution. Record the failure in the
|
|
475
|
+
# session so its history reflects what the caller saw (the success and
|
|
476
|
+
# callback paths already append their events).
|
|
477
|
+
Legate.logger.error { "Agent '#{@name}' runtime error: #{e.message}\n#{e.backtrace.join("\n")}" }
|
|
478
|
+
record_error_event(session_id, session_service, e.message)
|
|
479
|
+
ensure
|
|
480
|
+
session_service.unsubscribe(event_subscription) if event_subscription && session_service.respond_to?(:unsubscribe)
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Subscribes on_event (if given) to the session's appended events for the
|
|
485
|
+
# duration of a run. Returns a handle for #run_task's ensure to remove, or
|
|
486
|
+
# nil when there's nothing to stream / the service has no pub/sub.
|
|
487
|
+
def subscribe_events(session_service, session_id, on_event)
|
|
488
|
+
return nil unless on_event && session_service.respond_to?(:subscribe)
|
|
489
|
+
|
|
490
|
+
session_service.subscribe(session_id, &on_event)
|
|
491
|
+
end
|
|
492
|
+
private :subscribe_events
|
|
493
|
+
|
|
494
|
+
# Flushes a callback's accumulated state delta into the session via the
|
|
495
|
+
# session service. Optionally clears the delta afterward (when execution
|
|
496
|
+
# continues and the same context will be reused).
|
|
497
|
+
def apply_pending_state(callback_context, session_id, session_service, clear: false)
|
|
498
|
+
return if callback_context.pending_state_delta.empty?
|
|
499
|
+
|
|
500
|
+
callback_context.pending_state_delta.each do |key, value|
|
|
501
|
+
session_service.set_state(session_id: session_id, key: key, value: value)
|
|
502
|
+
end
|
|
503
|
+
callback_context.clear_pending_state_delta! if clear
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Builds an agent error event, records it in the session history (best-effort:
|
|
507
|
+
# a failed append must not mask the original error), and returns it.
|
|
508
|
+
# @return [Legate::Event] the error event
|
|
509
|
+
def record_error_event(session_id, session_service, message)
|
|
510
|
+
event = Legate::Event.new(role: :agent, content: { status: :error, error_message: message })
|
|
511
|
+
begin
|
|
512
|
+
session_service.append_event(session_id: session_id, event: event)
|
|
513
|
+
rescue StandardError => e
|
|
514
|
+
Legate.logger.error { "Agent '#{@name}': failed to record error event in session: #{e.message}" }
|
|
515
|
+
end
|
|
516
|
+
event
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Returns the root agent in the hierarchy (the topmost agent with no parent)
|
|
520
|
+
# @return [Legate::Agent] The root agent in the hierarchy
|
|
521
|
+
def root_agent
|
|
522
|
+
return self if @parent_agent.nil?
|
|
523
|
+
|
|
524
|
+
@parent_agent.root_agent
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Finds an agent with the given name in the hierarchy using DFS
|
|
528
|
+
# @param name_sym [Symbol] The name of the agent to find (as a symbol)
|
|
529
|
+
# @return [Legate::Agent, nil] The agent with the given name, or nil if not found
|
|
530
|
+
def find_agent(name_sym)
|
|
531
|
+
# Convert to symbol if string provided
|
|
532
|
+
name_sym = name_sym.to_sym if name_sym.is_a?(String)
|
|
533
|
+
|
|
534
|
+
# Check if this is the agent we're looking for
|
|
535
|
+
return self if @name.to_sym == name_sym
|
|
536
|
+
|
|
537
|
+
# Search sub-agents recursively
|
|
538
|
+
@sub_agents.each do |sub_agent|
|
|
539
|
+
found = sub_agent.find_agent(name_sym)
|
|
540
|
+
return found if found
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Not found in this branch
|
|
544
|
+
nil
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# Finds a direct sub-agent with the given name
|
|
548
|
+
# @param name_sym [Symbol] The name of the sub-agent to find
|
|
549
|
+
# @return [Legate::Agent, nil] The sub-agent with the given name, or nil if not found
|
|
550
|
+
def find_sub_agent(name_sym)
|
|
551
|
+
# Convert to symbol if string provided
|
|
552
|
+
name_sym = name_sym.to_sym if name_sym.is_a?(String)
|
|
553
|
+
|
|
554
|
+
return nil unless @sub_agents.is_a?(Array)
|
|
555
|
+
|
|
556
|
+
@sub_agents.find { |sub_agent| sub_agent.name.to_sym == name_sym }
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# Transfers control to another agent, executing a task with the same session context.
|
|
560
|
+
# This is a public version of the private transfer_to method
|
|
561
|
+
#
|
|
562
|
+
# @param target_agent_name [Symbol] The name of the target agent to delegate to
|
|
563
|
+
# @param task [String] The task to delegate to the target agent
|
|
564
|
+
# @param session_id [String] The current session ID
|
|
565
|
+
# @param session_service [Legate::SessionService::Base] The session service instance
|
|
566
|
+
# @return [Hash] A standard result hash { status: :success/:error, result/error_message: ... }
|
|
567
|
+
def transfer_to(target_agent_name, task, session_id, session_service)
|
|
568
|
+
# Verify the target agent is in the delegation_targets list if defined
|
|
569
|
+
if @definition.respond_to?(:delegation_targets) && @definition.delegation_targets&.any? && !@definition.delegation_targets.include?(target_agent_name)
|
|
570
|
+
error_msg = "Agent '#{target_agent_name}' is not in the delegation targets for '#{@name}'"
|
|
571
|
+
Legate.logger.error(error_msg)
|
|
572
|
+
return { status: :error, error_message: error_msg, error_class: 'InvalidDelegationTarget' }
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Find the target agent in the agent hierarchy, starting from the root
|
|
576
|
+
target_agent = root_agent.find_agent(target_agent_name)
|
|
577
|
+
|
|
578
|
+
# If not found in hierarchy, try to instantiate from definition store
|
|
579
|
+
unless target_agent
|
|
580
|
+
Legate.logger.info("Target agent '#{target_agent_name}' not found in hierarchy. Attempting to load from definition store.")
|
|
581
|
+
|
|
582
|
+
begin
|
|
583
|
+
# Try to find the definition in the global registry
|
|
584
|
+
target_def = Legate::GlobalDefinitionRegistry.find(target_agent_name)
|
|
585
|
+
|
|
586
|
+
unless target_def
|
|
587
|
+
error_msg = "Target agent definition '#{target_agent_name}' not found in registry"
|
|
588
|
+
Legate.logger.error(error_msg)
|
|
589
|
+
return { status: :error, error_message: error_msg, error_class: 'AgentDefinitionNotFound' }
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# Create a new agent instance from the definition
|
|
593
|
+
target_agent = Legate::Agent.new(
|
|
594
|
+
definition: target_def,
|
|
595
|
+
session_service: session_service
|
|
596
|
+
)
|
|
597
|
+
rescue StandardError => e
|
|
598
|
+
error_msg = "Failed to instantiate target agent '#{target_agent_name}': #{e.message}"
|
|
599
|
+
Legate.logger.error("#{error_msg}\n#{e.backtrace.join("\n")}")
|
|
600
|
+
return { status: :error, error_message: error_msg, error_class: e.class.name }
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# Verify the target agent exists
|
|
605
|
+
unless target_agent
|
|
606
|
+
error_msg = "Target agent '#{target_agent_name}' not found in hierarchy or definition store"
|
|
607
|
+
Legate.logger.error(error_msg)
|
|
608
|
+
return { status: :error, error_message: error_msg, error_class: 'AgentNotFound' }
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
# Start the target agent if it's not already running
|
|
612
|
+
target_agent.start unless target_agent.running?
|
|
613
|
+
|
|
614
|
+
# Execute the delegated task
|
|
615
|
+
begin
|
|
616
|
+
Legate.logger.info("Executing delegated task on agent '#{target_agent_name}': #{task}")
|
|
617
|
+
|
|
618
|
+
# Call run_task with the same session context
|
|
619
|
+
result_event = target_agent.run_task(
|
|
620
|
+
session_id: session_id,
|
|
621
|
+
user_input: task,
|
|
622
|
+
session_service: session_service
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Extract and format the result
|
|
626
|
+
result_content = result_event.respond_to?(:content) ? result_event.content : result_event
|
|
627
|
+
|
|
628
|
+
{
|
|
629
|
+
status: :success,
|
|
630
|
+
target_agent: target_agent_name.to_s,
|
|
631
|
+
result: result_content
|
|
632
|
+
}
|
|
633
|
+
rescue StandardError => e
|
|
634
|
+
error_msg = "Error executing task on target agent '#{target_agent_name}': #{e.message}"
|
|
635
|
+
Legate.logger.error("#{error_msg}\n#{e.backtrace.join("\n")}")
|
|
636
|
+
{ status: :error, error_message: error_msg, error_class: e.class.name }
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
private
|
|
641
|
+
|
|
642
|
+
def setup_tool_registry(definition)
|
|
643
|
+
tool_classes_to_load = definition.tool_names.map { |tn| Legate::GlobalToolManager.find_class(tn) }.compact
|
|
644
|
+
|
|
645
|
+
if tool_classes_to_load.length != definition.tool_names.length
|
|
646
|
+
found_tool_names = tool_classes_to_load.map { |tc|
|
|
647
|
+
begin
|
|
648
|
+
tc.tool_metadata[:name].to_sym
|
|
649
|
+
rescue StandardError
|
|
650
|
+
nil
|
|
651
|
+
end
|
|
652
|
+
}.compact.to_set
|
|
653
|
+
missing_tool_names = definition.tool_names.to_set - found_tool_names
|
|
654
|
+
Legate.logger.warn(missing_tools_warning(missing_tool_names, definition)) if missing_tool_names.any?
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
@tool_registry = Legate::ToolRegistry.new
|
|
658
|
+
Legate.logger.debug("Agent '#{@name}' created its ToolRegistry instance: #{@tool_registry.object_id}")
|
|
659
|
+
|
|
660
|
+
tool_classes_to_load.each do |tool_class|
|
|
661
|
+
Legate.logger.debug("[Agent Init '#{@name}'] Processing class from builder: #{tool_class.inspect} (Object ID: #{tool_class.object_id})")
|
|
662
|
+
register_tool_class(tool_class)
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
return if @tool_registry.find_class(:check_job_status)
|
|
666
|
+
|
|
667
|
+
begin
|
|
668
|
+
require_relative 'tools/check_job_status_tool'
|
|
669
|
+
register_tool_class(Legate::Tools::CheckJobStatusTool)
|
|
670
|
+
Legate.logger.info("Automatically registered CheckJobStatusTool for agent '#{@name}'.")
|
|
671
|
+
rescue LoadError => e
|
|
672
|
+
Legate.logger.error("Failed to load CheckJobStatusTool: #{e.message}")
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Builds an actionable warning for selected tools with no registered class:
|
|
677
|
+
# a did-you-mean suggestion per name + the available tools. If the agent has
|
|
678
|
+
# MCP servers configured, the names may be MCP tools (registered at connect
|
|
679
|
+
# time), so the message softens rather than crying wolf.
|
|
680
|
+
def missing_tools_warning(missing_tool_names, definition)
|
|
681
|
+
available = Legate::GlobalToolManager.registered_tool_names.map(&:to_s).sort
|
|
682
|
+
checker = DidYouMean::SpellChecker.new(dictionary: available)
|
|
683
|
+
described = missing_tool_names.map do |name|
|
|
684
|
+
suggestions = checker.correct(name.to_s)
|
|
685
|
+
suggestions.empty? ? name.to_s : "#{name} (did you mean: #{suggestions.join(', ')}?)"
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
msg = "Agent '#{@name}': no registered tool for #{described.join('; ')}. " \
|
|
689
|
+
"Available tools: #{available.join(', ')}."
|
|
690
|
+
has_mcp = definition.respond_to?(:mcp_servers) && Array(definition.mcp_servers).any?
|
|
691
|
+
msg + (has_mcp ? ' (MCP tools register when the agent connects, so this may be expected.)' : ' These tools will be unavailable.')
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def setup_mcp_config(definition)
|
|
695
|
+
mcp_servers_config_str = definition.mcp_servers || []
|
|
696
|
+
if mcp_servers_config_str.is_a?(String) && !mcp_servers_config_str.strip.empty?
|
|
697
|
+
# String-based MCP config parsing handled by existing logic
|
|
698
|
+
elsif mcp_servers_config_str.is_a?(Array)
|
|
699
|
+
@mcp_servers_config = mcp_servers_config_str
|
|
700
|
+
else
|
|
701
|
+
Legate.logger.debug("Agent '#{@name}': No valid MCP server config provided. Defaulting to empty array.")
|
|
702
|
+
@mcp_servers_config = []
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def setup_sub_agents(definition, sub_agents)
|
|
707
|
+
if sub_agents && !sub_agents.empty?
|
|
708
|
+
link_provided_sub_agents(sub_agents)
|
|
709
|
+
elsif definition.respond_to?(:sub_agent_names) && definition.sub_agent_names&.any?
|
|
710
|
+
instantiate_sub_agents_from_definition(definition)
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
# Sets this agent as the sub-agent's parent, or returns false if the sub-agent
|
|
715
|
+
# already belongs to a different parent (the caller should then skip it).
|
|
716
|
+
# Idempotent when the parent is already this agent.
|
|
717
|
+
# @return [Boolean] true if linked (or already linked to self), false to skip
|
|
718
|
+
def link_parent_or_skip(sub_agent)
|
|
719
|
+
if sub_agent.parent_agent.nil?
|
|
720
|
+
sub_agent.instance_variable_set(:@parent_agent, self)
|
|
721
|
+
true
|
|
722
|
+
elsif sub_agent.parent_agent == self
|
|
723
|
+
true
|
|
724
|
+
else
|
|
725
|
+
Legate.logger.error("Agent '#{@name}': sub-agent '#{sub_agent.name}' already has a different parent: '#{sub_agent.parent_agent.name}'. Skipping.")
|
|
726
|
+
false
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
def link_provided_sub_agents(sub_agents)
|
|
731
|
+
Legate.logger.info("Agent '#{@name}': Initializing with programmatically provided sub-agents (#{sub_agents.length} agents).")
|
|
732
|
+
sub_agents.each do |sub_agent|
|
|
733
|
+
unless sub_agent.is_a?(Legate::Agent)
|
|
734
|
+
Legate.logger.warn("Agent '#{@name}': Item in provided sub_agents list is not an Legate::Agent. Skipping: #{sub_agent.inspect}")
|
|
735
|
+
next
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
begin
|
|
739
|
+
_check_circular_dependency(sub_agent.name)
|
|
740
|
+
rescue Legate::ConfigurationError => e
|
|
741
|
+
Legate.logger.error("Agent '#{@name}': #{e.message}")
|
|
742
|
+
next
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
next unless link_parent_or_skip(sub_agent)
|
|
746
|
+
|
|
747
|
+
if sub_agent.instance_variable_get(:@session_service).nil? && @session_service
|
|
748
|
+
Legate.logger.debug("Agent '#{@name}': Setting session_service for programmatic sub-agent '#{sub_agent.name}' to match parent.")
|
|
749
|
+
sub_agent.instance_variable_set(:@session_service, @session_service)
|
|
750
|
+
elsif sub_agent.instance_variable_get(:@session_service) != @session_service && @session_service
|
|
751
|
+
Legate.logger.warn("Agent '#{@name}': Programmatic sub-agent '#{sub_agent.name}' has a different session_service than parent.")
|
|
752
|
+
end
|
|
753
|
+
@sub_agents << sub_agent
|
|
754
|
+
Legate.logger.info("Agent '#{@name}': Successfully instantiated and linked sub-agent '#{sub_agent.name}'.")
|
|
755
|
+
end
|
|
756
|
+
Legate.logger.info("Agent '#{@name}' finished linking programmatic sub-agents. Total sub-agents: #{@sub_agents.length}")
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def instantiate_sub_agents_from_definition(definition)
|
|
760
|
+
Legate.logger.info("Agent '#{@name}' attempting to instantiate sub-agents from definition: #{definition.sub_agent_names.to_a.inspect}")
|
|
761
|
+
definition.sub_agent_names.each do |sub_agent_name|
|
|
762
|
+
_check_circular_dependency(sub_agent_name)
|
|
763
|
+
|
|
764
|
+
sub_agent_definition = Legate::GlobalDefinitionRegistry.find(sub_agent_name)
|
|
765
|
+
unless sub_agent_definition
|
|
766
|
+
Legate.logger.error("Agent '#{@name}': Could not find definition for sub-agent '#{sub_agent_name}' in GlobalDefinitionRegistry. Skipping.")
|
|
767
|
+
next
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
Legate.logger.debug("Agent '#{@name}': Instantiating sub-agent '#{sub_agent_name}'...")
|
|
771
|
+
sub_agent = Legate::Agent.new(definition: sub_agent_definition, session_service: @session_service)
|
|
772
|
+
next unless link_parent_or_skip(sub_agent)
|
|
773
|
+
|
|
774
|
+
@sub_agents << sub_agent
|
|
775
|
+
Legate.logger.info("Agent '#{@name}': Successfully instantiated and linked sub-agent '#{sub_agent.name}'.")
|
|
776
|
+
rescue ArgumentError => e
|
|
777
|
+
Legate.logger.error("Agent '#{@name}': ArgumentError instantiating sub-agent '#{sub_agent_name}': #{e.message}")
|
|
778
|
+
rescue StandardError => e
|
|
779
|
+
Legate.logger.error("Agent '#{@name}': Unexpected error instantiating sub-agent '#{sub_agent_name}': #{e.class} - #{e.message}\n#{e.backtrace.first(5).join("\n")}")
|
|
780
|
+
end
|
|
781
|
+
Legate.logger.info("Agent '#{@name}' finished sub-agent instantiation. Total sub-agents: #{@sub_agents.length}")
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# Build the agent-specific authentication configuration hash for ToolContext
|
|
785
|
+
# @return [Hash, nil] The auth config hash or nil if no auth configured
|
|
786
|
+
def build_agent_auth_config
|
|
787
|
+
return nil if @auth_credential_names.empty? &&
|
|
788
|
+
@auth_url_mappings.empty? &&
|
|
789
|
+
@auth_scheme_assignments.empty? &&
|
|
790
|
+
@auth_credential_assignments.empty?
|
|
791
|
+
|
|
792
|
+
{
|
|
793
|
+
credential_names: @auth_credential_names,
|
|
794
|
+
url_mappings: @auth_url_mappings,
|
|
795
|
+
scheme_assignments: @auth_scheme_assignments,
|
|
796
|
+
credential_assignments: @auth_credential_assignments
|
|
797
|
+
}
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# Helper method to consistently determine the tool name from a tool class.
|
|
801
|
+
# Uses metadata, then deprecated @tool_name, then inferred_name.
|
|
802
|
+
def get_tool_name_from_class(tool_class)
|
|
803
|
+
return nil unless tool_class.is_a?(Class) && tool_class < Legate::Tool
|
|
804
|
+
|
|
805
|
+
begin
|
|
806
|
+
metadata = tool_class.tool_metadata
|
|
807
|
+
rescue StandardError => e
|
|
808
|
+
Legate.logger.error("Error calling tool_metadata on #{tool_class}: #{e.class} - #{e.message} - Backtrace: #{e.backtrace.first(3).join(' | ')}")
|
|
809
|
+
metadata = {} # Default to empty hash if metadata call fails, for diagnosis
|
|
810
|
+
end
|
|
811
|
+
name = metadata[:name]&.to_sym
|
|
812
|
+
|
|
813
|
+
if name.nil? || name == :''
|
|
814
|
+
# Check deprecated @tool_name (instance variable on the class itself)
|
|
815
|
+
if tool_class.instance_variable_defined?(:@tool_name)
|
|
816
|
+
name = tool_class.instance_variable_get(:@tool_name)&.to_sym
|
|
817
|
+
# Legate.logger.debug { "get_tool_name_from_class: Using name from deprecated @tool_name for #{tool_class}: #{name.inspect}" } if name
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
# If still no name, try inferred_name as a primary fallback if metadata[:name] is missing
|
|
821
|
+
if (name.nil? || name == '') && tool_class.respond_to?(:inferred_name)
|
|
822
|
+
name = tool_class.inferred_name
|
|
823
|
+
# Legate.logger.debug { "get_tool_name_from_class: Using inferred_name for #{tool_class}: #{name.inspect}" } if name
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
name && name != :'' ? name : nil
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
# --- REFACTORED: execute_plan now returns hash { details: [...], last_result: original_hash } ---
|
|
831
|
+
# Executes a plan, logging tool request/result events via the session service.
|
|
832
|
+
# @param plan [Hash, Array] The plan from the planner, either as a hash with :thought_process and :steps, or as an array of steps.
|
|
833
|
+
# @param session [Legate::Session] The current session object.
|
|
834
|
+
# @param session_service [Object] The session service instance.
|
|
835
|
+
# @return [Hash] { details: Array<Hash>, last_result: Hash } or { details: Hash, last_result: nil } on planning errors.
|
|
836
|
+
# Executes a planner-produced plan. Delegates to the agent's PlanExecutor;
|
|
837
|
+
# kept here as the entry point called by #run_task.
|
|
838
|
+
def execute_plan(plan, session, session_service, invocation_id)
|
|
839
|
+
@plan_executor.execute_plan(plan, session, session_service, invocation_id)
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
# True when this agent should use the agentic ReAct loop instead of the
|
|
843
|
+
# default plan-then-execute strategy.
|
|
844
|
+
def react_strategy?
|
|
845
|
+
@definition.respond_to?(:planning_strategy) && @definition.planning_strategy == :react
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
# Drives the agentic observe->think->act loop. Reuses the agent's existing
|
|
849
|
+
# planner and PlanExecutor, so tool execution, event logging, and state
|
|
850
|
+
# deltas behave identically to the default strategy.
|
|
851
|
+
def run_react_loop(user_input, session, session_service, invocation_id)
|
|
852
|
+
Legate::Agentic::Loop.new(
|
|
853
|
+
planner: @planner,
|
|
854
|
+
executor: @plan_executor,
|
|
855
|
+
logger: Legate.logger
|
|
856
|
+
).run(
|
|
857
|
+
user_input: user_input,
|
|
858
|
+
session: session,
|
|
859
|
+
session_service: session_service,
|
|
860
|
+
invocation_id: invocation_id
|
|
861
|
+
)
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
# --- REFACTORED: execute_step uses session context and passes it to tools ---
|
|
865
|
+
# Executes a single step, logging :tool_request and :tool_result events via session service.
|
|
866
|
+
# @param step [Hash] A hash like { tool: :symbol, params: {...} }.
|
|
867
|
+
# @param session [Legate::Session] The current session object.
|
|
868
|
+
# @param session_service [Object] The session service instance.
|
|
869
|
+
# @param invocation_id [String] The ID of the current agent invocation.
|
|
870
|
+
# @return [Hash] A standard result hash { status: ..., result/error_message/job_id: ... }.
|
|
871
|
+
# Executes a single plan step. Delegates to the agent's PlanExecutor; kept
|
|
872
|
+
# here as a private method because specs and the test-only custom_agent_patch
|
|
873
|
+
# drive it via `send(:execute_step, ...)`.
|
|
874
|
+
def execute_step(step, session, session_service, invocation_id = nil)
|
|
875
|
+
@plan_executor.execute_step(step, session, session_service, invocation_id)
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
# Connects to all configured MCP servers.
|
|
879
|
+
# Connects the agent's configured MCP servers and registers their tools.
|
|
880
|
+
# Delegates to the agent's McpConnectionManager (kept as a lifecycle hook
|
|
881
|
+
# called from #start and exercised directly in specs).
|
|
882
|
+
def connect_mcp_servers
|
|
883
|
+
@mcp_manager.connect(@mcp_servers_config)
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
# Disconnects all active MCP clients.
|
|
887
|
+
def disconnect_mcp_servers
|
|
888
|
+
@mcp_manager.disconnect
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
# Helper method to check for circular dependencies in the agent hierarchy
|
|
892
|
+
# @param new_sub_agent_name [Symbol] The name of the new sub-agent to check for cycles
|
|
893
|
+
# @raise [Legate::ConfigurationError] If a circular dependency is detected
|
|
894
|
+
private def _check_circular_dependency(new_sub_agent_name)
|
|
895
|
+
# Direct self-reference check
|
|
896
|
+
raise Legate::ConfigurationError, "Circular dependency detected: Agent '#{@name}' cannot include itself as a sub-agent" if new_sub_agent_name == @name
|
|
897
|
+
|
|
898
|
+
# Check if the sub-agent would create an indirect circular reference
|
|
899
|
+
# by traversing up the parent chain (backwards check)
|
|
900
|
+
current_agent = self
|
|
901
|
+
ancestry_path = [@name]
|
|
902
|
+
|
|
903
|
+
while (parent = current_agent.parent_agent)
|
|
904
|
+
# If any parent has the same name as the new sub-agent, it's a circular reference
|
|
905
|
+
if parent.name == new_sub_agent_name
|
|
906
|
+
circular_path = [new_sub_agent_name] + ancestry_path
|
|
907
|
+
raise Legate::ConfigurationError, "Circular dependency detected: #{circular_path.join(' → ')}"
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
ancestry_path.unshift(parent.name)
|
|
911
|
+
current_agent = parent
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
# --- MAS: Store result in session state if output_key is defined --- #
|
|
916
|
+
def _store_output_in_session(event, session_id, session_service)
|
|
917
|
+
return unless @definition.respond_to?(:output_key) && @definition.output_key && event
|
|
918
|
+
|
|
919
|
+
# Get the content, which now should be the last_result only
|
|
920
|
+
output_value = event.content
|
|
921
|
+
|
|
922
|
+
# For Hash results, ensure a plan_details key exists (back-compat). Non-Hash
|
|
923
|
+
# results (a tool/callback returning a bare string, number, array, …) are
|
|
924
|
+
# stored as-is — calling #key? on them would raise NoMethodError.
|
|
925
|
+
needs_plan_details = output_value.is_a?(Hash) &&
|
|
926
|
+
!output_value.key?(:plan_details) && !output_value.key?('plan_details')
|
|
927
|
+
output_value = output_value.merge(plan_details: []) if needs_plan_details
|
|
928
|
+
|
|
929
|
+
serialized_value = begin
|
|
930
|
+
# If the value is a Hash or Array, deep transform keys and symbolized values
|
|
931
|
+
if output_value.is_a?(Hash) || output_value.is_a?(Array)
|
|
932
|
+
# Convert to JSON and back to remove symbols
|
|
933
|
+
JSON.parse(output_value.to_json)
|
|
934
|
+
else
|
|
935
|
+
# For other values, just pass through
|
|
936
|
+
output_value
|
|
937
|
+
end
|
|
938
|
+
rescue StandardError => e
|
|
939
|
+
# If serialization fails, log and return the original
|
|
940
|
+
Legate.logger.warn("Agent '#{@name}': Failed to serialize output value: #{e.message}. Using original value.")
|
|
941
|
+
output_value
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
Legate.logger.info("Agent '#{@name}' storing output to session state with key '#{@definition.output_key}' for session '#{session_id}'.")
|
|
945
|
+
|
|
946
|
+
begin
|
|
947
|
+
# Ensure session_service has set_state. Add if missing for base/inmemory.
|
|
948
|
+
if session_service.respond_to?(:set_state)
|
|
949
|
+
session_service.set_state(session_id: session_id, key: @definition.output_key, value: serialized_value)
|
|
950
|
+
else
|
|
951
|
+
Legate.logger.warn("Agent '#{@name}': Session service does not support :set_state. Cannot store output for key '#{@definition.output_key}'.")
|
|
952
|
+
end
|
|
953
|
+
rescue StandardError => e
|
|
954
|
+
Legate.logger.error("Agent '#{@name}': Failed to set state for key '#{@definition.output_key}' in session '#{session_id}': #{e.class} - #{e.message}")
|
|
955
|
+
end
|
|
956
|
+
end
|
|
957
|
+
# --- End MAS State Management ---
|
|
958
|
+
end # End Agent class
|
|
959
|
+
end # End Legate module
|