"""Executor 接口:工具调用的总入口(DESIGN §7.5 落地清单 #5)。 `AgentLoop` 不直接调 `tool.execute`,而是 `executor.call_tool(name, args, ctx)`。 Backend 内部 dispatch: - `HostExecutor`(本步引入):全部 tools in-process,沿用原 `Tool.execute` 行为 - `DockerExecutor`(Step 3 引入):`shell` / `run_python` 走 `docker exec`, 其余按 §7.5 #6 信任域二分仍走 host —— 此时 DockerExecutor 内部组合 HostExecutor 接口形状刻意保持 backend 无关:`call_tool(name, args, ctx)` 不暴露 `docker exec` / `docker cp` / `docker stats` 等 Docker 假设。未来切 gVisor / Firecracker / e2b 时 应用层零改动,只换 backend driver(§7.5 #5 / §7.9 升级触发表)。 """ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from pathlib import Path from typing import Any, Callable, Dict, List, Optional from uuid import UUID @dataclass class ExecCtx: """每次 tool 调用的执行上下文。 带身份 / 范围 / 取消钩子;arg-irrelevant 信息从 args 剥离。 host backend 当前只用 cancel_check;docker backend 会: - user_id → 找 / 起 per-user 容器 - working_dir → 拼 `docker exec --workdir /workspace/` - task_id → 临时文件命名空间 `/tmp/zcbot//` - cancel_check → 轮询期间响应停止按钮(主动 `kill -- -PGID`) """ user_id: UUID task_id: UUID working_dir: Path cancel_check: Optional[Callable[[], bool]] = None @dataclass class ToolResult: """工具调用统一返回。 现状所有 `Tool.execute` 都返 str,docker backend 后续可能要带 stdout/stderr/ exit_code 分离。这里先留单 content 字段(LLM 拿到的就是这串),exit_code 作 backend 内部使用 hint(0=ok / 1=tool 抛异常 / 2=参数非法 / 124=timeout 等), 不影响 LLM 接口。 """ content: str exit_code: int = 0 class Executor(ABC): """工具调度抽象 —— 见模块 docstring。""" @abstractmethod def call_tool(self, name: str, args: Dict[str, Any], ctx: ExecCtx) -> ToolResult: """执行单次 tool 调用。永远返 ToolResult,不抛异常(异常包成 exit_code=1)。""" @abstractmethod def schemas(self) -> List[Dict[str, Any]]: """暴露给 LLM 的 OpenAI tool schema 列表;`AgentLoop._stream_llm` 用。""" @abstractmethod def has_tool(self, name: str) -> bool: """schema 列表覆盖的 tool 名;主要给测试 / 诊断用。"""