进行中2026年10月10日
Agent 任务执行运行时
为 Agent 的工具调用提供并发执行、超时、取消与进程组清理。按“最小复现 → 根因定位 → 修复 → 组合故障注入与变异测试”推进的运行时实验项目。
PythonasyncioLinuxpytest
- 状态
- 进行中
- 发布于
- 2026年10月10日
- 更新于
- 2026年10月29日
- 技术
- Python · asyncio · Linux · pytest
- 可见性
- 源码未公开
一个面向 Agent Runtime / Harness 底座的任务执行器项目,重点解决 Agent 在调用本地命令与工具时的并发控制、超时、取消传播、子进程组清理与管道死锁问题。
该项目采用 AI 协作方式推进:由我负责语义边界设计、验收矩阵、故障复现与部分根因定位(并亲手编写组合故障注入的回归测试);由 Coding Agent 负责主要代码实现与变异测试工具。
解决的六个核心问题
- 连按两次 Esc,第二次取消不生效(多等约 1.7 秒)
- 发现:在验收矩阵中按设计意图预期 0.6 秒退出,实测却耗时 2.31 秒。
- 根因:Python
asyncio.TaskGroup在首次取消置_aborting后不再向子任务转发后续取消;而原有的单元测试直接调用底层函数,绕过了批量组合层。 - 修复:改为由批量执行器自己等待子任务并转发每一次外部取消(内部快速失败时不重复转发),修复后实测 0.62 秒。
- 索引任务超时一次后再也跑不起来(锁文件残留)
- 根因:超时后直接发
SIGKILL,工具删除锁文件的收尾逻辑没有机会运行。 - 修复:对整个进程组先发
SIGTERM,宽限期内进程组清空即刻返回,到期仍未退出的再发SIGKILL。
- 根因:超时后直接发
- 按 Esc 显示“已取消”,但后台进程仍在继续写入
- 根因:取消异常在
await处直接抛出,原代码只捕获了TimeoutError,导致清理逻辑被全部跳过。 - 修复:将进程组清理放进受
asyncio.shield保护的独立任务;清理期间若再次收到取消则立刻将宽限期截断升级为SIGKILL;清理结果写入记录本交给调用方后,再向外抛出取消异常。
- 根因:取消异常在
- 编译失败被误记成成功,清理状态误报
- 根因:调度层与执行器两层各自计时、各自清理,且只看是否抛出异常而不检查子进程返回码。
- 修复:由执行器统一负责计时和清理,调度层只负责翻译结果;同时将**任务执行结果(
state)与资源清理情况(cleanup)**拆成两个独立维度记录,另设兜底计时识别执行器自身的卡死。
- 输出量大时死锁,或后台子孙进程让读取永远等不到 EOF
- 根因:先等进程退出再读输出会让 OS 管道写满死锁;
sh -c "sleep 20 & echo ok"这类命令的后台子进程继承了 stdout 管道写端,导致一直读不到 EOF。 - 修复:以直接子进程退出作为命令结束依据,执行期间持续读取并只保留尾部 64 KiB;结束后清理整个进程组;若后代进程通过
setsid逃出进程组并持有管道,最多等待 0.5 秒后放弃读取,并将cleanup如实报告为INCOMPLETE。
- 根因:先等进程退出再读输出会让 OS 管道写满死锁;
- 怎么证明测试真的能抓出错误?
- 组合故障注入:设计“从未见过的故障组合”进行测试——例如收尾很慢的工具在收尾途中放出逃逸进程(F1),以及 12 个忽略
SIGTERM的任务分三波排队、第二波中途连按两次 Esc(F2)。 - 变异测试:自建针对性的变异测试脚本,在 Python 3.14 下运行全部 114 个测试,45 个代码变异全部被检出(存活 0 个)。
- 沙箱代码审查:让本地小模型(
qwen2.5-coder 7B)按需求初稿编写清理逻辑,在user + PID namespace沙箱中运行并人工审查找出测试未能覆盖的逻辑漏洞。
- 组合故障注入:设计“从未见过的故障组合”进行测试——例如收尾很慢的工具在收尾途中放出逃逸进程(F1),以及 12 个忽略
当前状态与已知局限
- 状态:
v0.1收尾中(私有仓库,面试或交流时可提供完整源码与 ADR 记录)。 - 已知局限:
- 使用
setsid逃出进程组的后代进程目前只能如实报告INCOMPLETE,计划接入 Linuxcgroup v2强制回收; - 纯 Python 协程工具无法像子进程那样强制终止(不可信工具必须作为子进程运行);
- 任务记录目前保存在内存,计划在
v0.2引入 SQLite 持久化与重启恢复。
- 使用