zcbot/core/executor.py

67 lines
2.5 KiB
Python

"""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/<wd_name>`
- task_id → 临时文件命名空间 `/tmp/zcbot/<task_id>/`
- 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 名;主要给测试 / 诊断用。"""