feat(agent): 添加对持久化子智能体的支持并增强委派管理 添加了持久化子智能体的完整生命周期管理功能,包括创建、更新、删除和查询API接口。 新增了子智能体的JSON-RPC通信协议支持,实现了远程调用和任务管理功能。 同时增强了委派管理器的功能: - 添加了对本地委派、插件委派和本地回退的开关控制 - 实现了持久化子智能体任务的自动检测和本地执行保护 - 增加了对不同委派类型的权限验证机制 修改了智能体注册表以支持插件智能体的条件性包含,并更新了工具注册逻辑以支持可选工具。 BREAKING CHANGE: 委派管理器的构造函数签名已更改,添加了新的控制参数。 ```
247 lines
9.9 KiB
Python
247 lines
9.9 KiB
Python
"""本地委派执行器。
|
||
|
||
这个类不再负责“后台任务管理”和“结果回流”,只保留一件事:
|
||
在统一委派层要求执行本地任务时,提供一个受限工具集的本地 agent 执行环境。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
import time as _time
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
||
|
||
from loguru import logger
|
||
|
||
from nanobot.agent.run_result import AgentRunResult
|
||
from nanobot.agent.tools.filesystem import EditFileTool, ListDirTool, ReadFileTool, WriteFileTool
|
||
from nanobot.agent.tools.registry import ToolRegistry
|
||
from nanobot.agent.tools.shell import ExecTool
|
||
from nanobot.agent.tools.web import WebFetchTool, WebSearchTool
|
||
from nanobot.providers.base import LLMProvider
|
||
|
||
if TYPE_CHECKING:
|
||
from nanobot.config.schema import ExecToolConfig
|
||
|
||
|
||
class SubagentManager:
|
||
"""用受限工具集在本地执行委派任务。"""
|
||
|
||
def __init__(
|
||
self,
|
||
provider: LLMProvider,
|
||
workspace: Path,
|
||
model: str | None = None,
|
||
temperature: float = 0.7,
|
||
max_tokens: int = 4096,
|
||
brave_api_key: str | None = None,
|
||
exec_config: ExecToolConfig | None = None,
|
||
restrict_to_workspace: bool = False,
|
||
):
|
||
from nanobot.config.schema import ExecToolConfig
|
||
|
||
# 这里保存的都是本地执行所需的静态配置,不再维护后台任务表。
|
||
self.provider = provider
|
||
self.workspace = workspace
|
||
self.model = model or provider.get_default_model()
|
||
self.temperature = temperature
|
||
self.max_tokens = max_tokens
|
||
self.brave_api_key = brave_api_key
|
||
self.exec_config = exec_config or ExecToolConfig()
|
||
self.restrict_to_workspace = restrict_to_workspace
|
||
|
||
async def run_local_task(
|
||
self,
|
||
task: str,
|
||
label: str | None = None,
|
||
agent_id: str = "local-subagent",
|
||
agent_name: str = "Local Subagent",
|
||
system_prompt: str | None = None,
|
||
model: str | None = None,
|
||
progress_callback: Callable[..., Awaitable[None]] | None = None,
|
||
) -> AgentRunResult:
|
||
"""执行一次本地委派任务,并返回结构化结果。"""
|
||
# 每次任务都新建一套局部工具注册表,避免不同任务之间共享临时状态。
|
||
tools = self._build_local_tools()
|
||
prompt = self._build_subagent_prompt(
|
||
task,
|
||
agent_name=agent_name,
|
||
custom_system_prompt=system_prompt,
|
||
)
|
||
# 本地委派不共享主会话历史,只带“专用 system prompt + 当前任务”。
|
||
messages: list[dict[str, Any]] = [
|
||
{"role": "system", "content": prompt},
|
||
{"role": "user", "content": task},
|
||
]
|
||
|
||
# 本地子 agent 也走“模型 -> 工具 -> 模型”的短循环,但轮数更保守。
|
||
max_iterations = 15
|
||
iteration = 0
|
||
final_result: str | None = None
|
||
|
||
while iteration < max_iterations:
|
||
iteration += 1
|
||
response = await self.provider.chat(
|
||
messages=messages,
|
||
tools=tools.get_definitions(),
|
||
model=model or self.model,
|
||
temperature=self.temperature,
|
||
max_tokens=self.max_tokens,
|
||
)
|
||
|
||
if response.has_tool_calls:
|
||
if progress_callback:
|
||
# 进度回调只发对用户有价值的文本,不把 `<think>` 之类内部推理暴露出去。
|
||
clean = self._strip_think(response.content)
|
||
if clean:
|
||
await progress_callback(clean, tool_hint=False)
|
||
# 额外补一条短工具提示,让上层 UI 知道当前在做什么。
|
||
await progress_callback(self._tool_hint(response.tool_calls), tool_hint=True)
|
||
|
||
tool_call_dicts = [
|
||
{
|
||
"id": tc.id,
|
||
"type": "function",
|
||
"function": {
|
||
"name": tc.name,
|
||
"arguments": json.dumps(tc.arguments, ensure_ascii=False),
|
||
},
|
||
}
|
||
for tc in response.tool_calls
|
||
]
|
||
messages.append({
|
||
"role": "assistant",
|
||
"content": response.content or "",
|
||
"tool_calls": tool_call_dicts,
|
||
})
|
||
for tool_call in response.tool_calls:
|
||
args_str = json.dumps(tool_call.arguments, ensure_ascii=False)
|
||
logger.debug("Agent [{}] executing: {} with arguments: {}", agent_id, tool_call.name, args_str)
|
||
# 真正执行工具后,把结果回填到 messages,让下一轮模型能看到执行结果。
|
||
result = await tools.execute(tool_call.name, tool_call.arguments)
|
||
messages.append({
|
||
"role": "tool",
|
||
"tool_call_id": tool_call.id,
|
||
"name": tool_call.name,
|
||
"content": result,
|
||
})
|
||
else:
|
||
# 没有继续调用工具时,视为任务已收敛,直接采纳当前回复。
|
||
final_result = response.content
|
||
break
|
||
|
||
if final_result is None:
|
||
# 兜底避免出现“任务做完了但完全没文本”的空结果。
|
||
final_result = "Task completed but no final response was generated."
|
||
|
||
return AgentRunResult(
|
||
agent_id=agent_id,
|
||
agent_name=agent_name,
|
||
status="ok",
|
||
summary=final_result,
|
||
)
|
||
|
||
def _build_local_tools(self) -> ToolRegistry:
|
||
"""构建本地委派可用的受限工具集。"""
|
||
tools = ToolRegistry()
|
||
allowed_dir = self.workspace if self.restrict_to_workspace else None
|
||
protected_skill_paths = [self.workspace / "skills"]
|
||
# 文件工具统一按相同的 workspace / allowed_dir 约束注册。
|
||
tools.register(ReadFileTool(workspace=self.workspace, allowed_dir=allowed_dir))
|
||
tools.register(ListDirTool(workspace=self.workspace, allowed_dir=allowed_dir))
|
||
tools.register(
|
||
WriteFileTool(
|
||
workspace=self.workspace,
|
||
allowed_dir=allowed_dir,
|
||
protected_paths=protected_skill_paths,
|
||
)
|
||
)
|
||
tools.register(
|
||
EditFileTool(
|
||
workspace=self.workspace,
|
||
allowed_dir=allowed_dir,
|
||
protected_paths=protected_skill_paths,
|
||
)
|
||
)
|
||
# 本地命令执行沿用主配置里的超时和 workspace 限制。
|
||
tools.register(ExecTool(
|
||
working_dir=str(self.workspace),
|
||
timeout=self.exec_config.timeout,
|
||
restrict_to_workspace=self.restrict_to_workspace,
|
||
protected_paths=protected_skill_paths,
|
||
))
|
||
# 网络能力保持只读:搜索和抓取,不提供消息发送/再次委派等工具。
|
||
tools.register(WebSearchTool(api_key=self.brave_api_key))
|
||
tools.register(WebFetchTool())
|
||
return tools
|
||
|
||
@staticmethod
|
||
def _strip_think(text: str | None) -> str | None:
|
||
"""Remove provider-specific think blocks from visible progress text."""
|
||
if not text:
|
||
return None
|
||
return re.sub(r"<think>[\s\S]*?</think>", "", text).strip() or None
|
||
|
||
@staticmethod
|
||
def _tool_hint(tool_calls: list) -> str:
|
||
"""把工具调用列表格式化成简短进度提示。"""
|
||
|
||
def _fmt(tc):
|
||
val = next(iter(tc.arguments.values()), None) if tc.arguments else None
|
||
if not isinstance(val, str):
|
||
return tc.name
|
||
return f'{tc.name}("{val[:40]}...")' if len(val) > 40 else f'{tc.name}("{val}")'
|
||
|
||
return ", ".join(_fmt(tc) for tc in tool_calls)
|
||
|
||
def _build_subagent_prompt(
|
||
self,
|
||
task: str,
|
||
agent_name: str = "Local Subagent",
|
||
custom_system_prompt: str | None = None,
|
||
) -> str:
|
||
"""构建子代理专用 system prompt。"""
|
||
now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)")
|
||
tz = _time.strftime("%Z") or "UTC"
|
||
# plugin agent 的自定义系统提示拼到末尾,保留通用约束,再叠加个性化指令。
|
||
extra = f"\n\n## Agent Instructions\n{custom_system_prompt.strip()}" if custom_system_prompt else ""
|
||
|
||
return f"""# {agent_name}
|
||
|
||
## Current Time
|
||
{now} ({tz})
|
||
|
||
You are a delegated agent spawned by the main agent to complete a specific task.
|
||
|
||
## Rules
|
||
1. Stay focused - complete only the assigned task, nothing else
|
||
2. Your final response will be reported back to the main agent
|
||
3. Do not initiate conversations or take on side tasks
|
||
4. Be concise but informative in your findings
|
||
|
||
## What You Can Do
|
||
- Read and write files in the workspace
|
||
- Execute shell commands
|
||
- Search the web and fetch web pages
|
||
- Complete the task thoroughly
|
||
|
||
## What You Cannot Do
|
||
- Send messages directly to users (no message tool available)
|
||
- Spawn other subagents
|
||
- Access the main agent's conversation history
|
||
|
||
## Workspace
|
||
Your workspace is at: {self.workspace}
|
||
Skills are available at: {self.workspace}/skills/ (read SKILL.md files as needed)
|
||
|
||
## Special Workflow
|
||
- If the task is about creating, updating, repairing, or deleting a persistent local sub-agent, read `skills/subagent-manager/SKILL.md` before making changes.
|
||
- For persistent local sub-agents, follow only the canonical workflow from that skill.
|
||
- Do not manually create `workspace/agents/<id>/agent.json` as a substitute for a persistent sub-agent.
|
||
- Do not manually edit `workspace/agents/registry.json` to register a persistent sub-agent.
|
||
- A valid persistent sub-agent must be created through `subagentctl.py` or `/api/subagents` and must end up at `workspace/agents/<id>_agent/AGENTS.json`.
|
||
|
||
When you have completed the task, provide a clear summary of your findings or actions.{extra}"""
|