跳到主要内容

数据截至 (上游 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_idagent_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_commandacp_model 等 ACP 专有字段。
  • 工具清单是前端定的。 默认三件套 terminalfile_editortask_tracker(src/api/agent-server-adapter.ts:113),浏览器工具集和子 agent 工具集按开关与服务端能力追加(src/api/agent-server-adapter.ts:631-644shouldIncludeTool)。
  • 确认策略由设置推导。 confirmation_mode 开 + LLM 安全分析器 → ConfirmRisky(只拦高风险);开但没分析器 → AlwaysConfirm;没开 → NeverConfirm(src/api/agent-server-adapter.ts:593-605getConversationConfirmationPolicy)。
  • 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-788buildAgentContext)。技能加载是第 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):

  1. 生成会话 id:uuidv4()(src/api/conversation-service/agent-server-conversation-service.api.ts:457)。
  2. 定工作区:默认 workspace/project/<会话id的hex>(src/api/agent-server-config.ts:203-207buildConversationWorkingDir),再经 resolveAbsoluteAgentServerPath 解析成 agent-server 主机上的绝对路径(src/api/agent-server-home.ts:102-114)——它用 /api/file/home 拿到服务端 home 目录再拼接,避免相对路径被写成根目录下的只读路径。
  3. 拼 start 请求(上一节),用 ConversationClient.createConversation POST 出去(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 可能还在预热。
  4. local agent-server 同步建会话,返回即 READY,无需轮询(src/api/conversation-service/agent-server-conversation-service.api.ts:535-538 注释)。
  5. 把 repo/分支/工作区等 UI 侧元数据写进浏览器 localStorage(src/api/conversation-service/agent-server-conversation-service.api.ts:495-509setStoredConversationMetadata)——agent-server 不认识这些概念,存在前端(src/api/conversation-metadata-store.ts:9-42ConversationMetadata)。

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 消息:useWebSocketonopen 里调 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)condenseConversationsrc/api/conversation-service/agent-server-conversation-service.api.ts:723-745
分叉(从某事件复制出新会话)forkConversationsrc/api/conversation-service/agent-server-conversation-service.api.ts:797-827
换 LLM profile(本会话)switchProfilesrc/api/conversation-service/agent-server-conversation-service.api.ts:861-910
换 ACP 模型(保上下文)switchAcpModelsrc/api/conversation-service/agent-server-conversation-service.api.ts:925-944
取运行时状态/metricsgetRuntimeConversationsrc/api/conversation-service/agent-server-conversation-service.api.ts:690-715
导出轨迹downloadConversationsrc/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 兜底;确认走独立端点。
  • 下一章换个角度:这些请求里的「当前后端」到底是怎么被抽象、登记、切换的。