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.
Files changed (103) hide show
  1. autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +1 -1
  2. autobyteus/agent/bootstrap_steps/agent_runtime_queue_initialization_step.py +1 -1
  3. autobyteus/agent/bootstrap_steps/base_bootstrap_step.py +1 -1
  4. autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +1 -1
  5. autobyteus/agent/bootstrap_steps/workspace_context_initialization_step.py +1 -1
  6. autobyteus/agent/context/__init__.py +0 -5
  7. autobyteus/agent/context/agent_config.py +6 -2
  8. autobyteus/agent/context/agent_context.py +2 -5
  9. autobyteus/agent/context/agent_phase_manager.py +105 -5
  10. autobyteus/agent/context/agent_runtime_state.py +2 -2
  11. autobyteus/agent/context/phases.py +2 -0
  12. autobyteus/agent/events/__init__.py +0 -11
  13. autobyteus/agent/events/agent_events.py +0 -37
  14. autobyteus/agent/events/notifiers.py +25 -7
  15. autobyteus/agent/events/worker_event_dispatcher.py +1 -1
  16. autobyteus/agent/factory/agent_factory.py +6 -2
  17. autobyteus/agent/group/agent_group.py +16 -7
  18. autobyteus/agent/handlers/approved_tool_invocation_event_handler.py +28 -14
  19. autobyteus/agent/handlers/lifecycle_event_logger.py +1 -1
  20. autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +4 -2
  21. autobyteus/agent/handlers/tool_invocation_request_event_handler.py +40 -15
  22. autobyteus/agent/handlers/tool_result_event_handler.py +12 -7
  23. autobyteus/agent/hooks/__init__.py +7 -0
  24. autobyteus/agent/hooks/base_phase_hook.py +11 -2
  25. autobyteus/agent/hooks/hook_definition.py +36 -0
  26. autobyteus/agent/hooks/hook_meta.py +37 -0
  27. autobyteus/agent/hooks/hook_registry.py +118 -0
  28. autobyteus/agent/input_processor/base_user_input_processor.py +6 -3
  29. autobyteus/agent/input_processor/passthrough_input_processor.py +2 -1
  30. autobyteus/agent/input_processor/processor_meta.py +1 -1
  31. autobyteus/agent/input_processor/processor_registry.py +19 -0
  32. autobyteus/agent/llm_response_processor/base_processor.py +6 -3
  33. autobyteus/agent/llm_response_processor/processor_meta.py +1 -1
  34. autobyteus/agent/llm_response_processor/processor_registry.py +19 -0
  35. autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +2 -1
  36. autobyteus/agent/message/context_file_type.py +2 -3
  37. autobyteus/agent/phases/__init__.py +18 -0
  38. autobyteus/agent/phases/discover.py +52 -0
  39. autobyteus/agent/phases/manager.py +265 -0
  40. autobyteus/agent/phases/phase_enum.py +49 -0
  41. autobyteus/agent/phases/transition_decorator.py +40 -0
  42. autobyteus/agent/phases/transition_info.py +33 -0
  43. autobyteus/agent/remote_agent.py +1 -1
  44. autobyteus/agent/runtime/agent_runtime.py +5 -10
  45. autobyteus/agent/runtime/agent_worker.py +62 -19
  46. autobyteus/agent/streaming/agent_event_stream.py +58 -5
  47. autobyteus/agent/streaming/stream_event_payloads.py +24 -13
  48. autobyteus/agent/streaming/stream_events.py +14 -11
  49. autobyteus/agent/system_prompt_processor/base_processor.py +6 -3
  50. autobyteus/agent/system_prompt_processor/processor_meta.py +1 -1
  51. autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +45 -31
  52. autobyteus/agent/tool_invocation.py +29 -3
  53. autobyteus/agent/utils/wait_for_idle.py +1 -1
  54. autobyteus/agent/workspace/__init__.py +2 -0
  55. autobyteus/agent/workspace/base_workspace.py +33 -11
  56. autobyteus/agent/workspace/workspace_config.py +160 -0
  57. autobyteus/agent/workspace/workspace_definition.py +36 -0
  58. autobyteus/agent/workspace/workspace_meta.py +37 -0
  59. autobyteus/agent/workspace/workspace_registry.py +72 -0
  60. autobyteus/cli/__init__.py +4 -3
  61. autobyteus/cli/agent_cli.py +25 -207
  62. autobyteus/cli/cli_display.py +205 -0
  63. autobyteus/events/event_manager.py +2 -1
  64. autobyteus/events/event_types.py +3 -1
  65. autobyteus/llm/api/autobyteus_llm.py +2 -12
  66. autobyteus/llm/api/deepseek_llm.py +11 -173
  67. autobyteus/llm/api/grok_llm.py +11 -172
  68. autobyteus/llm/api/kimi_llm.py +24 -0
  69. autobyteus/llm/api/mistral_llm.py +4 -4
  70. autobyteus/llm/api/ollama_llm.py +2 -2
  71. autobyteus/llm/api/openai_compatible_llm.py +193 -0
  72. autobyteus/llm/api/openai_llm.py +11 -139
  73. autobyteus/llm/extensions/token_usage_tracking_extension.py +11 -1
  74. autobyteus/llm/llm_factory.py +168 -42
  75. autobyteus/llm/models.py +25 -29
  76. autobyteus/llm/ollama_provider.py +6 -2
  77. autobyteus/llm/ollama_provider_resolver.py +44 -0
  78. autobyteus/llm/providers.py +1 -0
  79. autobyteus/llm/token_counter/kimi_token_counter.py +24 -0
  80. autobyteus/llm/token_counter/token_counter_factory.py +3 -0
  81. autobyteus/llm/utils/messages.py +3 -3
  82. autobyteus/tools/__init__.py +2 -0
  83. autobyteus/tools/base_tool.py +7 -1
  84. autobyteus/tools/functional_tool.py +20 -5
  85. autobyteus/tools/mcp/call_handlers/stdio_handler.py +15 -1
  86. autobyteus/tools/mcp/config_service.py +106 -127
  87. autobyteus/tools/mcp/registrar.py +247 -59
  88. autobyteus/tools/mcp/types.py +5 -3
  89. autobyteus/tools/registry/tool_definition.py +8 -1
  90. autobyteus/tools/registry/tool_registry.py +18 -0
  91. autobyteus/tools/tool_category.py +11 -0
  92. autobyteus/tools/tool_meta.py +3 -1
  93. autobyteus/tools/tool_state.py +20 -0
  94. autobyteus/tools/usage/parsers/_json_extractor.py +99 -0
  95. autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +46 -77
  96. autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +87 -96
  97. autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +37 -47
  98. autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +112 -113
  99. {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/METADATA +13 -12
  100. {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/RECORD +103 -82
  101. {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/WHEEL +0 -0
  102. {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/licenses/LICENSE +0 -0
  103. {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  # file: autobyteus/autobyteus/tools/mcp/registrar.py
2
2
  import logging
3
- from typing import Dict
3
+ from typing import Any, Dict, List, Optional, Union
4
4
 
5
5
  # Import the new handler architecture components
6
6
  from .call_handlers import (
@@ -15,34 +15,32 @@ from autobyteus.tools.mcp import (
15
15
  McpConfigService,
16
16
  McpSchemaMapper,
17
17
  McpToolFactory,
18
- McpTransportType
18
+ McpTransportType,
19
+ BaseMcpConfig
19
20
  )
20
21
 
21
22
  from autobyteus.tools.registry import ToolRegistry, ToolDefinition
23
+ from autobyteus.tools.tool_category import ToolCategory
24
+ from autobyteus.utils.singleton import SingletonMeta
22
25
  from mcp import types as mcp_types
23
26
 
24
27
 
25
28
  logger = logging.getLogger(__name__)
26
29
 
27
- class McpToolRegistrar:
30
+ class McpToolRegistrar(metaclass=SingletonMeta):
28
31
  """
29
32
  Orchestrates the discovery of remote MCP tools and their registration
30
33
  with the AutoByteUs ToolRegistry using a handler-based architecture.
34
+ This class is a singleton.
31
35
  """
32
- def __init__(self,
33
- config_service: McpConfigService,
34
- schema_mapper: McpSchemaMapper,
35
- tool_registry: ToolRegistry):
36
- if not isinstance(config_service, McpConfigService):
37
- raise TypeError("config_service must be an McpConfigService instance.")
38
- if not isinstance(schema_mapper, McpSchemaMapper):
39
- raise TypeError("schema_mapper must be an McpSchemaMapper instance.")
40
- if not isinstance(tool_registry, ToolRegistry):
41
- raise TypeError("tool_registry must be a ToolRegistry instance.")
42
-
43
- self._config_service = config_service
44
- self._schema_mapper = schema_mapper
45
- self._tool_registry = tool_registry
36
+ def __init__(self):
37
+ """
38
+ Initializes the McpToolRegistrar singleton.
39
+ It retrieves singleton instances of its service dependencies and initializes
40
+ its internal state for tracking registered tools.
41
+ """
42
+ self._config_service: McpConfigService = McpConfigService()
43
+ self._tool_registry: ToolRegistry = ToolRegistry()
46
44
 
47
45
  # The handler registry maps a transport type to a reusable handler instance.
48
46
  self._handler_registry: Dict[McpTransportType, McpCallHandler] = {
@@ -50,35 +48,119 @@ class McpToolRegistrar:
50
48
  McpTransportType.STREAMABLE_HTTP: StreamableHttpMcpCallHandler(),
51
49
  McpTransportType.SSE: SseMcpCallHandler(),
52
50
  }
51
+
52
+ # Internal state to track which ToolDefinitions were registered from which server.
53
+ self._registered_tools_by_server: Dict[str, List[ToolDefinition]] = {}
53
54
 
54
55
  logger.info(f"McpToolRegistrar initialized with {len(self._handler_registry)} call handlers.")
55
56
 
56
- async def discover_and_register_tools(self) -> None:
57
+ def _create_tool_definition_from_remote(
58
+ self,
59
+ remote_tool: mcp_types.Tool,
60
+ server_config: BaseMcpConfig,
61
+ handler: McpCallHandler,
62
+ schema_mapper: McpSchemaMapper
63
+ ) -> ToolDefinition:
57
64
  """
58
- Discovers tools from all enabled MCP servers and registers them.
65
+ Maps a single remote tool from an MCP server to an AutoByteUs ToolDefinition.
66
+ This is a helper method to centralize the mapping logic.
59
67
  """
60
- logger.info("Starting MCP tool discovery and registration process.")
61
- all_server_configs = self._config_service.get_all_configs()
62
- if not all_server_configs:
63
- logger.info("No MCP server configurations found. Skipping discovery.")
64
- return
68
+ if hasattr(remote_tool, 'model_dump_json'):
69
+ logger.debug(f"Processing remote tool from server '{server_config.server_id}':\n{remote_tool.model_dump_json(indent=2)}")
70
+
71
+ actual_arg_schema = schema_mapper.map_to_autobyteus_schema(remote_tool.inputSchema)
72
+ actual_desc = remote_tool.description
73
+
74
+ registered_name = remote_tool.name
75
+ if server_config.tool_name_prefix:
76
+ registered_name = f"{server_config.tool_name_prefix.rstrip('_')}_{remote_tool.name}"
77
+
78
+ tool_factory = McpToolFactory(
79
+ mcp_server_config=server_config,
80
+ mcp_remote_tool_name=remote_tool.name,
81
+ mcp_call_handler=handler,
82
+ registered_tool_name=registered_name,
83
+ tool_description=actual_desc,
84
+ tool_argument_schema=actual_arg_schema
85
+ )
86
+
87
+ tool_def = ToolDefinition(
88
+ name=registered_name,
89
+ description=actual_desc,
90
+ argument_schema=actual_arg_schema,
91
+ category=ToolCategory.MCP,
92
+ custom_factory=tool_factory.create_tool,
93
+ config_schema=None,
94
+ tool_class=None
95
+ )
96
+ return tool_def
65
97
 
66
- registered_count = 0
67
- for server_config in all_server_configs:
98
+ async def discover_and_register_tools(self, mcp_config: Optional[Union[BaseMcpConfig, Dict[str, Any]]] = None) -> List[ToolDefinition]:
99
+ """
100
+ Discovers tools from MCP servers and registers them.
101
+
102
+ If `mcp_config` is provided (as a validated object or a raw dictionary),
103
+ it discovers tools only from that specific server, unregistering any
104
+ of its old tools first.
105
+
106
+ If `mcp_config` is None, it unregisters all existing MCP tools and then
107
+ discovers tools from all enabled servers found in the McpConfigService.
108
+
109
+ Returns:
110
+ A list of ToolDefinition objects for all tools that were successfully
111
+ discovered and registered during this call.
112
+ """
113
+ configs_to_process: List[BaseMcpConfig]
114
+
115
+ if mcp_config:
116
+ validated_config: BaseMcpConfig
117
+ # If the user provided a raw dictionary, parse and add it via the service.
118
+ if isinstance(mcp_config, dict):
119
+ try:
120
+ validated_config = self._config_service.load_config(mcp_config)
121
+ except ValueError as e:
122
+ logger.error(f"Failed to parse provided MCP config dictionary: {e}")
123
+ raise
124
+ # If a validated object was passed, add it to the service to ensure it's known.
125
+ elif isinstance(mcp_config, BaseMcpConfig):
126
+ validated_config = self._config_service.add_config(mcp_config)
127
+ else:
128
+ raise TypeError(f"mcp_config must be a BaseMcpConfig object or a dictionary, not {type(mcp_config)}.")
129
+
130
+ logger.info(f"Starting targeted MCP tool discovery for server: {validated_config.server_id}")
131
+ # Unregister any existing tools from this server before re-discovering.
132
+ self.unregister_tools_from_server(validated_config.server_id)
133
+ configs_to_process = [validated_config]
134
+ else:
135
+ logger.info("Starting full MCP tool discovery and registration process. Unregistering all existing MCP tools first.")
136
+ # Get a copy of all server IDs that have tools registered.
137
+ all_server_ids = list(self._registered_tools_by_server.keys())
138
+ for server_id in all_server_ids:
139
+ self.unregister_tools_from_server(server_id)
140
+
141
+ # The registrar's internal state should now be empty, but a clear() is a good safeguard.
142
+ self._registered_tools_by_server.clear()
143
+
144
+ configs_to_process = self._config_service.get_all_configs()
145
+
146
+ if not configs_to_process:
147
+ logger.info("No MCP server configurations to process. Skipping discovery.")
148
+ return []
149
+
150
+ schema_mapper = McpSchemaMapper()
151
+ registered_tool_definitions: List[ToolDefinition] = []
152
+ for server_config in configs_to_process:
68
153
  if not server_config.enabled:
69
154
  logger.info(f"MCP server '{server_config.server_id}' is disabled. Skipping.")
70
155
  continue
71
156
 
72
157
  logger.info(f"Discovering tools from MCP server: '{server_config.server_id}' ({server_config.transport_type.value})")
73
158
  try:
74
- # Get the correct handler for this server's transport type.
75
159
  handler = self._handler_registry.get(server_config.transport_type)
76
160
  if not handler:
77
161
  logger.error(f"No MCP call handler found for transport type '{server_config.transport_type.value}' on server '{server_config.server_id}'.")
78
162
  continue
79
163
 
80
- # Use the handler to call the special 'list_tools' method.
81
- # The registrar does not need to know how this is done; it's encapsulated in the handler.
82
164
  remote_tools_result = await handler.handle_call(
83
165
  config=server_config,
84
166
  remote_tool_name="list_tools",
@@ -94,42 +176,148 @@ class McpToolRegistrar:
94
176
 
95
177
  for remote_tool in actual_remote_tools:
96
178
  try:
97
- if hasattr(remote_tool, 'model_dump_json'):
98
- logger.debug(f"Processing remote tool from server '{server_config.server_id}':\n{remote_tool.model_dump_json(indent=2)}")
99
-
100
- actual_arg_schema = self._schema_mapper.map_to_autobyteus_schema(remote_tool.inputSchema)
101
- actual_desc = remote_tool.description
179
+ tool_def = self._create_tool_definition_from_remote(remote_tool, server_config, handler, schema_mapper)
102
180
 
103
- registered_name = remote_tool.name
104
- if server_config.tool_name_prefix:
105
- registered_name = f"{server_config.tool_name_prefix.rstrip('_')}_{remote_tool.name}"
106
-
107
- # Create the tool factory, injecting the server config and the correct handler.
108
- tool_factory = McpToolFactory(
109
- mcp_server_config=server_config,
110
- mcp_remote_tool_name=remote_tool.name,
111
- mcp_call_handler=handler,
112
- registered_tool_name=registered_name,
113
- tool_description=actual_desc,
114
- tool_argument_schema=actual_arg_schema
115
- )
181
+ self._tool_registry.register_tool(tool_def)
182
+ self._registered_tools_by_server.setdefault(server_config.server_id, []).append(tool_def)
183
+ registered_tool_definitions.append(tool_def)
116
184
 
117
- tool_def = ToolDefinition(
118
- name=registered_name,
119
- description=actual_desc,
120
- argument_schema=actual_arg_schema,
121
- custom_factory=tool_factory.create_tool,
122
- config_schema=None,
123
- tool_class=None
124
- )
185
+ logger.info(f"Successfully registered MCP tool '{remote_tool.name}' from server '{server_config.server_id}' as '{tool_def.name}'.")
125
186
 
126
- self._tool_registry.register_tool(tool_def)
127
- logger.info(f"Successfully registered MCP tool '{remote_tool.name}' from server '{server_config.server_id}' as '{registered_name}'.")
128
- registered_count +=1
129
187
  except Exception as e_tool:
130
188
  logger.error(f"Failed to process or register remote tool '{remote_tool.name}' from server '{server_config.server_id}': {e_tool}", exc_info=True)
131
189
 
132
190
  except Exception as e_server:
133
191
  logger.error(f"Failed to discover tools from MCP server '{server_config.server_id}': {e_server}", exc_info=True)
134
192
 
135
- logger.info(f"MCP tool discovery and registration process completed. Total tools registered: {registered_count}.")
193
+ logger.info(f"MCP tool discovery and registration process completed. Total tools registered: {len(registered_tool_definitions)}.")
194
+ return registered_tool_definitions
195
+
196
+ async def list_remote_tools(self, mcp_config: Union[BaseMcpConfig, Dict[str, Any]]) -> List[ToolDefinition]:
197
+ """
198
+ Previews tools from a remote MCP server without registering them or storing the configuration.
199
+ This is a stateless "dry-run" or "preview" operation.
200
+
201
+ Args:
202
+ mcp_config: A single MCP server configuration, as a validated object or a raw dictionary.
203
+
204
+ Returns:
205
+ A list of ToolDefinition objects for all tools discovered on the server.
206
+
207
+ Raises:
208
+ TypeError: If mcp_config is not a supported type.
209
+ ValueError: If the provided configuration is invalid.
210
+ RuntimeError: If the connection or tool call to the remote server fails.
211
+ """
212
+ validated_config: BaseMcpConfig
213
+ if isinstance(mcp_config, dict):
214
+ # Use the static method to parse the config without storing it in the service.
215
+ validated_config = McpConfigService.parse_mcp_config_dict(mcp_config)
216
+ elif isinstance(mcp_config, BaseMcpConfig):
217
+ validated_config = mcp_config
218
+ else:
219
+ raise TypeError(f"mcp_config must be a BaseMcpConfig object or a dictionary, not {type(mcp_config)}.")
220
+
221
+ logger.info(f"Previewing tools from MCP server: '{validated_config.server_id}' ({validated_config.transport_type.value})")
222
+
223
+ schema_mapper = McpSchemaMapper()
224
+ tool_definitions: List[ToolDefinition] = []
225
+
226
+ try:
227
+ handler = self._handler_registry.get(validated_config.transport_type)
228
+ if not handler:
229
+ raise ValueError(f"No MCP call handler found for transport type '{validated_config.transport_type.value}'.")
230
+
231
+ remote_tools_result = await handler.handle_call(
232
+ config=validated_config,
233
+ remote_tool_name="list_tools",
234
+ arguments={}
235
+ )
236
+
237
+ if not isinstance(remote_tools_result, mcp_types.ListToolsResult):
238
+ error_msg = f"Expected ListToolsResult from handler, but got {type(remote_tools_result)}. Cannot preview tools from server '{validated_config.server_id}'."
239
+ logger.error(error_msg)
240
+ raise RuntimeError(error_msg)
241
+
242
+ actual_remote_tools: list[mcp_types.Tool] = remote_tools_result.tools
243
+ logger.info(f"Discovered {len(actual_remote_tools)} tools from server '{validated_config.server_id}' for preview.")
244
+
245
+ for remote_tool in actual_remote_tools:
246
+ try:
247
+ tool_def = self._create_tool_definition_from_remote(remote_tool, validated_config, handler, schema_mapper)
248
+ tool_definitions.append(tool_def)
249
+ except Exception as e_tool:
250
+ logger.error(f"Failed to map remote tool '{remote_tool.name}' from server '{validated_config.server_id}' during preview: {e_tool}", exc_info=True)
251
+ # For a preview, we continue to show other valid tools.
252
+
253
+ except Exception as e_server:
254
+ logger.error(f"Failed to discover tools for preview from MCP server '{validated_config.server_id}': {e_server}", exc_info=True)
255
+ # Re-raise server-level exceptions to the caller as per AC5.
256
+ raise
257
+
258
+ logger.info(f"MCP tool preview completed. Found {len(tool_definitions)} tools.")
259
+ return tool_definitions
260
+
261
+ def get_registered_tools_for_server(self, server_id: str) -> List[ToolDefinition]:
262
+ """
263
+ Returns a list of ToolDefinition objects that were successfully registered
264
+ from a specific MCP server during the most recent discovery process.
265
+
266
+ Args:
267
+ server_id: The unique ID of the MCP server to query.
268
+
269
+ Returns:
270
+ A list of ToolDefinition objects. Returns an empty list if the server ID
271
+ is not found or registered no tools.
272
+ """
273
+ return self._registered_tools_by_server.get(server_id, [])
274
+
275
+ def get_all_registered_mcp_tools(self) -> List[ToolDefinition]:
276
+ """
277
+ Returns a flat list of all ToolDefinitions registered from any MCP server.
278
+ """
279
+ all_tools = []
280
+ for server_tools in self._registered_tools_by_server.values():
281
+ all_tools.extend(server_tools)
282
+ return all_tools
283
+
284
+ def is_server_registered(self, server_id: str) -> bool:
285
+ """
286
+ Checks if any tools from a specific MCP server are currently registered.
287
+
288
+ Args:
289
+ server_id: The unique ID of the MCP server.
290
+
291
+ Returns:
292
+ True if the server has tools registered, False otherwise.
293
+ """
294
+ is_registered = server_id in self._registered_tools_by_server
295
+ logger.debug(f"Checking if server '{server_id}' is registered: {is_registered}")
296
+ return is_registered
297
+
298
+ def unregister_tools_from_server(self, server_id: str) -> bool:
299
+ """
300
+ Unregisters all tools associated with a given MCP server ID from the
301
+ main ToolRegistry and removes the server from internal tracking.
302
+
303
+ Args:
304
+ server_id: The unique ID of the MCP server whose tools should be unregistered.
305
+
306
+ Returns:
307
+ True if the server was found and its tools were processed for unregistration,
308
+ False if the server was not found in the registrar's state.
309
+ """
310
+ if not self.is_server_registered(server_id):
311
+ logger.info(f"No tools found for server ID '{server_id}'. Nothing to unregister.")
312
+ return False
313
+
314
+ tools_to_unregister = self._registered_tools_by_server.get(server_id, [])
315
+ logger.info(f"Unregistering {len(tools_to_unregister)} tools from server ID: '{server_id}'...")
316
+
317
+ for tool_def in tools_to_unregister:
318
+ self._tool_registry.unregister_tool(tool_def.name)
319
+
320
+ # Remove the server from the tracking dictionary
321
+ del self._registered_tools_by_server[server_id]
322
+ logger.info(f"Successfully unregistered all tools and removed server '{server_id}' from registrar tracking.")
323
+ return True
@@ -44,7 +44,11 @@ class StdioMcpServerConfig(BaseMcpConfig):
44
44
  super().__post_init__()
45
45
  self.transport_type = McpTransportType.STDIO
46
46
 
47
- # Added: Validation for command
47
+ # BUG FIX: Normalize cwd. An empty string is invalid for subprocess creation
48
+ # and should be treated as None (use parent CWD).
49
+ if self.cwd == '':
50
+ self.cwd = None
51
+
48
52
  if self.command is None or not isinstance(self.command, str) or not self.command.strip():
49
53
  raise ValueError(f"StdioMcpServerConfig '{self.server_id}' 'command' must be a non-empty string.")
50
54
 
@@ -66,7 +70,6 @@ class SseMcpServerConfig(BaseMcpConfig):
66
70
  super().__post_init__()
67
71
  self.transport_type = McpTransportType.SSE
68
72
 
69
- # Added: Validation for url
70
73
  if self.url is None or not isinstance(self.url, str) or not self.url.strip():
71
74
  raise ValueError(f"SseMcpServerConfig '{self.server_id}' 'url' must be a non-empty string.")
72
75
 
@@ -86,7 +89,6 @@ class StreamableHttpMcpServerConfig(BaseMcpConfig):
86
89
  super().__post_init__()
87
90
  self.transport_type = McpTransportType.STREAMABLE_HTTP
88
91
 
89
- # Added: Validation for url
90
92
  if self.url is None or not isinstance(self.url, str) or not self.url.strip():
91
93
  raise ValueError(f"StreamableHttpMcpServerConfig '{self.server_id}' 'url' must be a non-empty string.")
92
94
 
@@ -6,6 +6,7 @@ from typing import Dict, Any, List as TypingList, Type, TYPE_CHECKING, Optional,
6
6
  from autobyteus.llm.providers import LLMProvider
7
7
  from autobyteus.tools.tool_config import ToolConfig
8
8
  from autobyteus.tools.parameter_schema import ParameterSchema
9
+ from autobyteus.tools.tool_category import ToolCategory
9
10
  from autobyteus.tools.usage.providers import (
10
11
  XmlSchemaProvider,
11
12
  JsonSchemaProvider,
@@ -27,6 +28,7 @@ class ToolDefinition:
27
28
  name: str,
28
29
  description: str,
29
30
  argument_schema: Optional['ParameterSchema'],
31
+ category: ToolCategory,
30
32
  config_schema: Optional['ParameterSchema'] = None,
31
33
  tool_class: Optional[Type['BaseTool']] = None,
32
34
  custom_factory: Optional[Callable[['ToolConfig'], 'BaseTool']] = None):
@@ -52,6 +54,8 @@ class ToolDefinition:
52
54
  raise TypeError(f"ToolDefinition '{name}' received an invalid 'argument_schema'. Expected ParameterSchema or None.")
53
55
  if config_schema is not None and not isinstance(config_schema, ParameterSchema):
54
56
  raise TypeError(f"ToolDefinition '{name}' received an invalid 'config_schema'. Expected ParameterSchema or None.")
57
+ if not isinstance(category, ToolCategory):
58
+ raise TypeError(f"ToolDefinition '{name}' requires a ToolCategory for 'category'. Got {type(category)}")
55
59
 
56
60
  self._name = name
57
61
  self._description = description
@@ -59,6 +63,7 @@ class ToolDefinition:
59
63
  self._config_schema: Optional['ParameterSchema'] = config_schema
60
64
  self._tool_class = tool_class
61
65
  self._custom_factory = custom_factory
66
+ self._category = category
62
67
 
63
68
  logger.debug(f"ToolDefinition created for tool '{self.name}'.")
64
69
 
@@ -75,6 +80,8 @@ class ToolDefinition:
75
80
  def argument_schema(self) -> Optional['ParameterSchema']: return self._argument_schema
76
81
  @property
77
82
  def config_schema(self) -> Optional['ParameterSchema']: return self._config_schema
83
+ @property
84
+ def category(self) -> ToolCategory: return self._category
78
85
 
79
86
  # --- Schema Generation API ---
80
87
  def get_usage_xml(self, provider: Optional[LLMProvider] = None) -> str:
@@ -129,4 +136,4 @@ class ToolDefinition:
129
136
 
130
137
  def __repr__(self) -> str:
131
138
  creator_repr = f"class='{self._tool_class.__name__}'" if self._tool_class else "factory=True"
132
- return (f"ToolDefinition(name='{self.name}', {creator_repr})")
139
+ return (f"ToolDefinition(name='{self.name}', category='{self.category.value}', {creator_repr})")
@@ -43,6 +43,24 @@ class ToolRegistry(metaclass=SingletonMeta):
43
43
  ToolRegistry._definitions[tool_name] = definition
44
44
  logger.info(f"Successfully registered tool definition: '{tool_name}'")
45
45
 
46
+ def unregister_tool(self, name: str) -> bool:
47
+ """
48
+ Unregisters a tool definition by its name.
49
+
50
+ Args:
51
+ name: The unique name of the tool definition to unregister.
52
+
53
+ Returns:
54
+ True if the tool was found and unregistered, False otherwise.
55
+ """
56
+ if name in self._definitions:
57
+ del self._definitions[name]
58
+ logger.info(f"Successfully unregistered tool definition: '{name}'")
59
+ return True
60
+ else:
61
+ logger.warning(f"Attempted to unregister tool '{name}', but it was not found in the registry.")
62
+ return False
63
+
46
64
  def get_tool_definition(self, name: str) -> Optional[ToolDefinition]:
47
65
  """
48
66
  Retrieves the definition for a specific tool name.
@@ -0,0 +1,11 @@
1
+ # file: autobyteus/autobyteus/tools/tool_category.py
2
+ from enum import Enum
3
+
4
+ class ToolCategory(str, Enum):
5
+ """Enumeration of tool categories to identify their origin."""
6
+ LOCAL = "local"
7
+ MCP = "mcp"
8
+ # BUILT_IN, USER_DEFINED etc. could be added later.
9
+
10
+ def __str__(self) -> str:
11
+ return self.value
@@ -5,6 +5,7 @@ from typing import Dict, Any
5
5
 
6
6
  from autobyteus.tools.registry import default_tool_registry, ToolDefinition
7
7
  from autobyteus.tools.parameter_schema import ParameterSchema
8
+ from autobyteus.tools.tool_category import ToolCategory
8
9
 
9
10
  logger = logging.getLogger(__name__)
10
11
 
@@ -60,7 +61,8 @@ class ToolMeta(ABCMeta):
60
61
  tool_class=cls,
61
62
  custom_factory=None,
62
63
  argument_schema=argument_schema,
63
- config_schema=instantiation_config_schema
64
+ config_schema=instantiation_config_schema,
65
+ category=ToolCategory.LOCAL
64
66
  )
65
67
  default_tool_registry.register_tool(definition)
66
68
 
@@ -0,0 +1,20 @@
1
+ # file: autobyteus/autobyteus/tools/tool_state.py
2
+ """
3
+ Defines the ToolState class, an explicit container for a tool's internal state,
4
+ providing a dictionary-like interface for backward compatibility.
5
+ """
6
+ from collections import UserDict
7
+
8
+ class ToolState(UserDict):
9
+ """
10
+ A specialized container for a tool's state.
11
+
12
+ This class inherits from collections.UserDict to provide a dictionary-like
13
+ interface, ensuring that existing tools can interact with the state attribute
14
+ (tool.tool_state) just as they would with a regular dictionary.
15
+
16
+ The primary purpose of this class is to make the concept of a tool's
17
+ state explicit in the framework's type system, improving code clarity
18
+ and developer experience.
19
+ """
20
+ pass
@@ -0,0 +1,99 @@
1
+ import re
2
+ import logging
3
+ from typing import List
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ def _find_json_blobs(text: str) -> List[str]:
8
+ """
9
+ Robustly finds and extracts all top-level JSON objects or arrays from a string,
10
+ maintaining their original order of appearance. It handles JSON within
11
+ markdown-style code blocks (```json ... ```) and inline JSON.
12
+
13
+ Args:
14
+ text: The string to search for JSON in.
15
+
16
+ Returns:
17
+ A list of strings, where each string is a valid-looking JSON blob,
18
+ ordered as they appeared in the input text.
19
+ """
20
+ found_blobs = []
21
+
22
+ # 1. Find all markdown blobs first and store them with their start positions.
23
+ markdown_matches = list(re.finditer(r"```(?:json)?\s*([\s\S]+?)\s*```", text))
24
+ for match in markdown_matches:
25
+ content = match.group(1).strip()
26
+ found_blobs.append((match.start(), content))
27
+
28
+ # 2. Create a "masked" version of the text by replacing markdown blocks with spaces.
29
+ # This prevents the inline scanner from finding JSON inside them, while preserving indices.
30
+ masked_text_list = list(text)
31
+ for match in markdown_matches:
32
+ for i in range(match.start(), match.end()):
33
+ masked_text_list[i] = ' '
34
+ masked_text = "".join(masked_text_list)
35
+
36
+ # 3. Scan the masked text for any other JSON using a single pass brace-counter.
37
+ idx = 0
38
+ while idx < len(masked_text):
39
+ start_idx = -1
40
+
41
+ # Find the next opening brace or bracket
42
+ next_brace = masked_text.find('{', idx)
43
+ next_bracket = masked_text.find('[', idx)
44
+
45
+ if next_brace == -1 and next_bracket == -1:
46
+ break # No more JSON starts
47
+
48
+ if next_brace != -1 and (next_bracket == -1 or next_brace < next_bracket):
49
+ start_idx = next_brace
50
+ start_char, end_char = '{', '}'
51
+ else:
52
+ start_idx = next_bracket
53
+ start_char, end_char = '[', ']'
54
+
55
+ brace_count = 1
56
+ in_string = False
57
+ is_escaped = False
58
+ end_idx = -1
59
+
60
+ for i in range(start_idx + 1, len(masked_text)):
61
+ char = masked_text[i]
62
+
63
+ if in_string:
64
+ if is_escaped:
65
+ is_escaped = False
66
+ elif char == '\\':
67
+ is_escaped = True
68
+ elif char == '"':
69
+ in_string = False
70
+ else:
71
+ if char == '"':
72
+ in_string = True
73
+ is_escaped = False
74
+ elif char == '{' or char == '[':
75
+ brace_count += 1
76
+ elif char == '}' or char == ']':
77
+ brace_count -= 1
78
+
79
+ if brace_count == 0:
80
+ if (start_char == '{' and char == '}') or \
81
+ (start_char == '[' and char == ']'):
82
+ end_idx = i
83
+ break
84
+
85
+ if end_idx != -1:
86
+ # We found a blob in the masked text, so its indices are correct
87
+ # for the original text. Extract the blob from the original text.
88
+ blob = text[start_idx : end_idx + 1]
89
+ found_blobs.append((start_idx, blob))
90
+ idx = end_idx + 1
91
+ else:
92
+ # No matching end brace found, move on from the start character.
93
+ idx = start_idx + 1
94
+
95
+ # 4. Sort all found blobs by their start position to ensure correct order
96
+ found_blobs.sort(key=lambda item: item[0])
97
+
98
+ # 5. Return only the content of the blobs
99
+ return [content for _, content in found_blobs]