跳到主要内容

智能体主循环:Soul、Wire 与 kosong 一步

30 秒导读: 你在终端敲一句话,Kimi CLI 要把它变成「模型说话 → 调工具 → 看结果 → 再说话」的循环,直到模型不再调工具。这一章讲清楚这个循环怎么被驱动:soul 是循环本体(KimiSoul._agent_loop),wire 是把循环里每一件事实时广播给界面的管道,kosong 是被循环反复调用的「跑一次模型生成 + 派发工具」的底层引擎。核心心智模型只有两个词:turn(一次完整应答)和 step(一次「生成 + 工具」)。

本章是整个项目最核心的一章。上一章 从命令到智能体 讲了 KimiSoul 怎么被装配出来;这一章讲它被 run 起来之后,一个 turn 如何一步步被驱动。上下文压缩/检查点的细节见 上下文与记忆,工具的加载与审批见 工具系统,D-Mail 时间回溯等巧思见 巧妙之处


1. 先分清两个词:turn 与 step(零基础也能懂)

要读懂这一章,只需先建立两个概念的直觉。

  • turn(回合) = 用户说一句话,到智能体这一轮彻底说完为止的整段应答。一个 turn 里模型可能来回好几次。
  • step(步) = turn 内部的一次心跳:调一次模型、拿到它这次说的话(可能带若干工具调用)、把工具跑完、看结果。

一句话直觉:turn 是一段对话,step 是这段对话里模型「开一次口」。 只要模型这次开口还要求调工具,就说明它没说完,循环就再走一个 step;直到某次它只说话、不调工具,turn 就结束。

用最小的例子感受一下:你说「帮我把 README 里的版本号改成 2.0」。

step模型这次做了什么循环怎么反应
step 1说「我先看看 README」+ 调 ReadFile有工具调用 → 跑工具 → 继续
step 2说「找到了,改这里」+ 调 StrReplaceFile有工具调用 → 跑工具 → 继续
step 3说「已改好,版本号现在是 2.0」,不调工具无工具调用 → turn 结束

这三个 step 合起来是一个 turn。本章剩下的内容,就是把「谁来数这些 step、谁去调模型、谁把每一步实时显示到屏幕上、出错了怎么办」讲透。


2. 顶层全景(一个 turn 大概怎么转)

2.1 三个主角

驱动一个 turn 需要三层协作,各司其职:

主角白话职责关键符号在哪个文件
run_soul编排层。用 asyncio 把「跑 soul」「刷 UI」「监听取消」「泵通知」四件事并起来跑run_soulsrc/kimi_cli/soul/__init__.py:179
Wire传输层。soul 单向广播事件,UI(可多个)订阅;顺带落盘 wire.jsonlWire / WireSoulSidesrc/kimi_cli/wire/__init__.py:18
KimiSoul循环层。真正数 step、调 kosong、处理压缩/检查点/错误/回溯KimiSoul._agent_loopsrc/kimi_cli/soul/kimisoul.py:814
kosong.step引擎层。被循环反复调用,跑「一次生成 + 派发工具」kosong.steppackages/kosong/src/kosong/__init__.py:104

2.2 一张图看清数据流

怎么读这张图:左边 soul 单向往 wire 灌事件,右边 UI 从 wire 取事件渲染;soul 内部才是那个反复转的 step 循环。

run_soul(编排,asyncio.wait FIRST_COMPLETED)
├─ soul_task ───────────► KimiSoul.run ─► _turn ─► _agent_loop
│ │ (反复跑 step)
│ ▼
│ kosong.step ─► 模型生成 + 工具 future
│ │
│ wire_send(事件) ◄──────────┘
│ │
│ ┌─────┴──────┐
│ ▼ ▼
│ raw 队列 merged 队列 ──► wire.jsonl 落盘
│ │ │
├─ ui_task ──────────────► └── UI 订阅 ─┘ ──► 终端渲染
├─ cancel_event_task ────► 用户按下取消 → soul_task.cancel() → RunCancelled
└─ notification_task ────► 每秒把后台任务通知泵进 wire

2.3 run_soul:把四件事接起来

run_soul 本身不含业务逻辑,它只做编排:开四个 asyncio task,然后等「soul 跑完」或「用户取消」谁先发生(soul/__init__.py:215, asyncio.wait(..., return_when=asyncio.FIRST_COMPLETED))。

  • 它先建一个 Wire,并用 ContextVar 把它设为「当前 wire」——_current_wire.set(wire)soul/__init__.py:203),这样 soul 里任何深处的代码都能靠 wire_send() 找到它,无需层层传参。
  • 若用户触发取消,就 soul_task.cancel(),吞掉 CancelledError 后抛出 RunCancelledsoul/__init__.py:223-227)。
  • 收尾时无论如何都 wire.shutdown() + await wire.join()soul/__init__.py:244-245),让 UI 循环自然结束、让落盘 recorder 冲刷完毕。

wire_send 是 soul 对外说话的唯一出口,作者把它类比成 soul 的 print

# 真实源码 src/kimi_cli/soul/__init__.py:268 wire_send
def wire_send(msg: WireMessage) -> None:
wire = get_wire_or_none()
assert wire is not None, "Wire is expected to be set when soul is running"
wire.soul_side.send(msg)

它从 ContextVar 取当前 wire,把消息交给 soul 侧。记住这个函数——本章后面所有 StepBeginStatusUpdate、内容片段都是靠它发出去的。


3. Wire 通道:一条会「合并」和「落盘」的广播管道

这一节讲 soul 和 UI 之间那根线到底怎么设计的。

3.1 它要解决的小问题

soul 在一个 step 里会疯狂产出流式碎片:模型一个 token 一个 token 地吐,每个 token 就是一条 TextPart。如果原样全推给 UI,一是消息量爆炸,二是落盘的 wire.jsonl 会碎成几千行。但有时你又确实想要逐字流式(打字机效果)。Wire 的设计正是同时满足这两种需求。

3.2 双队列 + spmc 广播

Wire 是一个 spmc(single-producer multi-consumer,单生产者多消费者)通道:只有 soul 往里写,可以有多个 UI 订阅(wire/__init__.py:18)。它内部开了两条广播队列(wire/__init__.py:24-25):

队列内容谁要它
_raw_queue原始碎片,一个不合并想要逐字打字机效果的 UI
_merged_queue尽量合并后的完整消息普通 UI + wire.jsonl 落盘

UI 订阅哪条,由 ui_side(merge=...) 决定(wire/__init__.py:39)。

3.3 MergeableMixin:合并的秘密

合并逻辑在 WireSoulSide.sendwire/__init__.py:76)。核心是一个 _merge_buffer:来一条消息,如果它是「可合并的」,就试着并进缓冲区;并不进去(类型变了)才把缓冲区冲刷出去、另起新缓冲:

# 真实源码 src/kimi_cli/wire/__init__.py:87 WireSoulSide.send(节选)
match msg:
case MergeableMixin():
if self._merge_buffer is None:
self._merge_buffer = copy.deepcopy(msg)
elif self._merge_buffer.merge_in_place(msg):
pass # 并进去了,什么都不发
else:
self.flush() # 类型变了,先把旧的发出去
self._merge_buffer = copy.deepcopy(msg)
case _:
self.flush() # 不可合并的消息,立即冲刷
self._send_merged(msg)

「能不能并」由消息自己实现的 merge_in_place 决定。以文本为例,两个 TextPart 直接字符串相加(packages/kosong/src/kosong/message.py:84 TextPart.merge_in_place);MergeableMixin 基类默认返回 Falsemessage.py:10),即「默认不可合并」。于是 raw 队列拿到每一个 token,merged 队列拿到的是「一整段连续文本合成一条」。

3.4 落盘:_WireRecorder

如果创建 Wire 时给了 file_backend,它会订阅 merged 队列,起一个后台 task 把每条完整消息 append 进 wire.jsonlwire/__init__.py:130 _WireRecorder:147 _recordappend_message)。用 merged 而非 raw,正是为了让落盘文件是「一条条完整消息」而不是碎 token。这个 jsonl 就是会话可回放、可审计的底账。

3.5 消息种类:Event 与 Request

wire 上流动的消息统称 WireMessage,分两大族(wire/types.py:603):

  • Event —— 单向广播,不等回复(wire/types.py:516)。
  • Request —— 期待一个回复(如工具审批),内部挂一个 asyncio.Futureawait request.wait() 直到 UI 侧 resolvewire/types.py:600)。

本章主循环里高频出现的 Event:

消息含义定义
TurnBegin / TurnEnd一个 turn 的开始/结束边界wire/types.py:37 / :56
StepBegin第 n 个 step 开始wire/types.py:66
StepInterrupted / StepRetrystep 被打断 / 即将重试wire/types.py:76 / :82
StatusUpdatetoken 用量、上下文占比、message id 等状态wire/types.py:176
CompactionBegin / CompactionEnd压缩的起止wire/types.py:99 / :110
ToolResult一个工具的执行结果来自 kosong.toolingwire/types.py:534
SteerInput用户在 turn 运行中追加的插话wire/types.py:46

4. 主循环:KimiSoul 怎么数 step

现在进入循环本体。三层嵌套:run(一个 turn 的外壳)→ _turn(准备上下文)→ _agent_loop(数 step 的 while)→ _step(单步)。

4.1 run:turn 的外壳与两个 hook

KimiSoul.runkimisoul.py:577)负责一个 turn 的边界与副作用,业务循环它转交给下层。它按顺序做:

  1. UserPromptSubmit hookkimisoul.py:610):用户输入先过一遍钩子,钩子可以 block 掉整个 turn。
  2. TurnBeginkimisoul.py:628),标记 turn 开始。
  3. 分派:如果输入是 slash 命令就走命令(kimisoul.py:636);否则如果开了 ralph 自动循环就走 FlowRunner.ralph_loopkimisoul.py:645,详见 巧妙之处);普通情况走 _turnkimisoul.py:652)。
  4. Stop hookkimisoul.py:655):turn 想结束时,钩子可以 block 并塞回一句话,逼智能体再跑一个 _turn——但只允许再触发一次_stop_hook_active 防死循环)。
  5. TurnEnd,并在首个真实 turn 后自动生成会话标题(取用户输入前 50 字,kimisoul.py:677-698)。
  6. finally 里兜底:若 turn 已开始但没正常结束,补发一个 TurnEnd 并打「中断」遥测(kimisoul.py:700-708)。

4.2 _turn:一次性准备

_turnkimisoul.py:717)很短:校验 LLM 与消息能力 → 生成新的 _current_turn_id打检查点 0await self._checkpoint()kimisoul.py:726)→ 把用户消息 append 进上下文 → 进入 _agent_loop。检查点是后面 D-Mail 时间回溯的落脚点。

4.3 _agent_loop:分段注释就是骨架

_agent_loopkimisoul.py:814)的类 docstring 把生命周期写成了阶段编号,源码里也用醒目的分隔注释一一对应——读这个 docstring 等于读骨架

1. TURN INIT 清掉上一 turn 的残留 steer;加载 MCP 工具(可能 defer)
2. STEP LOOP while True:
a. Step Guard step_no 超 max_steps_per_turn → 抛 MaxStepsReached
b. Step Begin wire_send(StepBegin(n))
c. Compaction 上下文超阈值 → compact_context()
d. Checkpoint 调 LLM 前先 _checkpoint() 落一个可回溯点
e. Execute step_outcome = await self._step()
f. Error Handling BackToTheFuture(回溯) / 其它异常(致命,抛出)
g. Outcome Resolution 有 steer→继续;否则按 stop_reason 收尾
3. TURN RESOLUTION 返回 TurnOutcome

几个要点:

  • step 上限在阶段 2a(kimisoul.py:881):step_no > max_steps_per_turn 直接抛 MaxStepsReached,防止模型无限调工具烧钱。
  • 压缩在调 LLM 之前(阶段 2c,kimisoul.py:893):先看加上待发消息后会不会超 max_context_size 的触发比例,超了就先 compact_context()
  • 检查点在压缩之后、执行之前(阶段 2d,kimisoul.py:913),保证每个 step 起点都有可回溯的快照。
  • 两类错误分流(阶段 2f):BackToTheFuture 是「回到过去」的信号而非故障——捕获后在阶段 2g 末尾 revert_to(checkpoint_id) 把上下文倒带并注入 D-Mail(kimisoul.py:991-996);其它异常则发 StepInterrupted、打遥测、触发 StopFailure hook 后抛出,终止整个 loop(kimisoul.py:923-968)。

4.4 outcome resolution:steer 优先,再决定停不停

单步返回后(kimisoul.py:971),循环先问一句:有没有用户插话(steer)? 有就把它当追加的 user 消息注入,continue 强制再走一个 step(kimisoul.py:973-975);没有才根据 _step 给的 stop_reason 组装 TurnOutcome 返回。stop_reason 有三种:no_tool_calls(模型只说话)、tool_rejected(用户拒绝了工具)、tool_call_repeat(重复调用被强停)。


5. _step:一个 step 的八个子阶段

_stepkimisoul.py:1001)是整章的核心。它同样用编号注释自述结构,对应关系如下:

子阶段干什么源码锚点
2e.1 通知投递root 会话把待发的后台任务通知 append 进上下文kimisoul.py:1025
2e.2 动态注入收集各 provider 的注入(如 plan/afk 提醒),并成一条 system-reminderkimisoul.py:1059
2e.3 历史归一化normalize_history 合并相邻 user 消息kimisoul.py:1072
2e.4 带重试的 LLM 调用kosong.step + tenacity 重试 + 连接恢复kimisoul.py:1077-1115
2e.5 用量与状态记录 token、发 StatusUpdatekimisoul.py:1130-1148
2e.6 工具执行await result.tool_results() 等所有工具跑完kimisoul.py:1155
2e.7 上下文生长asyncio.shield 保护地 append 助手消息 + 工具消息kimisoul.py:1171
2e.8 结果判定拒绝 / D-Mail / 强停 / 有无工具调用 → 决定返回什么kimisoul.py:1176-1222

5.1 2e.4:为什么调个模型要包三层

_run_step_oncekimisoul.py:1077)里真正调的是 kosong.stepkimisoul.py:1084),但它被包了三层,从内到外:

_run_step_once # 最内:begin_step 去重 + kosong.step 一次生成
└─ _run_with_connection_recovery # 中层:401 刷 token / 连接错误 recover 一次
└─ tenacity.retry # 外层:指数退避 + 抖动,最多 N 次
  • 最内层调 toolset.begin_step(self._last_tool_calls)kimisoul.py:1081),把上一步的工具调用喂进去,用于去重(同一批重复调用会被拦,见 工具系统)。
  • 外层 tenacity(kimisoul.py:1099)用 wait_exponential_jitter(initial=0.3, max=5, jitter=0.5),重试前会发 StepRetry 事件让 UI 显示「正在重试」。是否重试由 _is_retryable_error 判定(见 §7)。

5.2 2e.7:为什么 append 要 shield

await asyncio.shield(self._grow_context(result, results))kimisoul.py:1171)——工具执行阶段可以被用户取消打断,但一旦工具已经跑完、要把「助手说的话 + 工具结果」写进上下文这步,就不能被打断到一半,否则上下文会出现「有工具结果却没有对应的助手消息」的破损状态。shield 保证这段原子完成。

5.3 2e.8:这一步到底停不停

判定顺序(kimisoul.py:1176 起)也很讲究:

  1. 纯拒绝:若有工具被拒且没带用户反馈、且是 root → stop_reason="tool_rejected",停。子智能体不停,好让模型看到拒绝、换个法子。
  2. D-Mail:若有待处理的 D-Mail,抛 BackToTheFuture 让主循环倒带(kimisoul.py:1195)。
  3. 强停:若 toolset 标了 force_stop_turn(重复调用被识破)→ stop_reason="tool_call_repeat"
  4. 常规result.tool_calls 非空 → 返回 None(继续下一 step);为空 → stop_reason="no_tool_calls",turn 自然结束。

6. kosong 一步:一次生成 + 一批工具 future

前面 _step 把「调模型」这件事整个委托给了 kosong.step。这一节钻进去,看这一步在最底层怎么完成——这也是理解「工具为什么是异步 future」「step 与 turn 边界在哪」的关键。

6.1 kosong.step:生成时就把工具 future 攒起来

kosong.stepkosong/__init__.py:104)的巧妙在于:它不等生成完再处理工具,而是在流式生成过程中,每冒出一个完整的工具调用就立刻派发。派发的钩子是 on_tool_call

# 真实源码 packages/kosong/src/kosong/__init__.py:144 on_tool_call(节选)
async def on_tool_call(tool_call: ToolCall):
tool_calls.append(tool_call)
result = toolset.handle(tool_call) # 交给 toolset 处理
if isinstance(result, ToolResult): # 同步就有结果
future = ToolResultFuture()
future.set_result(result)
tool_result_futures[tool_call.id] = future
else: # 异步:拿到的是一个 future
tool_result_futures[tool_call.id] = result

关键点:toolset.handle 返回的可能是已就绪的 ToolResult,也可能是一个尚未完成的 ToolResultFuturekosong/tooling/__init__.py:328 ToolResultFuture = Future[ToolResult])。step 把它们统统塞进 tool_result_futures 字典,不在这里等

生成结束后,step 把消息、用量、工具调用列表、以及那一堆 future 一起打包成 StepResultkosong/__init__.py:174)。真正的「等工具跑完」推迟到调用方 await result.tool_results()kosong/__init__.py:200)——这正是 _step 的 2e.6 阶段。

6.2 为什么工具是异步 future(讲透)

这是本项目一个核心设计决策,值得单独说清。把工具做成 future,而不是「调用即阻塞拿结果」,带来三个好处:

  • 生成与执行重叠。 模型还在吐后面的 token 时,前面已经吐完的工具调用就能并发地先跑起来——handle 一返回 future,工具就在后台执行了,不必等整条消息生成完。
  • 一次可以调多个工具、并行等。 一个 step 里模型可能调 3 个工具,它们各自是一个 future,tool_results() 里按 tool_call.id 逐个 awaitkosong/__init__.py:207-211),底层是并发就绪的。
  • 可干净地取消。 生成或等待被打断时,step 会把所有未完成的 future cancel() 掉再 gatherkosong/__init__.py:166-171:213-216),避免留下「悬空的后台任务」。审批类工具(需要用户点「同意」)天然是长期挂起的 future——用户不点,它就一直 pending,正好契合这个模型。

一句话:future 让「模型说要调工具」和「工具真的跑完」在时间上解耦,这是异步 agent 能并发、能取消、能等审批的地基。

6.3 step 与 turn 的边界(讲透)

初学者最容易混的就是这条边界。用一句话钉死:

一个 step = 恰好一次 kosong.generate(模型开一次口)+ 这次开口派生的所有工具跑完。turn = 反复跑 step,直到某个 step 的模型开口里没有工具调用。

  • 谁数 step? _agent_loopwhile Truekimisoul.py:877)。它每转一圈发一个 StepBegin
  • 谁结束 turn? _step 返回一个非 NoneStepOutcomekimisoul.py:1222),_agent_loop 就跳出循环。最常见的结束是 no_tool_calls——模型只说话不调工具。
  • 边界为什么落在「有没有工具调用」? 因为工具结果必须回喂给模型让它继续,所以「还有工具」就意味着「模型还得再开一次口」,turn 没完;「没有工具」意味着模型给出了最终答复,turn 完。

这条边界也解释了 §5 里那些判定:D-Mail 回溯、拒绝、强停,本质都是「在正常的『有无工具』判定之外,额外提供的提前结束/倒带 step 的出口」。

6.4 kosong.generate:pending-part 合并

kosong.step 底层调 generatekosong/_generate.py:17)。模型是流式吐 part 的:一段文本会拆成很多小 TextPart,一个工具调用的参数 JSON 会拆成很多 ToolCallPartgenerate 的核心工作就是把这些碎片边流边合并成完整 part。

思路是维护一个 pending_part(当前还没拼完的 part):新碎片来了先试着并进 pending;并不进去(类型变了)说明 pending 已经完整,就把它推入消息、并且——如果它是个完整的工具调用,立刻回调 on_tool_call(这就是 §6.1 派发工具的触发点)。

用简化 Python 演示这个「pending-part 合并」思路(# 示意,非源码):

# 示意,非源码:generate 的流式合并骨架
async def generate(stream, on_part, on_tool_call):
message = Message(role="assistant", content=[])
pending = None # 当前还没拼完的 part
async for part in stream: # 模型流式吐碎片
if on_part:
on_part(part) # 逐碎片推给 UI(打字机效果)
if pending is None:
pending = part
elif not pending.merge_in_place(part): # 并不进去 = pending 已完整
append(message, pending)
if isinstance(pending, ToolCall): # 完整工具调用 → 立即派发
on_tool_call(pending)
pending = part # 换新的 pending
if pending is not None: # 收尾:最后一个 pending 也要处理
append(message, pending)
if isinstance(pending, ToolCall):
on_tool_call(pending)
return message

重点看:merge_in_place 返回 False 是「一个 part 拼完了」的信号,也是「该派发工具了」的时机。真实实现见 _generate.py:59-72。合并规则由各 part 自己实现——ToolCall.merge_in_place 把后续 ToolCallPart 的 arguments 片段续到 JSON 字符串后面(message.py:203),ToolCallPart 之间也能互相续(message.py:220)。

6.5 两个「空响应」防线

generate 收尾时有两处健壮性判断,都抛 APIEmptyResponseError(会被上层判为可重试,见 §7):

  • 纯空:消息既无内容也无工具调用(_generate.py:74)。
  • think-only:只有思考内容(ThinkPart),没有可见文本、也没有工具调用(_generate.py:81-89)。作者注释解释:模型思考后总该产出可见输出,只有思考往往意味着流被中断或 output token 预算在推理途中耗尽——这从来不是正常终止。

7. 错误分类与恢复:让一次抖动不至于炸掉 turn

网络会抖、token 会过期、provider 会 5xx。主循环用三件工具把这些「可恢复的坏事」挡在用户面前。

7.1 classify_api_error:把异常翻成标签

classify_api_errorkimisoul.py:99)把各种 provider 异常映射成 (error_type, status_code),供遥测统计。它被特意提到模块级,好让测试直接 import 真实分类表而非各写一份。分类表大致:

异常/状态标签
429rate_limit
401 / 403auth
≥5005xx_server
4xx 且提到 context length / max tokenscontext_overflow
APIConnectionErrornetwork
超时timeout
APIEmptyResponseErrorempty_response

7.2 _is_retryable_error:这个错该不该重试

tenacity 的重试条件是 _is_retryable_errorkimisoul.py:1396)。判定:

  • APIConnectionError / APITimeoutError 可重试——但如果已被标记 _kimi_recovery_exhausted(连接恢复也救不回来),就不再重试。
  • APIEmptyResponseError 可重试(对应 §6.5 的两条空响应防线)。
  • APIStatusError 只在 429/500/502/503/504 时重试;4xx(除 429)通常是请求本身的问题,重试无益。

7.3 _run_with_connection_recovery:重试之前先试着「修好连接」

_run_with_connection_recoverykimisoul.py:1409)夹在 tenacity 与实际调用之间,处理两类特殊错误,各只救一次

  • 401(token 过期):若当前 provider 用 OAuth,就 oauth.ensure_fresh(force=True) 强制刷新 token,然后重入恢复流程重试(kimisoul.py:1420-1450)。纯 API-key provider 没什么可刷,直接抛。
  • 连接/超时错误:若 provider 实现了 RetryableChatProvider,调 on_retryable_error 尝试恢复连接(比如重建底层连接),成功就重入重试一次;再不行就打上 _kimi_recovery_exhausted 标记抛出,让 _is_retryable_error 不再纠缠(kimisoul.py:1451-1492)。

两者都用「重入自身、置标志位」的写法,好让「刷了 token 后的那次重试若又撞上连接错误」仍能被连接恢复接住——递归地叠加两种恢复,而不是各管各的

7.4 一图看清三层防线的顺序

kosong.step 抛错


_run_with_connection_recovery ── 401? → 刷 token,重入
│ ── 连接错? → provider 恢复一次,重入
▼ (仍失败)
tenacity.retry ── _is_retryable_error? → 指数退避+抖动,发 StepRetry,最多 N 次
│ (仍失败)

_step 给异常挂上 model/duration/input_tokens 上下文,抛给 _agent_loop


_agent_loop 2f-ii:发 StepInterrupted、打 api_error 遥测、触发 StopFailure hook,抛出终止

8. 代码地图(导航索引)

主题文件符号
turn 编排(asyncio 接线)src/kimi_cli/soul/__init__.pyrun_soul
取消信号src/kimi_cli/soul/__init__.pyRunCancelled
当前 wire(ContextVar)与出口src/kimi_cli/soul/__init__.py_current_wire / wire_send
通知泵src/kimi_cli/soul/__init__.py_pump_notifications_to_wire
Wire 通道(spmc 双队列)src/kimi_cli/wire/__init__.pyWire
soul 侧发送与合并src/kimi_cli/wire/__init__.pyWireSoulSide.send
merged 队列落盘src/kimi_cli/wire/__init__.py_WireRecorder
合并规则基类packages/kosong/src/kosong/message.pyMergeableMixin.merge_in_place
消息种类(Event/Request)src/kimi_cli/wire/types.pyEvent / WireMessage
turn 外壳、两个 hook、自动标题src/kimi_cli/soul/kimisoul.pyKimiSoul.run
turn 准备与检查点 0src/kimi_cli/soul/kimisoul.pyKimiSoul._turn
数 step 的主循环src/kimi_cli/soul/kimisoul.pyKimiSoul._agent_loop
单步(八子阶段)src/kimi_cli/soul/kimisoul.pyKimiSoul._step
上下文原子生长src/kimi_cli/soul/kimisoul.pyKimiSoul._grow_context
D-Mail 回溯信号src/kimi_cli/soul/kimisoul.pyBackToTheFuture
错误分类表src/kimi_cli/soul/kimisoul.pyclassify_api_error
可重试判定src/kimi_cli/soul/kimisoul.pyKimiSoul._is_retryable_error
连接/401 恢复src/kimi_cli/soul/kimisoul.pyKimiSoul._run_with_connection_recovery
一步:生成 + 工具 futurepackages/kosong/src/kosong/__init__.pystep / StepResult.tool_results
流式 part 合并、空响应判定packages/kosong/src/kosong/_generate.pygenerate
工具结果 future 类型packages/kosong/src/kosong/tooling/__init__.pyToolResultFuture / Toolset.handle