设计¶
骨架按「能换、能拦、死不了」拆,而不是按功能堆文件。
原则¶
- Loop 不知道工具怎么实现。 它只认
LLMClient.complete和ToolRegistry.dispatch。换模型、加工具,都不改loop.py。 - 工具只返回字符串。 截断、未知工具、执行失败都变成文本,塞进
role=tool消息。异常不能把整轮打死。 - 可变操作和只读操作分开。
ToolSpec.mutating给许可闸门看,不给模型看。 - 路径必须过
Workspace。 沙箱集中在一处,比每个工具自己open(user_path)可靠。 - 配置来自环境变量。 以后要加
harness.toml,从config.py长出去即可。 - System prompt 每次重建。 会话里不存 system。
AGENTS.md和匹配到的 skills 在每一轮complete前重新装配。
一次请求怎么走¶
app.py 读入用户文本,匹配 skills,落盘 session
│
▼
Session 追加 role=user
│
▼
┌────────────────── AgentLoop.run ──────────────────┐
│ 必要时 compact(按上次 prompt_tokens) │
│ 重建 system(AGENTS.md + skills) │
│ LLM.complete([system, *transcript], tool_schemas) │
│ │ │
│ ├─ 没有 tool_calls → 结束(stopped=model) │
│ └─ 有 tool_calls │
│ │ │
│ ▼ │
│ PermissionGate.allow │
│ │ 拒绝 → 把 "permission denied" │
│ │ 当工具结果写回 │
│ ▼ 允许 │
│ ToolRegistry.dispatch │
│ │ 异常 → "tool error: ..." │
│ ▼ │
│ Session 追加 role=tool │
│ └──────── 下一轮 complete │
└───────────────────────────────────────────────────┘
停止条件:模型不再调工具、max_turns、或 Ctrl+C(cancelled)。
on_event 是 loop 对外的唯一 UI 钩子。TUI、REPL、以后的 Web,都应该听事件,而不是改 loop。
仓库地图¶
src/harness/
app.py CLI,按名字选 UI
runtime.py 会话 / 命令 / submit()
loop.py 心脏
tui.py Textual
ui/ 前端协议 + 注册表
session.py 一段对话(不含 system)
store.py .harness/sessions/
context.py AGENTS.md + skills → system
compact.py 按 token 预算压缩
llm/ complete(messages, tools, on_delta=None)
tools/ 一个文件一个工具
prompts/system.md 身份