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.
Files changed (167) hide show
  1. autobyteus/agent/context/__init__.py +4 -2
  2. autobyteus/agent/context/agent_config.py +0 -4
  3. autobyteus/agent/context/agent_context_registry.py +73 -0
  4. autobyteus/agent/events/notifiers.py +4 -0
  5. autobyteus/agent/handlers/inter_agent_message_event_handler.py +7 -2
  6. autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +19 -19
  7. autobyteus/agent/handlers/user_input_message_event_handler.py +15 -0
  8. autobyteus/agent/message/send_message_to.py +29 -23
  9. autobyteus/agent/runtime/agent_runtime.py +10 -2
  10. autobyteus/agent/sender_type.py +15 -0
  11. autobyteus/agent/streaming/agent_event_stream.py +6 -0
  12. autobyteus/agent/streaming/stream_event_payloads.py +12 -0
  13. autobyteus/agent/streaming/stream_events.py +3 -0
  14. autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +7 -4
  15. autobyteus/agent_team/__init__.py +1 -0
  16. autobyteus/agent_team/agent_team.py +93 -0
  17. autobyteus/agent_team/agent_team_builder.py +184 -0
  18. autobyteus/agent_team/base_agent_team.py +86 -0
  19. autobyteus/agent_team/bootstrap_steps/__init__.py +24 -0
  20. autobyteus/agent_team/bootstrap_steps/agent_configuration_preparation_step.py +73 -0
  21. autobyteus/agent_team/bootstrap_steps/agent_team_bootstrapper.py +54 -0
  22. autobyteus/agent_team/bootstrap_steps/agent_team_runtime_queue_initialization_step.py +25 -0
  23. autobyteus/agent_team/bootstrap_steps/base_agent_team_bootstrap_step.py +23 -0
  24. autobyteus/agent_team/bootstrap_steps/coordinator_initialization_step.py +41 -0
  25. autobyteus/agent_team/bootstrap_steps/coordinator_prompt_preparation_step.py +85 -0
  26. autobyteus/agent_team/bootstrap_steps/task_notifier_initialization_step.py +51 -0
  27. autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +45 -0
  28. autobyteus/agent_team/context/__init__.py +17 -0
  29. autobyteus/agent_team/context/agent_team_config.py +33 -0
  30. autobyteus/agent_team/context/agent_team_context.py +61 -0
  31. autobyteus/agent_team/context/agent_team_runtime_state.py +56 -0
  32. autobyteus/agent_team/context/team_manager.py +147 -0
  33. autobyteus/agent_team/context/team_node_config.py +76 -0
  34. autobyteus/agent_team/events/__init__.py +29 -0
  35. autobyteus/agent_team/events/agent_team_event_dispatcher.py +39 -0
  36. autobyteus/agent_team/events/agent_team_events.py +53 -0
  37. autobyteus/agent_team/events/agent_team_input_event_queue_manager.py +21 -0
  38. autobyteus/agent_team/exceptions.py +8 -0
  39. autobyteus/agent_team/factory/__init__.py +9 -0
  40. autobyteus/agent_team/factory/agent_team_factory.py +99 -0
  41. autobyteus/agent_team/handlers/__init__.py +19 -0
  42. autobyteus/agent_team/handlers/agent_team_event_handler_registry.py +23 -0
  43. autobyteus/agent_team/handlers/base_agent_team_event_handler.py +16 -0
  44. autobyteus/agent_team/handlers/inter_agent_message_request_event_handler.py +61 -0
  45. autobyteus/agent_team/handlers/lifecycle_agent_team_event_handler.py +27 -0
  46. autobyteus/agent_team/handlers/process_user_message_event_handler.py +46 -0
  47. autobyteus/agent_team/handlers/tool_approval_team_event_handler.py +48 -0
  48. autobyteus/agent_team/phases/__init__.py +11 -0
  49. autobyteus/agent_team/phases/agent_team_operational_phase.py +19 -0
  50. autobyteus/agent_team/phases/agent_team_phase_manager.py +48 -0
  51. autobyteus/agent_team/runtime/__init__.py +13 -0
  52. autobyteus/agent_team/runtime/agent_team_runtime.py +82 -0
  53. autobyteus/agent_team/runtime/agent_team_worker.py +117 -0
  54. autobyteus/agent_team/shutdown_steps/__init__.py +17 -0
  55. autobyteus/agent_team/shutdown_steps/agent_team_shutdown_orchestrator.py +35 -0
  56. autobyteus/agent_team/shutdown_steps/agent_team_shutdown_step.py +42 -0
  57. autobyteus/agent_team/shutdown_steps/base_agent_team_shutdown_step.py +16 -0
  58. autobyteus/agent_team/shutdown_steps/bridge_cleanup_step.py +28 -0
  59. autobyteus/agent_team/shutdown_steps/sub_team_shutdown_step.py +41 -0
  60. autobyteus/agent_team/streaming/__init__.py +26 -0
  61. autobyteus/agent_team/streaming/agent_event_bridge.py +48 -0
  62. autobyteus/agent_team/streaming/agent_event_multiplexer.py +70 -0
  63. autobyteus/agent_team/streaming/agent_team_event_notifier.py +64 -0
  64. autobyteus/agent_team/streaming/agent_team_event_stream.py +33 -0
  65. autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +32 -0
  66. autobyteus/agent_team/streaming/agent_team_stream_events.py +56 -0
  67. autobyteus/agent_team/streaming/team_event_bridge.py +50 -0
  68. autobyteus/agent_team/task_notification/__init__.py +11 -0
  69. autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +164 -0
  70. autobyteus/agent_team/task_notification/task_notification_mode.py +24 -0
  71. autobyteus/agent_team/utils/__init__.py +9 -0
  72. autobyteus/agent_team/utils/wait_for_idle.py +46 -0
  73. autobyteus/cli/agent_team_tui/__init__.py +4 -0
  74. autobyteus/cli/agent_team_tui/app.py +210 -0
  75. autobyteus/cli/agent_team_tui/state.py +180 -0
  76. autobyteus/cli/agent_team_tui/widgets/__init__.py +6 -0
  77. autobyteus/cli/agent_team_tui/widgets/agent_list_sidebar.py +149 -0
  78. autobyteus/cli/agent_team_tui/widgets/focus_pane.py +320 -0
  79. autobyteus/cli/agent_team_tui/widgets/logo.py +20 -0
  80. autobyteus/cli/agent_team_tui/widgets/renderables.py +77 -0
  81. autobyteus/cli/agent_team_tui/widgets/shared.py +60 -0
  82. autobyteus/cli/agent_team_tui/widgets/status_bar.py +14 -0
  83. autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +82 -0
  84. autobyteus/events/event_types.py +7 -2
  85. autobyteus/llm/api/autobyteus_llm.py +11 -12
  86. autobyteus/llm/api/lmstudio_llm.py +10 -13
  87. autobyteus/llm/api/ollama_llm.py +8 -13
  88. autobyteus/llm/autobyteus_provider.py +73 -46
  89. autobyteus/llm/llm_factory.py +102 -140
  90. autobyteus/llm/lmstudio_provider.py +63 -48
  91. autobyteus/llm/models.py +83 -53
  92. autobyteus/llm/ollama_provider.py +69 -61
  93. autobyteus/llm/ollama_provider_resolver.py +1 -0
  94. autobyteus/llm/providers.py +13 -13
  95. autobyteus/llm/runtimes.py +11 -0
  96. autobyteus/task_management/__init__.py +43 -0
  97. autobyteus/task_management/base_task_board.py +68 -0
  98. autobyteus/task_management/converters/__init__.py +11 -0
  99. autobyteus/task_management/converters/task_board_converter.py +64 -0
  100. autobyteus/task_management/converters/task_plan_converter.py +48 -0
  101. autobyteus/task_management/deliverable.py +16 -0
  102. autobyteus/task_management/deliverables/__init__.py +8 -0
  103. autobyteus/task_management/deliverables/file_deliverable.py +15 -0
  104. autobyteus/task_management/events.py +27 -0
  105. autobyteus/task_management/in_memory_task_board.py +126 -0
  106. autobyteus/task_management/schemas/__init__.py +15 -0
  107. autobyteus/task_management/schemas/deliverable_schema.py +13 -0
  108. autobyteus/task_management/schemas/plan_definition.py +35 -0
  109. autobyteus/task_management/schemas/task_status_report.py +27 -0
  110. autobyteus/task_management/task_plan.py +110 -0
  111. autobyteus/task_management/tools/__init__.py +14 -0
  112. autobyteus/task_management/tools/get_task_board_status.py +68 -0
  113. autobyteus/task_management/tools/publish_task_plan.py +113 -0
  114. autobyteus/task_management/tools/update_task_status.py +135 -0
  115. autobyteus/tools/bash/bash_executor.py +59 -14
  116. autobyteus/tools/mcp/config_service.py +63 -58
  117. autobyteus/tools/mcp/server/http_managed_mcp_server.py +14 -2
  118. autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +14 -2
  119. autobyteus/tools/mcp/server_instance_manager.py +30 -4
  120. autobyteus/tools/mcp/tool_registrar.py +103 -50
  121. autobyteus/tools/parameter_schema.py +17 -11
  122. autobyteus/tools/registry/tool_definition.py +24 -29
  123. autobyteus/tools/tool_category.py +1 -0
  124. autobyteus/tools/usage/formatters/default_json_example_formatter.py +78 -3
  125. autobyteus/tools/usage/formatters/default_xml_example_formatter.py +23 -3
  126. autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +6 -0
  127. autobyteus/tools/usage/formatters/google_json_example_formatter.py +7 -0
  128. autobyteus/tools/usage/formatters/openai_json_example_formatter.py +6 -4
  129. autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +23 -7
  130. autobyteus/tools/usage/parsers/provider_aware_tool_usage_parser.py +14 -25
  131. autobyteus/tools/usage/providers/__init__.py +2 -12
  132. autobyteus/tools/usage/providers/tool_manifest_provider.py +36 -29
  133. autobyteus/tools/usage/registries/__init__.py +7 -12
  134. autobyteus/tools/usage/registries/tool_formatter_pair.py +15 -0
  135. autobyteus/tools/usage/registries/tool_formatting_registry.py +58 -0
  136. autobyteus/tools/usage/registries/tool_usage_parser_registry.py +55 -0
  137. {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/METADATA +3 -3
  138. {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/RECORD +146 -72
  139. examples/agent_team/__init__.py +1 -0
  140. examples/run_browser_agent.py +17 -15
  141. examples/run_google_slides_agent.py +17 -16
  142. examples/run_poem_writer.py +22 -12
  143. examples/run_sqlite_agent.py +17 -15
  144. autobyteus/tools/mcp/call_handlers/__init__.py +0 -16
  145. autobyteus/tools/mcp/call_handlers/base_handler.py +0 -40
  146. autobyteus/tools/mcp/call_handlers/stdio_handler.py +0 -76
  147. autobyteus/tools/mcp/call_handlers/streamable_http_handler.py +0 -55
  148. autobyteus/tools/usage/providers/json_example_provider.py +0 -32
  149. autobyteus/tools/usage/providers/json_schema_provider.py +0 -35
  150. autobyteus/tools/usage/providers/json_tool_usage_parser_provider.py +0 -28
  151. autobyteus/tools/usage/providers/xml_example_provider.py +0 -28
  152. autobyteus/tools/usage/providers/xml_schema_provider.py +0 -29
  153. autobyteus/tools/usage/providers/xml_tool_usage_parser_provider.py +0 -26
  154. autobyteus/tools/usage/registries/json_example_formatter_registry.py +0 -51
  155. autobyteus/tools/usage/registries/json_schema_formatter_registry.py +0 -51
  156. autobyteus/tools/usage/registries/json_tool_usage_parser_registry.py +0 -42
  157. autobyteus/tools/usage/registries/xml_example_formatter_registry.py +0 -30
  158. autobyteus/tools/usage/registries/xml_schema_formatter_registry.py +0 -33
  159. autobyteus/tools/usage/registries/xml_tool_usage_parser_registry.py +0 -30
  160. examples/workflow/__init__.py +0 -1
  161. examples/workflow/run_basic_research_workflow.py +0 -189
  162. examples/workflow/run_code_review_workflow.py +0 -269
  163. examples/workflow/run_debate_workflow.py +0 -212
  164. examples/workflow/run_workflow_with_tui.py +0 -153
  165. {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/WHEEL +0 -0
  166. {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/licenses/LICENSE +0 -0
  167. {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, cwd: Optional[str] = None) -> str:
17
+ async def bash_executor(context: Optional['AgentContext'], command: str) -> str:
16
18
  """
17
- Executes bash commands and retrieves their standard output.
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
- 'cwd' is the optional directory to run the command in.
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
- logger.debug(f"Functional BashExecutor tool executing for '{agent_id_str}': {command} in cwd: {cwd or 'default'}")
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
- process = await asyncio.create_subprocess_shell(
27
- command,
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=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 = stderr.decode().strip() if stderr else "Unknown error"
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=stdout.decode().strip() if stdout else "",
78
+ output=stdout_output,
44
79
  stderr=error_message
45
80
  )
46
-
47
- output = stdout.decode().strip() if stdout else ""
48
- logger.debug(f"Command '{command}' output: {output}")
49
- return output
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 load_config(self, config_dict: Dict[str, Any]) -> BaseMcpConfig:
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 source, parsing and adding them.
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
- source: The data source. Can be:
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
- if isinstance(source, str):
153
- if not os.path.exists(source):
154
- logger.error(f"MCP configuration file not found at path: {source}")
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
- with open(source, 'r', encoding='utf-8') as f:
158
- json_data = json.load(f)
159
- logger.info(f"Successfully loaded JSON data from file: {source}")
160
- return self.load_configs(json_data)
161
- except json.JSONDecodeError as e:
162
- raise ValueError(f"Invalid JSON in MCP configuration file {source}: {e}") from e
163
- except Exception as e:
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
- elif isinstance(source, dict):
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
- logger.debug(f"ClientSession established for HTTP server '{self.server_id}'.")
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
- logger.debug(f"ClientSession established for stdio server '{self.server_id}'.")
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
- config = self._config_service.get_config(server_id)
44
- if not config:
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
- server_instance = self._create_server_instance(config)
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