跳到主要内容

会话生命周期:从一句话到 agent 动手

这章放大顶层主线的第 4–6 步——整个仓库工程含量最高的一段。它要解决的问题是:「把用户的一句话,可靠地落成一个在干净沙箱里、克隆好代码、装好工具、配好 LLM 的运行中会话。」

1. 它是一条「流式状态机」

启动不是一个返回值,而是一个 async 生成器:每完成一步就 yield 一个带状态的 AppConversationStartTask,前端据此实时显示「正在准备仓库 / 正在跑 setup / 正在启动会话…」。

入口很薄,只负责把每个中间状态存盘后转发:

# live_status_app_conversation_service.py:324(start_app_conversation)
async def start_app_conversation(self, request):
async for task in self._start_app_conversation(request):
await self.app_conversation_start_task_service.save_app_conversation_start_task(task)
yield task # 把进度推给前端

真正的流水线在 _start_app_conversation(live_status_app_conversation_service.py:333)。它的状态阶段(AppConversationStartTaskStatus)依次是:PREPARING_REPOSITORYRUNNING_SETUP_SCRIPTSETTING_UP_GIT_HOOKSSETTING_UP_SKILLSSTARTING_CONVERSATION

2. 主线七步(对照源码)

下面按代码执行顺序拆开,每步一句话 + 真实锚点。

① 解析与继承。 如果带 parent_conversation_id,从父会话继承配置(repo、分支、LLM 等);应用「建议任务」。见 _start_app_conversation 开头(:344-357)。

② 等沙箱 ready。 _wait_for_sandbox_start(task) 内部找 / 开沙箱,并轮询到 RUNNING + agent-server /alive 通。底层是 SandboxService.wait_for_sandbox_running(sandbox/sandbox_service.py:93)。

③ 校验 SDK 版本。 自定义沙箱镜像可能带不兼容的 openhands-sdk,这里先打 /server_info 比对版本,快速失败给清晰报错,而不是等到创建会话时 500。见 _verify_agent_server_version(:898)。

④ 建远程工作区。 关键对象:AsyncRemoteWorkspace——它是控制中心对「沙箱里 agent-server」的远程句柄,后续所有 execute_command / file_upload 都通过它走 HTTP:

# live_status_app_conversation_service.py:404
remote_workspace = AsyncRemoteWorkspace(
host=agent_server_url, # 沙箱暴露的 AGENT_SERVER 地址
api_key=sandbox.session_api_key, # 门票
working_dir=working_dir,
)

⑤ 跑 setup 脚本族。 run_setup_scripts 顺序做四件事(克隆 → setup.sh → git hook → 技能),每件改一次状态并 yield。见 app_conversation_service_base.py:249(run_setup_scripts)。这是控制中心「布置现场」的核心,下一节单独讲。

⑥ 拼 start 请求。 _build_start_conversation_request_for_user(...) 把 LLM、初始消息、技能、安全策略组装成 SDK 的 StartConversationRequest(ACP agent 走另一条 _build_acp_start_conversation_request 分支)。见 :1511

⑦ POST 给 agent-server。 序列化请求体(expose_secrets=True 才把密钥真正展开),POST 到 /api/conversations:

# live_status_app_conversation_service.py:439-465
body_json = start_conversation_request.model_dump(mode='json', context={'expose_secrets': True})
...
response = await self.httpx_client.post(
f'{agent_server_url}/api/conversations',
json=body_json,
headers={'X-Session-API-Key': sandbox.session_api_key},
timeout=self.sandbox_startup_timeout,
)

响应里拿回 ConversationInfo,控制中心据此存一条自己的 AppConversationInfo(标题、repo、分支、trigger…),作为本地索引。见 :483-521

3. 「布置现场」:克隆 / setup / git hook

这是第 ⑤ 步的内部,集中在基类 AppConversationServiceBase

项目根目录怎么算。 选了 repo,项目根是 {working_dir}/{repo_name};没选 repo,就是 working_dir 本身。所有 .openhands/ 相关功能都以它为基准。见 get_project_dir(app_conversation_service_base.py:53)。

克隆 repo。 默认浅克隆(--depth 1)省时间;没指定分支时新建一个随机分支 openhands-workspace-<随机>,避免直接动用户的分支:

# app_conversation_service_base.py:411-416(节选)
random_str = base62.encodebytes(os.urandom(16))
openhands_workspace_branch = f'openhands-workspace-{random_str}'
checkout_command = f'git checkout -b {shlex.quote(openhands_workspace_branch)}'

注意所有外部输入(repo url、分支名、目录名)都过 shlex.quote,防命令注入。见 clone_or_init_git_repo(:321)。

跑 setup.sh。 若项目根有 .openhands/setup.shsource 它(600s 超时),让 repo 自带的依赖安装跑起来。见 maybe_run_setup_script(:517)。

装 pre-commit hook。 把仓库自带的 git/pre-commit.sh 装成 .git/hooks/pre-commit;若已有别人的 hook,先挪到 pre-commit.local 保留,不粗暴覆盖。见 maybe_setup_git_hooks(:536)。

4. 拼请求时的几个「精华」配置

第 ⑥ 步组装请求时,有几个值得单独点出的设计决策。

条件压缩器(condenser)。 上下文会越滚越长,这里给 LLM 配一个 LLMSummarizingCondenser——超过 max_size 就自动把旧历史摘要掉。DEFAULTPLAN 两种 agent 用不同的 usage_id(condenser / planning_condenser)以便分别计费 / 追踪。见 _create_condenser(app_conversation_service_base.py:593)。

安全分析器 + 确认策略。 这两者组合决定 agent 动作要不要人确认:

confirmation_modesecurity_analyzer选用策略
任意NeverConfirm(全自动)
"llm"ConfirmRisky(LLM 判定有风险才拦)
其它 / 无AlwaysConfirm(每步都确认)

逻辑见 _select_confirmation_policy(:656),分析器构造见 _create_security_analyzer_from_string(:629,目前只支持 "llm"LLMSecurityAnalyzer,其它一律降级为 None)。

Laminar 追踪 user_id 的兼容补丁。 一个真实的工程细节:为了让链路追踪能归属到用户,代码把 user_id 直接塞进请求体 JSON,而不是走 SDK 的 model 字段——因为当前锁定的 openhands-sdk 版本还没在 StartConversationRequest 上暴露该字段,走 model 会被 pydantic 静默丢掉。注释明说「等 SDK 升级后删掉这段」。见 :442-454。这是控制中心与外部 SDK 版本错位时的典型兜底手法

5. 边界与坑

  • 强依赖沙箱里的 agent-server 健康。 任何一步 HTTP 失败(版本不匹配、/alive 不通)都会中断启动;代码对「自定义镜像 + 创建会话 500」专门给了 actionable 报错(:472-481)。
  • 浅克隆的副作用。 默认 --depth 1,需要完整历史的操作(blame、跨多 commit diff)可能受限——代码用 _maybe_append_shallow_clone_context 在系统提示里告知 agent 这是浅克隆(:240)。
  • setup.sh 用 source 而非执行子进程,意味着它能改当前 shell 环境,但也意味着脚本里的 exit 会影响后续——这是约定,不是 bug。