"""本地委派执行器。 这个类不再负责“后台任务管理”和“结果回流”,只保留一件事: 在统一委派层要求执行本地任务时,提供一个受限工具集的本地 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, has_meaningful_summary 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.spawn import NestedDelegateTool from nanobot.agent.tools.web import WebFetchTool, WebSearchTool from nanobot.providers.base import LLMProvider if TYPE_CHECKING: from nanobot.agent.delegation import DelegationManager 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 self._nested_delegate: DelegationManager | None = None def set_nested_delegate(self, manager: "DelegationManager | None") -> None: """注入 delegated worker 可用的受控下游委派器。""" self._nested_delegate = manager 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, allow_nested_delegation: bool = True, skill_context: str = "", skill_names: list[str] | None = None, ) -> AgentRunResult: """执行一次本地委派任务,并返回结构化结果。""" # 每次任务都新建一套局部工具注册表,避免不同任务之间共享临时状态。 tools = self._build_local_tools( allow_nested_delegation=allow_nested_delegation, skill_names=skill_names, ) prompt = self._build_subagent_prompt( task, agent_name=agent_name, custom_system_prompt=system_prompt, allow_nested_delegation=allow_nested_delegation, skill_context=skill_context, ) # 本地委派不共享主会话历史,只带“专用 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: # 进度回调只发对用户有价值的文本,不把 `` 之类内部推理暴露出去。 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 status = "ok" raw: dict[str, Any] | None = None if not has_meaningful_summary(final_result): # 兜底避免出现“任务做完了但完全没文本”的空结果,并显式标记为失败, # 防止上层把这类占位结果学习成 procedure。 final_result = "Task completed but no final response was generated." status = "error" raw = { "reason": "no_final_response_generated", "iterations": iteration, } return AgentRunResult( agent_id=agent_id, agent_name=agent_name, status=status, summary=final_result, raw=raw, ) def _build_local_tools( self, *, allow_nested_delegation: bool, skill_names: list[str] | None = None, ) -> 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()) if allow_nested_delegation and self._nested_delegate is not None: tools.register(NestedDelegateTool(manager=self._nested_delegate, default_skills=skill_names)) 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"[\s\S]*?", "", 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, allow_nested_delegation: bool = True, skill_context: str = "", ) -> 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 "" can_do_lines = [ "- Read and write files in the workspace", "- Execute shell commands", "- Search the web and fetch web pages", "- Complete the task thoroughly", ] cannot_do_lines = [ "- Send messages directly to users (no message tool available)", "- Access the main agent's conversation history", ] delegation_section = ( "\n## Downstream Delegation\n" "- Do not delegate further. Complete the task yourself with the tools you have." ) if allow_nested_delegation and self._nested_delegate is not None: can_do_lines.append( "- Use `delegate_task` for controlled downstream delegation when specialized help is required" ) cannot_do_lines.append("- Do not start agent teams or use background delegation tools") nested_summary = self._nested_delegate.build_nested_agents_summary() summary_block = f"\n\n{nested_summary}" if nested_summary else "" delegation_section = ( "\n## Downstream Delegation\n" "- Use `delegate_task` only when a specialized downstream worker is actually needed.\n" "- `strategy=\"a2a\"` delegates directly to an available A2A agent.\n" "- `strategy=\"ephemeral_subagent\"` runs a temporary local worker for this task only.\n" "- Never create, register, or persist a new local sub-agent through `subagentctl.py`, `/api/subagents`, or registry edits." f"{summary_block}" ) else: cannot_do_lines.append("- Spawn other subagents or downstream workers") skill_section = f"\n## Required Skills\n{skill_context.strip()}" if skill_context.strip() 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 5. Do not create or modify persistent local sub-agents unless the task explicitly requires that workflow ## What You Can Do {chr(10).join(can_do_lines)} ## What You Cannot Do {chr(10).join(cannot_do_lines)} {delegation_section} {skill_section} ## 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//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/_agent/AGENTS.json`. When you have completed the task, provide a clear summary of your findings or actions.{extra}"""