OpenHarness 是一个 AI agent harness。它包在 LLM 外面,负责管理对话、执行工具、控制权限、加载技能,以及协调多个 agent。刚开始读源码时,我先不追每个类的实现,而是先回答一个问题:从用户按下回车,到屏幕上出现回复,一条消息到底经过了什么?

先看核心循环

例Example text
用户输入 → CLI → QueryEngine → API Client → LLM
                ↑                              ↓
                └── ToolRegistry ← tool_calls ─┘

是这条链路的中心。它一边持有对话历史、工具注册表和权限检查器,一边把当前上下文交给 。模型如果返回纯文本,这一轮就结束;如果返回 ,Runtime 就执行工具、追加结果,再进入下一轮模型判断。

图Fig. 一句话怎样穿过 OpenHarness在浏览器里模拟,不会调用真实模型Simulated in your browser; no real model is called

就绪Ready

  1. cli.py · Typer接收用户输入

    CLI 解析当前会话参数,并把文本交给 QueryEngine。

  2. QueryEngine.submit_message()装配查询上下文

    写入对话历史,带上工具注册表、权限检查器与当前设置。

  3. run_query()进入 Turn 循环

    检查最大轮数与 auto-compact,再准备本轮模型请求。

  4. API Client统一流式请求

    把内部消息格式交给所选 Provider,并持续接收模型事件。

  5. LLM选择下一步动作

    为了演示完整链路,这次模拟返回一个 tool_use。

  6. ToolRegistry解析工具请求

    根据 name 查找 BaseTool,并使用 Pydantic input_model 校验参数。

  7. Hooks + PermissionChecker在执行前设置边界

    先触发 PRE_TOOL_USE,再根据当前 Mode 与规则评估权限。

  8. BaseTool.execute()执行工具并回传证据

    工具返回 ToolResult,POST_TOOL_USE 随后触发,结果被追加到消息。

  9. run_query() · Turn 2携带工具结果再次请求

    循环把 tool_result 交回 API Client,让模型基于新证据组织回复。

  10. LLM → QueryEngine提交最终响应

    纯文本结束本次循环,并把 assistant 消息保存到历史。

一条消息从 CLI 走到 LLM

1. cli.py:建立运行环境

CLI 使用 Typer 解析命令行参数,创建 Settings 和 ,再启动 REPL。它处理的不只是一段文本,还包括这次会话应该使用哪些配置。

2. QueryEngine.submit_message():装配上下文

维护 _messages,并持有工具注册表与权限检查器。收到用户消息后,它构建 ,再调用 。

3. run_query():控制多轮判断

在 turn_count < max_turns 的前提下,循环持续做四件事:

  1. 检查 token 是否超限,需要时自动压缩历史。
  2. 调用 把当前消息发给 LLM。
  3. 收到 时执行工具,并把结果追加回消息。
  4. 收到纯文本时结束循环。

API Client 层将 Anthropic、OpenAI、Copilot 和 Codex 等 Provider 收敛为统一的流式接口。上层只需要处理流式事件,而不必在主循环里重复每个 Provider 的差异。

工具调用不是直接执行

当 LLM 返回工具调用时, 还要经过一条完整的执行链:

例Example text
HookExecutor.pre_tool_use
  → PermissionChecker.evaluate
  → tool.execute(parsed_input)
  → HookExecutor.post_tool_use

这条链路把“模型建议做什么”和“系统允许什么真正发生”分开了。LLM 只是生成候选动作;运行时仍然可以在工具执行前拦截,也能在执行后记录和修改后续行为。

五个值得记住的设计模式

工具 = BaseTool + Pydantic Model

每个工具继承 ,声明 name、description 和作为 Pydantic 模型的 ,再实现异步 execute()。注册到 后,工具可以根据输入模型自动生成 JSON Schema。

例Example python
class MyTool(BaseTool):
    name = "my_tool"
    description = "Does something useful"
    input_model = MyInput

    async def execute(self, arguments: MyInput, context: ToolExecutionContext) -> ToolResult:
        return ToolResult(output="done")

配置 = 多层合并

设置按 CLI 参数 > 环境变量 > ~/.openharness/settings.json > 默认值 的优先级合并,并在 config/settings.py 中使用 Pydantic 模型定义。

权限 = Checker + Mode

结合 default、full_auto 和 plan 模式以及路径规则,决定工具是否可以执行。Hook 仍然可以在这个过程中拦截。

Hooks = 生命周期事件

在 PRE_TOOL_USE、POST_TOOL_USE 和 STOP 等事件上运行扩展逻辑,为阻断执行或修改行为提供了固定接入点。

系统提示词 = 组装

prompts/system_prompt.py 将 base prompt、environment info、CLAUDE.md 内容与技能注入组装为最终的系统提示词。这说明 agent 得到的并不是一段固定文本,而是当前工作环境的动态投影。

读到这里,我画出的第一张地图

OpenHarness 的主循环本身并不神秘:装配上下文、请求模型、解析动作、执行工具,再把结果放回上下文。真正的工程量在这个循环周围:消息如何被限制,工具如何自描述,权限如何被检查,行为如何被 Hook 观测,以及多个 Provider 如何被统一成稳定接口。

这是我刚开始学习 OpenHarness 时画出的第一张地图。后续继续向各个模块下钻时,这条 Query Loop 会成为定位每个细节的坐标系。