autobyteus 1.1.0__py3-none-any.whl → 1.1.2__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/bootstrap_steps/agent_bootstrapper.py +1 -1
- autobyteus/agent/bootstrap_steps/agent_runtime_queue_initialization_step.py +1 -1
- autobyteus/agent/bootstrap_steps/base_bootstrap_step.py +1 -1
- autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +1 -1
- autobyteus/agent/bootstrap_steps/workspace_context_initialization_step.py +1 -1
- autobyteus/agent/context/__init__.py +0 -5
- autobyteus/agent/context/agent_config.py +6 -2
- autobyteus/agent/context/agent_context.py +2 -5
- autobyteus/agent/context/agent_phase_manager.py +105 -5
- autobyteus/agent/context/agent_runtime_state.py +2 -2
- autobyteus/agent/context/phases.py +2 -0
- autobyteus/agent/events/__init__.py +0 -11
- autobyteus/agent/events/agent_events.py +0 -37
- autobyteus/agent/events/notifiers.py +25 -7
- autobyteus/agent/events/worker_event_dispatcher.py +1 -1
- autobyteus/agent/factory/agent_factory.py +6 -2
- autobyteus/agent/group/agent_group.py +16 -7
- autobyteus/agent/handlers/approved_tool_invocation_event_handler.py +28 -14
- autobyteus/agent/handlers/lifecycle_event_logger.py +1 -1
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +4 -2
- autobyteus/agent/handlers/tool_invocation_request_event_handler.py +40 -15
- autobyteus/agent/handlers/tool_result_event_handler.py +12 -7
- autobyteus/agent/hooks/__init__.py +7 -0
- autobyteus/agent/hooks/base_phase_hook.py +11 -2
- autobyteus/agent/hooks/hook_definition.py +36 -0
- autobyteus/agent/hooks/hook_meta.py +37 -0
- autobyteus/agent/hooks/hook_registry.py +118 -0
- autobyteus/agent/input_processor/base_user_input_processor.py +6 -3
- autobyteus/agent/input_processor/passthrough_input_processor.py +2 -1
- autobyteus/agent/input_processor/processor_meta.py +1 -1
- autobyteus/agent/input_processor/processor_registry.py +19 -0
- autobyteus/agent/llm_response_processor/base_processor.py +6 -3
- autobyteus/agent/llm_response_processor/processor_meta.py +1 -1
- autobyteus/agent/llm_response_processor/processor_registry.py +19 -0
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +2 -1
- autobyteus/agent/message/context_file_type.py +2 -3
- autobyteus/agent/phases/__init__.py +18 -0
- autobyteus/agent/phases/discover.py +52 -0
- autobyteus/agent/phases/manager.py +265 -0
- autobyteus/agent/phases/phase_enum.py +49 -0
- autobyteus/agent/phases/transition_decorator.py +40 -0
- autobyteus/agent/phases/transition_info.py +33 -0
- autobyteus/agent/remote_agent.py +1 -1
- autobyteus/agent/runtime/agent_runtime.py +5 -10
- autobyteus/agent/runtime/agent_worker.py +62 -19
- autobyteus/agent/streaming/agent_event_stream.py +58 -5
- autobyteus/agent/streaming/stream_event_payloads.py +24 -13
- autobyteus/agent/streaming/stream_events.py +14 -11
- autobyteus/agent/system_prompt_processor/base_processor.py +6 -3
- autobyteus/agent/system_prompt_processor/processor_meta.py +1 -1
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +45 -31
- autobyteus/agent/tool_invocation.py +29 -3
- autobyteus/agent/utils/wait_for_idle.py +1 -1
- autobyteus/agent/workspace/__init__.py +2 -0
- autobyteus/agent/workspace/base_workspace.py +33 -11
- autobyteus/agent/workspace/workspace_config.py +160 -0
- autobyteus/agent/workspace/workspace_definition.py +36 -0
- autobyteus/agent/workspace/workspace_meta.py +37 -0
- autobyteus/agent/workspace/workspace_registry.py +72 -0
- autobyteus/cli/__init__.py +4 -3
- autobyteus/cli/agent_cli.py +25 -207
- autobyteus/cli/cli_display.py +205 -0
- autobyteus/events/event_manager.py +2 -1
- autobyteus/events/event_types.py +3 -1
- autobyteus/llm/api/autobyteus_llm.py +2 -12
- autobyteus/llm/api/deepseek_llm.py +11 -173
- autobyteus/llm/api/grok_llm.py +11 -172
- autobyteus/llm/api/kimi_llm.py +24 -0
- autobyteus/llm/api/mistral_llm.py +4 -4
- autobyteus/llm/api/ollama_llm.py +2 -2
- autobyteus/llm/api/openai_compatible_llm.py +193 -0
- autobyteus/llm/api/openai_llm.py +11 -139
- autobyteus/llm/extensions/token_usage_tracking_extension.py +11 -1
- autobyteus/llm/llm_factory.py +168 -42
- autobyteus/llm/models.py +25 -29
- autobyteus/llm/ollama_provider.py +6 -2
- autobyteus/llm/ollama_provider_resolver.py +44 -0
- autobyteus/llm/providers.py +1 -0
- autobyteus/llm/token_counter/kimi_token_counter.py +24 -0
- autobyteus/llm/token_counter/token_counter_factory.py +3 -0
- autobyteus/llm/utils/messages.py +3 -3
- autobyteus/tools/__init__.py +2 -0
- autobyteus/tools/base_tool.py +7 -1
- autobyteus/tools/functional_tool.py +20 -5
- autobyteus/tools/mcp/call_handlers/stdio_handler.py +15 -1
- autobyteus/tools/mcp/config_service.py +106 -127
- autobyteus/tools/mcp/registrar.py +247 -59
- autobyteus/tools/mcp/types.py +5 -3
- autobyteus/tools/registry/tool_definition.py +8 -1
- autobyteus/tools/registry/tool_registry.py +18 -0
- autobyteus/tools/tool_category.py +11 -0
- autobyteus/tools/tool_meta.py +3 -1
- autobyteus/tools/tool_state.py +20 -0
- autobyteus/tools/usage/parsers/_json_extractor.py +99 -0
- autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +46 -77
- autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +87 -96
- autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +37 -47
- autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +112 -113
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/METADATA +13 -12
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/RECORD +103 -82
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/top_level.txt +0 -0
|
@@ -2,6 +2,7 @@ from autobyteus.llm.models import LLMModel
|
|
|
2
2
|
from autobyteus.llm.api.ollama_llm import OllamaLLM
|
|
3
3
|
from autobyteus.llm.providers import LLMProvider
|
|
4
4
|
from autobyteus.llm.utils.llm_config import LLMConfig, TokenPricingConfig
|
|
5
|
+
from autobyteus.llm.ollama_provider_resolver import OllamaProviderResolver
|
|
5
6
|
from typing import TYPE_CHECKING
|
|
6
7
|
import os
|
|
7
8
|
import logging
|
|
@@ -39,7 +40,7 @@ class OllamaModelProvider:
|
|
|
39
40
|
try:
|
|
40
41
|
from autobyteus.llm.llm_factory import LLMFactory # Local import to avoid circular dependency
|
|
41
42
|
|
|
42
|
-
ollama_host = os.getenv('
|
|
43
|
+
ollama_host = os.getenv('DEFAULT_OLLAMA_HOST', OllamaLLM.DEFAULT_OLLAMA_HOST)
|
|
43
44
|
|
|
44
45
|
if not OllamaModelProvider.is_valid_url(ollama_host):
|
|
45
46
|
logger.error(f"Invalid Ollama host URL: {ollama_host}")
|
|
@@ -73,11 +74,14 @@ class OllamaModelProvider:
|
|
|
73
74
|
model_name = model_info.get('model')
|
|
74
75
|
if not model_name:
|
|
75
76
|
continue
|
|
77
|
+
|
|
78
|
+
# Determine the provider based on the model name
|
|
79
|
+
provider = OllamaProviderResolver.resolve(model_name)
|
|
76
80
|
|
|
77
81
|
llm_model = LLMModel(
|
|
78
82
|
name=model_name,
|
|
79
83
|
value=model_name,
|
|
80
|
-
provider=
|
|
84
|
+
provider=provider,
|
|
81
85
|
llm_class=OllamaLLM,
|
|
82
86
|
canonical_name=model_name, # Use model_name as the canonical_name
|
|
83
87
|
default_config=LLMConfig(
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from autobyteus.llm.providers import LLMProvider
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
class OllamaProviderResolver:
|
|
7
|
+
"""
|
|
8
|
+
A utility class to resolve the correct LLMProvider for Ollama models
|
|
9
|
+
based on keywords in their names. This helps attribute models to their
|
|
10
|
+
original creators (e.g., Google for 'gemma').
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# A mapping from keywords to providers. The list is ordered to handle
|
|
14
|
+
# potential overlaps, though current keywords are distinct.
|
|
15
|
+
KEYWORD_PROVIDER_MAP = [
|
|
16
|
+
(['gemma', 'gemini'], LLMProvider.GEMINI),
|
|
17
|
+
(['llama'], LLMProvider.GROQ),
|
|
18
|
+
(['mistral'], LLMProvider.MISTRAL),
|
|
19
|
+
(['deepseek'], LLMProvider.DEEPSEEK),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def resolve(model_name: str) -> LLMProvider:
|
|
24
|
+
"""
|
|
25
|
+
Resolves the LLMProvider for a given model name from Ollama.
|
|
26
|
+
It checks for keywords in the model name and returns the corresponding
|
|
27
|
+
provider. If no specific provider is found, it defaults to OLLAMA.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
model_name (str): The name of the model discovered from Ollama (e.g., 'gemma:7b').
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
LLMProvider: The resolved provider for the model.
|
|
34
|
+
"""
|
|
35
|
+
lower_model_name = model_name.lower()
|
|
36
|
+
|
|
37
|
+
for keywords, provider in OllamaProviderResolver.KEYWORD_PROVIDER_MAP:
|
|
38
|
+
for keyword in keywords:
|
|
39
|
+
if keyword in lower_model_name:
|
|
40
|
+
logger.debug(f"Resolved provider for model '{model_name}' to '{provider.name}' based on keyword '{keyword}'.")
|
|
41
|
+
return provider
|
|
42
|
+
|
|
43
|
+
logger.debug(f"Model '{model_name}' did not match any specific provider keywords. Defaulting to OLLAMA provider.")
|
|
44
|
+
return LLMProvider.OLLAMA
|
autobyteus/llm/providers.py
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
from autobyteus.llm.token_counter.openai_token_counter import OpenAITokenCounter
|
|
3
|
+
from autobyteus.llm.models import LLMModel
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from autobyteus.llm.base_llm import BaseLLM
|
|
7
|
+
|
|
8
|
+
class KimiTokenCounter(OpenAITokenCounter):
|
|
9
|
+
"""
|
|
10
|
+
Token counter for Kimi (Moonshot AI) models. Uses the same token counting implementation as OpenAI.
|
|
11
|
+
|
|
12
|
+
This implementation inherits from OpenAITokenCounter as Kimi uses the same tokenization
|
|
13
|
+
approach as OpenAI's models.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, model: LLMModel, llm: 'BaseLLM' = None):
|
|
17
|
+
"""
|
|
18
|
+
Initialize the Kimi token counter.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
model (LLMModel): The Kimi model to count tokens for.
|
|
22
|
+
llm (BaseLLM, optional): The LLM instance. Defaults to None.
|
|
23
|
+
"""
|
|
24
|
+
super().__init__(model, llm)
|
|
@@ -3,6 +3,7 @@ from autobyteus.llm.token_counter.openai_token_counter import OpenAITokenCounter
|
|
|
3
3
|
from autobyteus.llm.token_counter.claude_token_counter import ClaudeTokenCounter
|
|
4
4
|
from autobyteus.llm.token_counter.mistral_token_counter import MistralTokenCounter
|
|
5
5
|
from autobyteus.llm.token_counter.deepseek_token_counter import DeepSeekTokenCounter
|
|
6
|
+
from autobyteus.llm.token_counter.kimi_token_counter import KimiTokenCounter
|
|
6
7
|
from autobyteus.llm.token_counter.base_token_counter import BaseTokenCounter
|
|
7
8
|
from autobyteus.llm.models import LLMModel
|
|
8
9
|
from autobyteus.llm.providers import LLMProvider
|
|
@@ -31,6 +32,8 @@ def get_token_counter(model: LLMModel, llm: 'BaseLLM') -> BaseTokenCounter:
|
|
|
31
32
|
return DeepSeekTokenCounter(model, llm)
|
|
32
33
|
elif model.provider == LLMProvider.GROK:
|
|
33
34
|
return DeepSeekTokenCounter(model, llm)
|
|
35
|
+
elif model.provider == LLMProvider.KIMI:
|
|
36
|
+
return KimiTokenCounter(model, llm)
|
|
34
37
|
elif model.provider == LLMProvider.OLLAMA:
|
|
35
38
|
return OpenAITokenCounter(model, llm)
|
|
36
39
|
elif model.provider == LLMProvider.GEMINI:
|
autobyteus/llm/utils/messages.py
CHANGED
|
@@ -20,9 +20,9 @@ class Message:
|
|
|
20
20
|
self.content = content
|
|
21
21
|
self.reasoning_content = reasoning_content # Optional field for reasoning content
|
|
22
22
|
|
|
23
|
-
def to_dict(self) -> Dict[str, Union[str,
|
|
24
|
-
result = {"role": self.role.value, "content": self.content}
|
|
25
|
-
if self.reasoning_content
|
|
23
|
+
def to_dict(self) -> Dict[str, Union[str, List[Dict]]]:
|
|
24
|
+
result: Dict[str, Union[str, List[Dict]]] = {"role": self.role.value, "content": self.content}
|
|
25
|
+
if self.reasoning_content:
|
|
26
26
|
result["reasoning_content"] = self.reasoning_content
|
|
27
27
|
return result
|
|
28
28
|
|
autobyteus/tools/__init__.py
CHANGED
|
@@ -10,6 +10,7 @@ from .base_tool import BaseTool
|
|
|
10
10
|
from .functional_tool import tool # The @tool decorator
|
|
11
11
|
from .parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
|
|
12
12
|
from .tool_config import ToolConfig # Configuration data object, primarily for class-based tools
|
|
13
|
+
from .tool_category import ToolCategory
|
|
13
14
|
|
|
14
15
|
# --- Re-export specific tools for easier access ---
|
|
15
16
|
|
|
@@ -47,6 +48,7 @@ __all__ = [
|
|
|
47
48
|
"ParameterDefinition",
|
|
48
49
|
"ParameterType",
|
|
49
50
|
"ToolConfig",
|
|
51
|
+
"ToolCategory",
|
|
50
52
|
|
|
51
53
|
# Re-exported functional tool instances
|
|
52
54
|
"ask_user_input",
|
autobyteus/tools/base_tool.py
CHANGED
|
@@ -9,10 +9,13 @@ from autobyteus.events.event_emitter import EventEmitter
|
|
|
9
9
|
from autobyteus.events.event_types import EventType
|
|
10
10
|
|
|
11
11
|
from .tool_meta import ToolMeta
|
|
12
|
+
from .tool_state import ToolState
|
|
13
|
+
|
|
12
14
|
if TYPE_CHECKING:
|
|
13
15
|
from autobyteus.agent.context import AgentContext
|
|
14
16
|
from autobyteus.tools.parameter_schema import ParameterSchema
|
|
15
17
|
from autobyteus.tools.tool_config import ToolConfig
|
|
18
|
+
from .tool_state import ToolState
|
|
16
19
|
|
|
17
20
|
logger = logging.getLogger('autobyteus')
|
|
18
21
|
|
|
@@ -25,7 +28,10 @@ class BaseTool(ABC, EventEmitter, metaclass=ToolMeta):
|
|
|
25
28
|
self.agent_id: Optional[str] = None
|
|
26
29
|
# The config is stored primarily for potential use by subclasses or future base features.
|
|
27
30
|
self._config = config
|
|
28
|
-
|
|
31
|
+
# Add a dedicated state dictionary for the tool instance
|
|
32
|
+
# CHANGED: Use ToolState class for explicit state management.
|
|
33
|
+
self.tool_state: 'ToolState' = ToolState()
|
|
34
|
+
logger.debug(f"BaseTool instance initializing for potential class {self.__class__.__name__}. tool_state initialized.")
|
|
29
35
|
|
|
30
36
|
@classmethod
|
|
31
37
|
def get_name(cls) -> str:
|
|
@@ -8,6 +8,7 @@ from autobyteus.tools.base_tool import BaseTool
|
|
|
8
8
|
from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
|
|
9
9
|
from autobyteus.tools.tool_config import ToolConfig
|
|
10
10
|
from autobyteus.tools.registry import default_tool_registry, ToolDefinition
|
|
11
|
+
from autobyteus.tools.tool_category import ToolCategory
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
from autobyteus.agent.context import AgentContext
|
|
@@ -28,15 +29,20 @@ class FunctionalTool(BaseTool):
|
|
|
28
29
|
config_schema: Optional[ParameterSchema],
|
|
29
30
|
is_async: bool,
|
|
30
31
|
expects_context: bool,
|
|
32
|
+
expects_tool_state: bool,
|
|
31
33
|
func_param_names: TypingList[str],
|
|
32
34
|
instantiation_config: Optional[Dict[str, Any]] = None):
|
|
33
35
|
super().__init__(config=ToolConfig(params=instantiation_config) if instantiation_config else None)
|
|
34
36
|
self._original_func = original_func
|
|
35
37
|
self._is_async = is_async
|
|
36
38
|
self._expects_context = expects_context
|
|
39
|
+
self._expects_tool_state = expects_tool_state
|
|
37
40
|
self._func_param_names = func_param_names
|
|
38
41
|
self._instantiation_config = instantiation_config or {}
|
|
39
42
|
|
|
43
|
+
# This instance has its own state dictionary, inherited from BaseTool's __init__
|
|
44
|
+
# self.tool_state: Dict[str, Any] = {} # This is now handled by super().__init__()
|
|
45
|
+
|
|
40
46
|
# Override instance methods to provide specific schema info
|
|
41
47
|
self.get_name = lambda: name
|
|
42
48
|
self.get_description = lambda: description
|
|
@@ -65,6 +71,9 @@ class FunctionalTool(BaseTool):
|
|
|
65
71
|
|
|
66
72
|
if self._expects_context:
|
|
67
73
|
call_args['context'] = context
|
|
74
|
+
|
|
75
|
+
if self._expects_tool_state:
|
|
76
|
+
call_args['tool_state'] = self.tool_state
|
|
68
77
|
|
|
69
78
|
if self._is_async:
|
|
70
79
|
return await self._original_func(**call_args)
|
|
@@ -143,15 +152,19 @@ def _get_parameter_type_from_hint(py_type: Any, param_name: str) -> Tuple[Parame
|
|
|
143
152
|
logger.warning(f"Unmapped type hint {py_type} (actual_type: {actual_type}) for param '{param_name}'. Defaulting to ParameterType.STRING.")
|
|
144
153
|
return ParameterType.STRING, None
|
|
145
154
|
|
|
146
|
-
def _parse_signature(sig: inspect.Signature, tool_name: str) -> Tuple[TypingList[str], bool, ParameterSchema]:
|
|
155
|
+
def _parse_signature(sig: inspect.Signature, tool_name: str) -> Tuple[TypingList[str], bool, bool, ParameterSchema]:
|
|
147
156
|
func_param_names = []
|
|
148
157
|
expects_context = False
|
|
158
|
+
expects_tool_state = False
|
|
149
159
|
generated_arg_schema = ParameterSchema()
|
|
150
160
|
|
|
151
161
|
for param_name, param_obj in sig.parameters.items():
|
|
152
162
|
if param_name == "context":
|
|
153
163
|
expects_context = True
|
|
154
|
-
continue
|
|
164
|
+
continue
|
|
165
|
+
if param_name == "tool_state":
|
|
166
|
+
expects_tool_state = True
|
|
167
|
+
continue
|
|
155
168
|
|
|
156
169
|
func_param_names.append(param_name)
|
|
157
170
|
|
|
@@ -177,7 +190,7 @@ def _parse_signature(sig: inspect.Signature, tool_name: str) -> Tuple[TypingList
|
|
|
177
190
|
)
|
|
178
191
|
generated_arg_schema.add_parameter(schema_param)
|
|
179
192
|
|
|
180
|
-
return func_param_names, expects_context, generated_arg_schema
|
|
193
|
+
return func_param_names, expects_context, expects_tool_state, generated_arg_schema
|
|
181
194
|
|
|
182
195
|
# --- The refactored @tool decorator ---
|
|
183
196
|
|
|
@@ -196,7 +209,7 @@ def tool(
|
|
|
196
209
|
|
|
197
210
|
sig = inspect.signature(func)
|
|
198
211
|
is_async = inspect.iscoroutinefunction(func)
|
|
199
|
-
func_param_names, expects_context, gen_arg_schema = _parse_signature(sig, tool_name)
|
|
212
|
+
func_param_names, expects_context, expects_tool_state, gen_arg_schema = _parse_signature(sig, tool_name)
|
|
200
213
|
|
|
201
214
|
final_arg_schema = argument_schema if argument_schema is not None else gen_arg_schema
|
|
202
215
|
|
|
@@ -209,6 +222,7 @@ def tool(
|
|
|
209
222
|
config_schema=config_schema,
|
|
210
223
|
is_async=is_async,
|
|
211
224
|
expects_context=expects_context,
|
|
225
|
+
expects_tool_state=expects_tool_state,
|
|
212
226
|
func_param_names=func_param_names,
|
|
213
227
|
instantiation_config=inst_config.params if inst_config else None
|
|
214
228
|
)
|
|
@@ -221,7 +235,8 @@ def tool(
|
|
|
221
235
|
argument_schema=final_arg_schema,
|
|
222
236
|
config_schema=config_schema,
|
|
223
237
|
custom_factory=factory,
|
|
224
|
-
tool_class=None
|
|
238
|
+
tool_class=None,
|
|
239
|
+
category=ToolCategory.LOCAL
|
|
225
240
|
)
|
|
226
241
|
default_tool_registry.register_tool(tool_def)
|
|
227
242
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# file: autobyteus/autobyteus/tools/mcp/call_handlers/stdio_handler.py
|
|
2
2
|
import logging
|
|
3
|
+
import asyncio
|
|
3
4
|
from typing import Dict, Any, cast, TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from .base_handler import McpCallHandler
|
|
@@ -11,6 +12,9 @@ if TYPE_CHECKING:
|
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
15
|
+
# A default timeout for STDIO subprocesses to prevent indefinite hangs.
|
|
16
|
+
DEFAULT_STDIO_TIMEOUT = 30 # seconds
|
|
17
|
+
|
|
14
18
|
class StdioMcpCallHandler(McpCallHandler):
|
|
15
19
|
"""Handles MCP tool calls over a stateless STDIO transport."""
|
|
16
20
|
|
|
@@ -23,6 +27,7 @@ class StdioMcpCallHandler(McpCallHandler):
|
|
|
23
27
|
"""
|
|
24
28
|
Creates a new subprocess, establishes a session, and executes the
|
|
25
29
|
requested tool call. It handles 'list_tools' as a special case.
|
|
30
|
+
Includes a timeout to prevent hanging on unresponsive subprocesses.
|
|
26
31
|
"""
|
|
27
32
|
logger.debug(f"Handling STDIO call to tool '{remote_tool_name}' on server '{config.server_id}'.")
|
|
28
33
|
|
|
@@ -39,7 +44,8 @@ class StdioMcpCallHandler(McpCallHandler):
|
|
|
39
44
|
cwd=stdio_config.cwd
|
|
40
45
|
)
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
async def _perform_call():
|
|
48
|
+
"""Inner function to be wrapped by the timeout."""
|
|
43
49
|
# The stdio_client context manager provides the read/write streams.
|
|
44
50
|
async with stdio_client(mcp_lib_stdio_params) as (read_stream, write_stream):
|
|
45
51
|
# The ClientSession is its own context manager that handles initialization.
|
|
@@ -54,6 +60,14 @@ class StdioMcpCallHandler(McpCallHandler):
|
|
|
54
60
|
|
|
55
61
|
logger.debug(f"STDIO call to tool '{remote_tool_name}' on server '{config.server_id}' completed.")
|
|
56
62
|
return result
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
return await asyncio.wait_for(_perform_call(), timeout=DEFAULT_STDIO_TIMEOUT)
|
|
66
|
+
except asyncio.TimeoutError:
|
|
67
|
+
error_message = (f"MCP call to '{remote_tool_name}' on server '{config.server_id}' timed out "
|
|
68
|
+
f"after {DEFAULT_STDIO_TIMEOUT} seconds. The subprocess may have hung.")
|
|
69
|
+
logger.error(error_message)
|
|
70
|
+
raise RuntimeError(error_message) from None
|
|
57
71
|
except Exception as e:
|
|
58
72
|
logger.error(
|
|
59
73
|
f"An error occurred during STDIO tool call to '{remote_tool_name}' on server '{config.server_id}': {e}",
|