__ _ _ / _| ___ _ __(_) | | |_ / _ \| '__| | | | _| (_) | | | | | |_| \___/|_| |_|_|
这次 retrofit 的目标,不是再做一个“能跑浏览器任务”的演示层,而是把 browser-use 改造成一个可被 Agent 直接调用的浏览器子代理能力。换句话说,旧能力更像一个低层的浏览器任务原语,负责把一段浏览器操作跑起来;改造后的能力,则需要在控制平面、运行平面、事件协议、人工介入、恢复语义和可观测性上补齐完整链路,才能真正接入更上层的 Agent 系统。
这篇文章记录的是一次典型的“原语升级”。它没有引入一个通用工作流引擎,也没有重写浏览器执行内核,而是沿着最小边界,把原来的 playground 友好型执行能力,改造成了面向 Agent 的 browser sub-agent。
最开始的 browser-use 更接近一个低层任务执行器。它擅长接收输入,然后在浏览器里逐步执行动作,但它缺少几个对上层系统很关键的能力。
第一,状态不够稳定。任务跑到哪一步、当前是否阻塞、是否进入等待人工介入,这些信息原本更偏向运行时日志,而不是可以被系统持续消费的状态模型。对 playground 来说,这通常够用,但对 Agent 来说不够,因为 Agent 需要知道任务能不能继续、要不要切换策略、什么时候可以安全恢复。
第二,恢复语义不清晰。旧模型更像“重新开跑”或“继续执行”,但没有明确的安全点边界。对于浏览器任务来说,任意时刻恢复并不安全,特别是涉及表单提交、页面跳转、cookie 变更或外部副作用时,盲目续跑很容易造成重复操作或状态错位。
第三,人工介入没有标准化。浏览器任务一旦需要人来点一下、确认一下、补一段信息,系统需要能明确表达“我现在在等人”,而不是只靠某种自定义日志来暗示。更进一步,人工输入回来后,系统要能验证这份输入是否对应当前安全点,是否还能继续。
第四,事件形状不统一。旧 playground 可以接受比较自由的输出格式,但一旦要接入更大的系统,事件就必须被规范成稳定协议,否则上层消费方会被不同版本、不同字段、不同命名拖慢。
所以这次 retrofit 的本质,不是加功能,而是把原本偏执行器的能力,升级成一个可调度、可恢复、可回放的浏览器子代理。
这次方案里,最核心的选择是 sub-agent-first,加一个很薄的 skill wrapper。
原因很简单。浏览器任务天然是“带上下文的执行过程”,它不是一个纯函数,也不是一次性 API 调用。上层系统真正需要的,是一个有生命周期、可中断、可恢复、可观测的子代理单元。把 browser-use 作为 sub-agent,意味着它能以完整任务实体存在,而不是被压扁成一串零散动作。
thin skill wrapper 的作用,则是把这个能力暴露成上层容易调用的入口,同时尽量不把业务逻辑塞进 wrapper 里。这样做有两个好处。
一是边界清楚。wrapper 只负责参数入口、会话挂接和最基本的能力暴露,真正的执行、恢复、事件流和人工交互,都放在 sub-agent 和运行时里。这样能避免上层入口变成一个越来越胖的“万能适配层”。
二是兼容性好。原来的 playground 用户路径并没有被彻底推翻。thin wrapper 让旧的提交方式还能继续工作,至少在基础任务提交上不会立刻断掉,同时新能力可以通过 v1 session 协议逐步接入。这个过渡方式,比一次性切换所有消费方更稳。
说白了,这不是把 browser-use 包成一个更花哨的壳,而是让它成为 Agent 架构里的一个可管理节点。
这次 retrofit 里最重要的架构拆分,是把 Node ecos-server 和 Python runtime 明确分工。
Node 侧不再参与具体浏览器动作执行,而是负责所有控制平面工作:
这样做的好处是,Node 侧只需要维护“任务是什么、现在到哪了、还能不能继续、如何回放”,而不需要知道浏览器细节。控制面一旦收敛,就更适合做稳定协议、状态机和对外 API。
Python 侧则保留运行时的复杂性,负责真正的浏览器执行:
这个拆分的价值在于,运行时的复杂性很高,但它不应该污染控制面。浏览器执行、页面状态、cookie 和人工中断都属于 runtime 关心的问题,把它们留在 Python 侧,能让控制平面保持更稳定的语义。
为了让上层系统真正“看得懂” browser-use,retrofit 做了统一的实时事件输出。对外的公开事件 taxonomy 只有五类:
stateprogressinterruptresulterrorping这套 taxonomy 的重点不在于种类少,而在于语义稳定。state 用来表达会话级状态变化,progress 用来表达执行进度,interrupt 用来表达需要人工介入的阻塞点,result 表达最终结果,error 表达失败,ping 则用于连通性和心跳。
Node 侧负责把来自 runtime 的原始事件归一化成 v1 协议,再对外输出。这里的“归一化”很关键,因为 runtime 内部可以有很多细碎事件,但对外接口不能跟着内部实现一起抖动。上层系统只应该面对稳定的 session 视图,而不是每次升级都要重新解析一套新的日志字段。
这也是为什么事件不能只是“打日志”。日志适合人看,事件协议适合系统看。两者不是一回事。
浏览器任务真正难的地方,往往不是自动化动作,而是中途需要人接手。比如需要登录确认、验证码处理、表单补全,或者页面信息不完整,需要用户补一段输入。
这次改造里,HITL 采用了 waiting_human 加 respond / resume / cancel 的命令模型。这个设计有两个要点。
runtime 一旦检测到需要人类介入,就发出 waiting_human interrupt,而不是默默挂起。这样控制面能立刻把任务标记成等待状态,上层也知道此时任务不是失败,只是阻塞。
v1 的 resume 不是“任意时刻续跑”,而是只允许 safe-point 恢复。这不是保守,而是必要。浏览器任务的中间状态经常不稳定,尤其是在点击、跳转、输入和 cookie 变化之后。只有在预定义的安全点,系统才知道当前状态足够稳定,继续执行不会重复副作用或破坏上下文。
这意味着 resume 不是一种“时间回溯”,而是一次“从安全点继续”。这套语义虽然更严格,但换来的是确定性。
很多浏览器自动化系统的问题,不出在执行,而出在恢复。中断之后,系统到底能不能准确判断“我已经执行过什么,我现在该从哪继续”,这是恢复能力的核心。
这次 retrofit 里,Python runtime 实现了 safe-point dedupe。它的目的,是避免同一个安全点被重复消费,避免因为重放、重试或重复通知导致状态错乱。配合 v1 resume only safe-point 的规则,系统就能把恢复语义收束在有限边界里。
这里的 tradeoff 也很明确。我们放弃了“看起来更灵活”的任意中断续跑,换来的是更强的可预期性。对浏览器自动化来说,这通常是更正确的选择,因为副作用场景太多,灵活并不等于安全。
浏览器任务常常不是短命任务。一个会话进入 waiting_human 后,不能继续占着执行槽位不放,否则队列会被阻塞,后续任务也没法进入。
所以这次设计里加入了 queue slot release/reacquire 语义。简单说,任务在进入人工等待时,运行侧要释放占用的队列槽位,让系统资源回到可调度状态;当用户完成输入、任务准备恢复时,再重新获取槽位,继续后续执行。
这套机制解决的是资源公平和调度效率的问题。没有释放机制,长时间等待会拖住整个队列。没有重新获取机制,恢复时又无法保证任务能回到受控执行路径。两者一起,才能让 HITL 不把系统拖死。
这次 retrofit 里,另一个很重要的能力是 observability and replay。对浏览器任务来说,可观测性不只是为了“出了问题好查”,更是为了“知道任务为什么走到了这一步”。
Node 侧的 durable run registry 保存了运行的关键元数据,配合事件流,就能把一次任务的生命周期串起来。这样做的价值有三个:
第一,可以看状态。任务当前是 running、waiting_human、completed 还是 error,一目了然。
第二,可以回放。事件和状态都有统一入口后,排障不再依赖碎片化日志,而是可以沿着 session 去重建路径。
第三,可以做兼容判断。旧 playground 和新 v1 协议并行期间,回放机制能帮助确认哪些消费方还依赖旧事件形状,哪些已经适配了新协议。
这里不是引入一个重型审计系统,而是把“任务发生了什么”这件事结构化了。对 Agent 场景来说,这很重要,因为上层系统需要在失败、恢复、人工介入之间做策略判断,而不是只看到最终成功或失败。
retrofit 完成后,旧 playground 大概率还能继续做基础任务提交,这一点很重要,因为它给了消费方一个缓冲期。但也要说清楚,依赖旧事件形状或旧 resume 假设的消费方,需要迁移到 v1 session 协议。
这不是向后破坏,而是协议演进。旧 playground 适合“发任务、看结果”的简单路径,新能力则要求更严格的 session 视角。只要消费方还把 browser-use 当成一个“立刻返回的执行函数”,它就会和新的中断、恢复、事件协议产生摩擦。
所以兼容策略不是伪装成没变化,而是保留基础提交路径,同时明确新语义的边界。这样最容易让系统平稳过渡。
回头看,这次 retrofit 里最有价值的,不是某一个具体接口,而是几项基础决策。
首先,把 browser-use 定义为 Agent 级子代理,而不是普通任务接口。这让它拥有了状态、恢复和事件边界。
其次,把控制平面和运行平面分开。Node 侧聚焦协议、注册、命令和调度,Python 侧聚焦执行、验证和中断,避免职责混杂。
再次,把事件协议收敛成稳定 taxonomy。上层消费方不再追着内部日志变动跑。
然后,把 HITL 做成状态机的一部分,而不是例外分支。等待人工、响应、恢复、取消,都是正常生命周期,不是补丁逻辑。
最后,把恢复限制在 safe-point。这条看起来保守,但它是保证一致性和避免重复副作用的关键。
这些决定的共同点,是都在收缩不确定性。浏览器自动化最怕的不是慢,而是状态不清、恢复不稳、协议乱跳。retrofit 的目标,就是把这些不确定性一个个收回来。
这次改造完成后,验证结果是清楚的:
这些结果说明,控制平面和运行平面的改动都已经通过了基础验证,协议、状态流转和运行时行为没有破坏现有可用路径。
这次 browser-use retrofit 的意义,不只是“让浏览器任务更像 Agent”。更准确地说,它把一个原本偏低层的浏览器原语,改造成了一个具备明确控制边界、稳定事件协议、人工介入能力和安全恢复语义的子代理能力。
如果说旧版本更适合 playground,那么新版本更适合进入真正的 Agent 调度体系。它不追求把所有问题一次性解决,而是把该收束的边界收束起来,把该稳定的协议稳定下来。对于浏览器自动化这种天然状态复杂的场景,这种工程取向,往往比“更聪明”的实现更重要。