# Harness 标准 —— agentic 程序如何接入 OpenProgram

> 本文档给出每个 **harness**（一个作为独立 repo 发布的自包含 agentic
> 程序）所满足的契约，使得将其克隆进 OpenProgram 的 `agentics/` 文件夹后
> 即可 **被自动检测并直接使用，无需改动 host**。三个第一方 harness
> （GUI / Research / Wiki）是参考实现；第三方遵循同样的规则。
> 相关：[`../installing-harnesses.md`](../../../capabilities/installing-harnesses.zh.md)
> （安装流程）、`openprogram/programs/_registry.py`（加载器）、
> `openprogram/programs/_programs.py`（第一方 harness 清单）。

## 0. 唯一重要的规则

**harness 就是任何你放进 `<openprogram>/programs/workflow/` 并通过
`<pkg>/agentics/__init__.py` 暴露其函数的东西。** host 在启动时（以及在
开启热重载时，每当该文件夹变化时）遍历该文件夹，找到内层 package，导入
`<pkg>.agentics`，随后 `Agent` method 装饰器自行完成注册。除此之外
无需任何东西，也不会改动任何 host 文件。

```
<Harness-Repo>/                    ← 你 git clone 进 agentics/ 的东西
└── <pkg>/                         ← 可导入的 package；文件夹名 == 导入名
    ├── __init__.py                ← 标记其为 package（可再导出一些便利项）
    └── agentics/
        └── __init__.py            ← 入口点 —— 暴露 PROGRAM_ENTRIES
```

如果克隆进来的文件夹不符合这个形状，host 会 **静默忽略它**（非 harness
的文件夹绝不能破坏加载）。所以"自动检测" == "符合此契约"。

## 1. 入口点：`<pkg>/agentics/__init__.py`

必须定义 `PROGRAM_ENTRIES` —— 一个由 `Agent` method 装饰的可调用对象
组成的列表。导入此模块会触发装饰器，将这些函数注册进共享的工具 registry。
该列表同时也是 harness 声明的公开接口面。

**必需模式 —— 优雅降级，绝不让 host 崩溃：**

```python
# <pkg>/agentics/__init__.py
"""Entry point for the Foo harness."""
try:
    from .foo_agent import foo_agent, foo_helper
    PROGRAM_ENTRIES = [foo_agent, foo_helper]
except Exception:          # 缺失可选依赖、不支持的平台，等等
    # host 在发现阶段会导入此模块。如果该 harness 无法在本机加载
    #（某个重依赖未安装、本 OS 没有对应 backend，等等），就什么都不
    # 暴露，而不是抛异常 —— 按发现契约："能跑 → 列出，不能跑 → 跳过"。
    # host 会在 OPENPROGRAM_DEBUG_REGISTRY=1 下记录原因。
    PROGRAM_ENTRIES = []
```

规则：
- `PROGRAM_ENTRIES` **必须始终被定义**（哪怕是 `[]`）。
- `try/except` 是 **强制的** —— 无法在本机运行的 harness 必须产出 `[]`，
  而不是把 `ImportError` 传播出去。正是这一点让"什么都能克隆进来、能跑的
  亮起、其余的保持熄灭"得以成立。
- 不要在导入期做繁重的工作（不下载模型、不联网、不抓取 GUI）。导入必须
  廉价，且除注册外无副作用。

## 2. package：`<pkg>/`

- **文件夹名 == 导入名。** `gui_harness/` 以 `gui_harness` 被导入。host 会把
  harness 根目录放上 `sys.path`，使 harness 自己的绝对导入
  （`from gui_harness.x import y`）能够解析。带连字符的 *repo* 文件夹
  （`GUI-Agent-Harness/`）没问题 —— host 会在其内部找到那个 ascii 标识符
  package。
- 可以内置（vendor）同级 package（例如 GUI harness 在 `gui_harness/` 旁边
  附带 `desktop_env/`）。host 会挑选 **唯一一个** 带有 `agentics/` 子 package
  的 package 作为入口；被内置的同级 package 在发现阶段被忽略，只是随之搭车
  上了 `sys.path`。

## 3. 配置 —— 统一约定

各 harness 在配置内容上千差万别（Research：providers + 工作目录；Wiki：一个
vault 路径；GUI：vision 模型 + 平台 backend）。**标准规定的是机制，而不是
具体参数**：

1. **优先使用按调用传入的参数。** 任何随每次调用而变化的东西，都作为带有
   合理默认值的函数参数：
   ```python
   from openprogram import Agent

   class HarnessAgent(Agent):
       method_options = {"foo_agent": {"tool": True, "name": "foo_agent"}}

       def foo_agent(self, task: str, max_steps: int = 15) -> dict:
           ...
   ```
2. **runtime 来自 host，绝不自行构造。** harness 方法在到达它的那次调用的
   Runtime（即 host 的）上运行，用 `llm()` / `agent()` 发起模型请求，不声明
   `runtime` 参数。第二个模型（比如评审）是在运行入口配置的第二个 Agent
   实例。对于 in-host 路径，harness
   **绝不能** 自己构建 provider / 自己读取 `ANTHROPIC_API_KEY` —— provider
   选择与鉴权归 host 管。（harness *可以* 保留一个独立的 CLI，调用
   `create_runtime(...)` 以便在 host 之外使用；那与 in-host 入口点是分开的。）
3. **机器级设置使用环境变量**，以 harness 自己的前缀作命名空间，并在 README
   中记录：
   ```
   WAH_VAULT          # Wiki：vault 根目录
   GUI_AGENT_MEMORY   # GUI：已学习组件的存储
   ```
4. **启动不需要任何配置文件。** harness 必须在零配置下可用（具备合理默认
   值）；配置是可选的覆盖项，绝不是一道启动门槛。不得有强制向导 / 首次运行
   提示。
5. **状态 / 临时数据放在 host 状态目录下**，而不是手搓的 home 路径。以
   `openprogram.paths.get_state_dir()`（→ `~/.openprogram/`）为基准；
   **绝不硬编码 `~/.agentic/...`**（该路径已废弃）。约定是为每个 harness 用
   一个子目录：`get_state_dir() / "harnesses" / "<pkg>"`。

## 4. 依赖

- **不要把 `openprogram` 声明为 git 依赖。** host 本身就是导入 harness 的那
  一方，所以它已经安装好了。harness 的 `pyproject.toml` 里的一行
  `openprogram @ git+https://...` 会导致 `pip install <harness>` 重新拉取、
  甚至可能降级 host。只把 openprogram 作为一个有文档说明的 *前提假设* 列出
  （"安装到一个已经有 openprogram 的环境里"），而不是一个硬依赖。
- **声明 harness 自己的第三方依赖**（torch、Jinja2，等等）于其
  `pyproject.toml` / `requirements.txt`。安装这些是 **harness 自己的** 安装
  步骤（在用户安装该 harness 时执行），不归 OpenProgram 管 —— host 绝不
  自动安装 harness 的依赖。
- **重 / 原生依赖放在一个 extra 之后**，让轻量克隆保持轻量：
  ```toml
  [project.optional-dependencies]
  ocr = ["easyocr"]
  ```
- §1 中那个优雅降级的 `try/except` 正是让 harness 能在其依赖装好之前就被
  *克隆* 进来而不破坏 host 的关键 —— 它只是保持熄灭，直到依赖到位。

## 5. 平台支持

- harness 在自己的代码里 *可以* 是平台特定的（GUI harness 驱动桌面；它的
  macOS / Linux backend 不同，Windows 可能尚未实现）。这是被允许的。
- **用 `PROGRAM_ENTRIES = []` 来表达"此处不支持"**，通过 §1 的
  `try/except` 或一个显式的 `platform.system()` 检查 —— 绝不要在导入期抛出
  未捕获的 `NotImplementedError`。安装 / 注册始终成功；某个函数是否被 *列出*
  反映的是它能否在本 OS 上运行。
- 在运行时检测 backend（`shutil.which`、`importlib.util.find_spec`），不要
  想当然。在 harness 的 README 中记录各 OS 的设置。

## 6. 发现与热重载（host 端 —— harness 可以依赖的东西）

- **启动：** host 导入 `agentics/` 下每个匹配的文件夹。
- **热重载：** host 监视 `agentics/`，当出现一个新文件夹时，对其运行同样的
  发现流程，并广播一个 `programs:changed` 事件，使 web UI 无需重启即可列出
  新 harness。harness 不需要做任何特殊处理 —— 满足 §1 即可。
- **第一方清单：** GUI / Research / Wiki 还被列在 `_programs.py` 中，于是
  `openprogram programs install <name>` 能按名字把它们克隆下来。第三方
  harness 直接通过克隆进 `agentics/` 来安装；发现流程对它们一视同仁。

## 7. 合规清单（供 harness 作者使用）

- [ ] repo 克隆进 `agentics/<Repo-Name>/`；其内部有一个 package
      `<pkg>/`，其文件夹名等于其导入名。
- [ ] `<pkg>/agentics/__init__.py` 定义 `PROGRAM_ENTRIES = [...]`。
- [ ] 该模块把其导入包在 `try/except` 中 → 失败时为 `[]`。
- [ ] 导入廉价：导入期不联网 / 不下载模型 / 不抓取 GUI。
- [ ] LLM 访问通过被注入的 `runtime` 参数，而非自建。
- [ ] 无必需配置文件 / 向导；在零配置默认值下可用。
- [ ] 状态置于 `get_state_dir()` 下，绝不用 `~/.agentic` 或其它硬编码的
      home 路径。
- [ ] `pyproject.toml` 中 `openprogram` 不是 git 依赖。
- [ ] 声明了自己的第三方依赖；重 / 原生依赖放在一个 extra 之后。
- [ ] 平台不支持 → `PROGRAM_ENTRIES = []`，而非崩溃。

## 附录：实现状态

三个第一方 harness 尚未完全符合本标准，剩余差距如下：

| Harness | 符合程度 | 待补齐的差距 |
|---|---|---|
| **Wiki** | 最接近 | `agentics/__init__.py`、`PROGRAM_ENTRIES`、`try/except` 均已具备。默认 vault 路径仍用着已废弃的 `~/.agentic/memory/wiki`，应移至 `get_state_dir()` 下。 |
| **GUI** | 部分符合 | 暴露了函数，但没有单一的、带 `PROGRAM_ENTRIES` 的 `<pkg>/agentics/__init__.py`；装饰器散落在多个模块里，仍需补上入口模块。重依赖已部分放在 extra 之后。Windows 路径需降级为 `[]`，而非崩溃。 |
| **Research** | 不符合 | 没有 `agentics/` 子 package —— 它使用自己的 `registry.py`，host 的自动发现看不到。需补上 `research_harness/agentics/__init__.py` 暴露 `PROGRAM_ENTRIES`。 |

三者都仍把 `openprogram @ git+…` 声明为依赖，而 §4 排除了这种做法；去掉它
是一个共通的修复。
