# agent-runner **Repository Path**: raverstern/agent-runner ## Basic Information - **Project Name**: agent-runner - **Description**: Bash library for running AI agent CLI with reliability features. - **Primary Language**: Unknown - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-09 - **Last Updated**: 2026-10-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # agent-runner (Python) 一个可靠的 agent-CLI 执行层 —— 具备 **watchdog 超时、反应式重试(限流/配额类错误可换模型平替)、会话复用** —— 以 **Python 库** 与 **进程**(`python -m` 或薄 `.sh` 封装)两种形态提供,既可充当 bash 流水线的"高可靠 agent"父进程,也可作为依赖嵌入 Python 代码库。项目独立、跨平台(POSIX)、无运行时依赖 bash 或 `jq`。实现与并发设计见 **[ARCHITECTURE.md](ARCHITECTURE.md)**。 ## 两种调用形态 **库形态**(Python 上层): ```python import os, sys sys.path.insert(0, "/path/to/agent-runner-py") # 或:pip install -e . os.environ["AR_RUN_DIR"] = "/var/run/mytask" # 必填(产出根) os.environ["AR_BACKEND"] = "claude-code" # 或 "opencode";默认 claude-code from agent_runner import agent_with_retry, agent_with_retry_session_resume res = agent_with_retry("总结这份文档", "summary") # 返回 Result if res: # 多步会话:用上一步的 session_id 续接 agent_with_retry_session_resume("精修 markdown", "refined", res.session_id) ``` ### 多线程使用 模块级函数(`agent_with_retry` 等)内部委托一个 **thread-local 默认 `Runner`** —— 即:单线程调用方**零改动**即可跨线程并发使用,每线程各自独立的编排状态、staged 配置、env 快照,无跨线程竞态。 需**多 Agent 微调**或显式隔离的场景,用 `Runner(Config({...}))` —— 注入单一配置对象,**不读任何文件**(显式 Runner 完全自持;要文件底座用 `Runner(Config.from_file(config_overrides={...}))`),backend 全隔离: ```python from agent_runner import Config, Runner # 不同线程跑不同 backend / 模型 / 超时,互不干扰 r_claude = Runner(Config({"backend": "claude-code", "primary_model": "glm-5.1"})) r_oc = Runner(Config({"backend": "opencode", "primary_model": "glm-4.7", "stall_timeout": 600})) # 各自在自己的线程里调用: res = r_claude.agent_with_retry("总结这份文档", "summary") ``` 隔离的实现机制(staged 配置装配、`Popen(env=...)` 隔离快照、per-call err 句柄脱单例、线程安全 ⟹ 进程安全的推理)见 [ARCHITECTURE.md § Concurrency model](ARCHITECTURE.md#concurrency-model-multi-threaded)。`tests/test_threading.py` 端到端验证了这些保证。 **进程形态**(bash / 任意语言上层 —— 本项目作为一个"高可靠 agent"子进程): ```bash # 经 .sh 封装(自动设 PYTHONPATH,exec python -m): sid=$(agent-runner.sh new "总结这份文档" "summary") # stdout = session_id echo $? # 0 = 成功,1 = 均失败 # 轻量任务可指定降级档([ARGUMENTS] 紧跟 entry、在位置参数之前): agent-runner.sh new --tier downgrade "/fix-json $file" "$log" # 多步会话:把上一步的 sid 传给 resume agent-runner.sh resume "精修 markdown" "refined" "$sid" # 需给底层 agent 透传 flag 时,用 '--' 分隔: agent-runner.sh new "prompt" "log" -- --model x --verbose ``` 进程形态的输出通道分工: - `$?` —— 成败(0 / 1)。 - **stdout —— session_id 一行**(无则空行),供 `sid=$(...)` 捕获。 - **stderr —— 诊断**(重试 / 超时 / 资源耗尽通告,给人看)。 - 结果文本不进 stdout —— 已全量留存于 `$run_dir/.jsonl`;库形态则经 `Result.text` 给到调用方。 entry(进程形态取一):`new` / `resume` / `fork` / `agent_with_retry`(全名如 `agent_with_retry_session_new` 亦接受)。CLI 形态: ``` agent-runner.sh [--tier primary|downgrade] [session_id] [-- <透传>] ``` `[ARGUMENTS]`(`--tier`)紧跟 entry、位置参数之前;`resume`/`fork` 在 `log_name` 之后还需 `session_id`;`--` 之后的参数透传给 agent(如 `--model x`)。`--tier` 选模型档(默认 `primary`),具体模型经 config 档位解析(见下)。 ## 配置(YAML 文件 + `AR_` env) 所有配置项走同一套机制:写进配置文件,或用 `AR_` 前缀的环境变量覆盖(env 优先),硬编码默认值兜底。优先级:**`AR_` env > 配置文件 > 默认**。哪项放哪由调用方决定,agent-runner 不做规定。 配置文件为 **YAML**(`.yaml` / `.yml`;TOML / JSON / JSONC 格式已废弃——发现旧格式文件时打一行告警并不再解析)。文件除下表的运行时键外,还可携带顶层 **`modelreg:` 节**(模型注册表,见下节)。解析需要 PyYAML(已声明为依赖)。注释模板见 [agent-runner.example.yaml](agent-runner.example.yaml)。 **配置文件查找**(取第一个存在的):`$AR_CONFIG_FILE`(任意扩展名) → `./agent-runner.{yaml,yml}` → `~/.config/agent-runner/config.{yaml,yml}`。同一档里 `.yaml` > `.yml`。无配置文件也能跑(默认 + env)。 **定位**:配置文件服务**进程形态**(`agent-runner.sh` → 默认 Runner 自动 `Config.from_file()`)与模块级懒调用(`agent_with_retry` 等);Python 上层显式 `Runner(Config({...}))` 完全自持、不读任何文件——`Config()` 构造本身无文件 I/O,想叠加文件底座用 `Config.from_file(config_overrides={...})`。 | 配置 key | `AR_` env | 默认值 | 用途 | |---|---|---|---| | `backend` | `AR_BACKEND` | `claude-code` | agent 后端 | | `primary_model` | `AR_PRIMARY_MODEL` | (无) | 主档模型 id(直传 agent) | | `downgrade_model` | `AR_DOWNGRADE_MODEL` | (无) | 降级档模型 id(同上) | | `run_dir` | `AR_RUN_DIR` | (必填) | 产出根(jsonl/err/产物) | | `retry_backoff` | `AR_RETRY_BACKOFF` | `5` | 重试退避基数(秒,指数增长、上限 60;0 = 不等待) | | `skip_permissions` | `AR_SKIP_PERMISSIONS` | `false` | 跳过权限提示 | | `stall_timeout` | `AR_STALL_TIMEOUT` | `300` | 无输出多少秒后杀掉 | | `total_timeout` | `AR_TOTAL_TIMEOUT` | `0` | 硬总超时(0 = 不限) | **模型选择**:`primary_model`/`downgrade_model` 表达调用方意愿,原样透传给 agent(`--model`);配置了模型注册表时另按模型装配 staged 启动配置(见下节)。agent-runner **从不硬编码**模型名——一律来自 config/注册表。与 models.yaml 的分工:**本配置表意愿**(选哪个 agent、从哪个模型起步),**注册表管供给**(端点凭据、模型参数、平替池);两文件以 backend 名与模型 id 互为连接键,非重复配置。 **模型档选择**:主尝试默认走主档(`primary_model`)。进程形态 `agent-runner.sh new --tier downgrade …` 显式以降级档起步(库形态传 `model_tier="downgrade"`)——适合轻量任务(如 JSON 修复),降级档模型经 config 的 `downgrade_model` 解析。这是**显式起步档,不是反应式降级**:失败后的反应式换模型是注册表 `substitutable` 声明的 switch 平替(见「重试语义」),与档位正交。 **重试语义**:每次失败经自有错误分类(`engine._react`)判定一步——**瞬时错误**(空载荷〔流中断/看门狗击杀〕、网络类 connection/reset/timeout、过载、5xx,以及无法识别的未知错误)给同模型裸重试:最多 3 次、指数退避(基数 `retry_backoff` 秒,默认 5,上限 60);**限流/配额类错误**(429、rate limit、quota 等)给 "switch"——模型注册表声明 `substitutable: true` 时换 models 列表的下一个模型重试(见下节),未声明(默认)落回同模型裸重试;**明确永久错误**(unrecognized_model、invalid api key、unauthorized、4xx 参数类等)立即停止并报"错误不可重试"。有会话记录的重试沿用续接语义(优先"继续"同一会话)。 ## 模型注册表(装配层) 改走中转站后,agent 的端点凭据与模型参数需要归属。模型注册表补上这一环:上层业务只指定 `(agent, model id)`,本层从配置文件顶层 **`modelreg:` 节**(每个 agent 记一个 url + 一个 key,加各模型的 `context`/`output`;原独立的 models.yaml 已并入)读取合并,为每个 Runner 装配"信息完整"的临时配置并指定其启动 agent——落盘格式与激活方式由各 backend 自带(`stage_config` op;agent 专属知识不进注册表)—— - **claude-code**:`/agent-config/.settings.json`,env 块注入 `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN`,模型参数经 `CLAUDE_CODE_MAX_CONTEXT_TOKENS` / `CLAUDE_CODE_MAX_OUTPUT_TOKENS` 注入(上下文缺省 200K,显式声明优于依赖 API 上报);经 argv `--settings ` 生效; - **opencode**:`/agent-config/.opencode.json`,单个合成 provider(`relay`,@ai-sdk/openai-compatible,baseURL + apiKey)+ 模型条目 `limit: {context, output}`;经 env `OPENCODE_CONFIG=` 生效,裸模型 id 自动补 `relay/` 前缀(上层继续传裸 id)。 staged 文件含凭据:原子写、权限 0600、紧邻 logs 落盘——每个 Runner 一份实配,天然可审计、可拿该文件手工复跑 CLI。`` 由 `backend + model` 派生。 `modelreg:` 节内可选布尔键 `substitutable: true` 声明 models 列表内模型互为平替——限流/配额类错误(429/rate limit/quota)重试时换列表里的下一个模型(声明序,循环回绕);未声明(默认)时换模型判定落回同模型裸重试。 **无 `modelreg:` 节 = 功能关闭,行为与无此层时完全一致**;节在而条目缺(模型 / 该 agent 节 / 字段非法)→ stderr 警告 + 不装配该项,不回落不失败。Config 的 overrides 键 `modelreg` 可**整体替换**该节(`Runner(Config({"modelreg": {...}}))`)——多线程/多 Agent 场景每 Runner 各挂一份注册表。cwd 残留旧 `./models.yaml` 时打一行迁移提示。单端点假设(一个 relay 服务所有模型)是有意的。 多模态说明:两个 agent CLI 均无多模态能力配置目标(claude 原生假设视觉;opencode 的能力元数据取自 models.dev,自定义 provider 取不到),故注册表不含相关字段;将来 CLI 出能力开关时再扩展。 ## 测试 ```bash pip install pytest # 唯一开发依赖 cd agent-runner python -m pytest -q ``` - `test_backends_jq_equiv.py` —— Python 的 jsonl 解析与 bash `jq` 过滤器逐字节等价(bash 后端不存在时跳过)。 - `test_engine.py` —— watchdog 早退、反应式重试、续接 vs 重跑分支、退出码(mock backend,不起真 agent)。 - `test_poolless.py` —— 重试语义:错误分类(retry/stop/switch)、模型平替门控、退避与重试上限。 - `test_modelreg.py` —— 模型注册表:`modelreg:` 节校验、staged 配置渲染与 0600 权限、缺节/缺条目降级、引擎接线。 - `test_platform.py` —— POSIX 进程组拉起 + 树杀契约。 - `test_cli.py` —— `python -m agent_runner` 派发、参数顺序、退出码映射。 - `test_threading.py` —— 多线程隔离:per-thread staged 配置/子进程 env 隔离、err 句柄脱单例、thread-local 默认 `Runner`、`Runner(Config({...}))` 配置隔离。 ## 目录结构 ``` agent-runner/ ├── agent-runner.sh # bash → `python -m agent_runner` 封装(进程形态) ├── agent-runner.example.yaml # 配置文件注释模板(YAML,含 modelreg: 节) ├── pyproject.toml # 包元数据(stdlib + PyYAML;支持 pip -e .) ├── README.md # 本文件 — 使用指南 ├── ARCHITECTURE.md # 实现与并发设计 ├── agent_runner/ # 可 import 的包 │ ├── __init__.py # 公开 API(库形态) │ ├── __main__.py # `python -m agent_runner`(进程形态) │ ├── engine.py # 编排(Runner)+ 错误分类(_react) │ ├── config.py # Config(per-实例配置视图) │ ├── platform.py # 跨平台进程树抽象 │ ├── modelreg.py # 模型注册表(按 Runner 装配 agent 启动配置) │ └── backends/{claude_code,opencode,_jsonl}.py └── tests/ ```