数据截至 (上游 commit 1916c9046c4e)
会话生命周期:从「发送」到事件流
这是全仓库工程含量最高的一章。你敲一句话点「发送」之后,前端到底给 Agent Server 递了什么、事件又怎么流回来?本章按时间顺序走一遍。记住 pivot 前提:会话的执行全在外部 Agent Server 里,本章讲的是前端怎么发起、连接、对话。
0. 全景:五个阶段
① 组装 ② 创建 ③ 连接 ④ 对话 ⑤ 运行中操作
拼 start 请求 POST 建会话 REST 补历史 WS 发消息 暂停/压缩/分叉
────────────► 同步 READY ─────────────► ◄── WS 事件流 切模型/确认
(local) 开 WebSocket (思考/工具/diff)
下面逐阶段展开。
1. 组装:start 请求里装了什么
入口是 useCreateConversation hook(src/hooks/mutation/use-create-conversation.ts:62),它先做一件容易被忽略的事:解析当前激活的 Agent Profile——通过 TanStack Query 缓存等 AgentProfilesService.listProfiles 就绪(src/hooks/mutation/use-create-conversation.ts:103-107),因为「从哪个 profile 启动」必须在建会话前定下来 。
真正的组装在 buildStartConversationRequestWithEncryptedSettings(src/api/agent-server-adapter.ts:1254-1293)。它并行取三样东西,再交给 buildStartConversationRequest(src/api/agent-server-adapter.ts:1050)拼 payload:
| 取什么 | 从哪 | 为什么 |
|---|---|---|
| 加密后的设置 | SettingsService.getSettingsForConversation(src/api/settings-service/settings-service.api.ts:486) | LLM key 等秘密以密文形式过浏览器,服务端再解密 |
| 自定义 secrets 名单 | SecretsService.getSecrets | 拼成 LookupSecret 引用(见下) |
| 运行时服务信息 | fetchBackendRuntimeServicesInfo(src/api/agent-server-adapter.ts:175-196) | 渲染成 system prompt 后缀,告诉 agent 栈内有哪些服务可访问 |
拼出来的 payload 骨架(示意,非源码;真实字段见 src/api/agent-server-adapter.ts:1085-1132):
// 示意,非源码
const payload = {
agent_profile_id, // 与 agent_settings 二选一(互斥)
agent_settings, // 内联 agent 配置(LLM/工具/agent_context)
workspace: { kind: "LocalWorkspace", working_dir },
client_tools, // 前端注册给 agent 回调的工具(见 04 章)
confirmation_policy, // 动作确认策略
max_iterations: 500,
stuck_detection: true,
autotitle: true,
worktree: true,
secrets, // { 名字: LookupSecret } 引用,不含明文
tags: { clientsource: "agentcanvas", ... },
initial_message, // 你的第一句话
};
几个值得停一下的设计点:
- agent 来源二选一。
agent_profile_id与agent_settings互斥(src/api/agent-server-adapter.ts:1107-1109):给了 profile 就由服务端解析配方,否则前端把当前设置内联进agent_settings。 - 内联设置分两种构建器。 OpenHands agent 走
buildConfiguredOpenHandsAgentSettings(强制llm.stream = true以开 token 流,src/api/agent-server-adapter.ts:895-897);ACP agent 走buildConfiguredAcpAgentSettings(src/api/agent-server-adapter.ts:817-881),带上acp_command、acp_model等 ACP 专有字段。 - 工具清单是前端定的。 默认三件套
terminal、file_editor、task_tracker(src/api/agent-server-adapter.ts:113),浏览器工具集和子 agent 工具集按开关与服务端能力追加(src/api/agent-server-adapter.ts:631-644的shouldIncludeTool)。 - 确认策略由设置推导。
confirmation_mode开 + LLM 安全分析器 →ConfirmRisky(只拦高风险);开但没分析器 →AlwaysConfirm;没开 →NeverConfirm(src/api/agent-server-adapter.ts:593-605的getConversationConfirmationPolicy)。 - secrets 只递引用,不递明文。 每个 secret 变成一个
LookupSecret——一条指向 agent-server 自己 secrets 端点的 URL,由服务端在 spawn agent 时回查解析(src/api/agent-server-adapter.ts:1208-1228)。明文从不进 start 请求。 - 技能在
agent_context.skills里随请求带走,并附带<RUNTIME_SERVICES>系统提示后缀(src/api/agent-server-adapter.ts:749-788的buildAgentContext)。技能加载是第 04 章的主题。
2. 创建:一次 POST,local 同步、cloud 异步
AgentServerConversationService.createConversation(src/api/conversation-service/agent-server-conversation-service.api.ts:403)按后端种类分两条路。
local 路径(src/api/conversation-service/agent-server-conversation-service.api.ts:449-526):
- 生成会话 id:
uuidv4()(src/api/conversation-service/agent-server-conversation-service.api.ts:457)。 - 定工作区:默认
workspace/project/<会话id的hex>(src/api/agent-server-config.ts:203-207的buildConversationWorkingDir),再经resolveAbsoluteAgentServerPath解析成 agent-server 主机上的绝对路径(src/api/agent-server-home.ts:102-114)——它用/api/file/home拿到服务端 home 目录再拼接,避免相对路径被写成根目录下的只读路径。 - 拼 start 请求(上一节),用
ConversationClient.createConversationPOST 出去(src/api/conversation-service/agent-server-conversation-service.api.ts:489-491)。这一步的超时被放宽到 5 分钟(src/api/conversation-service/agent-server-conversation-service.api.ts:79),因为冷启动的 agent-server 可能还在预热。 - local agent-server 同步建会话,返回即 READY,无需轮询(
src/api/conversation-service/agent-server-conversation-service.api.ts:535-538注释)。 - 把 repo/分支/工作区等 UI 侧元数据写进浏览器 localStorage(
src/api/conversation-service/agent-server-conversation-service.api.ts:495-509的setStoredConversationMetadata)——agent-server 不认识这些概念,存在前端(src/api/conversation-metadata-store.ts:9-42的ConversationMetadata)。
cloud 路径(src/api/conversation-service/agent-server-conversation-service.api.ts:420-447):拼一个扁平的 AppConversationStartRequest POST 给云端 /api/v1/app-conversations,返回一个 WORKING 状态的任务,前端轮询到 READY(沙箱要现开)。秘密都在服务端,所以没有加 密设置那一趟。
3. 连接:先 REST 补历史,再 WebSocket 订增量
会话页打开后,前端要拿到两条事件流:已经发生的历史 + 正在发生的实时。
历史走 REST。 EventService.searchEvents 分页拉事件(src/api/event-service/event-service.api.ts:102-181)。
实时走 WebSocket,但连接时机有讲究:wsUrl 要等历史拉取settle 之后才算出来(src/contexts/conversation-websocket-context.tsx:392-406)。为什么?因为 socket 建连时要把 after_timestamp 烧进 URL,订「只发我没见过的」增量:
- 有历史 →
resend_mode: "since"+ 最后一条事件的时间戳; - 全新会话 →
resend_mode: "all"兜底,重叠部分由事件 store 去重。
这段逻辑在 mainWebsocketOptions(src/contexts/conversation-websocket-context.tsx:963-975)。URL 本身由 buildWebSocketUrl 拼出(src/utils/websocket-url.ts:109),指向 agent-server 的 /sockets 端点(经 ingress 分流,见第 01 章)。
WebSocket 怎么鉴权? 不是 header——浏览器 WS API 设不了自定义头——而是连上后发第一帧 auth 消息:useWebSocket 在 onopen 里调 sendWebSocketAuth,把 session key 作为 JSON 帧发出(src/hooks/use-websocket.ts:67-74;帧格式见 src/utils/websocket-auth.ts:4-16)。
事件落 地。 每条事件经类型守卫分拣后写进 Zustand 的 useEventStore(src/stores/use-event-store.ts:153-158),组件按类型渲染成聊天气泡、终端输出、diff、浏览器截图。流式 token(StreamingDeltaEvent)会先过批处理器再入 store,避免每个 token 触发一次重渲染(src/contexts/conversation-websocket-context.tsx:44-47 引入的 createStreamingDeltaBatcher)。
4. 对话:WebSocket 优先,REST 兜底
发消息的统一入口是 ws context 的 sendMessage(src/contexts/conversation-websocket-context.tsx:1094-1141):
- socket 开着 → 直接
send(JSON.stringify({ ...message, run: true })),run: true让 agent 循环立即启动(src/contexts/conversation-websocket-context.tsx:1131-1134)。 - socket 没连上 → 降级为 REST
sendEvent排队,消息会在会话就绪后送达,返回{ queued: true }让 UI 别急着画乐观气泡(src/contexts/conversation-websocket-context.tsx:1111-1126)。
服务层还有一个同名方法 AgentServerConversationService.sendMessage(src/api/conversation-service/agent-server-conversation-service.api.ts:357-401),给不走 ws context 的调用方用;cloud 后端下它经云代理把事件 POST 到会话自己的运行时沙箱(src/api/conversation-service/agent-server-conversation-service.api.ts:381-389)。
确认循环(confirmation)。 agent 遇到需人工批准的动作时,execution_status 进入 waiting_for_confirmation,UI 弹出确认卡;你点「批准/拒绝」走 EventService.respondToConfirmation(src/api/event-service/event-service.api.ts:40-69),POST 到会话的 events/respond_to_confirmation 端点。
5. 运行中操作(一览)
会话跑着的时候,服务层还提供这些操作,每个都是一次 typed client 调用:
| 操作 | 方法 | 位置 |
|---|---|---|
| 压缩上下文(condense) | condenseConversation | src/api/conversation-service/agent-server-conversation-service.api.ts:723-745 |
| 分叉(从某事件复制出新会话) | forkConversation | src/api/conversation-service/agent-server-conversation-service.api.ts:797-827 |
| 换 LLM profile(本会话) | switchProfile | src/api/conversation-service/agent-server-conversation-service.api.ts:861-910 |
| 换 ACP 模型(保上下文) | switchAcpModel | src/api/conversation-service/agent-server-conversation-service.api.ts:925-944 |
| 取运行时状态/metrics | getRuntimeConversation | src/api/conversation-service/agent-server-conversation-service.api.ts:690-715 |
| 导出轨迹 | downloadConversation | src/api/conversation-service/agent-server-conversation-service.api.ts:673-681 |
6. 本章小结
- 创建 = 一次精心组装的 POST:加密设置、技能、客户端工具、LookupSecret 引用、工作区,全在 start 请求里。
- local 同步 READY;cloud 异步任务轮询。
- 事件 = REST 补历史 + WebSocket(
resend_mode: since)订增量,首帧发 auth,落地进 Zustand store。 - 发消息 WS 优先、REST 兜底;确认走独立端点。
- 下一章换个角度:这些请求里的「当前后端」到底是怎么被抽象、登记、切换的。