فهرست منبع

feat: tool groups

Talegorithm 1 ماه پیش
والد
کامیت
15579d2f9d

+ 128 - 12
README.md

@@ -94,10 +94,15 @@ async def check_inventory(product_id: str, warehouse: str = "default") -> ToolRe
 ### 限制工具范围
 
 ```python
-# 只启用指定工具(在内置工具基础上追加)
-config = RunConfig(tools=["check_inventory", "another_tool"])
+# 按分组选择(推荐)——只启用核心 + 内容搜索工具
+config = RunConfig(tool_groups=["core", "content"])
+
+# 精确指定(优先于 tool_groups)——只用这几个工具
+config = RunConfig(tools=["read_file", "bash_command", "check_inventory"])
 ```
 
+分组在 `@tool(groups=[...])` 装饰器中声明,详见[附录:工具分组](#附录工具分组)。
+
 ## 自定义 Skills
 
 Skills 是 Markdown 文件,提供领域知识,注入到 system prompt。
@@ -238,7 +243,8 @@ RunConfig(
     model="gpt-4o",          # 模型标识
     temperature=0.3,
     max_iterations=200,       # Agent loop 最大轮数
-    tools=None,               # None=全部已注册工具,List[str]=内置+指定工具
+    tools=None,               # 精确指定工具列表(优先于 tool_groups)
+    tool_groups=None,         # 工具分组白名单(None=全部),见附录
     system_prompt=None,       # None=从 skills 自动构建
     agent_type="default",     # 预设类型:default / explore / analyst
     trace_id=None,            # 续跑/回溯时传入已有 trace ID
@@ -248,15 +254,6 @@ RunConfig(
 )
 ```
 
-    system_prompt=None,       # None=从 skills 自动构建
-    agent_type="default",     # 预设类型:default / explore / analyst
-    trace_id=None,            # 续跑/回溯时传入已有 trace ID
-    after_sequence=None,      # 从哪条消息后续跑(message sequence)
-
-)
-
-````
-
 ## LLM Providers
 
 框架内置两个 provider:
@@ -505,3 +502,122 @@ examples/[your_example]/
 
 - **运行轨迹**:根目录下 `.trace/` 文件夹下的实际运行路径结果
 - **知识库**:KnowHub 服务中保存的知识条目(通过 API 访问)
+
+---
+
+## 附录:工具分组
+
+通过 `RunConfig(tool_groups=[...])` 控制 Agent 可用的工具范围。每个工具在 `@tool(groups=[...])` 中声明分组。
+
+### core — 基础能力
+
+| 工具 | 说明 |
+|------|------|
+| `read_file` | 读取单个文件(文本/图片/PDF) |
+| `read_images` | 批量读图 + 网格拼图 |
+| `edit_file` | 编辑文件 |
+| `write_file` | 写入文件 |
+| `glob_files` | 文件模式匹配 |
+| `grep_content` | 内容搜索(正则) |
+| `bash_command` | 执行 shell 命令 |
+| `skill` | 调用 skill |
+| `list_skills` | 列出可用 skill |
+| `agent` | 创建子 Agent |
+| `evaluate` | 评估执行结果 |
+| `goal` | 目标/计划管理 |
+| `get_current_context` | 获取当前执行上下文 |
+
+### browser — 浏览器自动化
+
+| 工具 | 说明 |
+|------|------|
+| `browser_navigate` | 导航到 URL |
+| `browser_search` | 搜索引擎搜索 |
+| `browser_back` | 返回上一页 |
+| `browser_interact` | 元素交互(click/type/send_keys/upload/dropdown) |
+| `browser_scroll` | 滚动页面 |
+| `browser_screenshot` | 截图(可带元素编号标注) |
+| `browser_elements` | 获取可交互元素列表 |
+| `browser_read` | 读取页面内容(html/find/long) |
+| `browser_extract` | LLM 驱动的结构化数据提取 |
+| `browser_tabs` | 标签页管理(switch/close) |
+| `browser_cookies` | Cookie/登录态(load/export/ensure_login) |
+| `browser_wait` | 等待(定时/等用户操作) |
+| `browser_js` | 执行 JavaScript |
+| `browser_download` | 下载文件 |
+
+### content — 内容搜索
+
+| 工具 | 说明 |
+|------|------|
+| `content_platforms` | 列出/查询平台及参数(支持模糊匹配) |
+| `content_search` | 跨 11 平台搜索(小红书/B站/知乎/GitHub/YouTube/X 等) |
+| `content_detail` | 查看内容详情(从搜索缓存按索引取) |
+| `content_suggest` | 搜索关键词补全建议 |
+| `extract_video_clip` | YouTube 视频片段截取 |
+| `import_content` | 批量导入文章到 CMS |
+
+### knowledge — 知识管理
+
+| 工具 | 说明 |
+|------|------|
+| `ask_knowledge` | 向知识库查询(通过 Librarian Agent) |
+| `upload_knowledge` | 上传调研结果到知识库 |
+
+### toolhub — 远程工具库
+
+| 工具 | 说明 |
+|------|------|
+| `toolhub_health` | 检查远程工具库状态 |
+| `toolhub_search` | 搜索远程 AI 工具 |
+| `toolhub_call` | 调用远程工具(图片参数支持本地路径) |
+
+### feishu — 飞书
+
+| 工具 | 说明 |
+|------|------|
+| `feishu_get_contact_list` | 获取联系人列表 |
+| `feishu_send_message_to_contact` | 给联系人发消息 |
+| `feishu_get_contact_replies` | 获取联系人回复 |
+| `feishu_get_chat_history` | 获取聊天历史 |
+
+### im — IM 通信
+
+| 工具 | 说明 |
+|------|------|
+| `im_setup` | 初始化 IM 连接 |
+| `im_send_message` | 发送消息 |
+| `im_receive_messages` | 接收消息 |
+| `im_check_notification` | 检查通知 |
+| `im_get_contacts` | 获取联系人 |
+| `im_get_chat_history` | 获取聊天历史 |
+| `im_open_window` | 打开 IM 窗口 |
+| `im_close_window` | 关闭 IM 窗口 |
+
+### resource — 资源查询
+
+| 工具 | 说明 |
+|------|------|
+| `resource_list_tools` | 列出资源工具 |
+| `resource_get_tool` | 获取工具详情 |
+
+### knowledge_internal — 知识库内部操作
+
+> 仅供 Librarian Agent 内部使用,普通 Agent 不可见。通过 `tools=[...]` 精确指定访问。
+
+| 工具 | 说明 |
+|------|------|
+| `knowledge_search` | 知识检索(语义 + 精排) |
+| `knowledge_save` | 保存知识条目 |
+| `knowledge_list` | 列出知识 |
+| `knowledge_update` | 更新知识反馈 |
+| `knowledge_batch_update` | 批量更新反馈 |
+| `knowledge_slim` | 知识瘦身 |
+| `resource_save` | 保存资源 |
+| `resource_get` | 获取资源 |
+| `tool_search` | 搜索工具记录 |
+| `tool_list` | 列出工具记录 |
+| `capability_search` | 搜索能力记录 |
+| `capability_list` | 列出能力记录 |
+| `requirement_search` | 搜索需求记录 |
+| `requirement_list` | 列出需求记录 |

+ 1 - 2
agent/core/__init__.py

@@ -7,7 +7,7 @@ Agent Core - 核心引擎模块
 3. Agent 预设(AgentPreset)
 """
 
-from agent.core.runner import AgentRunner, BUILTIN_TOOLS, CallResult, RunConfig
+from agent.core.runner import AgentRunner, CallResult, RunConfig
 from agent.core.presets import (
     AgentPreset,
     AGENT_PRESETS,
@@ -19,7 +19,6 @@ from agent.core.presets import (
 
 __all__ = [
     "AgentRunner",
-    "BUILTIN_TOOLS",
     "CallResult",
     "RunConfig",
     "AgentPreset",

+ 14 - 82
agent/core/runner.py

@@ -103,7 +103,8 @@ class RunConfig:
     model: str = "gpt-4o"
     temperature: float = 0.3
     max_iterations: int = 200
-    tools: Optional[List[str]] = None          # None = 全部已注册工具
+    tools: Optional[List[str]] = None          # None = 按 tool_groups 过滤;显式列表 = 精确指定
+    tool_groups: Optional[List[str]] = None    # 工具分组白名单(None = 全部 group)
     side_branch_max_turns: int = 5             # 侧分支最大轮次(压缩/反思)
     goal_compression: Literal["none", "on_complete", "on_overflow"] = "on_overflow"  # Goal 压缩模式
 
@@ -141,74 +142,8 @@ class RunConfig:
     knowledge: KnowledgeConfig = field(default_factory=KnowledgeConfig)
 
 
-    # 内置工具列表(始终自动加载)
-BUILTIN_TOOLS = [
-    # 文件操作工具
-    "read_file",
-    "edit_file",
-    "write_file",
-    "glob_files",
-    "grep_content",
-
-    # 系统工具
-    "bash_command",
-
-    # 技能和目标管理
-    "skill",
-    "list_skills",
-    "goal",
-    "agent",
-    "evaluate",
-    "get_current_context",
-
-    # 内容工具族
-    "content_platforms",
-    "content_search",
-    "content_detail",
-    "content_suggest",
-    "import_content",
-    "extract_video_clip",
-
-    # 知识管理工具
-    "ask_knowledge",
-    "upload_knowledge",
-    # "knowledge_search",
-    # "knowledge_save",
-    # "knowledge_update",
-    # "knowledge_batch_update",
-    # "knowledge_list",
-    # "knowledge_slim",
-
-    # 浏览器工具(14 个语义化入口,重构自 28 个原始工具)
-    "browser_navigate",
-    "browser_search",
-    "browser_back",
-    "browser_interact",
-    "browser_scroll",
-    "browser_screenshot",
-    "browser_elements",
-    "browser_read",
-    "browser_extract",
-    "browser_tabs",
-    "browser_cookies",
-    "browser_wait",
-    "browser_js",
-    "browser_download",
-
-    # 飞书工具
-    "feishu_send_message_to_contact",
-    "feishu_get_chat_history",
-    "feishu_get_contact_replies",
-    "feishu_get_contact_list",
-
-    # IM 工具
-    "im_setup",
-    "im_check_notification",
-    "im_receive_messages",
-    "im_send_message",
-    "im_get_contacts",
-    "im_get_chat_history",
-]
+    # BUILTIN_TOOLS 硬编码列表已移除(2026-04)。
+    # 工具可用性现在由 @tool(groups=[...]) 声明 + RunConfig.tool_groups 过滤控制。
 
 
 @dataclass
@@ -536,7 +471,7 @@ class AgentRunner:
         task_name = config.name or await self._generate_task_name(messages)
 
         # 准备工具 Schema
-        tool_schemas = self._get_tool_schemas(config.tools)
+        tool_schemas = self._get_tool_schemas(config.tools, config.tool_groups)
 
         trace_obj = Trace(
             trace_id=trace_id,
@@ -1070,7 +1005,7 @@ class AgentRunner:
     ) -> AsyncIterator[Union[Trace, Message]]:
         """ReAct 循环"""
         trace_id = trace.trace_id
-        tool_schemas = self._get_tool_schemas(config.tools)
+        tool_schemas = self._get_tool_schemas(config.tools, config.tool_groups)
 
         # 当前主路径头节点的 sequence(用于设置 parent_sequence)
         head_seq = trace.head_sequence
@@ -2822,22 +2757,19 @@ class AgentRunner:
         )
         return messages
 
-    def _get_tool_schemas(self, tools: Optional[List[str]]) -> List[Dict]:
+    def _get_tool_schemas(self, tools: Optional[List[str]] = None, tool_groups: Optional[List[str]] = None) -> List[Dict]:
         """
         获取工具 Schema
 
-        - tools=None: 使用 registry 中全部已注册工具(含内置 + 外部注册的)
-        - tools=["a", "b"]: 在 BUILTIN_TOOLS 基础上追加指定工具
+        优先级:
+        - tools 非空: 精确指定工具列表(忽略 tool_groups)
+        - tool_groups 非空: 按分组白名单过滤
+        - 两者都为 None: 返回所有已注册工具
         """
-        if tools is None:
-            # 全部已注册工具
-            tool_names = self.tools.get_tool_names()
+        if tools is not None:
+            tool_names = list(tools)
         else:
-            # BUILTIN_TOOLS + 显式指定的额外工具
-            tool_names = BUILTIN_TOOLS.copy()
-            for t in tools:
-                if t not in tool_names:
-                    tool_names.append(t)
+            tool_names = self.tools.get_tool_names(groups=tool_groups)
         return self.tools.get_schemas(tool_names)
 
     # 默认 system prompt 前缀(当 config.system_prompt 和前端都未提供 system message 时使用)

+ 46 - 1
agent/docs/tools.md

@@ -11,7 +11,8 @@
 3. [ToolResult 和记忆管理](#toolresult-和记忆管理)
 4. [ToolContext 和依赖注入](#toolcontext-和依赖注入)
 5. [高级特性](#高级特性)
-6. [内置基础工具](#内置基础工具)
+6. [工具分组](#工具分组)
+7. [内置基础工具](#内置基础工具)
 7. [集成 Browser-Use](#集成-browser-use)
 8. [最佳实践](#最佳实践)
 
@@ -790,6 +791,50 @@ print(f"Success rate: {stats['success_rate']:.1%}")
 
 ---
 
+## 工具分组
+
+工具通过 `@tool(groups=[...])` 声明所属分组,`RunConfig.tool_groups` 控制 Agent 可用哪些分组。
+
+### 机制
+
+```python
+# 注册时声明分组
+@tool(groups=["browser"])
+async def browser_navigate(url: str) -> ToolResult: ...
+
+@tool(groups=["content", "media"])  # 支持多标签
+async def extract_video_clip(...) -> ToolResult: ...
+```
+
+```python
+# 配置时按分组过滤
+RunConfig(tool_groups=["core", "content"])        # 只用核心 + 内容工具
+RunConfig(tool_groups=["core", "browser"])         # 只用核心 + 浏览器工具
+RunConfig(tool_groups=None)                        # 全部工具(默认)
+RunConfig(tools=["knowledge_search", "read_file"]) # 精确指定(优先于 tool_groups)
+```
+
+### 过滤逻辑
+
+`_get_tool_schemas(tools, tool_groups)` 的优先级:
+
+1. `tools` 非空 → 精确使用指定列表(忽略 `tool_groups`)
+2. `tool_groups` 非空 → 按分组白名单过滤(工具的任一 group 命中即选中)
+3. 两者都为 None → 返回所有已注册工具
+
+### 实现位置
+
+- 分组声明:`@tool(groups=[...])` — `agent/tools/registry.py`
+- 分组存储:`ToolRegistry._tools[name]["groups"]`
+- 分组过滤:`ToolRegistry.get_tool_names(groups=[...])`
+- 配置入口:`RunConfig.tool_groups` — `agent/core/runner.py`
+
+### 分组一览
+
+完整的分组和工具列表见 [README 附录](../../README.md#附录工具分组)。
+
+---
+
 ## 内置基础工具
 
 > 参考 opencode 实现的文件操作和命令执行工具

+ 1 - 1
agent/tools/builtin/bash.py

@@ -158,7 +158,7 @@ def _kill_process_tree(pid: int) -> None:
         pass
 
 
-@tool(description="执行 bash 命令", hidden_params=["context"])
+@tool(description="执行 bash 命令", hidden_params=["context"], groups=["core"])
 async def bash_command(
     command: str,
     timeout: Optional[int] = None,

+ 14 - 14
agent/tools/builtin/browser/baseClass.py

@@ -2154,7 +2154,7 @@ async def browser_load_cookies(url: str, name: str = "", auto_navigate: bool = T
 # ============================================================
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_navigate(url: str, new_tab: bool = False) -> ToolResult:
     """
     导航到指定 URL。
@@ -2166,7 +2166,7 @@ async def browser_navigate(url: str, new_tab: bool = False) -> ToolResult:
     return await browser_navigate_to_url(url=url, new_tab=new_tab)
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_search(query: str, engine: str = "bing") -> ToolResult:
     """
     使用搜索引擎搜索。
@@ -2178,13 +2178,13 @@ async def browser_search(query: str, engine: str = "bing") -> ToolResult:
     return await browser_search_web(query=query, engine=engine)
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_back() -> ToolResult:
     """返回上一页。"""
     return await browser_go_back()
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_interact(
     action: Literal["click", "type", "send_keys", "upload", "dropdown_list", "dropdown_select"],
     index: Optional[int] = None,
@@ -2245,7 +2245,7 @@ async def browser_interact(
         return ToolResult(title="未知 action", output="", error=f"不支持的 action: {action}")
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_scroll(
     down: bool = True,
     pages: float = 1.0,
@@ -2262,7 +2262,7 @@ async def browser_scroll(
     return await browser_scroll_page(down=down, pages=pages, index=into_view_index)
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_screenshot(highlight_elements: bool = False) -> ToolResult:
     """
     截取当前页面。
@@ -2277,7 +2277,7 @@ async def browser_screenshot(highlight_elements: bool = False) -> ToolResult:
         return await browser_screenshot_impl()
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_elements() -> ToolResult:
     """
     获取当前页面的可交互元素列表(纯文本,不截图)。
@@ -2286,7 +2286,7 @@ async def browser_elements() -> ToolResult:
     return await browser_get_selector_map()
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_read(
     mode: Literal["html", "find", "long"],
     query: Optional[str] = None,
@@ -2325,7 +2325,7 @@ async def browser_read(
         return ToolResult(title="未知 mode", output="", error=f"不支持的 mode: {mode}")
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_extract(
     query: str,
     extract_links: bool = False,
@@ -2349,7 +2349,7 @@ async def browser_extract(
     )
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_tabs(
     action: Literal["switch", "close"],
     tab_id: str = "",
@@ -2372,7 +2372,7 @@ async def browser_tabs(
         return ToolResult(title="未知 action", output="", error=f"不支持的 action: {action}")
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_cookies(
     action: Literal["load", "export", "ensure_login"],
     url: str = "",
@@ -2416,7 +2416,7 @@ async def browser_cookies(
         return ToolResult(title="未知 action", output="", error=f"不支持的 action: {action}")
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_wait(
     seconds: Optional[int] = None,
     user_message: Optional[str] = None,
@@ -2441,7 +2441,7 @@ async def browser_wait(
         return await browser_wait_impl(seconds=seconds or 3)
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_js(code: str) -> ToolResult:
     """
     在当前页面执行 JavaScript 代码。
@@ -2452,7 +2452,7 @@ async def browser_js(code: str) -> ToolResult:
     return await browser_evaluate(code=code)
 
 
-@tool()
+@tool(groups=["browser"])
 async def browser_download(url: str, save_name: str = "") -> ToolResult:
     """
     下载指定 URL 的文件到本地。

+ 1 - 1
agent/tools/builtin/content/ingestion.py

@@ -15,7 +15,7 @@ AIGC_BASE_URL = "http://aigc-channel.aiddit.com/aigc/channel"
 DEFAULT_TIMEOUT = 60.0
 
 
-@tool()
+@tool(groups=["content"])
 async def import_content(plan_name: str, content_data: List[Dict[str, Any]]) -> ToolResult:
     """
     导入长文内容到 CMS(微信公众号、小红书、抖音等通用链接)。

+ 1 - 1
agent/tools/builtin/content/media.py

@@ -60,7 +60,7 @@ def parse_srt_to_outline(srt_content: str) -> List[Dict[str, str]]:
 
 # ── @tool ──
 
-@tool()
+@tool(groups=["content"])
 async def extract_video_clip(
     video_id: str,
     start_time: str,

+ 4 - 4
agent/tools/builtin/content/tools.py

@@ -36,7 +36,7 @@ def _get_trace_id(context: Optional[ToolContext]) -> str:
 
 # ── content_platforms ──
 
-@tool(hidden_params=["context"])
+@tool(hidden_params=["context"], groups=["content"])
 async def content_platforms(
     platform: str = "",
     context: Optional[ToolContext] = None,
@@ -78,7 +78,7 @@ async def content_platforms(
 
 # ── content_search ──
 
-@tool(hidden_params=["context"])
+@tool(hidden_params=["context"], groups=["content"])
 async def content_search(
     platform: str,
     keyword: str,
@@ -135,7 +135,7 @@ async def content_search(
 
 # ── content_detail ──
 
-@tool(hidden_params=["context"])
+@tool(hidden_params=["context"], groups=["content"])
 async def content_detail(
     platform: str,
     index: int,
@@ -186,7 +186,7 @@ async def content_detail(
 
 # ── content_suggest ──
 
-@tool(hidden_params=["context"])
+@tool(hidden_params=["context"], groups=["content"])
 async def content_suggest(
     platform: str,
     keyword: str,

+ 2 - 1
agent/tools/builtin/context.py

@@ -14,7 +14,8 @@ from agent.tools import tool, ToolResult, ToolContext
 
 @tool(
     description="获取当前执行上下文,包括计划状态、焦点提醒、协作者信息等。当你感到困惑或需要回顾当前任务状态时调用。",
-    hidden_params=["context"]
+    hidden_params=["context"],
+    groups=["core"],
 )
 async def get_current_context(
     context: ToolContext,

+ 4 - 0
agent/tools/builtin/feishu/chat.py

@@ -133,6 +133,7 @@ def update_unread_count(contact_name: str, increment: int = 1, reset: bool = Fal
 
 @tool(
     hidden_params=["context"],
+    groups=["feishu"],
     display={
         "zh": {
             "name": "获取飞书联系人列表",
@@ -160,6 +161,7 @@ async def feishu_get_contact_list(context: Optional[ToolContext] = None) -> Tool
 
 @tool(
     hidden_params=["context"],
+    groups=["feishu"],
     display={
         "zh": {
             "name": "给飞书联系人发送消息",
@@ -293,6 +295,7 @@ async def feishu_send_message_to_contact(
 
 @tool(
     hidden_params=["context"],
+    groups=["feishu"],
     display={
         "zh": {
             "name": "获取飞书联系人回复",
@@ -409,6 +412,7 @@ def _convert_feishu_msg_to_openai_content(client: FeishuClient, msg: Dict[str, A
 
 @tool(
     hidden_params=["context"],
+    groups=["feishu"],
     display={
         "zh": {
             "name": "获取飞书聊天历史记录",

+ 1 - 1
agent/tools/builtin/file/edit.py

@@ -17,7 +17,7 @@ import re
 from agent.tools import tool, ToolResult, ToolContext
 
 
-@tool(description="编辑文件,使用精确字符串替换。支持多种智能匹配策略。", hidden_params=["context"])
+@tool(description="编辑文件,使用精确字符串替换。支持多种智能匹配策略。", hidden_params=["context"], groups=["core"])
 async def edit_file(
     file_path: str,
     old_string: str,

+ 1 - 1
agent/tools/builtin/file/grep.py

@@ -21,7 +21,7 @@ LIMIT = 100  # 最大返回匹配数(参考 opencode grep.ts:107)
 MAX_LINE_LENGTH = 2000  # 最大行长度(参考 opencode grep.ts:10)
 
 
-@tool(description="在文件内容中搜索模式", hidden_params=["context"])
+@tool(description="在文件内容中搜索模式", hidden_params=["context"], groups=["core"])
 async def grep_content(
     pattern: str,
     path: Optional[str] = None,

+ 1 - 1
agent/tools/builtin/file/read.py

@@ -27,7 +27,7 @@ MAX_LINE_LENGTH = 2000
 MAX_BYTES = 50 * 1024  # 50KB
 
 
-@tool(description="读取单个文件内容,支持文本文件、图片、PDF 等多种格式,也支持 HTTP/HTTPS URL", hidden_params=["context"])
+@tool(description="读取单个文件内容,支持文本文件、图片、PDF 等多种格式,也支持 HTTP/HTTPS URL", hidden_params=["context"], groups=["core"])
 async def read_file(
     file_path: str,
     offset: int = 0,

+ 1 - 0
agent/tools/builtin/file/read_images.py

@@ -71,6 +71,7 @@ def _adaptive_layout(count: int) -> Tuple[int, int]:
             },
         },
     },
+    groups=["core"],
 )
 async def read_images(
     paths: List[str],

+ 1 - 1
agent/tools/builtin/file/write.py

@@ -16,7 +16,7 @@ import difflib
 from agent.tools import tool, ToolResult, ToolContext
 
 
-@tool(description="写入文件内容(创建新文件、覆盖现有文件或追加内容)", hidden_params=["context"])
+@tool(description="写入文件内容(创建新文件、覆盖现有文件或追加内容)", hidden_params=["context"], groups=["core"])
 async def write_file(
     file_path: str,
     content: str,

+ 1 - 1
agent/tools/builtin/glob_tool.py

@@ -19,7 +19,7 @@ from agent.tools import tool, ToolResult, ToolContext
 LIMIT = 100  # 最大返回数量(参考 opencode glob.ts:35)
 
 
-@tool(description="使用 glob 模式匹配文件", hidden_params=["context"])
+@tool(description="使用 glob 模式匹配文件", hidden_params=["context"], groups=["core"])
 async def glob_files(
     pattern: str,
     path: Optional[str] = None,

+ 8 - 0
agent/tools/builtin/im/chat.py

@@ -43,6 +43,7 @@ class _ToolNotifier(AgentNotifier):
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "初始化 IM 连接", "params": {"contact_id": "你的身份 ID", "server_url": "服务器地址"}},
         "en": {"name": "Setup IM Connection", "params": {"contact_id": "Your identity ID", "server_url": "Server URL"}},
@@ -80,6 +81,7 @@ async def im_setup(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "打开 IM 窗口", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID"}},
         "en": {"name": "Open IM Window", "params": {"contact_id": "Agent ID", "chat_id": "Window ID"}},
@@ -109,6 +111,7 @@ async def im_open_window(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "关闭 IM 窗口", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID"}},
         "en": {"name": "Close IM Window", "params": {"contact_id": "Agent ID", "chat_id": "Window ID"}},
@@ -138,6 +141,7 @@ async def im_close_window(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "检查 IM 新消息通知", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID"}},
         "en": {"name": "Check IM Notifications", "params": {"contact_id": "Agent ID", "chat_id": "Window ID"}},
@@ -169,6 +173,7 @@ async def im_check_notification(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "接收 IM 消息", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID"}},
         "en": {"name": "Receive IM Messages", "params": {"contact_id": "Agent ID", "chat_id": "Window ID"}},
@@ -213,6 +218,7 @@ async def im_receive_messages(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "发送 IM 消息", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID", "receiver": "接收者 ID", "content": "消息内容"}},
         "en": {"name": "Send IM Message", "params": {"contact_id": "Agent ID", "chat_id": "Window ID", "receiver": "Receiver ID", "content": "Message content"}},
@@ -253,6 +259,7 @@ async def im_send_message(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "查询 IM 联系人", "params": {"contact_id": "Agent ID", "server_http_url": "服务器 HTTP 地址"}},
         "en": {"name": "Get IM Contacts", "params": {"contact_id": "Agent ID", "server_http_url": "Server HTTP URL"}},
@@ -297,6 +304,7 @@ async def im_get_contacts(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "查询 IM 聊天历史", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID", "peer_id": "联系人 ID", "limit": "最大条数"}},
         "en": {"name": "Get IM Chat History", "params": {"contact_id": "Agent ID", "chat_id": "Window ID", "peer_id": "Contact ID", "limit": "Max records"}},

+ 14 - 27
agent/tools/builtin/knowledge.py

@@ -95,13 +95,7 @@ class KnowledgeConfig:
         return owner
 
 
-@tool(
-    hidden_params=["context"],
-    inject_params={
-        "types": {"mode": "default", "key": "knowledge_config.default_search_types"},
-        "owner": {"mode": "default", "key": "knowledge_config.default_search_owner"},
-    }
-)
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def knowledge_search(
     query: str,
     top_k: int = 5,
@@ -183,14 +177,7 @@ async def knowledge_search(
         )
 
 
-@tool(
-    hidden_params=["context", "owner"],
-    inject_params={
-        "owner": {"mode": "default", "key": "knowledge_config.resolved_owner"},
-        "tags": {"mode": "merge", "key": "knowledge_config.default_tags"},
-        "scopes": {"mode": "merge", "key": "knowledge_config.default_scopes"},
-    }
-)
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def knowledge_save(
     task: str,
     content: str,
@@ -285,7 +272,7 @@ async def knowledge_save(
         )
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def knowledge_update(
     knowledge_id: str,
     add_helpful_case: Optional[Dict] = None,
@@ -355,7 +342,7 @@ async def knowledge_update(
         )
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def knowledge_batch_update(
     feedback_list: List[Dict[str, Any]],
     context: Optional[ToolContext] = None,
@@ -405,7 +392,7 @@ async def knowledge_batch_update(
         )
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def knowledge_list(
     limit: int = 10,
     types: Optional[List[str]] = None,
@@ -467,7 +454,7 @@ async def knowledge_list(
         )
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def knowledge_slim(
     model: str = "google/gemini-2.0-flash-001",
     context: Optional[ToolContext] = None,
@@ -513,7 +500,7 @@ async def knowledge_slim(
 
 # ==================== Resource 资源管理工具 ====================
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def resource_save(
     resource_id: str,
     title: str,
@@ -572,7 +559,7 @@ async def resource_save(
         )
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def resource_get(
     resource_id: str,
     org_key: Optional[str] = None,
@@ -627,7 +614,7 @@ async def resource_get(
 
 # ==================== Tool 表查询工具 ====================
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def tool_search(
     query: str,
     top_k: int = 5,
@@ -652,7 +639,7 @@ async def tool_search(
         return ToolResult(title="❌ 工具检索失败", output=str(e), error=str(e))
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def tool_list(
     limit: int = 20,
     offset: int = 0,
@@ -674,7 +661,7 @@ async def tool_list(
 
 # ==================== Capability (原子能力) 表查询工具 ====================
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def capability_search(
     query: str,
     top_k: int = 5,
@@ -696,7 +683,7 @@ async def capability_search(
         return ToolResult(title="❌ 原子能力检索失败", output=str(e), error=str(e))
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def capability_list(
     limit: int = 20,
     offset: int = 0,
@@ -716,7 +703,7 @@ async def capability_list(
 
 # ==================== Requirement (需求) 表查询工具 ====================
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def requirement_search(
     query: str,
     top_k: int = 5,
@@ -734,7 +721,7 @@ async def requirement_search(
         return ToolResult(title="❌ 需求检索失败", output=str(e), error=str(e))
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def requirement_list(
     limit: int = 20,
     offset: int = 0,

+ 4 - 2
agent/tools/builtin/librarian.py

@@ -24,7 +24,8 @@ KNOWHUB_API = os.getenv("KNOWHUB_API", "http://43.106.118.91:9999").rstrip("/")
     hidden_params=["context"],
     inject_params={
         "trace_id": {"mode": "default", "key": "trace_id"},
-    }
+    },
+    groups=["knowledge"],
 )
 async def ask_knowledge(
     query: str,
@@ -107,7 +108,8 @@ async def ask_knowledge(
     hidden_params=["context"],
     inject_params={
         "trace_id": {"mode": "default", "key": "trace_id"},
-    }
+    },
+    groups=["knowledge"],
 )
 async def upload_knowledge(
     data: Dict[str, Any],

+ 4 - 2
agent/tools/builtin/resource.py

@@ -10,8 +10,9 @@ from agent.tools import tool, ToolResult
 KNOWHUB_API = os.getenv("KNOWHUB_API", "http://43.106.118.91:9999").rstrip("/")
 
 
-@tool(    
-    description="列出知识库中的所有工具资源"
+@tool(
+    description="列出知识库中的所有工具资源",
+    groups=["resource"],
 )
 def resource_list_tools(
     category: Optional[str] = None,
@@ -49,6 +50,7 @@ def resource_list_tools(
 
 @tool(
     description="获取指定工具的详细信息",
+    groups=["resource"],
 )
 def resource_get_tool(tool_id: str) -> ToolResult:
     """获取工具详情

+ 4 - 2
agent/tools/builtin/skill.py

@@ -66,7 +66,8 @@ def _check_skill_setup(skill_name: str) -> Optional[str]:
 
 
 @tool(
-    description="加载指定的 skill 文档。Skills 提供领域知识和最佳实践指导。"
+    description="加载指定的 skill 文档。Skills 提供领域知识和最佳实践指导。",
+    groups=["core"],
 )
 async def skill(
     skill_name: str,
@@ -181,7 +182,8 @@ async def skill(
 
 
 @tool(
-    description="列出所有可用的 skills"
+    description="列出所有可用的 skills",
+    groups=["core"],
 )
 async def list_skills(
     skills_dir: Optional[str] = None,

+ 2 - 2
agent/tools/builtin/subagent.py

@@ -610,7 +610,7 @@ async def _run_agents(
 
 # ===== 工具定义 =====
 
-@tool(description="创建 Agent 执行任务", hidden_params=["context"])
+@tool(description="创建 Agent 执行任务", hidden_params=["context"], groups=["core"])
 async def agent(
     task: Union[str, List[str]],
     messages: Optional[Union[Messages, List[Messages]]] = None,
@@ -677,7 +677,7 @@ async def agent(
     )
 
 
-@tool(description="评估目标执行结果是否满足要求", hidden_params=["context"])
+@tool(description="评估目标执行结果是否满足要求", hidden_params=["context"], groups=["core"])
 async def evaluate(
     messages: Optional[Messages] = None,
     target_goal_id: Optional[str] = None,

+ 6 - 3
agent/tools/builtin/toolhub.py

@@ -250,7 +250,8 @@ async def _preprocess_params(params: Dict[str, Any]) -> Dict[str, Any]:
     display={
         "zh": {"name": "ToolHub 健康检查", "params": {}},
         "en": {"name": "ToolHub Health Check", "params": {}},
-    }
+    },
+    groups=["toolhub"],
 )
 async def toolhub_health() -> ToolResult:
     """检查 ToolHub 远程工具库服务是否可用
@@ -291,7 +292,8 @@ async def toolhub_health() -> ToolResult:
     display={
         "zh": {"name": "搜索 ToolHub 工具", "params": {"keyword": "搜索关键词"}},
         "en": {"name": "Search ToolHub", "params": {"keyword": "Search keyword"}},
-    }
+    },
+    groups=["toolhub"],
 )
 async def toolhub_search(keyword: Optional[str] = None) -> ToolResult:
     """搜索 ToolHub 远程工具库中可用的工具
@@ -456,7 +458,8 @@ async def toolhub_search(keyword: Optional[str] = None) -> ToolResult:
             "name": "Call ToolHub Tool",
             "params": {"tool_id": "Tool ID", "params": "Tool parameters"},
         },
-    }
+    },
+    groups=["toolhub"],
 )
 async def toolhub_call(
     tool_id: str,

+ 34 - 10
agent/tools/registry.py

@@ -68,7 +68,8 @@ class ToolRegistry:
 		display: Optional[Dict[str, Dict[str, Any]]] = None,
 		url_patterns: Optional[List[str]] = None,
 		hidden_params: Optional[List[str]] = None,
-		inject_params: Optional[Dict[str, Any]] = None
+		inject_params: Optional[Dict[str, Any]] = None,
+		groups: Optional[List[str]] = None,
 	):
 		"""
 		注册工具
@@ -82,6 +83,7 @@ class ToolRegistry:
 			url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
 			hidden_params: 隐藏参数列表(不生成 schema,LLM 看不到)
 			inject_params: 注入参数规则 {param_name: injector_func}
+			groups: 工具分组标签(如 ["core"]、["browser"]),用于 RunConfig.tool_groups 过滤
 		"""
 		func_name = func.__name__
 
@@ -100,6 +102,7 @@ class ToolRegistry:
 			"url_patterns": url_patterns,
 			"hidden_params": hidden_params or [],
 			"inject_params": inject_params or {},
+			"groups": groups or [],
 			"ui_metadata": {
 				"requires_confirmation": requires_confirmation,
 				"editable_params": editable_params or [],
@@ -166,27 +169,45 @@ class ToolRegistry:
 
 		return schemas
 
-	def get_tool_names(self, current_url: Optional[str] = None) -> List[str]:
+	def get_tool_names(self, current_url: Optional[str] = None, groups: Optional[List[str]] = None) -> List[str]:
 		"""
-		获取工具名称列表(可选 URL 过滤)
+		获取工具名称列表(可选 URL 过滤 + group 过滤
 
 		Args:
-			current_url: 当前 URL(None = 返回所有工具)
+			current_url: 当前 URL(None = 不过滤 URL)
+			groups: 工具分组白名单(None = 不过滤 group,返回所有工具)
 
 		Returns:
 			工具名称列表
 		"""
+		# 1. group 过滤
+		if groups is not None:
+			group_set = set(groups)
+			candidates = {
+				name for name, tool in self._tools.items()
+				if group_set & set(tool.get("groups", []))
+			}
+		else:
+			candidates = set(self._tools.keys())
+
+		# 2. URL 过滤
 		if current_url is None:
-			return list(self._tools.keys())
+			return list(candidates)
 
-		# 过滤工具
 		tool_items = [
-			{"name": name, "url_patterns": tool["url_patterns"]}
-			for name, tool in self._tools.items()
+			{"name": name, "url_patterns": self._tools[name]["url_patterns"]}
+			for name in candidates
 		]
 		filtered = filter_by_url(tool_items, current_url, url_field="url_patterns")
 		return [item["name"] for item in filtered]
 
+	def get_available_groups(self) -> List[str]:
+		"""获取所有已注册的工具分组"""
+		groups = set()
+		for tool in self._tools.values():
+			groups.update(tool.get("groups", []))
+		return sorted(groups)
+
 	def get_schemas_for_url(self, current_url: Optional[str] = None) -> List[Dict]:
 		"""
 		根据当前 URL 获取匹配的工具 Schema
@@ -484,7 +505,8 @@ def tool(
 	display: Optional[Dict[str, Dict[str, Any]]] = None,
 	url_patterns: Optional[List[str]] = None,
 	hidden_params: Optional[List[str]] = None,
-	inject_params: Optional[Dict[str, Any]] = None
+	inject_params: Optional[Dict[str, Any]] = None,
+	groups: Optional[List[str]] = None,
 ):
 	"""
 	工具装饰器 - 自动注册工具并生成 Schema
@@ -498,6 +520,7 @@ def tool(
 		url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
 		hidden_params: 隐藏参数列表(不生成 schema,LLM 看不到)
 		inject_params: 注入参数规则 {param_name: injector_func}
+		groups: 工具分组标签(如 ["core"]、["browser"]),用于 RunConfig.tool_groups 过滤
 
 	Example:
 		@tool(
@@ -531,7 +554,8 @@ def tool(
 			display=display,
 			url_patterns=url_patterns,
 			hidden_params=hidden_params,
-			inject_params=inject_params
+			inject_params=inject_params,
+			groups=groups,
 		)
 		return func
 

+ 1 - 1
agent/trace/goal_tool.py

@@ -103,7 +103,7 @@ async def inject_knowledge_for_goal(
 
 # ===== LLM 可调用的 goal 工具 =====
 
-@tool(description="管理执行计划,添加/完成/放弃目标,切换焦点", hidden_params=["context"])
+@tool(description="管理执行计划,添加/完成/放弃目标,切换焦点", hidden_params=["context"], groups=["core"])
 async def goal(
     add: Optional[str] = None,
     reason: Optional[str] = None,