源码:https://git.rc707blog.top/rose_cat707/claude-code.git

一、总体架构

architecture.png

二、事件驱动 & 会话编排层

driven_overview.png

  1. 三条互相独立的先进先出队列

第一条是入站命令队列,由 messageQueueManager 维护,所有生产者通过 enqueue 把意图投递进同一条队列,队列内部按 now/next/later 三档优先级排序,使得"用户立即中断"这种高优先级请求能插队到正在排队的低优先级任务之前。

第二条是出站消息队列 structuredIO.outbound,所有要写给客户端的内容——assistant 文本、user 回显、system 事件、result 终态、control_response 控制应答——全部走这一条单出口 FIFO 序列化后再写到 stdout,这样 NDJSON 行流永远不会被多个并发写入撕裂。

第三条是后台 SDK 事件缓冲,由 drainSdkEvents 在 run() 的若干安全节点统一 flush,用来把那些不在主流上发生(后台 agent 进度、任务通知、文件持久化完成)的旁支事件按正确时序插回主流。

  1. 消费者协程 run()

run_coroutine_flow.png

run() 并发安全性保障依赖于一个名为 running 的布尔变量当作互斥位。任何事件源在投递完命令后都可以调用 run() ——如果此刻 running 为 false,新一轮 run() 启动;如果为 true,新调用立刻 return,因为正在跑的那个 run() 会继续把队列里新增的命令一并处理掉。

run() 内部是一个 do-while 结构,循环体里反复调用 drainCommandQueue 把当前队列里所有命令按优先级取出、合并、送进 ask() / queryLoop 真正去跟 LLM 和工具交互;循环条件检查"是否还有后台 agent 在跑、是否还有刚才入队的新命令、是否还有等待 flush 的 SDK 事件",只要任何一项为真就继续转,直到完全空闲才退出 do-while。

退出之后还有一段 finally:把 running 置回 false 之后立刻再做一次 TOCTOU 复查——因为在"判断队列空"和"把锁释放"之间的微小窗口内可能又有事件源往队列塞了东西,这时若不复查就会漏处理;复查发现非空就立刻自重入触发新一轮 run()。

  1. run() 和 queryLoop 的调用

run() 与 ask()/queryLoop 之间是 for-await 关系:run() 把一批命令交给 ask(),ask() 内部以 async iterator 形式吐出一段段中间状态(assistant 增量、工具调用、工具结果、子任务消息),run() 边迭代边把每一段塞进 output.enqueue 走出站队列。

值run() 与 ask() 之间还存在一条横向回环的中断信号通道:当某个生产者投递的是 now 优先级命令(比如用户按下 Esc 或客户端发来 interrupt 控制帧),订阅在命令队列上的 subscribeToCommandQueue 回调会立刻调用 ask() 内部 AbortController 的 abort,让正在进行的 LLM 流式输出和工具调用尽快停下来,把控制权还给 run(),由 run() 的下一轮 do-while 去消费这条新插入的高优命令。

三、核心循环层

核心Loop(ReAct)

具体代码:src/query.ts → queryLoop()