跳到主要内容
进行中2026年10月10日

Agent 任务执行运行时

为 Agent 的工具调用提供并发执行、超时、取消与进程组清理。按“最小复现 → 根因定位 → 修复 → 组合故障注入与变异测试”推进的运行时实验项目。

PythonasyncioLinuxpytest
状态
进行中
发布于
2026年10月10日
更新于
2026年10月29日
技术
Python · asyncio · Linux · pytest
可见性
源码未公开

一个面向 Agent Runtime / Harness 底座的任务执行器项目,重点解决 Agent 在调用本地命令与工具时的并发控制、超时、取消传播、子进程组清理与管道死锁问题。

该项目采用 AI 协作方式推进:由我负责语义边界设计、验收矩阵、故障复现与部分根因定位(并亲手编写组合故障注入的回归测试);由 Coding Agent 负责主要代码实现与变异测试工具。

解决的六个核心问题

  1. 连按两次 Esc,第二次取消不生效(多等约 1.7 秒)
    • 发现:在验收矩阵中按设计意图预期 0.6 秒退出,实测却耗时 2.31 秒。
    • 根因:Python asyncio.TaskGroup 在首次取消置 _aborting 后不再向子任务转发后续取消;而原有的单元测试直接调用底层函数,绕过了批量组合层。
    • 修复:改为由批量执行器自己等待子任务并转发每一次外部取消(内部快速失败时不重复转发),修复后实测 0.62 秒。
  2. 索引任务超时一次后再也跑不起来(锁文件残留)
    • 根因:超时后直接发 SIGKILL,工具删除锁文件的收尾逻辑没有机会运行。
    • 修复:对整个进程组先发 SIGTERM,宽限期内进程组清空即刻返回,到期仍未退出的再发 SIGKILL。
  3. 按 Esc 显示“已取消”,但后台进程仍在继续写入
    • 根因:取消异常在 await 处直接抛出,原代码只捕获了 TimeoutError,导致清理逻辑被全部跳过。
    • 修复:将进程组清理放进受 asyncio.shield 保护的独立任务;清理期间若再次收到取消则立刻将宽限期截断升级为 SIGKILL;清理结果写入记录本交给调用方后,再向外抛出取消异常。
  4. 编译失败被误记成成功,清理状态误报
    • 根因:调度层与执行器两层各自计时、各自清理,且只看是否抛出异常而不检查子进程返回码。
    • 修复:由执行器统一负责计时和清理,调度层只负责翻译结果;同时将**任务执行结果(state)与资源清理情况(cleanup)**拆成两个独立维度记录,另设兜底计时识别执行器自身的卡死。
  5. 输出量大时死锁,或后台子孙进程让读取永远等不到 EOF
    • 根因:先等进程退出再读输出会让 OS 管道写满死锁;sh -c "sleep 20 & echo ok" 这类命令的后台子进程继承了 stdout 管道写端,导致一直读不到 EOF。
    • 修复:以直接子进程退出作为命令结束依据,执行期间持续读取并只保留尾部 64 KiB;结束后清理整个进程组;若后代进程通过 setsid 逃出进程组并持有管道,最多等待 0.5 秒后放弃读取,并将 cleanup 如实报告为 INCOMPLETE。
  6. 怎么证明测试真的能抓出错误?
    • 组合故障注入:设计“从未见过的故障组合”进行测试——例如收尾很慢的工具在收尾途中放出逃逸进程(F1),以及 12 个忽略 SIGTERM 的任务分三波排队、第二波中途连按两次 Esc(F2)。
    • 变异测试:自建针对性的变异测试脚本,在 Python 3.14 下运行全部 114 个测试,45 个代码变异全部被检出(存活 0 个)。
    • 沙箱代码审查:让本地小模型(qwen2.5-coder 7B)按需求初稿编写清理逻辑,在 user + PID namespace 沙箱中运行并人工审查找出测试未能覆盖的逻辑漏洞。

当前状态与已知局限

  • 状态:v0.1 收尾中(私有仓库,面试或交流时可提供完整源码与 ADR 记录)。
  • 已知局限:
    • 使用 setsid 逃出进程组的后代进程目前只能如实报告 INCOMPLETE,计划接入 Linux cgroup v2 强制回收;
    • 纯 Python 协程工具无法像子进程那样强制终止(不可信工具必须作为子进程运行);
    • 任务记录目前保存在内存,计划在 v0.2 引入 SQLite 持久化与重启恢复。