feat(extension): 接入 stdio MCP Plugin Host
This commit is contained in:
+44
-18
@@ -1,6 +1,7 @@
|
||||
"""Agent 工具注册与执行边界。"""
|
||||
|
||||
import inspect
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from time import perf_counter
|
||||
from typing import Any, Awaitable, Callable
|
||||
@@ -17,6 +18,7 @@ ToolExecutor = Callable[[BaseModel, "ToolExecutionContext"], Any | Awaitable[Any
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ToolExecutionContext:
|
||||
run_id: str
|
||||
tool_call_id: str | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -30,11 +32,21 @@ class ToolNotFoundError(LookupError):
|
||||
pass
|
||||
|
||||
|
||||
class ToolExecutionError(RuntimeError):
|
||||
"""Executor 可预期失败,保留领域错误码而不是折叠成通用异常。"""
|
||||
|
||||
def __init__(self, code: str, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""统一校验工具入参并隔离执行异常,避免单个工具击穿 Agent 主循环。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._tools: dict[str, RegisteredTool] = {}
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def register(
|
||||
self,
|
||||
@@ -42,33 +54,38 @@ class ToolRegistry:
|
||||
arguments_model: type[BaseModel],
|
||||
executor: ToolExecutor,
|
||||
) -> None:
|
||||
if definition.name in self._tools:
|
||||
raise ValueError(f"Tool already registered: {definition.name}")
|
||||
self._tools[definition.name] = RegisteredTool(
|
||||
definition=definition,
|
||||
arguments_model=arguments_model,
|
||||
executor=executor,
|
||||
)
|
||||
with self._lock:
|
||||
if definition.name in self._tools:
|
||||
raise ValueError(f"Tool already registered: {definition.name}")
|
||||
self._tools[definition.name] = RegisteredTool(
|
||||
definition=definition,
|
||||
arguments_model=arguments_model,
|
||||
executor=executor,
|
||||
)
|
||||
|
||||
def unregister(self, name: str) -> None:
|
||||
self._tools.pop(name, None)
|
||||
with self._lock:
|
||||
self._tools.pop(name, None)
|
||||
|
||||
def contains(self, name: str) -> bool:
|
||||
return name in self._tools
|
||||
with self._lock:
|
||||
return name in self._tools
|
||||
|
||||
def get(self, name: str) -> RegisteredTool:
|
||||
try:
|
||||
return self._tools[name]
|
||||
except KeyError as exc:
|
||||
raise ToolNotFoundError(name) from exc
|
||||
with self._lock:
|
||||
try:
|
||||
return self._tools[name]
|
||||
except KeyError as exc:
|
||||
raise ToolNotFoundError(name) from exc
|
||||
|
||||
def definitions(self, allowed: list[str] | None = None) -> list[ToolDefinition]:
|
||||
names = set(allowed) if allowed is not None else None
|
||||
return [
|
||||
item.definition.model_copy(deep=True)
|
||||
for name, item in self._tools.items()
|
||||
if names is None or name in names
|
||||
]
|
||||
with self._lock:
|
||||
return [
|
||||
item.definition.model_copy(deep=True)
|
||||
for name, item in self._tools.items()
|
||||
if names is None or name in names
|
||||
]
|
||||
|
||||
async def execute(self, call: ToolCall, context: ToolExecutionContext) -> ToolResult:
|
||||
started = perf_counter()
|
||||
@@ -108,6 +125,15 @@ class ToolRegistry:
|
||||
output=output,
|
||||
duration_ms=round((perf_counter() - started) * 1000),
|
||||
)
|
||||
except ToolExecutionError as exc:
|
||||
return ToolResult(
|
||||
tool_call_id=call.tool_call_id,
|
||||
name=call.name,
|
||||
success=False,
|
||||
error_code=exc.code,
|
||||
error_message=exc.message,
|
||||
duration_ms=round((perf_counter() - started) * 1000),
|
||||
)
|
||||
except Exception as exc: # 工具失败转换成结构化结果,由模型决定是否降级或重试。
|
||||
return ToolResult(
|
||||
tool_call_id=call.tool_call_id,
|
||||
|
||||
Reference in New Issue
Block a user