Lazy loaded image
runtime项目01丨做 mini agent,先把 runtime 搭起来
字数 2723阅读时长 7 分钟
2026-1-2
type
Post
status
Published
date
Jan 2, 2026
slug
agent01
summary
tags
Agent
category
icon
password
我做这个项目的起点其实很直接:我就是想做一个 mini agent。任务输入、文件读写、命令执行和测试一旦都摆到CLI界面上,看起来就像快做完了。我再往下做才发现,难点根本不在这些动作能不能触发,而在它们什么时候能发生、发生以后系统留下什么状态、最后又靠什么判断任务完成。
所以这篇文章我想讲的就一句话:我做的是一个受各种agent启发的 mini runtime。这里说的 runtime,你可以把它理解成 agent 的执行层。它负责的是:任务现在到哪一步了,模型现在能不能动代码,这次操作要不要审批,改完以后留下什么记录,最后又怎么判断这次任务算完成。

我为什么要自己做这件事

我第一次认真意识到自己只是“会用 agent”,还谈不上“理解 agent”,是在看一类 coding agent 演示的时候。屏幕上的流程都很顺:读文件、改代码、跑测试、通过。可如果这时有人问我,它为什么不会一上来就乱改 repo,为什么这次 patch 能信,失败以后状态记在哪里,我其实答不好。这种感觉很明确,我知道自己离真正理解这类系统还差一层。所以,我做这个项目,是因为我不想一直停在“会用”这一步。
所以我故意把范围压到一个自己能端到端看清的 mini runtime,用 Python 把最核心的几层先搭出来。这样我每改一个点,都能直接看到 session 状态怎么变,approval 怎么拦,diff 和 timeline 怎么长出来,本地测试为什么过或者不过。对我来说,这种能拆开、能改、能反复核对的过程,比做一个用来割韭菜的agent壳子更有学习价值。这个项目也是我给自己搭的一块练习场。

我先把范围压到最小闭环

为什么?因为范围不先收住,项目会滑向“什么都想做一点”。
我把闭环写成了:task input -> plan mode -> todo lifecycle -> restricted tools -> approval -> diff -> local test -> event timeline。如果换成更白的话,其实就是六步:先开始任务,再把计划确认下来,然后请求工具、过权限和审批、跑本地测试,最后决定这次任务能不能收尾。这个顺序重要,因为它直接决定了一次修改能不能安全地往下走。我先保住的是“任务怎么稳稳走完”这条线。
README 里也把这一轮先不做的东西写得很清楚:子代理、worktree 隔离、MCP、plugin、skill、长期记忆、自动恢复。这也是我自己的判断:我先迁移的几层,都是直接影响任务能不能继续推进的地方。plan mode 管开闸,todo 管当前只推进哪一件事,approval 管系统给不给权,diff、timeline 和本地测试管改完以后能不能说清楚,EvalRunner 再把这条链重复跑一遍,看看它到底稳不稳。我这一轮做的,都是决定任务能不能安全落地的机制。

我差点把问题想简单了

我一开始也差点把这个系统理解成一组命令组合:能读文件、能改代码、能跑测试,事情好像就差不多了。往下做了我才发现,关键的地方其实是 session 里的状态切换、approval 的规则、以及 agent 每一步是不是还被 runtime 约束着。这个认识一变,后面的实现顺序也就跟着变了。我差点先去做了最容易展示、却没那么关键的一层。
plan mode 这个词也很容易被听轻。它在这套 runtime 里先是权限状态,然后才是计划阶段。session 刚 begin_task() 时 repo 还是只读的,只有 plan 被更新并批准以后,变更型工具才有资格继续往下走。我专门写了一条很直接的回归:计划批准前直接 write_file 会被拒绝,批准以后同样的请求才会进入人工审批。这个路径很能说明问题,因为计划不只是写下来,它还先把变更入口关住了。plan mode 最有价值的地方,是它把计划变成了权限门。

有一段我是做错了再改对的

我最早以为,只要 plan mode 能挡住开局写入,离开 plan 以后再把 file_patch 和 write_file 放进 approval,就已经够稳了。再往下做我才发现,这个想法还是太乐观了。模型完全可能拿着旧理解,直接去改一个最近根本没读过的文件。这样一来,就算审批流还在,编辑本身也可能从一开始就不准确。我漏掉的不是“要不要审批”,而是“模型是不是先把目标文件看清楚了”。
所以我后来把注意力收到了编辑上下文上,也就是改之前有没有刚读过目标文件。现在 TaskSession 里的 validate_tool_request_edit_context() 会先做这层检查;如果目标文件最近没读过,file_patch 和已有文件上的 write_file 都会先被打回去,agent 必须先去 read_file,只有新文件上的 write_file 才能直接继续走后面的审批流。对应的回归里也能看到:模型第一次直接 patch README.md 会先被判成无效请求,下一步才改成先读文件。这个改动不花哨,但它把“模型先理解目标文件,再动手改”这件事真正写进了运行时。这一段让我开始更在意 edit context,而不是只看 approval。
我现在再跑 python3 -m unittest discover -s tests -v,先看的也不是一共有多少个测试,而是两类回归有没有守住。第一类是 plan mode 还能不能挡住变更工具,第二类是 agent 会不会在没有最近文件上下文的情况下直接 patch。前者防的是权限切换失守,后者防的是模型拿旧理解动手。对我来说,这两条比一个总数更能说明我到底写出了什么。

我现在确定好了哪几层

到这里为止,我其实先把三件关键事立住了。第一件事是,这轮任务得有一本总账。TaskSession 就在做这个角色:任务输入、当前权限模式、plan、todos、pending approvals、latest diff、latest tool result、timeline,以及最近读过哪些文件、最近测挂过什么,都放在这里。这样一次任务就不会散在聊天记录和几段临时变量里。我把“任务在系统里怎么活着”这件事确定好了。
第二件事是,模型提请求和系统放权得分开。todo 在这里不是顺手写的一串备忘,它会限制同一时间只能有一个 in_progress;ApprovalPolicy 也不只是弹个确认框,它会判断这次请求是直接放行、先问人,还是直接拒绝。危险 shell 前缀直接拒绝,测试命令走 allowlist,写操作默认进审批。这样一来,任务怎么推进,不再只靠模型自己决定。我把“谁来管放权”这件事也确定好了。
第三件事是,任务结束以后得留下能回看的结果。每次工具调用以后,session 都会记下最新结果、最新 diff 和时间线,任务要结束前,还得先看当前 repo 状态有没有通过本地测试。EvalRunner 在这里更像一套专门拿来重放任务过程、看失败落在哪一类的检查器:它会按 case 把整轮任务再跑一遍,推动 agent 往下走,处理审批,再跑验证,然后把失败原因分类记下来。我更关心的是失败以后能不能一下说清楚哪里错了,比如坏补丁会被归类成 bad_patch,没先读文件就改会被归类成 edit_without_read。当前边界我也保留得很明确:README 里直接写了,会话状态现在还是内存态,服务重启以后不会自动恢复。我现在已经先把“任务做完以后怎么解释、怎么复查”这件事确定好了。
我先确定好的层
代码里现在对应什么
我现在怎么讲它
TaskSession
task input、permission mode、plan、todos、pending approvals、latest diff、timeline、recent file contexts
已实现
plan mode + todo lifecycle
计划阶段只读,todo 单一 in_progress
已实现
ApprovalPolicy
allow / ask / deny、危险 shell deny、测试命令 allowlist
已实现
diff / timeline / local test
每步留下结果,finish 前要求当前 repo 状态有成功测试
已实现
EvalRunner
case 运行、approval 处理、失败原因分类
已实现
会话重启恢复 / 子代理 / worktree
README 里仍然写为下一步
还没做

总结

这个项目到底在做什么:我想做一个 mini agent,所以我先把 plan mode、todo lifecycle、approval、edit context、diff / timeline、本地测试验证和评测闭环这几层搭了出来。既能落到 TaskSession、ApprovalPolicy、AgentRunner、EvalRunner 这些核心对象上,也能落到我刚才提到的那两条回归。
当前 session 还是内存态,重启不会恢复,子代理和 worktree 也还没做,但最核心的链路立住了。我做下来的感觉是,难的不是让模型会改代码,而是把改代码这件事放进可约束、可解释、可验证的系统里。
 
回到首页