Remote agent 机制

这页回答什么

Claude Code 里,所谓 remote agent 到底是怎么被发起、托管、轮询、通知回本地会话的?

关键文件:

  • src/tools/AgentTool/AgentTool.tsx
  • src/tasks/RemoteAgentTask/RemoteAgentTask.tsx
  • src/utils/teleport.tsx
  • src/utils/messageQueueManager.ts

一句话结论

remote agent 不是“换个地方跑 runAgent()”,而是把一次 agent 工作转译成一个远端 session,然后由本地 RemoteAgentTask 充当该 session 的代理对象和轮询器。

也就是:

  • 发起 remote agent AgentTool.call()
  • 真正创建远端 session teleportToRemote()
  • 在本地注册生命周期对象 registerRemoteAgentTask()
  • 持续轮询远端事件 startRemoteSessionPolling()
  • 完成后通过 task-notification 回灌主会话

这是一套“远程作业控制”模式,不是本地子 agent 的简单变体。


1. 入口仍然是 AgentTool

AgentTool.call() 里,先算出:

  • effectiveIsolation = isolation ?? selectedAgent.isolation

然后判断:

  • 如果 effectiveIsolation === 'remote'
  • 且 remote eligibility 检查通过
  • 就进入 remote 路径

核心代码意图很清楚:

  1. checkRemoteAgentEligibility()
  2. teleportToRemote({...})
  3. registerRemoteAgentTask({...})
  4. 返回 remote_launched

所以 remote agent 在架构上的定位仍然是: 一次 Tool 调用,只是它的执行载体变成远端 session。


2. teleportToRemote() 做的不是执行,而是“把环境送过去”

从调用点能看出来,teleportToRemote() 接收:

  • initialMessage: prompt
  • description
  • signal
  • onBundleFail

结合 teleport.tsx 中大量关于 bundle / git source / env / repository 的逻辑,可以推断它的职责是:

A. 判断怎么把当前工作上下文带到远端

可能路径包括:

  • git source
  • bundle 打包上传
  • 空 sandbox

B. 在远端创建一个 session

返回:

  • session.id
  • session.title

C. 远端真正的执行入口不是本地继续 query()

而是远端平台接手该 session 的后续运行。

所以 teleportToRemote() 本质更像:

“远端执行环境 provisioning + session creation”

而不是:

“在本地启动一个 remote flavored runAgent”

这点非常关键。


3. RemoteAgentTask 是远端 session 的本地镜像

registerRemoteAgentTask() 会立刻做几件事:

  1. 生成 taskId
  2. 先创建 output file
  3. 构造 RemoteAgentTaskState
  4. 注册到 AppState.tasks
  5. 把 metadata 持久化到 session sidecar
  6. 启动 startRemoteSessionPolling()

它记录什么状态

RemoteAgentTaskState 关注的是:

  • sessionId
  • remoteTaskType
  • command
  • title
  • log
  • todoList
  • pollStartedAt
  • isRemoteReview
  • isUltraplan
  • isLongRunning
  • remoteTaskMetadata

这说明它关心的核心不是本地推理现场,而是:

一个远端 session 当前演化到了哪一步,以及本地如何把它展示和管理起来。


4. 为什么要写 sidecar

registerRemoteAgentTask() 会调用 persistRemoteAgentMetadata()

注释写得很直白:

  • 这是为了 --resume 时能重新连回还在跑的 remote session
  • status 不直接持久化,本地恢复时重新向远端拉

也就是说 remote agent 设计时就默认:

  • 本地 CLI 可能退出
  • 会话可能稍后恢复
  • 远端任务依然继续存在

这和本地同步 agent 完全不是一个生命周期模型。


5. 轮询器才是 remote agent 的核心

startRemoteSessionPolling() 是整条链真正的心脏。

它每秒钟:

  1. 从 AppState 取 task
  2. pollRemoteSessionEvents(task.sessionId, lastEventId)
  3. 合并 newEvents
  4. 把文本追加到 output file
  5. 判断 session 是否完成 / 失败 / archived
  6. 更新任务状态
  7. 必要时发 notification

这里的关键区别

本地 agent 的完成信号往往来自:

  • 本地函数返回
  • 本地 Promise resolve/reject

remote agent 的完成信号来自:

  • 远端 session event stream
  • 本地 poller 对事件流做归约后得出结论

所以 remote agent 的控制面其实是:

事件流驱动,而不是函数返回驱动。


6. 轮询器在做“状态解释”,不只是拿日志

pollRemoteSessionEvents() 返回后,本地不是简单显示一下输出就完了。

它会继续解释这些事件:

A. 日志累积

  • accumulatedLog
  • appendTaskOutput(taskId, deltaText + '\n')

B. session 归档判定

如果 sessionStatus === 'archived'

  • 直接认为完成
  • 发完成通知
  • 清理 output / metadata

C. completion checker

对于特定 remoteTaskType

  • 可注册自定义 completion checker
  • 每次 poll tick 都跑一次
  • 命中即视为完成

这说明 remote task 框架支持“不同产品形态挂不同完成判定器”。

D. result message 判定

常规 remote agent 会从累积日志中找最后一个 result 事件,作为成功/失败依据。

这意味着 remote session 在协议层已经不是纯文本流,而是 typed SDK event stream。


7. 为什么 remote-review / ultraplan 会更复杂

代码里专门分出了:

  • isRemoteReview
  • isUltraplan
  • isLongRunning
  • review-specific progress parsing
  • specialized failure notification

说明 RemoteAgentTask 早就不只是“给 remote agent 用一下”。 它已经演化成一个通用远端任务控制框架,承载多种产品能力。

尤其是 remote-review:

  • 会从 hook 事件中提取 <remote-review> tag
  • 解析 heartbeat 里的 JSON 进度
  • 对 idle 状态做 debounce
  • 超时 30 分钟失败

所以 remote task 的复杂度来自:

远端平台不是一次性返回结果,而是不断产生半结构化事件,本地要自己决定什么时候算“真的完成”。


8. 完成后怎么回到主会话

一旦 poller 认定完成,会调用:

  • enqueueRemoteNotification(...)
  • 或 remote-review 专用 notification

enqueueRemoteNotification() 最终走的是:

enqueuePendingNotification({ value: message, mode: 'task-notification' })

也就是说,remote agent 的完成不会直接“塞一段字符串给用户”。 它会被包装成标准的 task-notification 消息,再进入统一消息队列。

这一步很重要,因为它说明:

远程任务完成事件,最终会重新变成当前对话循环的一部分输入。

这和后台本地 agent 的通知模型是一致的。


9. AgentTool 在 remote 路径上返回什么

AgentTool.call() 在 remote 路径不会等待结果,而是立刻返回:

  • status: 'remote_launched'
  • taskId
  • sessionUrl
  • description
  • prompt
  • outputFile

这类返回值的语义不是“任务结果”,而是:

“我已经成功把这件事交给远端系统了,后续请通过 task 机制观察。”

这和 async local agent 的 async_launched 非常类似,只是 remote 多了 sessionUrl 和更强的恢复语义。


10. local subagent 和 remote agent 的根本区别

local subagent

  • 在本地进程里继续跑
  • 最核心执行器是 runAgent()
  • Task 主要负责后台生命周期

remote agent

  • 本地不直接执行 agent loop
  • 本地先创建远端 session
  • 之后依赖 poller 跟踪远端事件
  • Task 不只是生命周期壳,更像远端代理对象

所以 remote agent 不是“runAgent 换个 isolation 参数”。 它是另一条 orchestration 模式。


11. 这套设计透露出的架构判断

我认为这里最有价值的设计判断有三个:

A. 用 Tool 接住 agent 调度

这样无论 local / async / fork / remote,主循环都不用知道细节。

B. 用 Task 接住长期生命周期

这样后台、恢复、通知、UI、输出文件都能统一。

C. 用 event polling 接住 remote 不确定性

远端运行天然不可靠,必须把“完成判定”从单点返回值升级为持续归约。

这三层拆分让 remote agent 虽然复杂,但没有把复杂度直接污染进 query() 主循环。


当前结论

Claude Code 的 remote agent 机制,本质上是:

一次由 AgentTool 发起、由 teleportToRemote() 创建远端 session、由 RemoteAgentTask 在本地做代理与轮询、最后通过 task-notification 回灌主对话的远程任务系统。

它不是本地 subagent 的简单外包版,而是明确面向:

  • 可恢复
  • 可通知
  • 可追踪
  • 多产品形态复用

的一套远程 orchestration 设计。


下一步最值得继续追

  1. teleport.tsx 里 bundle / git source / env 注入的实际决策树
  2. pollRemoteSessionEvents() 返回的 SDK event schema
  3. remote-review / ultraplan 这两种特殊 remoteTaskType 如何复用同一套轮询框架