agent-runtime-sdk 0.1.0__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 (51) hide show
  1. agent_runtime/__init__.py +84 -0
  2. agent_runtime/builder.py +317 -0
  3. agent_runtime/config/__init__.py +29 -0
  4. agent_runtime/config/definitions.py +144 -0
  5. agent_runtime/config/policies.py +63 -0
  6. agent_runtime/config/storage.py +117 -0
  7. agent_runtime/context.py +10 -0
  8. agent_runtime/definitions.py +33 -0
  9. agent_runtime/discovery.py +16 -0
  10. agent_runtime/exceptions.py +74 -0
  11. agent_runtime/mcp/__init__.py +28 -0
  12. agent_runtime/mcp/discovery.py +146 -0
  13. agent_runtime/mcp/metadata.py +68 -0
  14. agent_runtime/mcp/utils.py +52 -0
  15. agent_runtime/model_registry.py +40 -0
  16. agent_runtime/plugins/__init__.py +4 -0
  17. agent_runtime/plugins/base.py +90 -0
  18. agent_runtime/plugins/default.py +19 -0
  19. agent_runtime/plugins/instructions.py +38 -0
  20. agent_runtime/plugins/loader.py +59 -0
  21. agent_runtime/policies.py +15 -0
  22. agent_runtime/runtime.py +110 -0
  23. agent_runtime/runtime_engine/__init__.py +22 -0
  24. agent_runtime/runtime_engine/a2a_bridge.py +190 -0
  25. agent_runtime/runtime_engine/a2a_task_io.py +165 -0
  26. agent_runtime/runtime_engine/agent_build.py +315 -0
  27. agent_runtime/runtime_engine/context.py +469 -0
  28. agent_runtime/runtime_engine/loading.py +170 -0
  29. agent_runtime/runtime_engine/observability.py +154 -0
  30. agent_runtime/runtime_engine/policy_registry.py +98 -0
  31. agent_runtime/runtime_engine/protocol_tools.py +94 -0
  32. agent_runtime/runtime_engine/task_flow.py +897 -0
  33. agent_runtime/runtime_engine/tool_flow.py +332 -0
  34. agent_runtime/sdk_agent.py +548 -0
  35. agent_runtime/server/__init__.py +15 -0
  36. agent_runtime/server/app_factory.py +37 -0
  37. agent_runtime/server/bootstrap.py +48 -0
  38. agent_runtime/server/endpoint_utils.py +37 -0
  39. agent_runtime/server/management.py +107 -0
  40. agent_runtime/smol/__init__.py +4 -0
  41. agent_runtime/smol/agents.py +431 -0
  42. agent_runtime/smol/llm_models.py +212 -0
  43. agent_runtime/smol/memory.py +111 -0
  44. agent_runtime/smol/models.py +69 -0
  45. agent_runtime/standalone.py +57 -0
  46. agent_runtime/storage.py +5 -0
  47. agent_runtime/tools.py +5 -0
  48. agent_runtime_sdk-0.1.0.dist-info/METADATA +125 -0
  49. agent_runtime_sdk-0.1.0.dist-info/RECORD +51 -0
  50. agent_runtime_sdk-0.1.0.dist-info/WHEEL +5 -0
  51. agent_runtime_sdk-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ """definition 的持久化与环境变量展开。"""
4
+
5
+ import logging
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import yaml
12
+
13
+ from .definitions import AgentDefinition
14
+ from ..exceptions import DefinitionLoadError
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ _ENV_PATTERN = re.compile(r"\$\{(?P<name>[A-Z0-9_]+)(?::-(?P<default>[^}]*))?\}")
20
+
21
+
22
+ def _resolve_env_placeholders(value: Any) -> Any:
23
+ """递归展开 YAML 中的 ${ENV} / ${ENV:-default} 占位符。"""
24
+ if isinstance(value, dict):
25
+ return {k: _resolve_env_placeholders(v) for k, v in value.items()}
26
+ if isinstance(value, list):
27
+ return [_resolve_env_placeholders(v) for v in value]
28
+ if not isinstance(value, str):
29
+ return value
30
+
31
+ def _replace(match: re.Match[str]) -> str:
32
+ name = match.group("name")
33
+ default = match.group("default")
34
+ return os.getenv(name, default or "")
35
+
36
+ return _ENV_PATTERN.sub(_replace, value)
37
+
38
+
39
+ def _default_base_root(definition_path: Path) -> Path:
40
+ """约定:如果 definition 放在 agent_app/ 下,默认相对项目根目录解析。"""
41
+ if definition_path.parent.name == "agent_app":
42
+ return definition_path.parent.parent
43
+ return definition_path.parent
44
+
45
+
46
+ class AgentDefinitionStore:
47
+ """面向单机文件系统的 definition 存储。
48
+
49
+ 当前实现很轻,只负责 YAML 读写;真正的校验交给 AgentDefinition。
50
+ """
51
+
52
+ def __init__(self, root: str | Path):
53
+ self.root = Path(root)
54
+ self.root.mkdir(parents=True, exist_ok=True)
55
+
56
+ def definition_path(self, agent_id: str) -> Path:
57
+ return self.root / f"{agent_id}.yaml"
58
+
59
+ def list_paths(self) -> list[Path]:
60
+ return sorted(self.root.glob("*.yaml"))
61
+
62
+ def list_definitions(self) -> list[AgentDefinition]:
63
+ return [self.load_path(path) for path in self.list_paths()]
64
+
65
+ def load(self, agent_id: str) -> AgentDefinition:
66
+ return self.load_path(self.definition_path(agent_id))
67
+
68
+ def load_path(self, path: str | Path) -> AgentDefinition:
69
+ # 读取后先做环境变量替换,再交给 pydantic 做结构校验。
70
+ resolved_path = Path(path).resolve()
71
+ logger.debug("Loading agent definition path=%s", resolved_path)
72
+ try:
73
+ with resolved_path.open("r", encoding="utf-8") as handle:
74
+ raw = yaml.safe_load(handle) or {}
75
+ except FileNotFoundError as exc:
76
+ logger.exception("Definition file not found path=%s", resolved_path)
77
+ raise DefinitionLoadError(
78
+ f"definition file not found: '{resolved_path}'",
79
+ user_message="Agent 配置文件不存在,请检查 agent_app/agent.yaml 路径",
80
+ ) from exc
81
+ except yaml.YAMLError as exc:
82
+ logger.exception("Invalid YAML in definition path=%s", resolved_path)
83
+ raise DefinitionLoadError(
84
+ f"invalid YAML in definition file '{resolved_path}': {exc}",
85
+ user_message="Agent 配置文件格式错误,请检查 YAML 语法",
86
+ ) from exc
87
+
88
+ try:
89
+ resolved_raw = _resolve_env_placeholders(raw)
90
+ runtime_raw = resolved_raw.get("runtime")
91
+ if not isinstance(runtime_raw, dict):
92
+ runtime_raw = {}
93
+ raw_base_path = runtime_raw.get("base_path", ".")
94
+ base_path = Path(raw_base_path)
95
+ if not base_path.is_absolute():
96
+ base_path = (_default_base_root(resolved_path) / base_path).resolve()
97
+ resolved_raw["runtime"] = {**runtime_raw, "base_path": base_path.as_posix()}
98
+ definition = AgentDefinition.model_validate(resolved_raw)
99
+ logger.debug("Loaded agent definition agent_id=%s path=%s", definition.agent.agent_id, resolved_path)
100
+ return definition
101
+ except Exception as exc:
102
+ logger.exception("Failed to validate definition path=%s", resolved_path)
103
+ raise DefinitionLoadError(
104
+ f"failed to validate definition file '{resolved_path}': {exc}",
105
+ user_message="Agent 配置校验失败,请检查 agent_app/agent.yaml 必填项和字段格式",
106
+ ) from exc
107
+
108
+ def save(self, definition: AgentDefinition) -> Path:
109
+ path = self.definition_path(definition.agent.agent_id)
110
+ with path.open("w", encoding="utf-8") as handle:
111
+ yaml.safe_dump(definition.model_dump(mode="python"), handle, allow_unicode=True, sort_keys=False)
112
+ return path
113
+
114
+ def delete(self, agent_id: str) -> None:
115
+ path = self.definition_path(agent_id)
116
+ if path.exists():
117
+ path.unlink()
@@ -0,0 +1,10 @@
1
+ """对外暴露的运行时任务上下文入口。"""
2
+
3
+ from .runtime_engine.context import (
4
+ TaskContext,
5
+ TaskPool,
6
+ current_task_id,
7
+ current_task_pool,
8
+ )
9
+
10
+ __all__ = ["TaskContext", "TaskPool", "current_task_id", "current_task_pool"]
@@ -0,0 +1,33 @@
1
+ """Public facade for configuration models."""
2
+
3
+ from .config.definitions import (
4
+ DEFAULT_PASS_THROUGH_HEADERS,
5
+ DEFAULT_PLUGIN_CLASS_NAME,
6
+ DEFAULT_PLUGIN_MODULE_PATH,
7
+ A2ASettings,
8
+ AgentConfig,
9
+ AgentDefinition,
10
+ InputFieldDefinition,
11
+ MCPSettings,
12
+ ModelSettings,
13
+ PluginSettings,
14
+ RuntimeConfig,
15
+ SkillDefinition,
16
+ ToolPolicy,
17
+ )
18
+
19
+ __all__ = [
20
+ "DEFAULT_PASS_THROUGH_HEADERS",
21
+ "DEFAULT_PLUGIN_CLASS_NAME",
22
+ "DEFAULT_PLUGIN_MODULE_PATH",
23
+ "A2ASettings",
24
+ "AgentConfig",
25
+ "AgentDefinition",
26
+ "InputFieldDefinition",
27
+ "MCPSettings",
28
+ "ModelSettings",
29
+ "PluginSettings",
30
+ "RuntimeConfig",
31
+ "SkillDefinition",
32
+ "ToolPolicy",
33
+ ]
@@ -0,0 +1,16 @@
1
+ """对外暴露的 MCP discovery 辅助入口。"""
2
+
3
+ from .mcp.discovery import (
4
+ default_discover_tools,
5
+ load_mcp_client_and_tools,
6
+ load_mcp_tools,
7
+ )
8
+ from .mcp.metadata import DiscoveredTool, discovered_tool_from_tool
9
+
10
+ __all__ = [
11
+ "DiscoveredTool",
12
+ "default_discover_tools",
13
+ "discovered_tool_from_tool",
14
+ "load_mcp_client_and_tools",
15
+ "load_mcp_tools",
16
+ ]
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ """框架统一异常类型。
4
+
5
+ 原则:
6
+ - 日志里保留内部真实异常
7
+ - 对用户返回稳定、可预期的文案
8
+ - 启动期和请求期都尽量用语义明确的异常类型,而不是到处混用 RuntimeError
9
+ """
10
+
11
+
12
+ class AgentRuntimeError(Exception):
13
+ """框架异常基类,附带稳定的用户可见文案。"""
14
+
15
+ default_user_message = "任务执行失败,请稍后重试"
16
+ default_code = "agent_runtime_error"
17
+
18
+ def __init__(
19
+ self,
20
+ message: str,
21
+ *,
22
+ user_message: str | None = None,
23
+ code: str | None = None,
24
+ ) -> None:
25
+ super().__init__(message)
26
+ self.user_message = user_message or self.default_user_message
27
+ self.code = code or self.default_code
28
+
29
+
30
+ class DefinitionLoadError(AgentRuntimeError):
31
+ default_user_message = "Agent 配置加载失败,请检查 agent_app/agent.yaml"
32
+ default_code = "definition_load_error"
33
+
34
+
35
+ class PluginLoadError(AgentRuntimeError):
36
+ default_user_message = "Agent 插件加载失败,请检查 plugin 配置"
37
+ default_code = "plugin_load_error"
38
+
39
+
40
+ class MCPToolLoadError(AgentRuntimeError):
41
+ default_user_message = "MCP 工具加载失败,请检查 MCP 服务和网络连通性"
42
+ default_code = "mcp_tool_load_error"
43
+
44
+
45
+ class AgentBuildError(AgentRuntimeError):
46
+ default_user_message = "Agent 构建失败,请检查模型、插件和工具配置"
47
+ default_code = "agent_build_error"
48
+
49
+
50
+ class TaskWaitTimeoutError(AgentRuntimeError):
51
+ default_user_message = "任务等待超时,已自动结束"
52
+ default_code = "task_wait_timeout"
53
+
54
+
55
+ class TaskExecutionTimeoutError(AgentRuntimeError):
56
+ default_user_message = "任务执行超时,已自动取消"
57
+ default_code = "task_execution_timeout"
58
+
59
+
60
+ class UserCancelledError(AgentRuntimeError):
61
+ default_user_message = "用户已取消操作"
62
+ default_code = "user_cancelled"
63
+
64
+
65
+ class TaskCancelledError(AgentRuntimeError):
66
+ default_user_message = "任务已取消"
67
+ default_code = "task_cancelled"
68
+
69
+
70
+ def user_message_for_error(error: Exception) -> str:
71
+ """把内部异常映射成稳定的用户可见文案。"""
72
+ if isinstance(error, AgentRuntimeError):
73
+ return error.user_message
74
+ return AgentRuntimeError.default_user_message
@@ -0,0 +1,28 @@
1
+ """MCP 相关内部实现。
2
+
3
+ 模块划分:
4
+
5
+ - `metadata.py`: 工具描述对象与规整逻辑
6
+ - `utils.py`: MCP 共用规则
7
+ - `discovery.py`: discover/load 主流程
8
+ """
9
+
10
+ from .discovery import default_discover_tools, load_mcp_client_and_tools, load_mcp_tools
11
+ from .metadata import (
12
+ DiscoveredTool,
13
+ discovered_tool_from_tool,
14
+ normalize_discovered_tools,
15
+ )
16
+ from .utils import index_tool_sources, merge_mcp_headers, register_tool_source
17
+
18
+ __all__ = [
19
+ "DiscoveredTool",
20
+ "default_discover_tools",
21
+ "discovered_tool_from_tool",
22
+ "index_tool_sources",
23
+ "load_mcp_client_and_tools",
24
+ "load_mcp_tools",
25
+ "merge_mcp_headers",
26
+ "normalize_discovered_tools",
27
+ "register_tool_source",
28
+ ]
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ """MCP 工具发现与加载。
4
+
5
+ 这个文件只保留 discover/load 主流程,不再承载 descriptor 模型和共用工具函数。
6
+
7
+ 这里区分两类动作:
8
+
9
+ 1. discover:只拿工具元数据,给管理面、prompt、agent card 使用
10
+ 2. load:真正创建 MCPClient,并返回可执行的 tool 对象
11
+ """
12
+
13
+ import logging
14
+ import time
15
+ from typing import Any
16
+
17
+ from ..config.definitions import MCPSettings
18
+ from ..exceptions import MCPToolLoadError
19
+ from .metadata import (
20
+ DiscoveredTool,
21
+ discovered_tool_from_tool,
22
+ normalize_discovered_tools,
23
+ )
24
+ from .utils import merge_mcp_headers
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def default_discover_tools(
30
+ mcp: MCPSettings, headers: dict[str, str] | None = None
31
+ ) -> list[DiscoveredTool]:
32
+ """默认的工具发现流程。
33
+
34
+ 这里只做“看工具”(list_tools) 拿工具 description ,因此 client 用完即关,不把连接留到运行时。
35
+ """
36
+ client, tools = load_mcp_client_and_tools(mcp, headers=headers)
37
+ try:
38
+ logger.debug(
39
+ "Discovered MCP tools mcp=%s url=%s tool_count=%s",
40
+ mcp.name,
41
+ mcp.url,
42
+ len(tools),
43
+ )
44
+ return normalize_discovered_tools(
45
+ (discovered_tool_from_tool(tool) for tool in tools), mcp.name
46
+ )
47
+ finally:
48
+ try:
49
+ client.disconnect()
50
+ except Exception:
51
+ logger.warning(
52
+ "Failed to disconnect discovery MCP client mcp=%s url=%s",
53
+ mcp.name,
54
+ mcp.url,
55
+ exc_info=True,
56
+ )
57
+
58
+
59
+ def load_mcp_tools(
60
+ mcp: MCPSettings, headers: dict[str, str] | None = None
61
+ ) -> list[Any]:
62
+ """兼容旧用法:仅返回 tools,但把 client 挂到 tool 上,避免生命周期丢失。"""
63
+ client, tools = load_mcp_client_and_tools(mcp, headers=headers)
64
+ for tool in tools:
65
+ try:
66
+ tool._runtime_mcp_client = client
67
+ except Exception:
68
+ pass
69
+ return tools
70
+
71
+
72
+ def load_mcp_client_and_tools(
73
+ mcp: MCPSettings, headers: dict[str, str] | None = None
74
+ ) -> tuple[Any, list[Any]]:
75
+ """创建一个 MCPClient,并把这个 MCP 暴露的工具全部取出来。
76
+
77
+ 这里是运行时真正执行工具前的入口,所以失败时要负责把刚创建的 client 清干净。
78
+ """
79
+ from smolagents import MCPClient
80
+
81
+ merged_headers = merge_mcp_headers(mcp, headers)
82
+ total_attempts = mcp.retry_count + 1
83
+ last_exc: Exception | None = None
84
+
85
+ for attempt in range(1, total_attempts + 1):
86
+ client = None
87
+ try:
88
+ client = MCPClient(
89
+ {"url": mcp.url, "transport": mcp.transport, "headers": merged_headers},
90
+ structured_output=False,
91
+ )
92
+ tools = client.get_tools()
93
+ logger.debug(
94
+ "Loaded MCP client and tools mcp=%s url=%s tool_count=%s attempt=%s/%s",
95
+ mcp.name,
96
+ mcp.url,
97
+ len(tools),
98
+ attempt,
99
+ total_attempts,
100
+ )
101
+ return client, tools
102
+ except Exception as exc:
103
+ last_exc = exc
104
+ logger.exception(
105
+ "Failed to create/load MCP client mcp=%s url=%s attempt=%s/%s",
106
+ mcp.name,
107
+ mcp.url,
108
+ attempt,
109
+ total_attempts,
110
+ )
111
+ if client is not None:
112
+ try:
113
+ client.disconnect()
114
+ except Exception:
115
+ logger.warning(
116
+ "Failed to disconnect MCP client after tool load failure mcp=%s url=%s attempt=%s/%s",
117
+ mcp.name,
118
+ mcp.url,
119
+ attempt,
120
+ total_attempts,
121
+ exc_info=True,
122
+ )
123
+
124
+ if attempt < total_attempts:
125
+ logger.warning(
126
+ "Retrying MCP client load mcp=%s url=%s in %ss after failure (%s/%s)",
127
+ mcp.name,
128
+ mcp.url,
129
+ mcp.retry_delay_seconds,
130
+ attempt,
131
+ total_attempts,
132
+ )
133
+ if mcp.retry_delay_seconds > 0:
134
+ time.sleep(mcp.retry_delay_seconds)
135
+
136
+ retry_note = f"after {total_attempts} attempts ({mcp.retry_count} retries)"
137
+ raise MCPToolLoadError(
138
+ (
139
+ f"failed to create/load MCP client for mcp '{mcp.name}' at '{mcp.url}' "
140
+ f"{retry_note}: {last_exc}"
141
+ ),
142
+ user_message=(
143
+ f"MCP 服务 '{mcp.name}' 连接失败,已重试 {mcp.retry_count} 次后仍不可用,"
144
+ "请检查服务状态和网络连通性"
145
+ ),
146
+ ) from last_exc
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ """MCP 工具元数据模型。
4
+
5
+ 这个文件只处理“工具长什么样”:
6
+
7
+ - `DiscoveredTool`: discovery 阶段的只读工具摘要
8
+ - `discovered_tool_from_tool`: 把具体 tool 对象压平为摘要
9
+ - `normalize_discovered_tools`: 规整 discoverer 返回值,并补齐 `source_mcp`
10
+
11
+ 注意:
12
+
13
+ - 这里的对象不是原生 MCP 可执行 tool
14
+ - 它只服务于管理面、prompt 展示、工具列表和策略定位
15
+ - 真正执行工具时,runtime 仍然使用原生 tool 对象
16
+ """
17
+
18
+ from typing import Any, Iterable
19
+
20
+ from pydantic import BaseModel, Field
21
+
22
+
23
+ class DiscoveredTool(BaseModel):
24
+ """discovery 阶段的只读工具摘要。
25
+
26
+ 之所以不直接缓存原生 MCP tool,是因为 discovery 阶段只需要稳定、可序列化、
27
+ 可长期持有的元数据;真正执行工具时,会重新拿原生 tool 实例。
28
+ """
29
+
30
+ name: str
31
+ source_mcp: str | None = None
32
+ description: str = ""
33
+ inputs: dict[str, Any] = Field(default_factory=dict)
34
+ output_type: str | None = None
35
+
36
+
37
+ def discovered_tool_from_tool(
38
+ tool: Any, source_mcp: str | None = None
39
+ ) -> DiscoveredTool:
40
+ """把具体 tool 对象压平成稳定的 discovered tool 摘要。"""
41
+
42
+ return DiscoveredTool(
43
+ name=getattr(tool, "name", ""),
44
+ source_mcp=source_mcp,
45
+ description=getattr(tool, "description", "") or "",
46
+ inputs=getattr(tool, "inputs", {}) or {},
47
+ output_type=getattr(tool, "output_type", None),
48
+ )
49
+
50
+
51
+ def normalize_discovered_tools(
52
+ items: Iterable[Any], source_mcp: str
53
+ ) -> list[DiscoveredTool]:
54
+ """把 discoverer 返回值统一规整成带 `source_mcp` 的 discovered tools。"""
55
+
56
+ discovered_tools: list[DiscoveredTool] = []
57
+ for item in items:
58
+ discovered_tool = (
59
+ item
60
+ if isinstance(item, DiscoveredTool)
61
+ else DiscoveredTool.model_validate(item)
62
+ )
63
+ discovered_tools.append(
64
+ discovered_tool.model_copy(
65
+ update={"source_mcp": discovered_tool.source_mcp or source_mcp}
66
+ )
67
+ )
68
+ return discovered_tools
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ """MCP 共用工具函数。
4
+
5
+ 这个文件只放多处复用的 MCP 规则:
6
+
7
+ - header 合并
8
+ - tool source 建索引
9
+ - 重名工具冲突检测
10
+ """
11
+
12
+ from collections.abc import Iterable
13
+
14
+ from ..config.definitions import MCPSettings
15
+ from .metadata import DiscoveredTool
16
+
17
+
18
+ def merge_mcp_headers(
19
+ mcp: MCPSettings, headers: dict[str, str] | None = None
20
+ ) -> dict[str, str]:
21
+ """合并 MCP 静态头和调用期透传头。"""
22
+
23
+ merged_headers = dict(mcp.static_headers)
24
+ if headers:
25
+ merged_headers.update({key: value for key, value in headers.items() if value})
26
+ return merged_headers
27
+
28
+
29
+ def register_tool_source(
30
+ tool_sources: dict[str, str], tool_name: str, source_mcp: str
31
+ ) -> None:
32
+ """记录工具来源,并在同名冲突时抛出一致错误。"""
33
+
34
+ existing_source = tool_sources.get(tool_name)
35
+ if existing_source is not None:
36
+ raise ValueError(
37
+ f"duplicate MCP tool name '{tool_name}' from '{existing_source}' and '{source_mcp}'"
38
+ )
39
+ tool_sources[tool_name] = source_mcp
40
+
41
+
42
+ def index_tool_sources(discovered_tools: Iterable[DiscoveredTool]) -> dict[str, str]:
43
+ """为已发现工具建立 `name -> source_mcp` 索引。"""
44
+
45
+ tool_sources: dict[str, str] = {}
46
+ for discovered_tool in discovered_tools:
47
+ register_tool_source(
48
+ tool_sources,
49
+ discovered_tool.name,
50
+ discovered_tool.source_mcp or "",
51
+ )
52
+ return tool_sources
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class ModelRegistry:
7
+ """Pluggable LLM model factory.
8
+
9
+ Register model classes by provider name and create instances via
10
+ ``ModelRegistry.create(provider, **kwargs)``.
11
+ """
12
+
13
+ _providers: dict[str, type] = {}
14
+
15
+ @classmethod
16
+ def register(cls, name: str, model_cls: type) -> None:
17
+ cls._providers[name] = model_cls
18
+
19
+ @classmethod
20
+ def create(cls, provider: str, **kwargs: Any):
21
+ model_cls = cls._providers.get(provider)
22
+ if model_cls is None:
23
+ raise ValueError(
24
+ f"Unknown model provider: {provider}. "
25
+ f"Registered: {list(cls._providers.keys())}"
26
+ )
27
+ return model_cls(**kwargs)
28
+
29
+ @classmethod
30
+ def list_providers(cls) -> list[str]:
31
+ return list(cls._providers.keys())
32
+
33
+
34
+ def _register_defaults() -> None:
35
+ from .smol import DeepSeekModel
36
+
37
+ ModelRegistry.register("openai_compatible", DeepSeekModel)
38
+
39
+
40
+ _register_defaults()
@@ -0,0 +1,4 @@
1
+ from .base import BaseAgentPlugin, BeforeToolDecision, ToolExecutionContext
2
+ from .loader import load_plugin
3
+
4
+ __all__ = ["BaseAgentPlugin", "BeforeToolDecision", "ToolExecutionContext", "load_plugin"]
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ """插件扩展点定义。
4
+
5
+ 框架把“工具如何调用、提示词如何生成、结果如何格式化”这些可变逻辑
6
+ 都收敛到 plugin 接口里,避免把业务定制写死在 runtime 中。
7
+ """
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Protocol
11
+
12
+ from ..config.definitions import AgentDefinition, InputFieldDefinition
13
+
14
+
15
+ @dataclass
16
+ class BeforeToolDecision:
17
+ """插件在工具执行前给出的治理决策。"""
18
+
19
+ requires_confirmation: bool | None = None
20
+ input_fields: list[InputFieldDefinition] = field(default_factory=list)
21
+ prompt: str | None = None
22
+ allow_arg_override: bool | None = None
23
+
24
+
25
+ @dataclass
26
+ class ToolExecutionContext:
27
+ """传给 plugin 的统一上下文。
28
+
29
+ 这里尽量把 plugin 关心的信息收全:当前 agent 定义、工具名、参数、
30
+ 来源 MCP、原始 tool_call、执行结果等。
31
+ """
32
+
33
+ agent_definition: AgentDefinition
34
+ tool_name: str
35
+ args: dict[str, Any]
36
+ mcp_name: str | None = None
37
+ task_info: Any = None
38
+ raw_tool_call: Any = None
39
+ observation: Any = None
40
+ result: Any = None
41
+
42
+
43
+ class AgentPlugin(Protocol):
44
+ """插件协议:描述一个 plugin 至少要提供哪些扩展点。"""
45
+
46
+ def build_instructions(self, definition: AgentDefinition, tools: list[Any]) -> str:
47
+ ...
48
+
49
+ def extra_tools(self, definition: AgentDefinition) -> list[Any]:
50
+ ...
51
+
52
+ def before_tool(self, context: ToolExecutionContext) -> BeforeToolDecision | None:
53
+ ...
54
+
55
+ def normalize_args(self, context: ToolExecutionContext) -> dict[str, Any] | None:
56
+ ...
57
+
58
+ def after_tool(self, context: ToolExecutionContext) -> None:
59
+ ...
60
+
61
+ def format_result(self, context: ToolExecutionContext) -> str | dict[str, Any] | None:
62
+ ...
63
+
64
+
65
+ class BaseAgentPlugin:
66
+ """插件基类。
67
+
68
+ 所有扩展点默认都是 no-op,这样业务插件只需要覆盖自己关心的那几项。
69
+ """
70
+
71
+ def __init__(self, config: dict[str, Any] | None = None):
72
+ self.config = config or {}
73
+
74
+ def build_instructions(self, definition: AgentDefinition, tools: list[Any]) -> str:
75
+ return ""
76
+
77
+ def extra_tools(self, definition: AgentDefinition) -> list[Any]:
78
+ return []
79
+
80
+ def before_tool(self, context: ToolExecutionContext) -> BeforeToolDecision | None:
81
+ return None
82
+
83
+ def normalize_args(self, context: ToolExecutionContext) -> dict[str, Any] | None:
84
+ return None
85
+
86
+ def after_tool(self, context: ToolExecutionContext) -> None:
87
+ return None
88
+
89
+ def format_result(self, context: ToolExecutionContext) -> str | dict[str, Any] | None:
90
+ return None