RemoteAgentTask 到 TaskNotification 回流 trace

这页回答什么

AgentTool 走 remote 路径时,Claude Code 是如何创建远端 session、在本地注册 RemoteAgentTask、轮询远端事件、判定完成,并把结果重新回灌进主 query() 循环的?

关键文件:

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

一句话结论

remote agent 的执行不发生在本地 runAgent() 里,而是先通过 teleportToRemote() 在远端创建 session,再由本地 RemoteAgentTask 持续轮询远端事件流;一旦完成,结果会被编码成 task-notification 放入全局队列,最后由主 query() 循环主动 drain 并转成 attachment 回流。

这条链和本地 async agent 的回流终点相同,但中段执行模型完全不同。


1. 起点:AgentTool.call() 选择 remote 路径

AgentTool.tsx 在算出:

  • effectiveIsolation = isolation ?? selectedAgent.isolation

之后,如果:

  • effectiveIsolation === 'remote'
  • 且 remote eligibility 检查通过

就走 remote 分支:

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

所以远程 agent 的第一原则是:

对父 query loop 来说,它仍然只是一种 tool call 结果,只不过结果类型从 completed 变成了 remote_launched


2. teleportToRemote() 负责创建远端 session,不负责本地继续执行

调用点传入的核心参数包括:

  • initialMessage: prompt
  • description
  • signal
  • onBundleFail

teleport.tsx 里的注释与实现说明它负责的是:

  • 准备远端会话上下文
  • 根据仓库情况决定 git source / bundle / empty sandbox 等来源模式
  • 调用远端 Sessions API 创建 session

它并不会在本地继续进入一个 remote flavored runAgent()

也就是说:

teleportToRemote() 是“远端执行环境 + 会话创建器”,不是本地 agent runtime 的一部分。


3. 父 agent 先拿到的是 remote_launched

远端 session 创建成功后,AgentTool.call() 会:

  • registerRemoteAgentTask(...)
  • 构造 RemoteLaunchedOutput

其内容大致包括:

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

这和本地 async agent 的 async_launched 很像。

含义都不是“结果已经有了”,而是:

任务已经交给长期生命周期系统,后续请通过 task 语义观察。


4. registerRemoteAgentTask() 在本地创建远端 session 的代理对象

RemoteAgentTask.tsxregisterRemoteAgentTask() 会立即做这些事:

  1. 生成 taskId
  2. initTaskOutput(taskId)
  3. 构造 RemoteAgentTaskState
  4. registerTask(taskState, context.setAppState)
  5. persistRemoteAgentMetadata(...)
  6. startRemoteSessionPolling(taskId, context)

这说明 remote task 的本地实体不是“执行器”,而是:

远端 session 在本地 runtime 中的代理状态对象。


5. sidecar 持久化意味着 remote task 天然面向恢复

persistRemoteAgentMetadata() 的注释写得很清楚:

  • 这是为了 --resume 时重新连回 still-running remote session
  • 状态不盲存,本地恢复时重新向远端拉取

所以 remote task 与 local async task 的差别之一是:

它从设计上就假设本地进程可能退出,而远端任务继续存在。

这也是为什么它必须引入 session sidecar 和 restore 逻辑。


6. 核心心脏:startRemoteSessionPolling()

注册完 task 后,真正的运行控制交给 startRemoteSessionPolling()

这个 poller 周期性地:

  1. 读取当前 task state
  2. pollRemoteSessionEvents(task.sessionId, lastEventId)
  3. 追加新日志到 accumulatedLog
  4. 文本增量写进 task output file
  5. 根据 sessionStatus / result / hook / idle / timeout 判定状态
  6. 更新 RemoteAgentTaskState
  7. 在完成或失败时 enqueue notification

所以 remote task 的完成机制不是:

  • 某个 Promise resolve

而是:

  • 本地 poller 对远端 event stream 做持续归约

这是 remote 与 local 的根本分叉。


7. pollRemoteSessionEvents() 提供的是 typed event stream,不只是日志文本

teleport.tsx 里的 pollRemoteSessionEvents() 会:

  • /v1/sessions/{id}/events
  • 分页获取 delta
  • 过滤掉部分控制类事件
  • 把剩余事件映射为 SDKMessage[]
  • 可选再取 metadata,补 branch / sessionStatus

返回结构包括:

  • newEvents
  • lastEventId
  • branch
  • sessionStatus

这说明本地轮询器消费的不是一坨字符串,而是:

一条带类型信息的远端事件流。

因此它才能区分:

  • assistant
  • result
  • hook_progress
  • archived / idle / running 状态

8. 完成判定是“事件解释”,不是单一返回值

startRemoteSessionPolling() 里有几层判定:

A. sessionStatus === 'archived'

  • 直接视为完成
  • enqueue 完成通知

B. completionCheckers.get(task.remoteTaskType)

  • 某些特定 remoteTaskType 可以挂自定义 completion checker
  • 命中就完成

C. 普通 remote agent

  • accumulatedLog.findLast(msg => msg.type === 'result') 找结果
  • success → completed
  • 非 success → failed

D. remote-review / ultraplan / long-running

  • 有额外判定逻辑
  • 如 stable idle debounce、hook tag、超时等

所以 remote completion 不是统一规则,而是:

在同一 poller 框架内,按任务类型做状态归约。


9. 完成后进入 enqueueRemoteNotification()

一旦 poller 判断任务终结,会调用:

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

其本质和 LocalAgentTask 的通知一样:

  • 生成标准 task_notification payload
  • 带上 taskId
  • 可选带上 toolUseId
  • 写入 enqueuePendingNotification({ value: message, mode: 'task-notification' })

也就是说:

remote task 和 local async task 在回流协议层最终是统一的。

这是非常重要的收束点。


10. query.ts 在回合末主动 drain 队列

和 local async trace 一样,query.ts 在工具执行后会:

  1. getCommandsByMaxPriority(...)
  2. 按 main thread / subagent 范围过滤 queued commands
  3. getAttachmentMessages(...)
  4. yield attachment
  5. push 进 toolResults
  6. removeFromQueue(consumedCommands)

这说明 remote task 的通知并不是“留到未来某个 REPL 层自己想办法”。

而是:

query() 循环自己会主动把 remote task completion 吞回来。


11. attachments.ts 把 task-notification 重新翻译成 attachment

getQueuedCommandAttachments() 会把:

  • mode === 'task-notification'

的 queued command 转成:

  • type: 'queued_command'
  • commandMode: 'task-notification'
  • prompt: original payload

因此进入模型上下文前,remote completion 已经不再只是一个后台系统事件,而是:

一个 attachment 形态的、可继续被主 agent 理解的输入。

这也是为什么 task 系统能真正和对话系统缝合。


12. local async agent 与 remote agent 的共同点和差异

共同点

  • 都从 AgentTool.call() 发起
  • 都先返回 launched 状态
  • 都通过 task layer 承接长期生命周期
  • 都最终用 task-notification 回流
  • 都由 query.ts 主动 drain 并转成 attachment

差异

local async agent

  • 本地继续跑 runAgent()
  • 完成信号来自本地执行流
  • Task 更像本地长期执行的壳

remote agent

  • 本地不继续跑 agent loop
  • 完成信号来自远端 session event stream
  • Task 更像远端 session 的代理 + poller + 恢复器

所以两者共享的是回流协议,不共享的是执行模型。


13. 完整链路图

用户输入
  → QueryEngine.submitMessage()
  → query()
  → assistant 输出 tool_use(name=Agent, isolation=remote)
  → AgentTool.call()
  → teleportToRemote()
  → 远端 session 创建成功
  → registerRemoteAgentTask()
  → 返回 remote_launched tool_result
  → startRemoteSessionPolling()
  → pollRemoteSessionEvents() 持续拉远端 event stream
  → 判定完成 / 失败 / 超时
  → enqueueRemoteNotification()
  → enqueuePendingNotification(mode='task-notification')
  → query() 回合末 drain queue
  → getAttachmentMessages()
  → task-notification 变成 attachment
  → 进入后续轮次上下文

这就是 remote agent 的端到端回流路径。


当前结论

RemoteAgentTaskTaskNotification 的回流链说明:

Claude Code 对 remote agent 的处理,并不是让主循环直接理解远端执行细节,而是把远端 session 封装成一个本地任务代理对象;远端完成后再用统一的 task-notification 协议回到主 query loop。

这让系统同时获得:

  • remote 执行能力
  • 本地恢复能力
  • 统一通知协议
  • 与主对话一致的 continuation 语义

下一步最值得继续追

  1. 对照 local async trace,整理一页 “LocalAgentTask vs RemoteAgentTask”
  2. queued_command attachment 最终如何被序列化为模型输入内容
  3. 单独深挖 teleportToRemote() 的 source/bundle/environment 决策树