RemoteAgentTask 到 TaskNotification 回流 trace
这页回答什么
当
AgentTool走 remote 路径时,Claude Code 是如何创建远端 session、在本地注册 RemoteAgentTask、轮询远端事件、判定完成,并把结果重新回灌进主query()循环的?
关键文件:
src/tools/AgentTool/AgentTool.tsxsrc/utils/teleport.tsxsrc/tasks/RemoteAgentTask/RemoteAgentTask.tsxsrc/query.tssrc/utils/attachments.tssrc/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 分支:
checkRemoteAgentEligibility()teleportToRemote({...})registerRemoteAgentTask({...})- 返回
remote_launched
所以远程 agent 的第一原则是:
对父 query loop 来说,它仍然只是一种 tool call 结果,只不过结果类型从
completed变成了remote_launched。
2. teleportToRemote() 负责创建远端 session,不负责本地继续执行
调用点传入的核心参数包括:
initialMessage: promptdescriptionsignalonBundleFail
而 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'taskIdsessionUrldescriptionpromptoutputFile
这和本地 async agent 的 async_launched 很像。
含义都不是“结果已经有了”,而是:
任务已经交给长期生命周期系统,后续请通过 task 语义观察。
4. registerRemoteAgentTask() 在本地创建远端 session 的代理对象
RemoteAgentTask.tsx 的 registerRemoteAgentTask() 会立即做这些事:
- 生成
taskId - 先
initTaskOutput(taskId) - 构造
RemoteAgentTaskState registerTask(taskState, context.setAppState)persistRemoteAgentMetadata(...)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 周期性地:
- 读取当前 task state
- 调
pollRemoteSessionEvents(task.sessionId, lastEventId) - 追加新日志到
accumulatedLog - 文本增量写进 task output file
- 根据 sessionStatus / result / hook / idle / timeout 判定状态
- 更新
RemoteAgentTaskState - 在完成或失败时 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
返回结构包括:
newEventslastEventIdbranchsessionStatus
这说明本地轮询器消费的不是一坨字符串,而是:
一条带类型信息的远端事件流。
因此它才能区分:
- 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_notificationpayload - 带上
taskId - 可选带上
toolUseId - 写入
enqueuePendingNotification({ value: message, mode: 'task-notification' })
也就是说:
remote task 和 local async task 在回流协议层最终是统一的。
这是非常重要的收束点。
10. query.ts 在回合末主动 drain 队列
和 local async trace 一样,query.ts 在工具执行后会:
getCommandsByMaxPriority(...)- 按 main thread / subagent 范围过滤 queued commands
- 调
getAttachmentMessages(...) yield attachment- push 进
toolResults 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 的端到端回流路径。
当前结论
RemoteAgentTask 到 TaskNotification 的回流链说明:
Claude Code 对 remote agent 的处理,并不是让主循环直接理解远端执行细节,而是把远端 session 封装成一个本地任务代理对象;远端完成后再用统一的 task-notification 协议回到主 query loop。
这让系统同时获得:
- remote 执行能力
- 本地恢复能力
- 统一通知协议
- 与主对话一致的 continuation 语义
下一步最值得继续追
- 对照 local async trace,整理一页 “LocalAgentTask vs RemoteAgentTask”
- 追
queued_commandattachment 最终如何被序列化为模型输入内容 - 单独深挖
teleportToRemote()的 source/bundle/environment 决策树