feat(agent): 预设本机工具、stdio MCP 传输与多轮上下文

- 预设 12 个本机工具并默认启用,装好即是可操作电脑的 Agent、无需总控配置或安装:
  文件与文件夹的查看/读取/写入/建目录/移动/删除(默认不覆盖、删目录需 recursive、
  拒绝删除系统目录与用户根目录)、用默认程序打开网址或文件、时间与时区换算
  (tzdata 已随包内置)、查看进程、屏幕截图、读写剪贴板。
- 工具下发改为通用:任意 HTTP MCP 服务与 stdio 本机命令(Agent 拉起子进程,
  容忍 stdout 噪声,命令缺失或进程崩溃会明确回报),支持多工具并存、
  定义未变更时复用连接、替换/删除/退出时回收子进程。
- 多轮上下文:task_dispatch.history 作为此前的 user/assistant 文本拼接到本次请求前,
  OpenAI 兼容、Anthropic、Gemini 分别适配;Agent 侧独立校验与限幅。
- 工具级实时进度以 progress 事件回传;使用预设工具的任务标记为 builtin 执行来源。
- Skill 与历史内容上限与总控对齐,多个已批准 Skill 不再被静默截断。
- 修复 build.ps1 缺 UTF-8 BOM 导致 build.bat 在 PowerShell 5.1 下语法报错。
This commit is contained in:
toot
2026-09-16 15:31:00 +08:00
parent aa57ea69de
commit 9e01cbbec0
21 changed files with 2110 additions and 144 deletions
+74
View File
@@ -216,6 +216,80 @@ def test_executor_does_not_fall_back_to_llm_when_desktop_tool_is_not_applied(tmp
assert "未部署或尚未应用" in str(events[-1]["error"])
def test_executor_streams_tool_progress_while_the_task_runs(tmp_path):
database = AgentDatabase(tmp_path / "agent.sqlite3")
def runner(_instruction, _attachments, toolbox):
return f"结果:{toolbox.call('computer_time', {'timezone': 'UTC'})}"
executor = TaskExecutor(database, runner)
completed = Event()
events: list[dict[str, object]] = []
def capture(event: object) -> None:
assert isinstance(event, dict)
events.append(event)
if event.get("status") == "completed":
completed.set()
executor.task_event.connect(capture)
executor.submit("progress-task", "看看现在几点")
assert completed.wait(10)
progress = [str(event["message"]) for event in events if event.get("status") == "progress"]
assert any(message.startswith("调用 computer_time") for message in progress), progress
assert any(message.startswith("computer_time 返回:") for message in progress), progress
# Progress must arrive before the terminal event so the UI can stream it live.
statuses = [event.get("status") for event in events]
assert statuses.index("progress") < statuses.index("completed")
def test_executor_ignores_progress_after_the_task_ended(tmp_path):
database = AgentDatabase(tmp_path / "agent.sqlite3")
executor = TaskExecutor(database, lambda instruction: "完成")
completed = Event()
events: list[dict[str, object]] = []
def capture(event: object) -> None:
assert isinstance(event, dict)
events.append(event)
if event.get("status") == "completed":
completed.set()
executor.task_event.connect(capture)
executor.submit("finished-task", "总结")
assert completed.wait(5)
reported = len(events)
executor._report_progress("finished-task")("迟到的进展")
assert len(events) == reported, "结束后的任务不应再产生中间步骤"
def test_executor_passes_the_conversation_history_to_the_provider(tmp_path):
database = AgentDatabase(tmp_path / "agent.sqlite3")
received: list[tuple[tuple[str, str], ...]] = []
def runner(_instruction, _attachments, _toolbox, history):
received.append(history)
return "已复制到 D:\\target"
executor = TaskExecutor(database, runner)
completed = Event()
executor.task_event.connect(
lambda event: completed.set() if isinstance(event, dict) and event.get("status") == "completed" else None
)
executor.submit(
"follow-up-task",
"把它复制到 D:\\target",
history=(("user", "下载到 D:\\demo"), ("assistant", "已下载到 D:\\demo\\a.zip")),
)
assert completed.wait(5)
assert received == [(("user", "下载到 D:\\demo"), ("assistant", "已下载到 D:\\demo\\a.zip"))]
def test_image_task_never_falls_back_to_text_only_model(tmp_path):
database = AgentDatabase(tmp_path / "agent.sqlite3")
calls: list[str] = []