AgentTool sync async remote 三路总图
这页回答什么
同样是
AgentTool.call(),Claude Code 到底怎么分流成 sync、local async、remote 三条执行路径?三条路哪些阶段相同,哪些阶段不同,最后又在哪里重新汇合?
关键文件:
src/tools/AgentTool/AgentTool.tsxsrc/tools/AgentTool/runAgent.tssrc/tasks/LocalAgentTask/LocalAgentTask.tsxsrc/tasks/RemoteAgentTask/RemoteAgentTask.tsxsrc/query.tssrc/utils/teleport.tsx
一句话结论
AgentTool本质上是一个统一入口,前半段共享“选 agent / 组 prompt / 建上下文 / 决定隔离模式”,中段分裂成 sync、local async、remote 三条执行管线,尾段又重新汇合到同一种 reflow 协议:task-notification → queued command → attachment → 下一轮 query context。
也就是说:
- 入口基本统一
- 执行中段不同
- 完成回流协议统一
1. 统一入口:AgentTool.call() 先做什么
无论最后走哪条路,AgentTool.call() 前半段都会做这些事:
-
解析参数
promptdescriptionsubagent_typerun_in_backgroundcwdmodelisolation
-
决定 agent definition
- fork path
- 普通 agent path
-
检查前置约束
- deny rule
- MCP 依赖
- teammate / background 限制
-
解析执行元信息
resolvedAgentModeleffectiveIsolation- 是否 async
- 是否 fork
这一段说明 AgentTool 不是只有一个“tool body”,而是一个完整的 agent launch planner。
2. 第一处分叉:effectiveIsolation === 'remote'
在 AgentTool.tsx 里,remote 是最早发生的硬分叉。
如果:
effectiveIsolation === 'remote'
就不会进入本地 runAgent() 主流程,而是直接:
checkRemoteAgentEligibility()teleportToRemote({ initialMessage, description, ... })registerRemoteAgentTask(...)- 立即返回:
status: 'remote_launched'taskIdsessionUrloutputFile
所以 remote 路径的特征是:
本地不会直接执行子 agent query loop,而是先把任务投送成一个远端 session,再由 RemoteAgentTask 在外侧追踪。
3. 第二处分叉:本地路径里的 sync / async
如果没走 remote,就进入本地 agent 路径。
这时真正的分叉条件是:
shouldRunAsync
它受这些因素影响:
run_in_background === true- agent definition 自带
background: true - coordinator mode
- fork-subagent 实验强制 async
- kairos / proactive mode 强制 async
- background task 是否被禁用
所以 sync 和 async 不是简单看用户有没有传 run_in_background,而是多个产品策略共同决定。
4. 三条路的共同前置阶段
虽然三路执行中段不同,但 remote 之外的本地两路 在分叉前共享很多组装步骤:
4.1 组系统提示词和 prompt 消息
- fork path: 继承父 prompt / forked messages
- normal path:
selectedAgent.getSystemPrompt() - 可能再走
enhanceSystemPromptWithEnvDetails(...)
4.2 组 worker tools
assembleToolPool(...)- 按 worker permission mode 重建工具池
4.3 建立运行参数
runAgentParamsquerySourceavailableToolsforkContextMessagesoverride.systemPrompt- 可选
worktreePath
4.4 可选 worktree 隔离
effectiveIsolation === 'worktree'时先建 worktree- 后续 agent 在 cwd override 下执行
所以本地 sync/async 的真正差别,不在 prompt 组装层,而在 task lifecycle 的外壳。
5. Sync 路径:当前 tool call 内直接跑完
shouldRunAsync === false 时,进入同步执行路径。
核心特征:
- 创建
syncAgentId registerAgentForeground(...)- 直接拿
runAgent(...)[Symbol.asyncIterator]() - 当前
AgentTool.call()持续消费 iterator - 一边转发 progress,一边累积
agentMessages - 最终
finalizeAgentTool(...) - 返回
status: 'completed'
这条路的本质是:
agent 结果在当前 tool 调用里 inline 收敛。
它不需要等待后续 task-notification 回流,因为结果已经当场变成 tool result。
6. 但 sync 还有一个“中途转 async”出口
sync 路径里有个很重要但容易漏掉的点:
foreground agent 可以被 background 化
流程大致是:
- 先
registerAgentForeground(...) - 持有
backgroundSignal - loop 里
Promise.race(nextMessage, backgroundSignal) - 一旦 backgroundSignal 触发
- 清理 foreground iterator
- 重新以
isAsync: true跑 agent - 走 background lifecycle
- 返回
status: 'async_launched'
所以 sync 并不意味着“绝对同步到底”。
更准确地说:
它是“先按前台跑,必要时可以热切换成后台任务”。
这个设计解释了为什么 foreground/background UI 和 LocalAgentTask 紧密耦合。
7. Local async 路径:本地 query loop 在后台继续
shouldRunAsync === true 时:
-
registerAsyncAgent(...) -
立即返回:
status: 'async_launched'agentIdoutputFile
-
后台
void runAsyncAgentLifecycle(...) -
里面真正执行:
runAgent(...)- 跟踪 progress
finalizeAgentTool(...)completeAsyncAgent(...)enqueueAgentNotification(...)
这条路和 sync 最大区别是:
runAgent()仍然在本地执行,但不再阻塞当前 tool 调用,而是变成 LocalAgentTask 管理的后台生命周期。
所以 local async 不是另一套 agent engine,它仍然是同一套 runAgent/query 引擎,只是外面套了 task runtime。
8. Remote 路径:不跑本地 runAgent(),改跑 session teleport
remote 路径则更彻底。
它不是本地 runAgent() 后台化,而是:
- 先
teleportToRemote()创建远端 session - 再
registerRemoteAgentTask(...) - 后续由 remote poll / completion 机制推动
- 最终
enqueueRemoteNotification(...)
所以 remote 和 local async 的关系是:
相同点
- 都会立即返回 launched 状态
- 都有 taskId
- 都通过 task-notification 回流
不同点
- local async 的执行引擎仍在本地
runAgent() - remote 的执行引擎已经移到 CCR session
这也是为什么之前那句总结成立:
shared reflow protocol, different execution middle
9. 三条路的状态返回差异
AgentTool 的用户可见返回状态大致分三类:
sync
status: 'completed'- 结果直接在当前 tool result
local async
status: 'async_launched'- 当前只拿到 task/agent 标识
- 结果稍后通知回流
remote
status: 'remote_launched'- 当前拿到 taskId + sessionUrl
- 结果稍后通知回流
如果压成一句:
- sync = 现在给结果
- async = 现在给句柄,稍后给结果
- remote = 现在给远端句柄,稍后给结果
10. 三条路的真正汇合点不在 AgentTool,而在 notification reflow
这点非常关键。
local async 完成时:
enqueueAgentNotification(...)
remote 完成时:
enqueueRemoteNotification(...)
它们最后都会收束成:
enqueuePendingNotification({ value: message, mode: 'task-notification' })
然后由:
src/query.ts在 turn 末尾 drain queued commandssrc/utils/attachments.ts转成 attachment- attachment 重新进入后续 query context
也就是说三路最终统一在:
不是统一在执行引擎层,而是统一在“完成后如何重新回到主对话”这一层。
11. runAgent() 的地位:只属于本地两路,不属于 remote
从代码上看:
sync / local async
都直接或间接依赖:
runAgent()query()createSubagentContext(...)
remote
不进入本地 runAgent() 主循环
而是走:
teleportToRemote()RemoteAgentTask- 远端 session
所以如果做架构分层,可以这么看:
runAgent()是 local agent execution engineteleportToRemote()是 remote launch bridgeLocalAgentTask / RemoteAgentTask是 两种 task runtime wrappertask-notification reflow是 统一 completion protocol
12. 最简三路图
AgentTool.call()
→ resolve agent / prompts / tools / isolation
→ effectiveIsolation === remote ?
→ teleportToRemote()
→ registerRemoteAgentTask()
→ return remote_launched
: shouldRunAsync ?
→ registerAsyncAgent()
→ background runAgent()
→ enqueueAgentNotification()
→ return async_launched
:
→ foreground runAgent()
→ finalizeAgentTool()
→ return completed如果把“后续回流”也加进去:
local async complete → enqueueAgentNotification()
remote complete → enqueueRemoteNotification()
→ enqueuePendingNotification(mode=task-notification)
→ query.ts drain queued commands
→ attachments.ts
→ next query context13. 一个更准确的总判断
以前容易把 Claude Code 理解成:
- AgentTool 调 runAgent
- remote 只是 runAgent 的远端版
但更准确的说法应该是:
AgentTool是统一入口没错,但 Claude Code 其实维护了两种不同的执行中台:本地runAgent中台 和 远端 teleport/session 中台。两者靠统一的 task-notification 回流协议粘合在一起。
这也是为什么:
- 本地 async / remote 在用户体验上相似
- 但源码结构上明显不是同一条线
当前结论
三条路径的关系可以压缩成一句:
同一个
AgentTool入口,分流到两种 execution engine(local / remote)和三种 lifecycle mode(sync / async / remote),最后再用同一种 notification re-entry 协议回到主线程。
这基本就是 Claude Code agent runtime 的总骨架。
下一步最值得继续追
- 把
task-notification -> queued command -> attachment再细化成序列图 - 追
teleportToRemote()的session_context/outcomes字段语义 - 单独拆一页:
foreground -> background热切换为什么需要重新跑一条 async lifecycle