Day 05 / 共 20 天 · 第 1 周收官

一次任务的完整旅程

把前四天(概念/事件/运行/配置)串成一条故事线:你下一个任务,到它被自主完成,数据在"双进程"之间怎么流动。这是第 1 周的收官,也是理解 OpenHands 全局的关键一天——今天在脑子里跑通这条旅程,下周(Day 06 起)逐行读源码时就有了地图。

📍 第 1 周 核心概念 · 你在这里
Day1 全景 Day2 Action/Obs Day3 事件流 Day4 运行 Day5 完整旅程
L01

双进程全景图

OpenHands V1 的核心是"双进程 + 事件回推"架构。先记住三个角色:

前端 frontend

你在这里下任务、看进度。通过 WebSocket 直连沙箱里的 agent-server 看实时事件。

app_server(本仓)

"管家":管会话、拉起沙箱、持久化事件、鉴权、代理转发。不亲自跑 Agent。

agent-server(沙箱容器内,外部包)

"工人":真正跑 Agent 的 Action/Observation 循环。每产生一个事件,就通过 webhook 回推给 app_server 持久化。

前端 下任务 / 看进度 app_server 管家 · 记账 agent-server 实习生 · 在沙箱干活 REST 建会话 拉起沙箱 webhook 回推事件(存档) WebSocket 直连(实时事件流) 直连求快 · 回推求稳 · 管家不碰实时推送
图注:双进程 + 两条数据通道(直连 WebSocket / webhook 回推)
一句话记住这个架构 这套架构就像生活中"带实习生做项目":app_server 是管家/组长(派活、记账、对外沟通),agent-server 是新来的实习生(在自己隔离的工位/实验室里埋头执行)。app_server 是管家,agent-server 是工人,工人在隔离的工作间(沙箱)里干活。管家负责安排工作间、记录工人干了啥;工人埋头执行;你(前端)既看管家的记录,也能开个窗户直接看工人现场。为什么这么分?为了安全(工人隔离在沙箱)和扩展(一个管家能管很多工人)
L02

① 你下任务

💡 换个视角:现在"你"就是这条任务接下来五节,请把自己想象成那条刚被发出的用户消息,跟着第一人称走完全程:「现在我是一条用户消息,我先被前端打包成一个 HTTP 请求,飞到管家 app_server 门口……管家还没给我安排实习生,我得先等它布置好工位……」——每一节我们就往前走一步,看你(这条任务)会经历什么。
📝 贯穿全篇的例子你输入的任务:帮我给项目加个 /health 接口。记住这句话,后面每一步我们都拿它当"主角"跟踪。
前端
你在聊天框输入"帮我给项目加个 /health 接口",点发送。前端 POST /api/v1/app-conversations 请求 app_server 启动一个会话(携带 repo、初始消息、要用的模型等)。
读法:一切从一个"启动会话"的 HTTP 请求开始。对应源码 app_conversation_router.py:364start_app_conversation。此刻还没有沙箱、没有 Agent,只有一个请求到达了管家。
L03

② app_server 建会话与沙箱(慢启动)

app_server
找/建沙箱:没有可复用的就 start_sandbox 拉起一个 Docker 容器(里面是 agent-server),轮询等它 /alive 就绪。
app_server
准备仓库:在沙箱里 clone 你的 repo、跑 .openhands/setup.sh、装 git hooks、加载 skills。
app_server
构建 Agent:组装 LLM + 工具集(get_default_tools)+ 系统提示,POST {agent_server_url}/api/conversations 在沙箱内真正创建会话。
app_server
状态机推进:整个过程是一个状态机 WORKING → WAITING_FOR_SANDBOX → PREPARING_REPOSITORY → RUNNING_SETUP_SCRIPT → … → READY,每一步都以进度事件反馈前端。
为什么启动要搞这么复杂的状态机? 这一步就像实习生入职第一天:得先领工位(拉容器)、把项目代码 checkout 下来(clone repo)、按 README 装好开发环境(setup.sh)、配好 git——全弄利索了才能真正开始干活。
因为"拉起容器 + clone 仓库 + 装环境"可能要几十秒。用户干等着会以为卡死了。所以 app_server 用一个启动任务状态机AppConversationStartTask),每完成一步就吐一个状态更新,前端就能显示"正在准备沙箱…""正在克隆仓库…"。把慢操作变成可见的进度,是好产品的基本功。源码在 live_status_app_conversation_service.py:361_start_app_conversation——第2周 Day 07 逐行读。
L04

③ agent-server 跑 Action/Observation 循环

agent-server
感知:把系统提示 + 你的任务 + 历史事件喂给大模型。
agent-server
决策 → Action:大模型输出"要跑 ls" → 产生一个 ActionEvent(Day 03)。
agent-server
执行 → Observation:在沙箱里真的跑 ls,输出包成 ObservationEvent,action_id 指回那个动作。
agent-server
循环:把观察结果加进历史,回到"感知"。反复,直到大模型产出 FinishAction。
读法:这就是第1周反复讲的 Action→Observation 循环,此刻真正在沙箱里转起来了。注意这一切发生在 agent-server(外部包)里——app_server 此刻只是在旁边"记账"。循环的每一步都产生事件。
一句话:这就是实习生干活的节奏 Agent 主循环就像实习生干活:先想一步(该干嘛)→ 动手做(跑命令/改文件)→ 看结果(成了没)→ 再想下一步……周而复始,直到活干完喊一声"搞定了"(FinishAction)。你会的一切"边做边试",它也这么来。
L05

④ 事件回推闭环(webhook)

🤔 痛点:实习生埋头干活,组长怎么知道进展?如果实习生(agent-server)只在自己工位闷头干,干完才交,中途出了岔子、或者你想事后复盘"它到底改了啥",组长手上一条记录都没有。工位一关(沙箱回收),过程就永远消失了。
💡 本质:回推 = 实习生每做一件事就发一条"工作汇报"就像实习生每完成一小步就在群里 @组长汇报一句,组长把每条汇报都记进项目台账。webhook 回推就是这套"边干边汇报"机制:哪怕工位事后拆了,台账(持久化事件)还在,可查、可导出、可审计。
agent-server
每产生一个事件,就 POST 到 app_server 的 /api/v1/webhooks/events/{会话id}(用启动时注入的 session_api_key 鉴权)。
app_server
on_eventwebhook_router.py:468):把事件持久化(存成 JSON 文件)、更新会话状态、后台跑回调(如自动生成会话标题)。
这个"回推闭环"是双进程架构的精髓。启动沙箱时,app_server 往容器里注入了两个值(docker_sandbox_service.py:418):① session_api_key(工人回家的钥匙);② webhook 回调地址 http://host.docker.internal:{port}/api/v1/webhooks(工人往哪汇报)。于是沙箱里的工人每干一件事,就拿钥匙敲管家的门汇报一次。管家由此拥有完整的事件记录(可查询、可导出、可审计),哪怕工作间关了记录还在。
L06

⑤ 前端实时展示

前端
前端直连沙箱里 agent-server 的 WebSocket(用会话的 conversation_url + session_api_key),实时收到每个事件。
前端
handleEventForUI:Action 来了画一张"⏳ 要跑命令"卡片;对应 Observation 来了,用 action_id 找到那张卡片原地替换成"✅ 命令输出"。
等等——前端为什么直连沙箱,不走管家? 好问题!实时性。如果每个事件都"工人→管家→前端"转一道,会有延迟。所以实时事件流(打字机效果、命令输出滚动)走"前端↔工人"的 WebSocket 直连(低延迟);而"持久化存档"走"工人→管家"的 webhook(可靠记录)。两条路各司其职:直连求快,回推求稳。历史记录(比如你刷新页面重进)则是前端走 REST 从管家 /events/search 拉。(app_server 本身不做实时推送,只有 REST 只读 + webhook 入站——这点第2周 Day 08 会确认。)
L07

为什么架构要这么"绕"

🔎 单步走查:跟着"帮我加 /health 接口"这条任务从头走到尾
步骤此刻发生什么关键状态 / 数据
① 下任务前端把你的话打包成 HTTP 请求发给管家POST /app-conversations
② 建会话/沙箱管家拉起沙箱、clone repo、装环境、建 Agent状态机 WORKING → … → READY
③ Agent 循环实习生在沙箱里想→做→看:读代码、写 health.py、跑测试一串 ActionEvent/ObservationEvent
④ 事件回推每产生一个事件,实习生 webhook 汇报给管家存档POST /webhooks/events/{id}
⑤ 实时展示前端直连沙箱 WebSocket,卡片实时更新action_id 原地替换卡片
✅ 完成Agent 产出 FinishAction,任务收工execution_status = finished

你可能觉得:一个进程全干了不好吗?为什么拆成双进程 + webhook + 直连 WebSocket?三个原因:

  • 安全:Agent 执行必须隔离在沙箱。把"跑 Agent"和"管理/网关"分开,管家进程永远不直接执行 AI 产生的命令。
  • 扩展:一个 app_server 能管理很多沙箱(很多并发会话、甚至远程/K8s 沙箱)。工人可以随意增减。
  • 职责分离:管家管"编排与记录",工人管"执行",前端管"展示"。各自能独立演进、独立部署(这也是 V1 拆包的动机,Day 01)。
浏览器连不上沙箱怎么办? 有些操作(看 git diff、下载文件)前端无法直连沙箱(网络隔离),这时就由 app_server 做服务端代理转发——前端请求管家,管家再转给工人。所以 app_conversation_router 里有大量"薄代理"端点(Day 07 会看到)。这套架构的每一处"绕",背后都是"安全 / 扩展 / 隔离"的权衡。
L08

🎓 第 1 周收官 + 动手

第 1 周(Day 01-05)你已建立完整心智

  • Day 01 OpenHands 是什么、V1 拆包架构
  • Day 02 Action / Observation:Agent 的动作与结果
  • Day 03 事件流:一切皆事件、action_id 配对、安全评估
  • Day 04 运行与配置:旋钮决定能力
  • Day 05 完整旅程:双进程 + 事件回推 + 前端直连

你现在理解了"一个任务如何被自主完成"的全局。下周(Day 06-10)进入 app_server 编排层源码——逐行读管家怎么管会话、事件、沙箱。

🎵 一句口诀记住整条旅程 "下活 → 开工位 → 循环干 → 回推账 → 直连看":你下活(HTTP 请求),管家给实习生开工位(建沙箱),实习生循环干活(Action/Observation),干一步回推汇报存档(webhook),你前端直连现场实时看(WebSocket)。五步一背,双进程架构就再也不会忘。

✋ 动手:验证这条旅程

# 1. 启动会话的入口
grep -n 'def start_app_conversation' openhands/app_server/app_conversation/app_conversation_router.py

# 2. 启动状态机 + 全流程
grep -n 'WAITING_FOR_SANDBOX\|PREPARING_REPOSITORY\|READY' openhands/app_server/app_conversation/app_conversation_models.py

# 3. 事件回推闭环
grep -n 'def on_event\|WEBHOOK_CALLBACK\|SESSION_API_KEY' openhands/app_server/event_callback/webhook_router.py openhands/app_server/sandbox/docker_sandbox_service.py
下周预告 · Day 06:进入 app_server 架构总览——顶层模块地图、FastAPI 应用装配、以及贯穿全代码库的 Injector 依赖注入机制("改环境变量就换实现"的魔法)。
← Day 04 运行 Day 06 · app_server 总览 →