autobyteus 1.1.4__py3-none-any.whl → 1.1.5__py3-none-any.whl
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.
- autobyteus/agent/context/__init__.py +4 -2
- autobyteus/agent/context/agent_config.py +0 -4
- autobyteus/agent/context/agent_context_registry.py +73 -0
- autobyteus/agent/events/notifiers.py +4 -0
- autobyteus/agent/handlers/inter_agent_message_event_handler.py +7 -2
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +19 -19
- autobyteus/agent/handlers/user_input_message_event_handler.py +15 -0
- autobyteus/agent/message/send_message_to.py +29 -23
- autobyteus/agent/runtime/agent_runtime.py +10 -2
- autobyteus/agent/sender_type.py +15 -0
- autobyteus/agent/streaming/agent_event_stream.py +6 -0
- autobyteus/agent/streaming/stream_event_payloads.py +12 -0
- autobyteus/agent/streaming/stream_events.py +3 -0
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +7 -4
- autobyteus/agent_team/__init__.py +1 -0
- autobyteus/agent_team/agent_team.py +93 -0
- autobyteus/agent_team/agent_team_builder.py +184 -0
- autobyteus/agent_team/base_agent_team.py +86 -0
- autobyteus/agent_team/bootstrap_steps/__init__.py +24 -0
- autobyteus/agent_team/bootstrap_steps/agent_configuration_preparation_step.py +73 -0
- autobyteus/agent_team/bootstrap_steps/agent_team_bootstrapper.py +54 -0
- autobyteus/agent_team/bootstrap_steps/agent_team_runtime_queue_initialization_step.py +25 -0
- autobyteus/agent_team/bootstrap_steps/base_agent_team_bootstrap_step.py +23 -0
- autobyteus/agent_team/bootstrap_steps/coordinator_initialization_step.py +41 -0
- autobyteus/agent_team/bootstrap_steps/coordinator_prompt_preparation_step.py +85 -0
- autobyteus/agent_team/bootstrap_steps/task_notifier_initialization_step.py +51 -0
- autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +45 -0
- autobyteus/agent_team/context/__init__.py +17 -0
- autobyteus/agent_team/context/agent_team_config.py +33 -0
- autobyteus/agent_team/context/agent_team_context.py +61 -0
- autobyteus/agent_team/context/agent_team_runtime_state.py +56 -0
- autobyteus/agent_team/context/team_manager.py +147 -0
- autobyteus/agent_team/context/team_node_config.py +76 -0
- autobyteus/agent_team/events/__init__.py +29 -0
- autobyteus/agent_team/events/agent_team_event_dispatcher.py +39 -0
- autobyteus/agent_team/events/agent_team_events.py +53 -0
- autobyteus/agent_team/events/agent_team_input_event_queue_manager.py +21 -0
- autobyteus/agent_team/exceptions.py +8 -0
- autobyteus/agent_team/factory/__init__.py +9 -0
- autobyteus/agent_team/factory/agent_team_factory.py +99 -0
- autobyteus/agent_team/handlers/__init__.py +19 -0
- autobyteus/agent_team/handlers/agent_team_event_handler_registry.py +23 -0
- autobyteus/agent_team/handlers/base_agent_team_event_handler.py +16 -0
- autobyteus/agent_team/handlers/inter_agent_message_request_event_handler.py +61 -0
- autobyteus/agent_team/handlers/lifecycle_agent_team_event_handler.py +27 -0
- autobyteus/agent_team/handlers/process_user_message_event_handler.py +46 -0
- autobyteus/agent_team/handlers/tool_approval_team_event_handler.py +48 -0
- autobyteus/agent_team/phases/__init__.py +11 -0
- autobyteus/agent_team/phases/agent_team_operational_phase.py +19 -0
- autobyteus/agent_team/phases/agent_team_phase_manager.py +48 -0
- autobyteus/agent_team/runtime/__init__.py +13 -0
- autobyteus/agent_team/runtime/agent_team_runtime.py +82 -0
- autobyteus/agent_team/runtime/agent_team_worker.py +117 -0
- autobyteus/agent_team/shutdown_steps/__init__.py +17 -0
- autobyteus/agent_team/shutdown_steps/agent_team_shutdown_orchestrator.py +35 -0
- autobyteus/agent_team/shutdown_steps/agent_team_shutdown_step.py +42 -0
- autobyteus/agent_team/shutdown_steps/base_agent_team_shutdown_step.py +16 -0
- autobyteus/agent_team/shutdown_steps/bridge_cleanup_step.py +28 -0
- autobyteus/agent_team/shutdown_steps/sub_team_shutdown_step.py +41 -0
- autobyteus/agent_team/streaming/__init__.py +26 -0
- autobyteus/agent_team/streaming/agent_event_bridge.py +48 -0
- autobyteus/agent_team/streaming/agent_event_multiplexer.py +70 -0
- autobyteus/agent_team/streaming/agent_team_event_notifier.py +64 -0
- autobyteus/agent_team/streaming/agent_team_event_stream.py +33 -0
- autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +32 -0
- autobyteus/agent_team/streaming/agent_team_stream_events.py +56 -0
- autobyteus/agent_team/streaming/team_event_bridge.py +50 -0
- autobyteus/agent_team/task_notification/__init__.py +11 -0
- autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +164 -0
- autobyteus/agent_team/task_notification/task_notification_mode.py +24 -0
- autobyteus/agent_team/utils/__init__.py +9 -0
- autobyteus/agent_team/utils/wait_for_idle.py +46 -0
- autobyteus/cli/agent_team_tui/__init__.py +4 -0
- autobyteus/cli/agent_team_tui/app.py +210 -0
- autobyteus/cli/agent_team_tui/state.py +180 -0
- autobyteus/cli/agent_team_tui/widgets/__init__.py +6 -0
- autobyteus/cli/agent_team_tui/widgets/agent_list_sidebar.py +149 -0
- autobyteus/cli/agent_team_tui/widgets/focus_pane.py +320 -0
- autobyteus/cli/agent_team_tui/widgets/logo.py +20 -0
- autobyteus/cli/agent_team_tui/widgets/renderables.py +77 -0
- autobyteus/cli/agent_team_tui/widgets/shared.py +60 -0
- autobyteus/cli/agent_team_tui/widgets/status_bar.py +14 -0
- autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +82 -0
- autobyteus/events/event_types.py +7 -2
- autobyteus/llm/api/autobyteus_llm.py +11 -12
- autobyteus/llm/api/lmstudio_llm.py +10 -13
- autobyteus/llm/api/ollama_llm.py +8 -13
- autobyteus/llm/autobyteus_provider.py +73 -46
- autobyteus/llm/llm_factory.py +102 -140
- autobyteus/llm/lmstudio_provider.py +63 -48
- autobyteus/llm/models.py +83 -53
- autobyteus/llm/ollama_provider.py +69 -61
- autobyteus/llm/ollama_provider_resolver.py +1 -0
- autobyteus/llm/providers.py +13 -13
- autobyteus/llm/runtimes.py +11 -0
- autobyteus/task_management/__init__.py +43 -0
- autobyteus/task_management/base_task_board.py +68 -0
- autobyteus/task_management/converters/__init__.py +11 -0
- autobyteus/task_management/converters/task_board_converter.py +64 -0
- autobyteus/task_management/converters/task_plan_converter.py +48 -0
- autobyteus/task_management/deliverable.py +16 -0
- autobyteus/task_management/deliverables/__init__.py +8 -0
- autobyteus/task_management/deliverables/file_deliverable.py +15 -0
- autobyteus/task_management/events.py +27 -0
- autobyteus/task_management/in_memory_task_board.py +126 -0
- autobyteus/task_management/schemas/__init__.py +15 -0
- autobyteus/task_management/schemas/deliverable_schema.py +13 -0
- autobyteus/task_management/schemas/plan_definition.py +35 -0
- autobyteus/task_management/schemas/task_status_report.py +27 -0
- autobyteus/task_management/task_plan.py +110 -0
- autobyteus/task_management/tools/__init__.py +14 -0
- autobyteus/task_management/tools/get_task_board_status.py +68 -0
- autobyteus/task_management/tools/publish_task_plan.py +113 -0
- autobyteus/task_management/tools/update_task_status.py +135 -0
- autobyteus/tools/bash/bash_executor.py +59 -14
- autobyteus/tools/mcp/config_service.py +63 -58
- autobyteus/tools/mcp/server/http_managed_mcp_server.py +14 -2
- autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +14 -2
- autobyteus/tools/mcp/server_instance_manager.py +30 -4
- autobyteus/tools/mcp/tool_registrar.py +103 -50
- autobyteus/tools/parameter_schema.py +17 -11
- autobyteus/tools/registry/tool_definition.py +24 -29
- autobyteus/tools/tool_category.py +1 -0
- autobyteus/tools/usage/formatters/default_json_example_formatter.py +78 -3
- autobyteus/tools/usage/formatters/default_xml_example_formatter.py +23 -3
- autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +6 -0
- autobyteus/tools/usage/formatters/google_json_example_formatter.py +7 -0
- autobyteus/tools/usage/formatters/openai_json_example_formatter.py +6 -4
- autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +23 -7
- autobyteus/tools/usage/parsers/provider_aware_tool_usage_parser.py +14 -25
- autobyteus/tools/usage/providers/__init__.py +2 -12
- autobyteus/tools/usage/providers/tool_manifest_provider.py +36 -29
- autobyteus/tools/usage/registries/__init__.py +7 -12
- autobyteus/tools/usage/registries/tool_formatter_pair.py +15 -0
- autobyteus/tools/usage/registries/tool_formatting_registry.py +58 -0
- autobyteus/tools/usage/registries/tool_usage_parser_registry.py +55 -0
- {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/METADATA +3 -3
- {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/RECORD +146 -72
- examples/agent_team/__init__.py +1 -0
- examples/run_browser_agent.py +17 -15
- examples/run_google_slides_agent.py +17 -16
- examples/run_poem_writer.py +22 -12
- examples/run_sqlite_agent.py +17 -15
- autobyteus/tools/mcp/call_handlers/__init__.py +0 -16
- autobyteus/tools/mcp/call_handlers/base_handler.py +0 -40
- autobyteus/tools/mcp/call_handlers/stdio_handler.py +0 -76
- autobyteus/tools/mcp/call_handlers/streamable_http_handler.py +0 -55
- autobyteus/tools/usage/providers/json_example_provider.py +0 -32
- autobyteus/tools/usage/providers/json_schema_provider.py +0 -35
- autobyteus/tools/usage/providers/json_tool_usage_parser_provider.py +0 -28
- autobyteus/tools/usage/providers/xml_example_provider.py +0 -28
- autobyteus/tools/usage/providers/xml_schema_provider.py +0 -29
- autobyteus/tools/usage/providers/xml_tool_usage_parser_provider.py +0 -26
- autobyteus/tools/usage/registries/json_example_formatter_registry.py +0 -51
- autobyteus/tools/usage/registries/json_schema_formatter_registry.py +0 -51
- autobyteus/tools/usage/registries/json_tool_usage_parser_registry.py +0 -42
- autobyteus/tools/usage/registries/xml_example_formatter_registry.py +0 -30
- autobyteus/tools/usage/registries/xml_schema_formatter_registry.py +0 -33
- autobyteus/tools/usage/registries/xml_tool_usage_parser_registry.py +0 -30
- examples/workflow/__init__.py +0 -1
- examples/workflow/run_basic_research_workflow.py +0 -189
- examples/workflow/run_code_review_workflow.py +0 -269
- examples/workflow/run_debate_workflow.py +0 -212
- examples/workflow/run_workflow_with_tui.py +0 -153
- {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import TYPE_CHECKING, Optional, List, Dict, Any
|
|
3
|
+
|
|
4
|
+
from pydantic import ValidationError
|
|
5
|
+
|
|
6
|
+
from autobyteus.tools.base_tool import BaseTool
|
|
7
|
+
from autobyteus.tools.tool_category import ToolCategory
|
|
8
|
+
from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
|
|
9
|
+
from autobyteus.task_management.base_task_board import TaskStatus
|
|
10
|
+
from autobyteus.task_management.deliverable import FileDeliverable
|
|
11
|
+
from autobyteus.task_management.schemas import FileDeliverableSchema
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from autobyteus.agent.context import AgentContext
|
|
15
|
+
from autobyteus.agent_team.context import AgentTeamContext
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
class UpdateTaskStatus(BaseTool):
|
|
20
|
+
"""A tool for member agents to update their progress and submit file deliverables on the TaskBoard."""
|
|
21
|
+
|
|
22
|
+
CATEGORY = ToolCategory.TASK_MANAGEMENT
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def get_name(cls) -> str:
|
|
26
|
+
return "UpdateTaskStatus"
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def get_description(cls) -> str:
|
|
30
|
+
return (
|
|
31
|
+
"Updates the status of a specific task on the team's shared task board. "
|
|
32
|
+
"When completing a task, this tool can also be used to formally submit a list of file deliverables."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def get_argument_schema(cls) -> Optional[ParameterSchema]:
|
|
37
|
+
schema = ParameterSchema()
|
|
38
|
+
schema.add_parameter(ParameterDefinition(
|
|
39
|
+
name="task_name",
|
|
40
|
+
param_type=ParameterType.STRING,
|
|
41
|
+
description="The unique name of the task to update (e.g., 'implement_scraper').",
|
|
42
|
+
required=True
|
|
43
|
+
))
|
|
44
|
+
schema.add_parameter(ParameterDefinition(
|
|
45
|
+
name="status",
|
|
46
|
+
param_type=ParameterType.ENUM,
|
|
47
|
+
description=f"The new status for the task. Must be one of: {', '.join([s.value for s in TaskStatus])}.",
|
|
48
|
+
required=True,
|
|
49
|
+
enum_values=[s.value for s in TaskStatus]
|
|
50
|
+
))
|
|
51
|
+
schema.add_parameter(ParameterDefinition(
|
|
52
|
+
name="deliverables",
|
|
53
|
+
param_type=ParameterType.ARRAY,
|
|
54
|
+
description="Optional. A list of file deliverables to submit for this task, typically when status is 'completed'. Each deliverable must include a file_path and a summary.",
|
|
55
|
+
required=False,
|
|
56
|
+
array_item_schema=FileDeliverableSchema.model_json_schema()
|
|
57
|
+
))
|
|
58
|
+
return schema
|
|
59
|
+
|
|
60
|
+
async def _execute(self, context: 'AgentContext', task_name: str, status: str, deliverables: Optional[List[Dict[str, Any]]] = None) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Executes the tool to update a task's status and optionally submit deliverables.
|
|
63
|
+
"""
|
|
64
|
+
agent_name = context.config.name
|
|
65
|
+
log_msg = f"Agent '{agent_name}' is executing UpdateTaskStatus for task '{task_name}' to status '{status}'"
|
|
66
|
+
if deliverables:
|
|
67
|
+
log_msg += f" with {len(deliverables)} deliverable(s)."
|
|
68
|
+
logger.info(log_msg)
|
|
69
|
+
|
|
70
|
+
team_context: Optional['AgentTeamContext'] = context.custom_data.get("team_context")
|
|
71
|
+
if not team_context:
|
|
72
|
+
error_msg = "Error: Team context is not available. Cannot access the task board."
|
|
73
|
+
logger.error(f"Agent '{agent_name}': {error_msg}")
|
|
74
|
+
return error_msg
|
|
75
|
+
|
|
76
|
+
task_board = getattr(team_context.state, 'task_board', None)
|
|
77
|
+
if not task_board:
|
|
78
|
+
error_msg = "Error: Task board has not been initialized for this team."
|
|
79
|
+
logger.error(f"Agent '{agent_name}': {error_msg}")
|
|
80
|
+
return error_msg
|
|
81
|
+
|
|
82
|
+
if not task_board.current_plan:
|
|
83
|
+
error_msg = "Error: No task plan is currently loaded on the task board."
|
|
84
|
+
logger.warning(f"Agent '{agent_name}' tried to update task status, but no plan is loaded.")
|
|
85
|
+
return error_msg
|
|
86
|
+
|
|
87
|
+
# Find the task by name
|
|
88
|
+
target_task = None
|
|
89
|
+
for task in task_board.current_plan.tasks:
|
|
90
|
+
if task.task_name == task_name:
|
|
91
|
+
target_task = task
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
if not target_task:
|
|
95
|
+
error_msg = f"Failed to update status for task '{task_name}'. The task name does not exist on the current plan."
|
|
96
|
+
logger.warning(f"Agent '{agent_name}' failed to update status for non-existent task '{task_name}'.")
|
|
97
|
+
return f"Error: {error_msg}"
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
status_enum = TaskStatus(status)
|
|
101
|
+
except ValueError:
|
|
102
|
+
error_msg = f"Invalid status '{status}'. Must be one of: {', '.join([s.value for s in TaskStatus])}."
|
|
103
|
+
logger.warning(f"Agent '{agent_name}' provided invalid status for UpdateTaskStatus: {status}")
|
|
104
|
+
return f"Error: {error_msg}"
|
|
105
|
+
|
|
106
|
+
# --- Process Deliverables FIRST --- (CORRECTED ORDER)
|
|
107
|
+
if deliverables:
|
|
108
|
+
try:
|
|
109
|
+
for d_data in deliverables:
|
|
110
|
+
# Validate and create the internal deliverable object
|
|
111
|
+
deliverable_schema = FileDeliverableSchema(**d_data)
|
|
112
|
+
full_deliverable = FileDeliverable(
|
|
113
|
+
**deliverable_schema.model_dump(),
|
|
114
|
+
author_agent_name=agent_name
|
|
115
|
+
)
|
|
116
|
+
# Append to the task object
|
|
117
|
+
target_task.file_deliverables.append(full_deliverable)
|
|
118
|
+
logger.info(f"Agent '{agent_name}' successfully processed and added {len(deliverables)} deliverables to task '{task_name}'.")
|
|
119
|
+
except (ValidationError, TypeError) as e:
|
|
120
|
+
error_msg = f"Failed to process deliverables due to invalid data: {e}. Task status was NOT updated."
|
|
121
|
+
logger.warning(f"Agent '{agent_name}': {error_msg}")
|
|
122
|
+
return f"Error: {error_msg}"
|
|
123
|
+
|
|
124
|
+
# --- Update Status SECOND --- (CORRECTED ORDER)
|
|
125
|
+
# This will now emit an event with the deliverables already attached to the task.
|
|
126
|
+
if not task_board.update_task_status(target_task.task_id, status_enum, agent_name):
|
|
127
|
+
error_msg = f"Failed to update status for task '{task_name}'. An unexpected error occurred on the task board."
|
|
128
|
+
logger.error(f"Agent '{agent_name}': {error_msg}")
|
|
129
|
+
return f"Error: {error_msg}"
|
|
130
|
+
|
|
131
|
+
success_msg = f"Successfully updated status of task '{task_name}' to '{status}'."
|
|
132
|
+
if deliverables:
|
|
133
|
+
success_msg += f" and submitted {len(deliverables)} deliverable(s)."
|
|
134
|
+
logger.info(f"Agent '{agent_name}': {success_msg}")
|
|
135
|
+
return success_msg
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import subprocess
|
|
3
3
|
import logging
|
|
4
|
+
import shutil
|
|
5
|
+
import tempfile
|
|
4
6
|
from typing import TYPE_CHECKING, Optional
|
|
5
7
|
|
|
6
8
|
from autobyteus.tools import tool
|
|
@@ -12,27 +14,60 @@ if TYPE_CHECKING:
|
|
|
12
14
|
logger = logging.getLogger(__name__)
|
|
13
15
|
|
|
14
16
|
@tool(name="BashExecutor", category=ToolCategory.SYSTEM)
|
|
15
|
-
async def bash_executor(context: Optional['AgentContext'], command: str
|
|
17
|
+
async def bash_executor(context: Optional['AgentContext'], command: str) -> str:
|
|
16
18
|
"""
|
|
17
|
-
Executes bash commands
|
|
19
|
+
Executes bash commands using the '/bin/bash' interpreter.
|
|
20
|
+
On success, it returns a formatted string containing the command's standard output (stdout) and/or diagnostic logs.
|
|
21
|
+
On failure, it raises an exception.
|
|
22
|
+
- If a command has only stdout, its content is returned directly.
|
|
23
|
+
- If a command has diagnostic output (from stderr), it will be included and labeled as 'LOGS' in the output.
|
|
18
24
|
'command' is the bash command string to be executed.
|
|
19
|
-
|
|
20
|
-
Errors during command execution are raised as exceptions.
|
|
25
|
+
The command is executed in the agent's workspace directory if available.
|
|
21
26
|
"""
|
|
27
|
+
if not shutil.which("bash"):
|
|
28
|
+
error_msg = "'bash' executable not found in system PATH. The BashExecutor tool cannot be used."
|
|
29
|
+
logger.error(error_msg)
|
|
30
|
+
raise FileNotFoundError(error_msg)
|
|
31
|
+
|
|
22
32
|
agent_id_str = context.agent_id if context else "Non-Agent"
|
|
23
|
-
|
|
33
|
+
|
|
34
|
+
effective_cwd = None
|
|
35
|
+
log_cwd_source = ""
|
|
36
|
+
|
|
37
|
+
if context and hasattr(context, 'workspace') and context.workspace:
|
|
38
|
+
try:
|
|
39
|
+
base_path = context.workspace.get_base_path()
|
|
40
|
+
if base_path and isinstance(base_path, str):
|
|
41
|
+
effective_cwd = base_path
|
|
42
|
+
log_cwd_source = f"agent workspace: {effective_cwd}"
|
|
43
|
+
else:
|
|
44
|
+
logger.warning(f"Agent '{agent_id_str}' has a workspace, but it provided an invalid base path ('{base_path}'). "
|
|
45
|
+
f"Falling back to system temporary directory.")
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.warning(f"Could not retrieve workspace for agent '{agent_id_str}': {e}. "
|
|
48
|
+
f"Falling back to system temporary directory.")
|
|
49
|
+
|
|
50
|
+
if not effective_cwd:
|
|
51
|
+
effective_cwd = tempfile.gettempdir()
|
|
52
|
+
log_cwd_source = f"system temporary directory: {effective_cwd}"
|
|
53
|
+
|
|
54
|
+
logger.debug(f"Functional BashExecutor tool executing for '{agent_id_str}': {command} in cwd from {log_cwd_source}")
|
|
24
55
|
|
|
25
56
|
try:
|
|
26
|
-
|
|
27
|
-
|
|
57
|
+
# Explicitly use 'bash -c' for reliable execution
|
|
58
|
+
process = await asyncio.create_subprocess_exec(
|
|
59
|
+
'bash', '-c', command,
|
|
28
60
|
stdout=asyncio.subprocess.PIPE,
|
|
29
61
|
stderr=asyncio.subprocess.PIPE,
|
|
30
|
-
cwd=
|
|
62
|
+
cwd=effective_cwd
|
|
31
63
|
)
|
|
32
64
|
stdout, stderr = await process.communicate()
|
|
65
|
+
|
|
66
|
+
stdout_output = stdout.decode().strip() if stdout else ""
|
|
67
|
+
stderr_output = stderr.decode().strip() if stderr else ""
|
|
33
68
|
|
|
34
69
|
if process.returncode != 0:
|
|
35
|
-
error_message =
|
|
70
|
+
error_message = stderr_output if stderr_output else "Unknown error"
|
|
36
71
|
if not error_message and process.returncode != 0:
|
|
37
72
|
error_message = f"Command failed with exit code {process.returncode} and no stderr output."
|
|
38
73
|
|
|
@@ -40,16 +75,26 @@ async def bash_executor(context: Optional['AgentContext'], command: str, cwd: Op
|
|
|
40
75
|
raise subprocess.CalledProcessError(
|
|
41
76
|
returncode=process.returncode,
|
|
42
77
|
cmd=command,
|
|
43
|
-
output=
|
|
78
|
+
output=stdout_output,
|
|
44
79
|
stderr=error_message
|
|
45
80
|
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
81
|
+
|
|
82
|
+
# Adaptive return for successful commands to provide maximum context to the agent.
|
|
83
|
+
if stdout_output and stderr_output:
|
|
84
|
+
return f"STDOUT:\n{stdout_output}\n\nLOGS:\n{stderr_output}"
|
|
85
|
+
elif stdout_output:
|
|
86
|
+
return stdout_output # Keep it simple for commands with only stdout
|
|
87
|
+
elif stderr_output:
|
|
88
|
+
return f"LOGS:\n{stderr_output}"
|
|
89
|
+
else:
|
|
90
|
+
return "Command executed successfully with no output."
|
|
50
91
|
|
|
51
92
|
except subprocess.CalledProcessError:
|
|
52
93
|
raise
|
|
94
|
+
except FileNotFoundError:
|
|
95
|
+
# This can be raised by create_subprocess_exec if 'bash' is not found, despite the initial check.
|
|
96
|
+
logger.error("'bash' executable not found when attempting to execute command. Please ensure it is installed and in the PATH.")
|
|
97
|
+
raise
|
|
53
98
|
except Exception as e:
|
|
54
99
|
logger.exception(f"An error occurred while preparing or executing command '{command}': {str(e)}")
|
|
55
100
|
raise RuntimeError(f"Failed to execute command '{command}': {str(e)}")
|
|
@@ -118,9 +118,10 @@ class McpConfigService(metaclass=SingletonMeta):
|
|
|
118
118
|
f"Total unique configs stored: {len(self._configs)}.")
|
|
119
119
|
return config_object
|
|
120
120
|
|
|
121
|
-
def
|
|
121
|
+
def load_config_from_dict(self, config_dict: Dict[str, Any]) -> BaseMcpConfig:
|
|
122
122
|
"""
|
|
123
123
|
Parses a single raw configuration dictionary and adds it to the service.
|
|
124
|
+
This method handles loading a single configuration.
|
|
124
125
|
|
|
125
126
|
Args:
|
|
126
127
|
config_dict: A dictionary representing a single server config,
|
|
@@ -132,77 +133,81 @@ class McpConfigService(metaclass=SingletonMeta):
|
|
|
132
133
|
config_object = self.parse_mcp_config_dict(config_dict)
|
|
133
134
|
return self.add_config(config_object)
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
def load_configs(self, source: Union[str, List[Dict[str, Any]], Dict[str, Any]]) -> List[BaseMcpConfig]:
|
|
136
|
+
def load_configs_from_dict(self, configs_data: Dict[str, Dict[str, Any]]) -> List[BaseMcpConfig]:
|
|
137
137
|
"""
|
|
138
|
-
Loads multiple MCP configurations from a
|
|
138
|
+
Loads multiple MCP configurations from a dictionary where keys are server_ids.
|
|
139
139
|
This will overwrite any existing configurations with the same server_id.
|
|
140
140
|
|
|
141
141
|
Args:
|
|
142
|
-
|
|
143
|
-
1. A file path (str) to a JSON file.
|
|
144
|
-
2. A list of MCP server configuration dictionaries.
|
|
145
|
-
3. A dictionary of configurations, keyed by server_id.
|
|
142
|
+
configs_data: A dictionary of configurations, keyed by server_id.
|
|
146
143
|
|
|
147
144
|
Returns:
|
|
148
145
|
A list of the successfully added McpServerConfig objects.
|
|
149
146
|
"""
|
|
147
|
+
if not isinstance(configs_data, dict):
|
|
148
|
+
raise TypeError("configs_data must be a dictionary of server configurations keyed by server_id.")
|
|
149
|
+
|
|
150
150
|
loaded_mcp_configs: List[BaseMcpConfig] = []
|
|
151
|
+
logger.info(f"McpConfigService loading {len(configs_data)} configs from dictionary.")
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
if not
|
|
154
|
-
|
|
155
|
-
raise FileNotFoundError(f"MCP configuration file not found: {source}")
|
|
153
|
+
for server_id, single_config_data in configs_data.items():
|
|
154
|
+
if not isinstance(single_config_data, dict):
|
|
155
|
+
raise ValueError(f"Configuration for server_id '{server_id}' must be a dictionary.")
|
|
156
156
|
try:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
except
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
raise ValueError(f"Could not read MCP configuration file {source}: {e}") from e
|
|
165
|
-
|
|
166
|
-
elif isinstance(source, list):
|
|
167
|
-
for i, config_item_dict in enumerate(source):
|
|
168
|
-
if not isinstance(config_item_dict, dict):
|
|
169
|
-
raise ValueError(f"Item at index {i} in source list is not a dictionary.")
|
|
170
|
-
|
|
171
|
-
server_id = config_item_dict.get('server_id')
|
|
172
|
-
if not server_id:
|
|
173
|
-
raise ValueError(f"Item at index {i} in source list is missing 'server_id' field.")
|
|
174
|
-
|
|
175
|
-
try:
|
|
176
|
-
# A list item is a single config, but doesn't have the server_id as the key,
|
|
177
|
-
# so we wrap it to use the parser.
|
|
178
|
-
config_obj = McpConfigService.parse_mcp_config_dict({server_id: config_item_dict})
|
|
179
|
-
self.add_config(config_obj)
|
|
180
|
-
loaded_mcp_configs.append(config_obj)
|
|
181
|
-
except ValueError as e:
|
|
182
|
-
logger.error(f"Invalid MCP configuration for list item at index {i}: {e}")
|
|
183
|
-
raise
|
|
157
|
+
# The parser expects the server_id to be the key, so we reconstruct that.
|
|
158
|
+
config_obj = McpConfigService.parse_mcp_config_dict({server_id: single_config_data})
|
|
159
|
+
self.add_config(config_obj)
|
|
160
|
+
loaded_mcp_configs.append(config_obj)
|
|
161
|
+
except ValueError as e:
|
|
162
|
+
logger.error(f"Invalid MCP configuration for server_id '{server_id}': {e}")
|
|
163
|
+
raise
|
|
184
164
|
|
|
185
|
-
|
|
186
|
-
logger.info("Loading MCP server configurations from a dictionary of configs (keyed by server_id).")
|
|
187
|
-
for server_id, config_data in source.items():
|
|
188
|
-
if not isinstance(config_data, dict):
|
|
189
|
-
raise ValueError(f"Configuration for server_id '{server_id}' must be a dictionary.")
|
|
190
|
-
|
|
191
|
-
try:
|
|
192
|
-
config_obj = McpConfigService.parse_mcp_config_dict({server_id: config_data})
|
|
193
|
-
self.add_config(config_obj)
|
|
194
|
-
loaded_mcp_configs.append(config_obj)
|
|
195
|
-
except ValueError as e:
|
|
196
|
-
logger.error(f"Invalid MCP configuration for server_id '{server_id}': {e}")
|
|
197
|
-
raise
|
|
198
|
-
else:
|
|
199
|
-
raise TypeError(f"Unsupported source type for load_configs: {type(source)}. "
|
|
200
|
-
"Expected file path, list of dicts, or dict of dicts.")
|
|
201
|
-
|
|
202
|
-
logger.info(f"McpConfigService load_configs completed. {len(loaded_mcp_configs)} new configurations processed. "
|
|
203
|
-
f"Total unique configs stored: {len(self._configs)}.")
|
|
165
|
+
logger.info(f"McpConfigService load_configs_from_dict completed. {len(loaded_mcp_configs)} configurations processed.")
|
|
204
166
|
return loaded_mcp_configs
|
|
205
167
|
|
|
168
|
+
def load_configs_from_file(self, filepath: str) -> List[BaseMcpConfig]:
|
|
169
|
+
"""
|
|
170
|
+
Loads MCP configurations from a JSON file. The file should contain a single
|
|
171
|
+
JSON object where keys are server_ids.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
filepath: The path to the JSON configuration file.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A list of the successfully added McpServerConfig objects.
|
|
178
|
+
"""
|
|
179
|
+
if not os.path.exists(filepath):
|
|
180
|
+
logger.error(f"MCP configuration file not found at path: {filepath}")
|
|
181
|
+
raise FileNotFoundError(f"MCP configuration file not found: {filepath}")
|
|
182
|
+
try:
|
|
183
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
184
|
+
json_data = json.load(f)
|
|
185
|
+
logger.info(f"Successfully loaded JSON data from file: {filepath}")
|
|
186
|
+
|
|
187
|
+
if isinstance(json_data, dict):
|
|
188
|
+
return self.load_configs_from_dict(json_data)
|
|
189
|
+
else:
|
|
190
|
+
# To maintain some flexibility, we can check for the list format.
|
|
191
|
+
# But the primary documented format should be the dict.
|
|
192
|
+
logger.warning("Loading MCP configs from a list in a JSON file is supported but deprecated. "
|
|
193
|
+
"The recommended format is a dictionary keyed by server_id.")
|
|
194
|
+
configs_as_dict = {}
|
|
195
|
+
if isinstance(json_data, list):
|
|
196
|
+
for item in json_data:
|
|
197
|
+
if isinstance(item, dict) and 'server_id' in item:
|
|
198
|
+
server_id = item['server_id']
|
|
199
|
+
configs_as_dict[server_id] = item
|
|
200
|
+
else:
|
|
201
|
+
raise ValueError("When loading from a list, each item must be a dict with a 'server_id'.")
|
|
202
|
+
return self.load_configs_from_dict(configs_as_dict)
|
|
203
|
+
|
|
204
|
+
raise TypeError(f"Unsupported JSON structure in {filepath}. Expected a dictionary of configurations.")
|
|
205
|
+
|
|
206
|
+
except json.JSONDecodeError as e:
|
|
207
|
+
raise ValueError(f"Invalid JSON in MCP configuration file {filepath}: {e}") from e
|
|
208
|
+
except Exception as e:
|
|
209
|
+
raise IOError(f"Could not read or process MCP configuration file {filepath}: {e}") from e
|
|
210
|
+
|
|
206
211
|
def get_config(self, server_id: str) -> Optional[BaseMcpConfig]:
|
|
207
212
|
"""Retrieves an MCP server configuration by its unique server ID."""
|
|
208
213
|
return self._configs.get(server_id)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# file: autobyteus/autobyteus/tools/mcp/server/http_managed_mcp_server.py
|
|
2
1
|
import logging
|
|
2
|
+
import asyncio
|
|
3
3
|
from typing import cast
|
|
4
4
|
|
|
5
5
|
from mcp import ClientSession
|
|
@@ -10,6 +10,8 @@ from ..types import StreamableHttpMcpServerConfig
|
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
13
|
+
INITIALIZE_TIMEOUT = 10 # seconds
|
|
14
|
+
|
|
13
15
|
class HttpManagedMcpServer(BaseManagedMcpServer):
|
|
14
16
|
"""Manages the lifecycle of a streamable_http-based MCP server."""
|
|
15
17
|
|
|
@@ -25,5 +27,15 @@ class HttpManagedMcpServer(BaseManagedMcpServer):
|
|
|
25
27
|
streamablehttp_client(config.url, headers=config.headers)
|
|
26
28
|
)
|
|
27
29
|
session = await self._exit_stack.enter_async_context(ClientSession(read_stream, write_stream))
|
|
28
|
-
|
|
30
|
+
|
|
31
|
+
# --- FIX: Initialize the session after creation with a timeout ---
|
|
32
|
+
try:
|
|
33
|
+
logger.debug(f"Initializing ClientSession for HTTP server '{self.server_id}' with a {INITIALIZE_TIMEOUT}s timeout.")
|
|
34
|
+
await asyncio.wait_for(session.initialize(), timeout=INITIALIZE_TIMEOUT)
|
|
35
|
+
except asyncio.TimeoutError:
|
|
36
|
+
logger.error(f"Timeout occurred while initializing session for HTTP server '{self.server_id}'. The server did not respond in time.")
|
|
37
|
+
# Re-raise as a standard exception to be handled by the BaseManagedMcpServer's connect method.
|
|
38
|
+
raise ConnectionError(f"Server '{self.server_id}' failed to initialize within the timeout period.")
|
|
39
|
+
|
|
40
|
+
logger.debug(f"ClientSession established and initialized for HTTP server '{self.server_id}'.")
|
|
29
41
|
return session
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# file: autobyteus/autobyteus/tools/mcp/server/stdio_managed_mcp_server.py
|
|
2
1
|
import logging
|
|
2
|
+
import asyncio
|
|
3
3
|
from typing import cast
|
|
4
4
|
|
|
5
5
|
from mcp import ClientSession, StdioServerParameters
|
|
@@ -10,6 +10,8 @@ from ..types import StdioMcpServerConfig
|
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
13
|
+
INITIALIZE_TIMEOUT = 10 # seconds
|
|
14
|
+
|
|
13
15
|
class StdioManagedMcpServer(BaseManagedMcpServer):
|
|
14
16
|
"""Manages the lifecycle of a stdio-based MCP server."""
|
|
15
17
|
|
|
@@ -29,5 +31,15 @@ class StdioManagedMcpServer(BaseManagedMcpServer):
|
|
|
29
31
|
logger.debug(f"Establishing stdio connection for server '{self.server_id}' with command: {config.command}")
|
|
30
32
|
read_stream, write_stream = await self._exit_stack.enter_async_context(stdio_client(stdio_params))
|
|
31
33
|
session = await self._exit_stack.enter_async_context(ClientSession(read_stream, write_stream))
|
|
32
|
-
|
|
34
|
+
|
|
35
|
+
# --- FIX: Initialize the session after creation with a timeout ---
|
|
36
|
+
try:
|
|
37
|
+
logger.debug(f"Initializing ClientSession for stdio server '{self.server_id}' with a {INITIALIZE_TIMEOUT}s timeout.")
|
|
38
|
+
await asyncio.wait_for(session.initialize(), timeout=INITIALIZE_TIMEOUT)
|
|
39
|
+
except asyncio.TimeoutError:
|
|
40
|
+
logger.error(f"Timeout occurred while initializing session for server '{self.server_id}'. The server did not respond in time.")
|
|
41
|
+
# Re-raise as a standard exception to be handled by the BaseManagedMcpServer's connect method.
|
|
42
|
+
raise ConnectionError(f"Server '{self.server_id}' failed to initialize within the timeout period.")
|
|
43
|
+
|
|
44
|
+
logger.debug(f"ClientSession established and initialized for stdio server '{self.server_id}'.")
|
|
33
45
|
return session
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# file: autobyteus/autobyteus/tools/mcp/server_instance_manager.py
|
|
2
2
|
import logging
|
|
3
|
+
import copy
|
|
3
4
|
from typing import Dict, List, AsyncIterator
|
|
4
5
|
from contextlib import asynccontextmanager
|
|
5
6
|
|
|
6
7
|
from autobyteus.utils.singleton import SingletonMeta
|
|
8
|
+
from autobyteus.agent.context import AgentContextRegistry
|
|
7
9
|
from .config_service import McpConfigService
|
|
8
10
|
from .server import BaseManagedMcpServer, StdioManagedMcpServer, HttpManagedMcpServer
|
|
9
|
-
from .types import McpTransportType, McpServerInstanceKey, BaseMcpConfig
|
|
11
|
+
from .types import McpTransportType, McpServerInstanceKey, BaseMcpConfig, StdioMcpServerConfig
|
|
10
12
|
|
|
11
13
|
logger = logging.getLogger(__name__)
|
|
12
14
|
|
|
@@ -17,6 +19,7 @@ class McpServerInstanceManager(metaclass=SingletonMeta):
|
|
|
17
19
|
"""
|
|
18
20
|
def __init__(self):
|
|
19
21
|
self._config_service = McpConfigService()
|
|
22
|
+
self._context_registry = AgentContextRegistry()
|
|
20
23
|
self._active_servers: Dict[McpServerInstanceKey, BaseManagedMcpServer] = {}
|
|
21
24
|
logger.info("McpServerInstanceManager initialized.")
|
|
22
25
|
|
|
@@ -40,11 +43,34 @@ class McpServerInstanceManager(metaclass=SingletonMeta):
|
|
|
40
43
|
return self._active_servers[instance_key]
|
|
41
44
|
|
|
42
45
|
logger.info(f"Creating new persistent server instance for {instance_key}.")
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
|
|
47
|
+
base_config = self._config_service.get_config(server_id)
|
|
48
|
+
if not base_config:
|
|
45
49
|
raise ValueError(f"No configuration found for server_id '{server_id}'.")
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
final_config = base_config
|
|
52
|
+
# --- DYNAMIC WORKSPACE ENV VARIABLE INJECTION ---
|
|
53
|
+
if isinstance(base_config, StdioMcpServerConfig):
|
|
54
|
+
agent_context = self._context_registry.get_context(agent_id)
|
|
55
|
+
if agent_context and agent_context.workspace:
|
|
56
|
+
workspace_path = agent_context.workspace.get_base_path()
|
|
57
|
+
if workspace_path:
|
|
58
|
+
logger.info(f"Agent '{agent_id}' has a workspace. Injecting AUTOBYTEUS_AGENT_WORKSPACE='{workspace_path}' for MCP server '{server_id}'.")
|
|
59
|
+
# Create a copy of the config to avoid modifying the global one
|
|
60
|
+
config_copy = copy.deepcopy(base_config)
|
|
61
|
+
# Ensure env dict exists
|
|
62
|
+
if config_copy.env is None:
|
|
63
|
+
config_copy.env = {}
|
|
64
|
+
# Add our environment variable
|
|
65
|
+
config_copy.env['AUTOBYTEUS_AGENT_WORKSPACE'] = workspace_path
|
|
66
|
+
final_config = config_copy
|
|
67
|
+
else:
|
|
68
|
+
logger.warning(f"Agent '{agent_id}' workspace for server '{server_id}' did not provide a base path. No workspace environment variable will be set.")
|
|
69
|
+
else:
|
|
70
|
+
logger.debug(f"No workspace found for agent '{agent_id}'. No workspace environment variable will be set for MCP server '{server_id}'.")
|
|
71
|
+
# --- END DYNAMIC WORKSPACE ENV VARIABLE INJECTION ---
|
|
72
|
+
|
|
73
|
+
server_instance = self._create_server_instance(final_config)
|
|
48
74
|
self._active_servers[instance_key] = server_instance
|
|
49
75
|
return server_instance
|
|
50
76
|
|