Harness · Coding Agent Book14 / Kimi CLI
研究总览
M14 · SOURCE-GROUNDED TUTORIAL

Kimi CLI
从源码学会它怎么工作

检查点、D-Mail、后台任务和持久子 Agent 让长任务体验突出;默认 KAOS 仍是宿主执行。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。

Python · Checkpointed Long-running AgentApache-2.04a550effdfcb30 个结论 · 95 处引用
这门课怎么读

先建立直觉,再沿一条任务链钻进代码

参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 Kimi CLI 的 30 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。

你会得到一张可复述的架构地图一次完整任务的链路追踪能迁移到自研 Harness 的设计判断
你不会得到把 README 功能当成已验证事实把 prompt 约束说成 OS 沙箱把一次双模型调用夸成多 Agent 平台
M00 · MAP

先看全景:这个 Agent 的控制面在哪里

下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。

架构图全屏打开 ↗
一轮执行链路全屏打开 ↗
核心机制

带 checkpoint 的多 step;notification injection;D-Mail 回退

上下文

可挽救 JSONL;摘要保尾;压缩后恢复活动后台任务

适用建设

超长自主任务、后台 build、可恢复研究与编码

M00.5 · TRACE

跟踪一个任务:从输入到交付

把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。

读图提醒

箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。

M01 · ORIENTATION

先把 Agent 看成一台会交付的机器

如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。

先用一个生活比喻

把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 1

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M02 · LOOP

主循环:模型为什么会继续动

一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?

先用一个生活比喻

像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。

这套实现先回答了什么?

一次用户输入可能触发多次“模型思考—调工具—看结果—再思考”,每一步前都留存档点。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
每轮是带检查点的多步状态机,不是单次聊天请求src/kimi_cli/soul/kimisoul.py:659一次用户输入可能触发多次“模型思考—调工具—看结果—再思考”,每一步前都留存档点。
每 step 先注入通知/动态约束,再一次生成并等待并发工具src/kimi_cli/soul/kimisoul.py:1132模型每次再开口前都会先收新通知和当前模式规则;工具可以边生成边启动,结果到齐后才记账。
steer 与 D-Mail 提供两种中途改道机制src/kimi_cli/soul/kimisoul.py:622用户插话是在当前路线后追加新指令;D-Mail 则像读档,把对话回到旧存档再塞入一张“未来经验”纸条。
错误恢复分 OAuth、连接、空响应和 HTTP 状态码src/kimi_cli/soul/kimisoul.py:1220不是遇错一律重试:令牌过期先换令牌,连接坏了让 provider 重建连接,服务繁忙才退避。
01
L1 · fact · kimi-loop-001

每轮是带检查点的多步状态机,不是单次聊天请求

先看源码事实

run 先刷新 OAuth、建立 approval source、运行 UserPromptSubmit hook,再进入 _turn;_turn 先 checkpoint、持久化 user message,然后 _agent_loop 逐 step 做压缩、checkpoint、LLM+工具执行,直到无工具调用、拒绝或重复调用强停。

翻译成白话

一次用户输入可能触发多次“模型思考—调工具—看结果—再思考”,每一步前都留存档点。

为什么这对自研重要

长任务可恢复、可插话,也需要严格的步数上限和副作用治理。

固定提交源码摘录
  659      async def run(
  660          self,
  661          user_input: str | list[ContentPart],
  662          *,
  663          skip_user_prompt_hook: bool = False,
  664      ):
  665          approval_source_token = None
  666          created_approval_source: ApprovalSource | None = None
  667          turn_started = False
  668          turn_finished = False
  669          interrupt_reason: str | None = None
  670          turn_t0 = time.monotonic()
  671          self._set_trace_id(None)
  672          if get_current_approval_source_or_none() is None:
  673              created_approval_source = ApprovalSource(kind="foreground_turn", id=uuid.uuid4().hex)
  674              approval_source_token = set_current_approval_source(created_approval_source)
  675          try:
  676              # Refresh OAuth tokens on each turn to avoid idle-time expirations.
  677              await self._runtime.oauth.ensure_fresh(self._runtime)
  678  
  679              # Set session_id ContextVar for toolset hooks
  680              from kimi_cli.soul.toolset import set_session_id
  681  
  682              set_session_id(self._runtime.session.id)
  683  
      … 48 lines omitted; exact range 659–742 …
  732                      if isinstance(ret, Awaitable):
  733                          await ret
  734              elif self._loop_control.max_ralph_iterations != 0:
  735                  runner = FlowRunner.ralph_loop(
  736                      user_message,
  737                      self._loop_control.max_ralph_iterations,
  738                  )
  739                  await runner.run(self, "")
  740              else:
  741                  await self._turn(user_message)
  742  
为什么相信这条结论?查看 3 处证据
02
L1 · fact · kimi-loop-002

每 step 先注入通知/动态约束,再一次生成并等待并发工具

先看源码事实

root 每 step 最多领取 4 条 pending notification;Plan/AFK 等 injection 合并成 user-side system reminder;历史归一化后调用 kosong.step,流式回调消息和工具结果,随后 await 所有 tool results 并 shield 写回上下文。

翻译成白话

模型每次再开口前都会先收新通知和当前模式规则;工具可以边生成边启动,结果到齐后才记账。

为什么这对自研重要

实时性和并行性强;动态注入是上下文的一部分,会影响 token 与缓存。

固定提交源码摘录
 1132          # ═══════════════════════════════════════════════════════════════════════
 1133          # 2e.1. NOTIFICATION DELIVERY (root role only)
 1134          # ═══════════════════════════════════════════════════════════════════════
 1135          if self.is_root:
 1136  
 1137              async def _append_notification(view: NotificationView) -> None:
 1138                  await self._context.append_message(build_notification_message(view, self._runtime))
 1139                  # --- Notification hook ---
 1140                  from kimi_cli.hooks import events
 1141  
 1142                  _hook_task = asyncio.create_task(
 1143                      self._hook_engine.trigger(
 1144                          "Notification",
 1145                          matcher_value=view.event.type,
 1146                          input_data=events.notification(
 1147                              session_id=self._runtime.session.id,
 1148                              cwd=str(Path.cwd()),
 1149                              sink="llm",
 1150                              notification_type=view.event.type,
 1151                              title=view.event.title,
 1152                              body=view.event.body,
 1153                              severity=view.event.severity,
 1154                          ),
 1155                      )
 1156                  )
      … 27 lines omitted; exact range 1132–1194 …
 1184              chat_provider,
 1185              system_prompt=self._agent.system_prompt,
 1186              tools=self._agent.toolset.tools,
 1187              history=effective_history,
 1188              input_tokens_floor=self._context.token_count_with_pending,
 1189          )
 1190          request_chat_provider = with_kimi_generation_overrides(chat_provider, generation_overrides)
 1191          request_chat_provider = with_trace_callback(
 1192              request_chat_provider,
 1193              self._set_trace_id,
 1194          )
为什么相信这条结论?查看 3 处证据
03
L1 · fact · kimi-loop-003

steer 与 D-Mail 提供两种中途改道机制

先看源码事实

steer queue 会在 step 结束或相邻 step 间作为新的 user message 注入;SendDMail 可携 checkpoint id 触发 BackToTheFuture,主循环旋转并回退 context,再注入未来消息并清空重复调用状态。

翻译成白话

用户插话是在当前路线后追加新指令;D-Mail 则像读档,把对话回到旧存档再塞入一张“未来经验”纸条。

为什么这对自研重要

适合长程纠偏和探索回溯,但文件系统副作用不会随 context 自动回滚。

边界与风险
  • 回滚的是对话 JSONL,不是 git/worktree 或外部服务事务。
固定提交源码摘录
  622      def steer(self, content: str | list[ContentPart]) -> None:
  623          """Queue a steer message for injection into the current turn."""
  624          self._steer_queue.put_nowait(content)
  625  
  626      async def _consume_pending_steers(self) -> bool:
  627          """Drain the steer queue and inject as follow-up user messages.
  628  
  629          Returns True if any steers were consumed.
  630  
  631          Note: /btw is intercepted at the UI layer (``classify_input``) before
  632          reaching the steer queue, so it never appears here.
  633          """
  634          consumed = False
  635          while not self._steer_queue.empty():
  636              content = self._steer_queue.get_nowait()
  637              await self._inject_steer(content)
  638              wire_send(SteerInput(user_input=content))
  639              consumed = True
  640          return consumed
  641  
  642      async def _inject_steer(self, content: str | list[ContentPart]) -> None:
  643          """Inject a single steer as a regular follow-up user message."""
  644          parts = cast(
  645              list[ContentPart],
  646              [TextPart(text=content)] if isinstance(content, str) else list(content),
  647          )
  648          message = Message(role="user", content=parts)
  649          if self._runtime.llm is None:
  650              raise LLMNotSet()
  651          if missing_caps := check_message(message, self._runtime.llm.capabilities):
  652              raise LLMNotSupported(self._runtime.llm, list(missing_caps))
  653          await self._context.append_message(message)
为什么相信这条结论?查看 3 处证据
04
L1 · fact · kimi-loop-004

错误恢复分 OAuth、连接、空响应和 HTTP 状态码

先看源码事实

step/compaction 使用指数抖动重试;401 仅对 OAuth provider 强制刷新一次;连接/超时可调用 RetryableChatProvider.on_retryable_error 并只恢复一次;空响应和 429/500/502/503/504 进入 tenacity。

翻译成白话

不是遇错一律重试:令牌过期先换令牌,连接坏了让 provider 重建连接,服务繁忙才退避。

为什么这对自研重要

恢复语义清楚;408/409/529 在遥测里可标 retryable,但当前 Python 重试循环并不重试它们。

固定提交源码摘录
 1220          def _before_step_retry_sleep(retry_state: RetryCallState) -> None:
 1221              self._retry_log("step", retry_state)
 1222              self._emit_step_retry(retry_state, max_attempts=max_attempts)
 1223  
 1224          @tenacity.retry(
 1225              retry=retry_if_exception(self._is_retryable_error),
 1226              before_sleep=_before_step_retry_sleep,
 1227              wait=wait_exponential_jitter(initial=0.3, max=5, jitter=0.5),
 1228              stop=stop_after_attempt(max_attempts),
 1229              reraise=True,
 1230          )
 1231          async def _kosong_step_with_retry() -> StepResult:
 1232              return await self._run_with_connection_recovery(
 1233                  "step",
 1234                  _run_step_once,
 1235                  chat_provider=chat_provider,
 1236              )
 1237  
 1238          t0 = time.monotonic()
 1239          try:
 1240              result = await _kosong_step_with_retry()
 1241          except Exception as _step_exc:
 1242              if isinstance(_step_exc, ChatProviderError):
 1243                  _track_api_error(
 1244                      _step_exc,
 1245                      llm=self._runtime.llm,
 1246                      duration_ms=int((time.monotonic() - t0) * 1000),
 1247                      input_tokens=self._context.token_count,
 1248                  )
 1249              raise
为什么相信这条结论?查看 3 处证据
小练习 2

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M03 · MODEL

模型调用:流式输出如何变成可执行步骤

模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?

先用一个生活比喻

模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。

这套实现先回答了什么?

底层负责把碎片拼成完整回复,上层负责一看到完整工具单就开工;若模型流中断,尚未完成的工具任务会被收拢取消。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
Kosong 把流式生成和工具调度拆成两层契约packages/kosong/src/kosong/_generate.py:52底层负责把碎片拼成完整回复,上层负责一看到完整工具单就开工;若模型流中断,尚未完成的工具任务会被收拢取消。
同一 LLM 层支持 Kimi、OpenAI、Anthropic、Gemini 与 Vertexsrc/kimi_cli/llm.py:326主循环不绑某一家 API,同一套工具和上下文可换不同模型协议。
completion 上限按真实请求估算并留安全边际src/kimi_cli/llm.py:181不只数聊天正文,还把工具说明书和图片等隐藏成本算进去,再决定模型最多还能写多少。
05
L2 · fact · kimi-provider-001

Kosong 把流式生成和工具调度拆成两层契约

先看源码事实

generate 合并流式 Text/Think/ToolCall part,拒绝完全空或仅 thinking 的异常响应;step 在完整 tool call 到达时立即交 Toolset.handle,并收集异步 futures,生成失败/取消时取消所有在途工具。

翻译成白话

底层负责把碎片拼成完整回复,上层负责一看到完整工具单就开工;若模型流中断,尚未完成的工具任务会被收拢取消。

为什么这对自研重要

Provider 和 Harness 解耦,同时避免中断后悬挂任务。

固定提交源码摘录
   52      message = Message(role="assistant", content=[])
   53      pending_part: StreamedMessagePart | None = None  # message part that is currently incomplete
   54  
   55      logger.trace("Generating with history: {history}", history=history)
   56      stream = await chat_provider.generate(system_prompt, tools, history)
   57      if on_trace_id:
   58          # getattr for robustness against third-party StreamedMessage
   59          # implementations that predate the trace_id property.
   60          await callback(on_trace_id, getattr(stream, "trace_id", None))
   61      async for part in stream:
   62          logger.trace("Received part: {part}", part=part)
   63          if on_message_part:
   64              await callback(on_message_part, part.model_copy(deep=True))
   65  
   66          if pending_part is None:
   67              pending_part = part
   68          elif not pending_part.merge_in_place(part):  # try merge into the pending part
   69              # unmergeable part must push the pending part to the buffer
   70              _message_append(message, pending_part)
   71              if isinstance(pending_part, ToolCall) and on_tool_call:
   72                  await callback(on_tool_call, pending_part)
   73              pending_part = part
   74  
   75      # end of message
   76      if pending_part is not None:
      … 16 lines omitted; exact range 52–103 …
   93              "without any text or tool calls. This usually indicates the "
   94              "stream was interrupted or the output token budget was exhausted "
   95              "during reasoning."
   96          )
   97  
   98      return GenerateResult(
   99          id=stream.id,
  100          message=message,
  101          usage=stream.usage,
  102          trace_id=getattr(stream, "trace_id", None),
  103      )
为什么相信这条结论?查看 2 处证据
06
L1 · fact · kimi-provider-002

同一 LLM 层支持 Kimi、OpenAI、Anthropic、Gemini 与 Vertex

先看源码事实

create_llm 按 provider type 构造 Kimi、OpenAI legacy/Responses、Anthropic、GoogleGenAI/Vertex,以及测试 Echo/Chaos;模型 capability 决定 thinking/image 等能力,thinking 可按配置开启或关闭。

翻译成白话

主循环不绑某一家 API,同一套工具和上下文可换不同模型协议。

为什么这对自研重要

可移植性好,但 Kimi 专属 generation override、prompt cache key 与 preserved thinking 只在 Kimi provider 生效。

固定提交源码摘录
  326  def create_llm(
  327      provider: LLMProvider,
  328      model: LLMModel,
  329      *,
  330      thinking: bool | None = None,
  331      session_id: str | None = None,
  332      oauth: OAuthManager | None = None,
  333  ) -> LLM | None:
  334      if provider.type not in {"_echo", "_scripted_echo"} and (
  335          not provider.base_url or not model.model
  336      ):
  337          logger.warning(
  338              "Cannot create LLM: missing base_url or model (provider_type={provider_type})",
  339              provider_type=provider.type,
  340          )
  341          return None
  342  
  343      resolved_api_key = (
  344          oauth.resolve_api_key(provider.api_key, provider.oauth)
  345          if oauth and provider.oauth
  346          else provider.api_key.get_secret_value()
  347      )
  348  
  349      match provider.type:
  350          case "kimi":
      … 109 lines omitted; exact range 326–470 …
  460                  provider=Kimi(
  461                      model=model.model,
  462                      base_url=provider.base_url,
  463                      api_key=resolved_api_key,
  464                      default_headers=_kimi_default_headers(provider, oauth),
  465                  ),
  466                  chaos_config=ChaosConfig(
  467                      error_probability=0.8,
  468                      error_types=[429, 500, 503],
  469                  ),
  470              )
为什么相信这条结论?查看 2 处证据
07
L1 · fact · kimi-provider-003

completion 上限按真实请求估算并留安全边际

先看源码事实

请求估算包含 system prompt、工具 schema、role/metadata、tool calls、media 与历史;Kimi completion cap 取配置预算和剩余 context 的较小值,主 step 还把 pending token floor 与 safety margin 算入。

翻译成白话

不只数聊天正文,还把工具说明书和图片等隐藏成本算进去,再决定模型最多还能写多少。

为什么这对自研重要

减少输出顶破窗口;估算仍非 provider tokenizer 的精确值。

固定提交源码摘录
  181  def compute_max_completion_tokens(
  182      *,
  183      max_context_size: int,
  184      input_tokens: int,
  185      response_budget: int | None,
  186      fallback_budget: int = DEFAULT_UNKNOWN_CONTEXT_COMPLETION_TOKENS,
  187  ) -> int:
  188      """Compute the Kimi completion cap from the hard cap and remaining context."""
  189      if max_context_size <= 0:
  190          return max(1, response_budget if response_budget is not None else fallback_budget)
  191  
  192      input_tokens = max(0, input_tokens)
  193      remaining = max(1, max_context_size - input_tokens)
  194      requested = response_budget if response_budget is not None else max_context_size
  195      return max(1, min(requested, remaining))
  196  
  197  
  198  def estimate_request_tokens(
  199      system_prompt: str,
  200      tools: Sequence[Tool],
  201      history: Sequence[Message],
  202  ) -> int:
  203      """Estimate all token-bearing parts of a chat request.
  204  
  205      The estimate is deliberately request-scoped: unlike ``Context.token_count_with_pending``,
      … 47 lines omitted; exact range 181–263 …
  253              total += _estimate_text_tokens(part.model_dump_json(exclude_none=True))
  254  
  255      for tool_call in message.tool_calls or ():
  256          total += _estimate_text_tokens(tool_call.id)
  257          total += _estimate_text_tokens(tool_call.function.name)
  258          total += _estimate_text_tokens(tool_call.function.arguments or "")
  259          if tool_call.extras:
  260              total += _estimate_text_tokens(
  261                  json.dumps(tool_call.extras, ensure_ascii=False, separators=(",", ":"))
  262              )
  263      return total
为什么相信这条结论?查看 2 处证据
小练习 3

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M04 · TOOLS

工具系统:Agent 的手脚怎样被注册和调度

模型看见的工具说明,和真正执行工具的代码,是不是同一个东西?并发、编辑和失败结果怎么处理?

先用一个生活比喻

工具系统像机场:模型提交登机牌,注册表确认航班,权限闸机检查证件,调度器决定跑道,最后才允许真正起飞。

这套实现先回答了什么?

工具箱不是写死在循环里,而是由角色配置装配;本地插件和远程 MCP 最终都变成模型看到的同类工具。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
工具表由 agent spec 动态装配,插件和 MCP 追加进入同一 Toolsetsrc/kimi_cli/soul/agent.py:411工具箱不是写死在循环里,而是由角色配置装配;本地插件和远程 MCP 最终都变成模型看到的同类工具。
同一模型回复里的不同工具并发启动,完全重复调用共享结果packages/kosong/src/kosong/__init__.py:134模型一口气开多张不同工单会一起跑;两张完全相同的工单只做一次,第二张拿同一结果。
跨 step 重复调用按 3/5/8/12 阶梯提醒并强停src/kimi_cli/soul/toolset.py:116不是第一次重复就封杀,而是逐级提醒;连续十二次原地打转才硬踩刹车。
前台 Shell 有 5 分钟上限,后台任务可到 24 小时并持久通知src/kimi_cli/tools/shell/__init__.py:22短命令当场等,长 build/server 作为有编号的后台任务继续跑,不用让对话一直卡住。
08
L1 · fact · kimi-tools-001

工具表由 agent spec 动态装配,插件和 MCP 追加进入同一 Toolset

先看源码事实

load_agent 从 YAML 选 tools/allowed_tools 并应用 exclude_tools,通过依赖注入实例化;插件冲突时跳过;MCP config 验证后可后台加载或延迟到首轮。

翻译成白话

工具箱不是写死在循环里,而是由角色配置装配;本地插件和远程 MCP 最终都变成模型看到的同类工具。

为什么这对自研重要

角色最小权限容易表达;工具名称冲突按先到内建优先。

固定提交源码摘录
  411      # Register built-in subagent types before loading tools because some tools render
  412      # descriptions from the labor market on initialization.
  413      for subagent_name, subagent_spec in agent_spec.subagents.items():
  414          logger.debug(
  415              "Registering builtin subagent type: {subagent_name}", subagent_name=subagent_name
  416          )
  417          builtin_spec = load_agent_spec(subagent_spec.path)
  418          tool_policy = (
  419              ToolPolicy(mode="allowlist", tools=tuple(builtin_spec.allowed_tools))
  420              if builtin_spec.allowed_tools is not None
  421              else ToolPolicy(mode="inherit")
  422          )
  423          runtime.labor_market.add_builtin_type(
  424              AgentTypeDefinition(
  425                  name=subagent_name,
  426                  description=subagent_spec.description,
  427                  agent_file=subagent_spec.path,
  428                  when_to_use=builtin_spec.when_to_use,
  429                  default_model=builtin_spec.model,
  430                  tool_policy=tool_policy,
  431              )
  432          )
  433  
  434      toolset = KimiToolset()
  435      tool_deps = {
      … 5 lines omitted; exact range 411–451 …
  441          Session: runtime.session,
  442          DenwaRenji: runtime.denwa_renji,
  443          Approval: runtime.approval,
  444          LaborMarket: runtime.labor_market,
  445          Environment: runtime.environment,
  446      }
  447      tools = agent_spec.allowed_tools if agent_spec.allowed_tools is not None else agent_spec.tools
  448      if agent_spec.exclude_tools:
  449          logger.debug("Excluding tools: {tools}", tools=agent_spec.exclude_tools)
  450          tools = [tool for tool in tools if tool not in agent_spec.exclude_tools]
  451      toolset.load_tools(tools, tool_deps)
为什么相信这条结论?查看 3 处证据
09
L1 · fact · kimi-tools-002

同一模型回复里的不同工具并发启动,完全重复调用共享结果

先看源码事实

Kosong 收到完整 tool call 即调用 Toolset.handle;KimiToolset 为每个新 call 创建 asyncio task。同 step 相同 name+canonical args 不重复执行,而是等待原 task 并复制 result。

翻译成白话

模型一口气开多张不同工单会一起跑;两张完全相同的工单只做一次,第二张拿同一结果。

为什么这对自研重要

吞吐高且减少重复副作用;不同工具之间没有全局串行保证。

固定提交源码摘录
  134      tool_calls: list[ToolCall] = []
  135      tool_result_futures: dict[str, ToolResultFuture] = {}
  136  
  137      def future_done_callback(future: ToolResultFuture):
  138          if on_tool_result:
  139              try:
  140                  result = future.result()
  141                  on_tool_result(result)
  142              except asyncio.CancelledError:
  143                  return
  144  
  145      async def on_tool_call(tool_call: ToolCall):
  146          tool_calls.append(tool_call)
  147          result = toolset.handle(tool_call)
  148  
  149          if isinstance(result, ToolResult):
  150              future = ToolResultFuture()
  151              future.add_done_callback(future_done_callback)
  152              future.set_result(result)
  153              tool_result_futures[tool_call.id] = future
  154          else:
  155              result.add_done_callback(future_done_callback)
  156              tool_result_futures[tool_call.id] = result
  157  
  158      try:
  159          result = await generate(
  160              chat_provider,
  161              system_prompt,
  162              toolset.tools,
  163              history,
  164              on_message_part=on_message_part,
  165              on_tool_call=on_tool_call,
  166              on_trace_id=on_trace_id,
  167          )
为什么相信这条结论?查看 3 处证据
10
L1 · fact · kimi-tools-003

跨 step 重复调用按 3/5/8/12 阶梯提醒并强停

先看源码事实

Toolset 对 canonical tool call 维护连续 streak:第 3 次开始提示换方法,第 5 次展示调用细节,第 8 次要求停止工具并总结,第 12 次设置 force_stop_turn。

翻译成白话

不是第一次重复就封杀,而是逐级提醒;连续十二次原地打转才硬踩刹车。

为什么这对自研重要

降低模型卡死和费用失控,也允许合理重试;canonicalization 只比较名称和 JSON 参数,不判断外部状态是否变化。

固定提交源码摘录
  116  _REMINDER_TEXT_1 = (
  117      "\n\n<system-reminder>\n"
  118      "You are repeating the exact same tool call with identical parameters."
  119      " Please carefully analyze the previous result. If the task is not yet complete,"
  120      " try a different method or parameters instead of repeating the same call."
  121      "\n</system-reminder>"
  122  )
  123  
  124  
  125  def _make_reminder_text_2(tool_name: str, repeat_count: int, canonical_args: str) -> str:
  126      return (
  127          "\n\n<system-reminder>\n"
  128          "You have repeatedly called the same tool with identical parameters many times.\n"
  129          "Repeated tool call detected:\n"
  130          f"- tool: {tool_name}\n"
  131          f"- repeated_times: {repeat_count}\n"
  132          f"- arguments: {canonical_args}\n"
  133          "The previous repeated calls did not make progress. Do not call this exact same tool "
  134          "with the exact same arguments again.\n"
  135          "Carefully inspect the latest tool result and choose a different next action, "
  136          "different parameters, or finish the task if enough evidence has been gathered."
  137          "\n</system-reminder>"
  138      )
  139  
  140  
      … 21 lines omitted; exact range 116–172 …
  162      streak: int, tool_name: str, canonical_args: str
  163  ) -> tuple[RepeatAction, str | None]:
  164      if streak >= _REPEAT_FORCE_STOP_STREAK:
  165          return "stop", _REMINDER_TEXT_3
  166      if streak >= _REPEAT_REMINDER_3_START:
  167          return "r3", _REMINDER_TEXT_3
  168      if streak >= _REPEAT_REMINDER_2_START:
  169          return "r2", _make_reminder_text_2(tool_name, streak, canonical_args)
  170      if streak >= _REPEAT_REMINDER_1_START:
  171          return "r1", _REMINDER_TEXT_1
  172      return "none", None
为什么相信这条结论?查看 3 处证据
11
L1 · fact · kimi-tools-004

前台 Shell 有 5 分钟上限,后台任务可到 24 小时并持久通知

先看源码事实

Shell 参数限制前台 timeout <=300s、后台 <=86400s;两者都先 approval。前台通过 KAOS 流式读 stdout/stderr,取消/超时 kill 当前进程;后台交 BackgroundTaskManager 持久化,完成后自动通知,并可 TaskList/Output/Stop 管理。

翻译成白话

短命令当场等,长 build/server 作为有编号的后台任务继续跑,不用让对话一直卡住。

为什么这对自研重要

长任务体验完整;前台 LocalKaos.kill 只 kill 直接子进程,后台 worker 才显式管理整个进程组。

固定提交源码摘录
   22  MAX_FOREGROUND_TIMEOUT = 5 * 60
   23  MAX_BACKGROUND_TIMEOUT = 24 * 60 * 60
   24  
   25  
   26  class Params(BaseModel):
   27      command: str = Field(description="The command to execute.")
   28      timeout: int = Field(
   29          description=(
   30              "The timeout in seconds for the command to execute. "
   31              "If the command takes longer than this, it will be killed."
   32          ),
   33          default=60,
   34          ge=1,
   35          le=MAX_BACKGROUND_TIMEOUT,
   36      )
   37      run_in_background: bool = Field(
   38          default=False,
   39          description="Whether to run the command as a background task.",
   40      )
   41      description: str = Field(
   42          default="",
   43          description=(
   44              "A short description for the background task. Required when run_in_background=true."
   45          ),
   46      )
   47  
   48      @model_validator(mode="after")
   49      def _validate_background_fields(self) -> Self:
   50          if self.run_in_background and not self.description.strip():
   51              raise ValueError("description is required when run_in_background is true")
   52          if not self.run_in_background and self.timeout > MAX_FOREGROUND_TIMEOUT:
   53              raise ValueError(
   54                  f"timeout must be <= {MAX_FOREGROUND_TIMEOUT}s for foreground commands; "
   55                  f"use run_in_background=true for longer timeouts (up to {MAX_BACKGROUND_TIMEOUT}s)"
   56              )
   57          return self
为什么相信这条结论?查看 4 处证据
小练习 4

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M05 · CONTEXT

上下文:有限窗口怎样装下长任务

当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?

先用一个生活比喻

上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。

这套实现先回答了什么?

每条对话、存档点和 token 仪表读数都单独写一行;尾部坏一行不会让整场会话报废。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
上下文是可增量恢复的 JSONL 事件账本src/kimi_cli/soul/context.py:20每条对话、存档点和 token 仪表读数都单独写一行;尾部坏一行不会让整场会话报废。
checkpoint 回退会保留旧文件并重算状态src/kimi_cli/soul/context.py:123读档时旧账本不会消失,而是改名存档;新账本只抄到目标存档点。
压缩由阈值触发,摘要旧历史并保留最近轮次src/kimi_cli/soul/compaction.py:37箱子快满时,把旧账压成一页摘要,最近几轮原样保留;摘要过程不能再调用工具。
压缩后显式恢复活动后台任务与动态约束状态src/kimi_cli/soul/kimisoul.py:1432摘要不会让正在跑的 build/test 凭空消失;压缩后会重新贴一张活动任务清单,并让 Plan/AFK 提醒重新校准。
12
L1 · fact · kimi-context-001

上下文是可增量恢复的 JSONL 事件账本

先看源码事实

Context 将普通消息与 _system_prompt、_checkpoint、_usage 记录写入同一 JSONL;restore 跳过 malformed/non-object/invalid record,重建 history、checkpoint cursor、最新 token count 和 usage 之后的 pending estimate。

翻译成白话

每条对话、存档点和 token 仪表读数都单独写一行;尾部坏一行不会让整场会话报废。

为什么这对自研重要

易审计、易增量追加;没有数据库事务,靠逐行容错和旋转文件恢复。

固定提交源码摘录
   20  class Context:
   21      def __init__(self, file_backend: Path):
   22          self._file_backend = file_backend
   23          self._history: list[Message] = []
   24          self._token_count: int = 0
   25          self._pending_token_estimate: int = 0
   26          self._next_checkpoint_id: int = 0
   27          """The ID of the next checkpoint, starting from 0, incremented after each checkpoint."""
   28          self._system_prompt: str | None = None
   29  
   30      async def restore(self) -> bool:
   31          logger.debug("Restoring context from file: {file_backend}", file_backend=self._file_backend)
   32          if self._history:
   33              logger.error("The context storage is already modified")
   34              raise RuntimeError("The context storage is already modified")
   35          if not self._file_backend.exists():
   36              logger.debug("No context file found, skipping restoration")
   37              return False
   38          if self._file_backend.stat().st_size == 0:
   39              logger.debug("Empty context file, skipping restoration")
   40              return False
   41  
   42          messages_after_last_usage: list[Message] = []
   43          async with aiofiles.open(self._file_backend, encoding="utf-8", errors="replace") as f:
   44              line_no = 0
      … 10 lines omitted; exact range 20–65 …
   55                      continue
   56                  self._apply_context_record(
   57                      line_json,
   58                      history=self._history,
   59                      messages_after_last_usage=messages_after_last_usage,
   60                      file_backend=self._file_backend,
   61                      line_no=line_no,
   62                  )
   63  
   64          self._pending_token_estimate = estimate_text_tokens(messages_after_last_usage)
   65          return True
为什么相信这条结论?查看 3 处证据
13
L1 · fact · kimi-context-002

checkpoint 回退会保留旧文件并重算状态

先看源码事实

revert_to 先把当前 context 文件旋转为备份,再逐行复制到指定 checkpoint 之前,重新构建 history/usage/system prompt/checkpoint 与 pending estimate;clear 也先旋转而非直接覆盖。

翻译成白话

读档时旧账本不会消失,而是改名存档;新账本只抄到目标存档点。

为什么这对自研重要

误回退可人工取回旋转文件;磁盘占用会随压缩/回退累计。

固定提交源码摘录
  123      async def checkpoint(self, add_user_message: bool):
  124          checkpoint_id = self._next_checkpoint_id
  125          self._next_checkpoint_id += 1
  126          logger.debug("Checkpointing, ID: {id}", id=checkpoint_id)
  127  
  128          async with aiofiles.open(self._file_backend, "a", encoding="utf-8") as f:
  129              await f.write(json.dumps({"role": "_checkpoint", "id": checkpoint_id}) + "\n")
  130          if add_user_message:
  131              await self.append_message(
  132                  Message(role="user", content=[system(f"CHECKPOINT {checkpoint_id}")])
  133              )
  134  
  135      async def revert_to(self, checkpoint_id: int):
  136          """
  137          Revert the context to the specified checkpoint.
  138          After this, the specified checkpoint and all subsequent content will be
  139          removed from the context. File backend will be rotated.
  140  
  141          Args:
  142              checkpoint_id (int): The ID of the checkpoint to revert to. 0 is the first checkpoint.
  143  
  144          Raises:
  145              ValueError: When the checkpoint does not exist.
  146              RuntimeError: When no available rotation path is found.
  147          """
      … 42 lines omitted; exact range 123–200 …
  190                  keep_line = self._apply_context_record(
  191                      line_json,
  192                      history=self._history,
  193                      messages_after_last_usage=messages_after_last_usage,
  194                      file_backend=rotated_file_path,
  195                      line_no=line_no,
  196                  )
  197                  if keep_line:
  198                      await new_file.write(line)
  199  
  200          self._pending_token_estimate = estimate_text_tokens(messages_after_last_usage)
为什么相信这条结论?查看 2 处证据
14
L1 · fact · kimi-context-003

压缩由阈值触发,摘要旧历史并保留最近轮次

先看源码事实

should_auto_compact 同时检查 context ratio 和 reserved context;SimpleCompaction 默认保留最近两组 user/assistant,把更早历史交 EmptyToolset 摘要、去除 thinking,再用 compaction prefix + summary + preserved messages 重建 context。

翻译成白话

箱子快满时,把旧账压成一页摘要,最近几轮原样保留;摘要过程不能再调用工具。

为什么这对自研重要

实现简单稳定,但摘要是有损的,字符/4 估算对 CJK 偏低,代码中也明确承认。

固定提交源码摘录
   37          The estimate is intentionally conservative — it will be replaced by the
   38          real value on the next LLM call.
   39          """
   40          if self.usage is not None and len(self.messages) > 0:
   41              summary_tokens = self.usage.output
   42              preserved_tokens = estimate_text_tokens(self.messages[1:])
   43              return summary_tokens + preserved_tokens
   44  
   45          return estimate_text_tokens(self.messages)
   46  
   47  
   48  def estimate_text_tokens(messages: Sequence[Message]) -> int:
   49      """Estimate tokens from message text content using a character-based heuristic."""
   50      total_chars = 0
   51      for msg in messages:
   52          for part in msg.content:
   53              if isinstance(part, TextPart):
   54                  total_chars += len(part.text)
   55      # ~4 chars per token for English; somewhat underestimates for CJK text,
   56      # but this is a temporary estimate that gets corrected on the next LLM call.
   57      return total_chars // 4
   58  
   59  
   60  def should_auto_compact(
   61      token_count: int,
      … 10 lines omitted; exact range 37–82 …
   72      """
   73      return (
   74          token_count >= max_context_size * trigger_ratio
   75          or token_count + reserved_context_size >= max_context_size
   76      )
   77  
   78  
   79  @runtime_checkable
   80  class Compaction(Protocol):
   81      async def compact(
   82          self,
为什么相信这条结论?查看 3 处证据
15
L1 · fact · kimi-context-004

压缩后显式恢复活动后台任务与动态约束状态

先看源码事实

root compaction 前估算摘要后请求预算;压缩成功后 clear/重写 system prompt/checkpoint/messages,再追加 active task snapshot、重设 token count、通知 injection providers reset,并发 CompactionEnd/telemetry/PostCompact。

翻译成白话

摘要不会让正在跑的 build/test 凭空消失;压缩后会重新贴一张活动任务清单,并让 Plan/AFK 提醒重新校准。

为什么这对自研重要

避免长后台任务在摘要后失联;活动任务快照本身也占上下文。

固定提交源码摘录
 1432          chat_provider = self._runtime.llm.chat_provider if self._runtime.llm is not None else None
 1433          compaction_overrides = None
 1434          if chat_provider is not None and isinstance(self._compaction, SimpleCompaction):
 1435              compact_message, to_preserve = self._compaction.prepare(
 1436                  self._context.history,
 1437                  custom_instruction=custom_instruction,
 1438              )
 1439              if compact_message is not None:
 1440                  post_compaction_history = [
 1441                      Message(
 1442                          role="user",
 1443                          content=[system(COMPACTION_OUTPUT_PREFIX)],
 1444                      ),
 1445                      *to_preserve,
 1446                  ]
 1447                  if self.is_root:
 1448                      active_task_snapshot = build_active_task_snapshot(
 1449                          self._runtime.background_tasks
 1450                      )
 1451                      if active_task_snapshot is not None:
 1452                          post_compaction_history.append(
 1453                              Message(
 1454                                  role="user",
 1455                                  content=[
 1456                                      system(
      … 9 lines omitted; exact range 1432–1476 …
 1466                      self._agent.system_prompt,
 1467                      self._agent.toolset.tools,
 1468                      post_compaction_history,
 1469                  )
 1470                  compaction_overrides = self._compute_completion_overrides(
 1471                      chat_provider,
 1472                      system_prompt=COMPACTION_SYSTEM_PROMPT,
 1473                      tools=(),
 1474                      history=[compact_message],
 1475                      input_tokens_floor=post_compaction_input_tokens,
 1476                  )
为什么相信这条结论?查看 3 处证据
小练习 5

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M06 · SECURITY

权限与沙箱:能做什么,在哪里做

审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?

先用一个生活比喻

审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。

这套实现先回答了什么?

每种危险动作可这次放行、整场放行或拒绝并告诉模型原因;无人值守模式等同自动批准。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
统一审批支持单次、整会话、拒绝反馈、YOLO 与 AFKsrc/kimi_cli/soul/approval.py:130每种危险动作可这次放行、整场放行或拒绝并告诉模型原因;无人值守模式等同自动批准。
Plan 模式不是隐藏工具,而是写工具调用时再强制拒绝src/kimi_cli/soul/kimisoul.py:409root 的 Plan 模式仍让模型知道写工具存在,但真调用时门禁拦下;专门 plan 子 Agent 更严格,工具箱里压根没有写和 shell。
默认本地 KAOS 是宿主执行抽象,不是 OS 级沙箱src/kimi_cli/agents/default/system.md:67KAOS 让同一套代码能接本地或 SSH,但没有把命令关进隔离房间;一旦用户批准,进程拿的是当前用户权限。
16
L1 · fact · kimi-security-001

统一审批支持单次、整会话、拒绝反馈、YOLO 与 AFK

先看源码事实

Approval 绑定当前 tool call/source;普通请求进入共享 ApprovalRuntime/wire。approve_for_session 缓存 action 并批准同 action pending requests;拒绝反馈返回模型。YOLO 自动批准,AFK 也隐含自动批准且可持久或仅本次 invocation。

翻译成白话

每种危险动作可这次放行、整场放行或拒绝并告诉模型原因;无人值守模式等同自动批准。

为什么这对自研重要

root、前台和后台子 Agent 共用一个审批面;AFK 是高风险开关,不能理解为只关闭提问。

固定提交源码摘录
  130  class Approval:
  131      def __init__(
  132          self,
  133          yolo: bool = False,
  134          *,
  135          state: ApprovalState | None = None,
  136          runtime: ApprovalRuntime | None = None,
  137      ):
  138          self._state = state or ApprovalState(yolo=yolo)
  139          self._runtime = runtime or ApprovalRuntime()
  140  
  141      def share(self) -> Approval:
  142          """Create a new approval queue that shares approval state."""
  143          return Approval(state=self._state, runtime=self._runtime)
  144  
  145      def set_runtime(self, runtime: ApprovalRuntime) -> None:
  146          self._runtime = runtime
  147  
  148      @property
  149      def runtime(self) -> ApprovalRuntime:
  150          return self._runtime
  151  
  152      def set_yolo(self, yolo: bool) -> None:
  153          self._state.yolo = yolo
  154          self._state.notify_change()
      … 34 lines omitted; exact range 130–199 …
  189          """True when no user is present (away-from-keyboard)."""
  190          return self._state.afk or self._state.runtime_afk
  191  
  192      def is_afk_flag(self) -> bool:
  193          """True only when persisted afk mode is active."""
  194          return self._state.afk
  195  
  196      def is_runtime_afk(self) -> bool:
  197          """True only when afk came from this invocation."""
  198          return self._state.runtime_afk
  199  
为什么相信这条结论?查看 4 处证据
17
L1 · fact · kimi-security-002

Plan 模式不是隐藏工具,而是写工具调用时再强制拒绝

先看源码事实

KimiSoul 把 plan-mode checker/path 绑定给 WriteFile/StrReplaceFile,并明确工具不 hide/unhide;Plan 子 Agent 的 YAML 则代码级 allowlist 不包含 Shell/写工具,测试验证。

翻译成白话

root 的 Plan 模式仍让模型知道写工具存在,但真调用时门禁拦下;专门 plan 子 Agent 更严格,工具箱里压根没有写和 shell。

为什么这对自研重要

运行时 gate 能给出清楚反馈;真正最小权限应优先用 subagent allowlist。

固定提交源码摘录
  409      def _bind_plan_mode_tools(self) -> None:
  410          """Bind plan mode state to tools that support it."""
  411          if not isinstance(self._agent.toolset, KimiToolset):
  412              return
  413  
  414          def checker() -> bool:
  415              return self._plan_mode
  416  
  417          def path_getter() -> Path | None:
  418              return self.get_plan_file_path()
  419  
  420          # WriteFile gets both checker and path_getter (for plan file auto-approve)
  421          from kimi_cli.tools.file.write import WriteFile
  422  
  423          write_tool = self._agent.toolset.find("WriteFile")
  424          if isinstance(write_tool, WriteFile):
  425              write_tool.bind_plan_mode(checker, path_getter)
  426  
  427          from kimi_cli.tools.file.replace import StrReplaceFile
  428  
  429          replace_tool = self._agent.toolset.find("StrReplaceFile")
  430          if isinstance(replace_tool, StrReplaceFile):
  431              replace_tool.bind_plan_mode(checker, path_getter)
  432  
  433          # ExitPlanMode has a special bind() method
      … 19 lines omitted; exact range 409–463 …
  453                  checker,
  454                  self._approval.is_auto_approve,
  455              )
  456  
  457          # AskUserQuestion — bind afk checker for auto-dismiss.
  458          # Yolo alone keeps the tool live; only afk (no user present) dismisses.
  459          from kimi_cli.tools.ask_user import AskUserQuestion
  460  
  461          ask_tool = self._agent.toolset.find("AskUserQuestion")
  462          if isinstance(ask_tool, AskUserQuestion):
  463              ask_tool.bind_afk(self._approval.is_afk)
为什么相信这条结论?查看 4 处证据
18
L1 · limitation · kimi-security-003

默认本地 KAOS 是宿主执行抽象,不是 OS 级沙箱

先看源码事实

默认 system prompt 明确 operating environment is not in a sandbox;LocalKaos 直接用 pathlib/aiofiles 操作宿主路径,并用 asyncio.create_subprocess_exec 启动宿主进程,没有容器、seatbelt、seccomp 或 restricted token。

翻译成白话

KAOS 让同一套代码能接本地或 SSH,但没有把命令关进隔离房间;一旦用户批准,进程拿的是当前用户权限。

为什么这对自研重要

企业场景必须把 Kimi CLI 外包进容器/VM/受限账户,审批与提示词不能替代隔离。

固定提交源码摘录
   67  # Working Environment
   68  
   69  ## Operating System
   70  
   71  You are running on **${KIMI_OS}**. The Shell tool executes commands using **${KIMI_SHELL}**.{% if KIMI_OS == "Windows" %} Use Unix shell syntax inside Shell commands — `/dev/null` not `NUL`, forward slashes in paths (backslashes are escape characters in bash).{% endif +%}
   72  
   73  The operating environment is not in a sandbox. Any actions you do will immediately affect the user's system. So you MUST be extremely cautious. Unless being explicitly instructed to do so, you should never access (read/write/execute) files outside of the working directory.
   74  
   75  ## Date and Time
   76  
   77  The current date and time in ISO format is `${KIMI_NOW}`. This is only a reference for you when searching the web, or checking file modification time, etc. If you need the exact time, use Shell tool with proper command.
   78  
   79  ## Working Directory
   80  
   81  The current working directory is `${KIMI_WORK_DIR}`. This should be considered as the project root if you are instructed to perform tasks on the project. Every file system operation will be relative to the working directory if you do not explicitly specify the absolute path. Tools may require absolute paths for some parameters, IF SO, YOU MUST use absolute paths for these parameters.
为什么相信这条结论?查看 3 处证据
小练习 6

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M07 · ECOSYSTEM

指令、MCP、Skills 与插件:能力如何接进来

一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?

先用一个生活比喻

这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。

这套实现先回答了什么?

组织可以在模型动作前后插入自己的门卫/审计脚本,也可以让 IDE 客户端参与判断。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
Hook 同时支持本地命令与客户端 wire subscriptionsrc/kimi_cli/hooks/engine.py:65组织可以在模型动作前后插入自己的门卫/审计脚本,也可以让 IDE 客户端参与判断。
插件工具是经审批的本地子进程,可获得新鲜 Host 凭证src/kimi_cli/plugin/tool.py:37插件本质是本机程序,不是受限脚本;它能按配置拿到主机凭证,所以安装来源和权限同样重要。
MCP 延迟启动、逐服务器状态化,并对富媒体共享 100K 字符预算src/kimi_cli/soul/agent.py:467远程工具不必拖慢 CLI 启动,但真正开始工作前会等它们连好;一个巨型网页 DOM 或截图不能把上下文塞爆。
系统指令是 Jinja 模板,运行环境与项目说明在启动时冻结src/kimi_cli/soul/agent.py:494系统提示词像带变量的表单,启动时填入 OS、shell、目录、文件树、AGENTS 和技能;恢复旧子 Agent 时继续沿用当时版本。
19
L1 · fact · kimi-hooks-001

Hook 同时支持本地命令与客户端 wire subscription

先看源码事实

HookEngine 按 regex 匹配并去重 server command,同时加入 wire subscriptions,并发执行;任意 result=block 即整体 block。UserPromptSubmit、Pre/PostToolUse、Stop/Failure、Compact、Notification、Subagent 等事件均接入。

翻译成白话

组织可以在模型动作前后插入自己的门卫/审计脚本,也可以让 IDE 客户端参与判断。

为什么这对自研重要

扩展面完整;engine、命令错误与超时通常 fail-open,只有成功返回 block/exit 2 才阻止。

固定提交源码摘录
   65  class HookEngine:
   66      """Loads hook definitions and executes matching hooks in parallel.
   67  
   68      Supports two hook sources:
   69      - Server-side (config.toml): shell commands executed locally
   70      - Client-side (wire subscriptions): forwarded to client via HookRequest
   71      """
   72  
   73      def __init__(
   74          self,
   75          hooks: list[HookDef] | None = None,
   76          cwd: str | None = None,
   77          *,
   78          on_triggered: OnTriggered | None = None,
   79          on_resolved: OnResolved | None = None,
   80          on_wire_hook: OnWireHookRequest | None = None,
   81      ):
   82          self._hooks: list[HookDef] = list(hooks) if hooks else []
   83          self._wire_subs: list[WireHookSubscription] = []
   84          self._cwd = cwd
   85          self._on_triggered = on_triggered
   86          self._on_resolved = on_resolved
   87          self._on_wire_hook = on_wire_hook
   88          self._by_event: dict[str, list[HookDef]] = {}
   89          self._wire_by_event: dict[str, list[WireHookSubscription]] = {}
   90          self._pending_fire_and_forget: set[asyncio.Task[Any]] = set()
   91          self._rebuild_index()
为什么相信这条结论?查看 4 处证据
20
L1 · risk · kimi-plugin-001

插件工具是经审批的本地子进程,可获得新鲜 Host 凭证

先看源码事实

PluginTool 每次调用先请求 action=plugin:name 审批,把 JSON params 写 stdin,在插件目录执行声明 command,120 秒超时;inject mapping 可把当前 API key/base URL 注入干净环境。插件安装校验名称防 path traversal,并用同文件系统 staging swap。

翻译成白话

插件本质是本机程序,不是受限脚本;它能按配置拿到主机凭证,所以安装来源和权限同样重要。

为什么这对自研重要

能力强但供应链风险高;approval 只确认“运行插件”,不审计插件内部每个文件/网络动作。

固定提交源码摘录
   37  class PluginTool(CallableTool):
   38      """A tool that executes a plugin command in a subprocess.
   39  
   40      Parameters are passed via stdin as JSON.
   41      stdout is captured as the tool result.
   42      Host credentials are injected as environment variables at runtime
   43      (not baked into config files) to handle OAuth token refresh.
   44      """
   45  
   46      def __init__(
   47          self,
   48          tool_spec: PluginToolSpec,
   49          plugin_dir: Path,
   50          *,
   51          inject: dict[str, str],
   52          config: Config,
   53          approval: Approval | None = None,
   54          **kwargs: Any,
   55      ):
   56          super().__init__(
   57              name=tool_spec.name,
   58              description=tool_spec.description,
   59              parameters=tool_spec.parameters or {"type": "object", "properties": {}},
   60              **kwargs,
   61          )
      … 58 lines omitted; exact range 37–130 …
  120          if proc.returncode != 0:
  121              error_msg = err_output or output or f"Exit code {proc.returncode}"
  122              return ToolError(
  123                  message=f"Plugin tool '{self.name}' failed: {error_msg}",
  124                  brief=f"Exit {proc.returncode}",
  125              )
  126  
  127          if err_output:
  128              logger.debug("Plugin tool {name} stderr: {err}", name=self.name, err=err_output)
  129  
  130          return ToolOk(output=output)
为什么相信这条结论?查看 3 处证据
21
L1 · fact · kimi-mcp-001

MCP 延迟启动、逐服务器状态化,并对富媒体共享 100K 字符预算

先看源码事实

agent 可 defer MCP,首个 agent loop 后台启动并等待,向 UI 发 loading/status;server 状态区分 connected/unauthorized/failed。MCP 结果的文本、image/audio/video data URL 共用 100000 字符预算,超额媒体丢弃并加 truncation notice。

翻译成白话

远程工具不必拖慢 CLI 启动,但真正开始工作前会等它们连好;一个巨型网页 DOM 或截图不能把上下文塞爆。

为什么这对自研重要

体验和上下文安全较好;首轮仍会被 MCP 连接完成阻塞,OAuth token 持久落在 share dir。

固定提交源码摘录
  467      if mcp_configs:
  468          validated_mcp_configs: list[MCPConfig] = []
  469          if mcp_configs:
  470              from fastmcp.mcp_config import MCPConfig
  471  
  472              for mcp_config in mcp_configs:
  473                  try:
  474                      validated_mcp_configs.append(
  475                          mcp_config
  476                          if isinstance(mcp_config, MCPConfig)
  477                          else MCPConfig.model_validate(mcp_config)
  478                      )
  479                  except pydantic.ValidationError as e:
  480                      raise MCPConfigError(f"Invalid MCP config: {e}") from e
  481          if start_mcp_loading:
  482              await toolset.load_mcp_tools(validated_mcp_configs, runtime, in_background=True)
  483          else:
  484              toolset.defer_mcp_tool_loading(validated_mcp_configs, runtime)
  485  
为什么相信这条结论?查看 4 处证据
22
L1 · fact · kimi-instructions-001

系统指令是 Jinja 模板,运行环境与项目说明在启动时冻结

先看源码事实

load_agent 用 StrictUndefined Jinja 渲染 system.md 和 builtin/spec args;Context 首次写入 _system_prompt,子 Agent resume 时优先复用持久 prompt 而非当前模板。

翻译成白话

系统提示词像带变量的表单,启动时填入 OS、shell、目录、文件树、AGENTS 和技能;恢复旧子 Agent 时继续沿用当时版本。

为什么这对自研重要

保证 resume 行为稳定;模板升级不会自动改变旧子 Agent 的规则。

固定提交源码摘录
  494  def _load_system_prompt(
  495      path: Path, args: dict[str, str], builtin_args: BuiltinSystemPromptArgs
  496  ) -> str:
  497      logger.info("Loading system prompt: {path}", path=path)
  498      system_prompt = path.read_text(encoding="utf-8").strip()
  499      logger.debug(
  500          "Substituting system prompt with builtin args: {builtin_args}, spec args: {spec_args}",
  501          builtin_args=builtin_args,
  502          spec_args=args,
  503      )
  504      env = JinjaEnvironment(
  505          loader=FileSystemLoader(path.parent),
  506          keep_trailing_newline=True,
  507          lstrip_blocks=True,
  508          trim_blocks=True,
  509          variable_start_string="${",
  510          variable_end_string="}",
  511          undefined=StrictUndefined,
  512      )
  513      try:
  514          template = env.from_string(system_prompt)
  515          return template.render(asdict(builtin_args), **args)
  516      except UndefinedError as exc:
  517          raise SystemPromptTemplateError(f"Missing system prompt arg in {path}: {exc}") from exc
为什么相信这条结论?查看 3 处证据
23
L1 · fact · kimi-instructions-002

AGENTS.md 按 root→cwd 合并,叶子优先分享 32KiB 总预算

先看源码事实

load_agents_md 从 project root 走到 work dir,每层检查 .kimi/AGENTS.md、AGENTS.md、agents.md;普通大小写候选按优先级择一而 .kimi 可并存,内容预算从叶子向根分配,最后按根到叶顺序拼接并标源。

翻译成白话

越靠近正在工作的目录越有机会完整进入上下文,但展示顺序仍是先总规则、后局部规则。

为什么这对自研重要

符合层级覆盖直觉;超过 32KiB 的上层说明可能被截短。

固定提交源码摘录
   87  async def load_agents_md(work_dir: KaosPath) -> str | None:
   88      """Discover and merge ``AGENTS.md`` files from the project root down to *work_dir*.
   89  
   90      For each directory on the path, the following candidates are checked in order:
   91  
   92      1. ``.kimi/AGENTS.md``  — project-local kimi config (highest priority)
   93      2. ``AGENTS.md``        — standard location
   94      3. ``agents.md``        — lowercase variant (mutually exclusive with 2)
   95  
   96      Within a single directory, ``.kimi/AGENTS.md`` and ``AGENTS.md``/``agents.md``
   97      are **both** loaded (with ``.kimi/`` first), but ``AGENTS.md`` and ``agents.md``
   98      are mutually exclusive (uppercase wins).
   99  
  100      All discovered files are concatenated root→leaf, separated by ``\\n\\n``, with
  101      source annotations.  Total size is capped at :data:`_AGENTS_MD_MAX_BYTES`.
  102      Budget is allocated leaf-first so deeper (more specific) files are never
  103      truncated in favour of shallower ones.
  104      """
  105      project_root = await find_project_root(work_dir)
  106      dirs = await _dirs_root_to_leaf(work_dir, project_root)
  107  
  108      # Phase 1: collect all candidate files (root → leaf order)
  109      discovered: list[tuple[KaosPath, str]] = []  # (path, content)
  110      for d in dirs:
  111          # .kimi/AGENTS.md is always checked independently (can coexist with root-level file)
      … 46 lines omitted; exact range 87–168 …
  158              logger.warning("AGENTS.md truncated due to size limit: {path}", path=path)
  159          remaining -= len(content.encode())
  160          budgeted[i] = (path, content)
  161  
  162      # Phase 3: assemble in root → leaf order, skipping entries emptied by truncation
  163      parts: list[str] = []
  164      for path, content in budgeted:
  165          if content:
  166              parts.append(f"<!-- From: {path} -->\n{content}")
  167  
  168      return "\n\n".join(parts) if parts else None
为什么相信这条结论?查看 2 处证据
24
L1 · fact · kimi-skills-001

Skills 跨 kimi/claude/codex 目录合并,并映射为 slash/flow 命令

先看源码事实

Config 默认 merge_all_available_skills=true,支持 extra skill dirs;system prompt 只列摘要并要求按需读取 SKILL.md。KimiSoul 把 standard/flow skill 注册为 slash command,flow 还构造成 FlowRunner。

翻译成白话

技能不是把全文常驻提示词,而是先给目录卡片,需要时再读;同一套 CLI 还能复用其他 Agent 生态的技能目录。

为什么这对自研重要

节省上下文并增强兼容;skill 发现冲突需依赖 scope precedence。

固定提交源码摘录
  242      merge_all_available_skills: bool = Field(
  243          default=True,
  244          description=(
  245              "Merge skills from all existing brand directories (kimi/claude/codex) "
  246              "instead of using only the first one found. Defaults to true so users "
  247              "who keep skills in multiple brand directories see everything out of "
  248              "the box; set to false to restore the first-match-only behaviour."
  249          ),
  250      )
  251      extra_skill_dirs: list[str] = Field(
  252          default_factory=list,
  253          description=(
  254              "Extra directories to discover skills from, added on top of the "
  255              "built-in / user / project locations. Each entry may be an absolute "
  256              "path, ``~``-prefixed (expanded against $HOME), or relative to the "
  257              "project root (the nearest ``.git`` directory above the work dir). "
  258              "Missing paths are silently skipped."
  259          ),
为什么相信这条结论?查看 3 处证据
小练习 7

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M08 · COLLABORATION

子 Agent:把一个大任务拆成可治理的协作

什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?

先用一个生活比喻

不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。

这套实现先回答了什么?

主 Agent 能雇三种工人:能改代码、只探索、只规划;每种工人拿到的钥匙不同,而且不能继续无限招下级。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
内建 coder/explore/plan 用代码级工具白名单切分角色src/kimi_cli/soul/agent.py:411主 Agent 能雇三种工人:能改代码、只探索、只规划;每种工人拿到的钥匙不同,而且不能继续无限招下级。
子 Agent 是可恢复的持久实例,可前台或后台运行src/kimi_cli/tools/agent/__init__.py:17子 Agent 不是一次性函数调用,而是有身份证和独立笔记本的小会话;以后可以继续找同一个人接着做。
子 Agent 共享审批/任务/通知底座,但拥有独立 soul/context/模型src/kimi_cli/soul/agent.py:339工人各有自己的对话脑和可选模型,但共用老板的审批台、后台任务台和通知总线。
25
L1 · fact · kimi-subagent-001

内建 coder/explore/plan 用代码级工具白名单切分角色

先看源码事实

root 加载 spec 时为每个 subagent 注册 AgentTypeDefinition 与 allowlist policy;coder 有 Shell/写工具但无 Agent/AskUser/Todo,explore 无写工具但仍有 Shell,plan 连 Shell 也没有。SubagentBuilder 不加载 MCP。

翻译成白话

主 Agent 能雇三种工人:能改代码、只探索、只规划;每种工人拿到的钥匙不同,而且不能继续无限招下级。

为什么这对自研重要

避免递归爆炸并缩小权限;explore 的 Shell 只读要求部分依赖 prompt,不是命令级只读 parser。

边界与风险
  • explore 虽无文件写工具,但 Shell 本身能运行任意获批命令;只读 Shell 主要由 prompt 约束。
固定提交源码摘录
  411      # Register built-in subagent types before loading tools because some tools render
  412      # descriptions from the labor market on initialization.
  413      for subagent_name, subagent_spec in agent_spec.subagents.items():
  414          logger.debug(
  415              "Registering builtin subagent type: {subagent_name}", subagent_name=subagent_name
  416          )
  417          builtin_spec = load_agent_spec(subagent_spec.path)
  418          tool_policy = (
  419              ToolPolicy(mode="allowlist", tools=tuple(builtin_spec.allowed_tools))
  420              if builtin_spec.allowed_tools is not None
  421              else ToolPolicy(mode="inherit")
  422          )
  423          runtime.labor_market.add_builtin_type(
  424              AgentTypeDefinition(
  425                  name=subagent_name,
  426                  description=subagent_spec.description,
  427                  agent_file=subagent_spec.path,
  428                  when_to_use=builtin_spec.when_to_use,
  429                  default_model=builtin_spec.model,
  430                  tool_policy=tool_policy,
  431              )
为什么相信这条结论?查看 3 处证据
26
L1 · fact · kimi-subagent-002

子 Agent 是可恢复的持久实例,可前台或后台运行

先看源码事实

Agent tool 可新建或按 agent_id resume,前台默认无 timeout、后台默认配置 timeout,均上限 1 小时;实例独立保存 context.jsonl、wire.jsonl、meta、prompt、output。resume 复用历史和原始 type,并拒绝并发 resume。

翻译成白话

子 Agent 不是一次性函数调用,而是有身份证和独立笔记本的小会话;以后可以继续找同一个人接着做。

为什么这对自研重要

适合长期分工;需要清理实例磁盘,并防止后台和前台同时写同一上下文。

固定提交源码摘录
   17  MAX_FOREGROUND_TIMEOUT = 60 * 60  # 1 hour
   18  MAX_BACKGROUND_TIMEOUT = 60 * 60  # 1 hour
   19  
   20  
   21  class Params(BaseModel):
   22      description: str = Field(description="A short (3-5 word) description of the task")
   23      prompt: str = Field(description="The task for the agent to perform")
   24      subagent_type: str = Field(
   25          default="coder",
   26          description="The built-in agent type to use. Defaults to `coder`.",
   27      )
   28      model: str | None = Field(
   29          default=None,
   30          description=(
   31              "Optional model override. Selection priority is: this parameter, then the built-in "
   32              "type default model, then the parent agent's current model."
   33          ),
   34      )
   35      resume: str | None = Field(
   36          default=None,
   37          description="Optional agent ID to resume instead of creating a new instance.",
   38      )
   39      run_in_background: bool = Field(
   40          default=False,
   41          description=(
      … 8 lines omitted; exact range 17–60 …
   50              "Timeout in seconds for the agent task. "
   51              "Foreground: no default timeout (runs until completion), max 3600s (1hr). "
   52              "Background: default from config (15min), max 3600s (1hr). "
   53              "The agent is stopped if it exceeds this limit."
   54          ),
   55          ge=30,
   56          le=MAX_BACKGROUND_TIMEOUT,
   57      )
   58  
   59      @property
   60      def effective_timeout(self) -> int | None:
为什么相信这条结论?查看 4 处证据
27
L1 · fact · kimi-subagent-003

子 Agent 共享审批/任务/通知底座,但拥有独立 soul/context/模型

先看源码事实

Runtime.copy_for_subagent 共享 session、approval state/runtime、labor market、environment、notifications、background manager、store/root wire hub,创建独立 DenwaRenji 与 role;builder 可按 tool override→type default→parent 顺序克隆模型。子 Agent wire events 包装回 parent tool call,approval request 直达 root。

翻译成白话

工人各有自己的对话脑和可选模型,但共用老板的审批台、后台任务台和通知总线。

为什么这对自研重要

协作可观测且审批统一;共享 session 级资源使隔离不是进程级/租户级。

固定提交源码摘录
  339      def copy_for_subagent(
  340          self,
  341          *,
  342          agent_id: str,
  343          subagent_type: str,
  344          llm_override: LLM | None = None,
  345      ) -> Runtime:
  346          """Clone runtime for a subagent."""
  347          return Runtime(
  348              config=self.config,
  349              oauth=self.oauth,
  350              llm=llm_override if llm_override is not None else self.llm,
  351              session=self.session,
  352              builtin_args=self.builtin_args,
  353              denwa_renji=DenwaRenji(),  # subagent must have its own DenwaRenji
  354              approval=self.approval.share(),
  355              labor_market=self.labor_market,
  356              environment=self.environment,
  357              notifications=self.notifications,
  358              background_tasks=self.background_tasks.copy_for_role("subagent"),
  359              skills=self.skills,
  360              # Share the same list reference so /add-dir mutations propagate to all agents
  361              additional_dirs=self.additional_dirs,
  362              skills_dirs=self.skills_dirs,
  363              subagent_store=self.subagent_store,
  364              approval_runtime=self.approval_runtime,
  365              root_wire_hub=self.root_wire_hub,
  366              subagent_id=agent_id,
  367              subagent_type=subagent_type,
  368              role="subagent",
  369          )
为什么相信这条结论?查看 4 处证据
小练习 8

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M09 · STATE

会话、持久化与观测:让一次运行变成可追溯事实

如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?

先用一个生活比喻

内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。

这套实现先回答了什么?

界面看到的不是一行最终文本,而是一条可还原执行过程的事件河流,并能把 API 请求、工具和审批串起来。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
Wire 事件与 trace id 贯穿 turn、step、tool、approval、MCP、compactionsrc/kimi_cli/soul/kimisoul.py:1009界面看到的不是一行最终文本,而是一条可还原执行过程的事件河流,并能把 API 请求、工具和审批串起来。
匿名遥测默认开启但可配置/环境变量关闭,代码禁止传用户内容src/kimi_cli/config.py:261默认会上报事件名、耗时、模型/平台等运行指标,但设计规则不允许把提示词、路径和代码塞进事件;用户可以关闭。
会话、子 Agent 与遥测均有损坏/并发防护,许可证为 Apache-2.0src/kimi_cli/session.py:84它把“崩一次后还能继续”和“后台任务结束后别留下悬空审批”当成正式设计,而不是只做 happy path。
28
L1 · fact · kimi-observe-001

Wire 事件与 trace id 贯穿 turn、step、tool、approval、MCP、compaction

先看源码事实

KimiSoul 发 Turn/Step/Retry/Status/Compaction/MCP 等 wire events;provider x-trace-id 存 ContextVar 并继承到工具 task;Toolset 记录 outcome/duration/dedup/error,approval 记录 surface/mode/result。

翻译成白话

界面看到的不是一行最终文本,而是一条可还原执行过程的事件河流,并能把 API 请求、工具和审批串起来。

为什么这对自研重要

适合 IDE/ACP/Wire 客户端和故障分析;不是完整 deterministic replay,外部副作用仍不可重放。

固定提交源码摘录
 1009              # ── 2b. Step Begin ──────────────────────────────────────────────────
 1010              wire_send(StepBegin(n=step_no))
 1011              back_to_the_future: BackToTheFuture | None = None
 1012              step_outcome: StepOutcome | None = None
 1013  
 1014              try:
 1015                  # ── 2c. Context Compaction ──────────────────────────────────────
 1016                  if should_auto_compact(
 1017                      self._context.token_count_with_pending,
 1018                      self._runtime.llm.max_context_size,
 1019                      trigger_ratio=self._loop_control.compaction_trigger_ratio,
 1020                      reserved_context_size=self._loop_control.reserved_context_size,
 1021                  ):
 1022                      logger.info("Context too long, compacting...")
 1023                      try:
 1024                          await self.compact_context()
 1025                      except Exception as compact_err:
 1026                          logger.error(
 1027                              "Context compaction failed at step {step_no}: {error_type}: {error}",
 1028                              step_no=step_no,
 1029                              error_type=type(compact_err).__name__,
 1030                              error=compact_err,
 1031                          )
 1032                          raise
 1033  
      … 32 lines omitted; exact range 1009–1076 …
 1066                          input_data=_hook_events.stop_failure(
 1067                              session_id=self._runtime.session.id,
 1068                              cwd=str(Path.cwd()),
 1069                              error_type=type(e).__name__,
 1070                              error_message=str(e),
 1071                          ),
 1072                      )
 1073                  )
 1074                  _hook_task.add_done_callback(lambda t: t.exception() if not t.cancelled() else None)
 1075                  # break the agent loop
 1076                  raise
为什么相信这条结论?查看 4 处证据
29
L1 · fact · kimi-observe-002

匿名遥测默认开启但可配置/环境变量关闭,代码禁止传用户内容

先看源码事实

Config telemetry 默认 true;app 在 false 或 KIMI_DISABLE_TELEMETRY 时永久 disable。track 只接受 primitive properties,注释明确禁止 user input、file paths、code snippets;sink 30 秒或 50 条 flush,网络失败写本地 JSONL 下次重试。

翻译成白话

默认会上报事件名、耗时、模型/平台等运行指标,但设计规则不允许把提示词、路径和代码塞进事件;用户可以关闭。

为什么这对自研重要

隐私边界主要靠调用方遵守字段规范与 primitive validator,不是自动内容脱敏器。

固定提交源码摘录
  261      telemetry: bool = Field(
  262          default=True,
  263          description="Enable anonymous telemetry to help improve kimi-cli. Set to false to disable.",
  264      )
为什么相信这条结论?查看 4 处证据
30
L2 · fact · kimi-maturity-001

会话、子 Agent 与遥测均有损坏/并发防护,许可证为 Apache-2.0

先看源码事实

Session state 保存采用 fresh read-modify-write,Context 跳过坏 JSONL,SubagentStore 跳过非法 meta,approval 按 source 生命周期取消 pending;项目 LICENSE 为 Apache License 2.0。

翻译成白话

它把“崩一次后还能继续”和“后台任务结束后别留下悬空审批”当成正式设计,而不是只做 happy path。

为什么这对自研重要

工程成熟度较高;跨文件 state/context/wire 并非单事务,极端断电仍可能出现局部不同步。

固定提交源码摘录
   84      def save_state(self) -> None:
   85          """Persist the session state to disk.
   86  
   87          Reloads externally-mutable fields (title, archive) from disk first
   88          to avoid overwriting concurrent changes made by the web API.
   89          """
   90          fresh = load_session_state(self.dir)
   91          self.state.custom_title = fresh.custom_title
   92          self.state.title_generated = fresh.title_generated
   93          self.state.title_generate_attempts = fresh.title_generate_attempts
   94          self.state.archived = fresh.archived
   95          self.state.archived_at = fresh.archived_at
   96          self.state.auto_archive_exempt = fresh.auto_archive_exempt
   97          save_session_state(self.state, self.dir)
为什么相信这条结论?查看 4 处证据
小练习 9

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M10 · ENGINEERING

测试、恢复与工程取舍:把漂亮机制变成可靠产品

哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?

先用一个生活比喻

这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 10

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M11 · PRACTICE

把读懂变成会判断

下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。

Q1

每轮是带检查点的多步状态机,不是单次聊天请求

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

长任务可恢复、可插话,也需要严格的步数上限和副作用治理。

证据:src/kimi_cli/soul/kimisoul.py:659
Q2

每 step 先注入通知/动态约束,再一次生成并等待并发工具

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

实时性和并行性强;动态注入是上下文的一部分,会影响 token 与缓存。

证据:src/kimi_cli/soul/kimisoul.py:1132
Q3

steer 与 D-Mail 提供两种中途改道机制

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

适合长程纠偏和探索回溯,但文件系统副作用不会随 context 自动回滚。

证据:src/kimi_cli/soul/kimisoul.py:622
Q4

错误恢复分 OAuth、连接、空响应和 HTTP 状态码

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

恢复语义清楚;408/409/529 在遥测里可标 retryable,但当前 Python 重试循环并不重试它们。

证据:src/kimi_cli/soul/kimisoul.py:1220
Q5

Kosong 把流式生成和工具调度拆成两层契约

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

Provider 和 Harness 解耦,同时避免中断后悬挂任务。

证据:packages/kosong/src/kosong/_generate.py:52
APPENDIX · SOURCE INDEX

本课读过的实现文件

文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。

  1. 01src/kimi_cli/soul/kimisoul.pyL659–742, L841–852, L998–1041, L1132–1194, L1196–1216, L1274–1295, L622–653, L1099–1109, L1312–1336, L1220–1249, L1647–1658, L1660–1743, L1365–1387, L1432–1476, L1573–1606, L1608–1644, L1338–1346, L409–463, L519–526, L962–993, L854–900, L1009–1076, L832–839
  2. 02packages/kosong/src/kosong/_generate.pyL52–103
  3. 03packages/kosong/src/kosong/__init__.pyL104–174, L134–167
  4. 04src/kimi_cli/llm.pyL326–470, L472–501, L181–263
  5. 05src/kimi_cli/soul/context.pyL20–65, L232–248, L250–339, L123–200, L202–230
  6. 06src/kimi_cli/soul/compaction.pyL37–82, L85–174
  7. 07tests/core/test_simple_compaction.pyL1–120
  8. 08src/kimi_cli/soul/agent.pyL411–451, L453–485, L467–485, L494–517, L87–168, L411–431, L339–369
  9. 09src/kimi_cli/agents/default/agent.yamlL1–36
  10. 10src/kimi_cli/soul/toolset.pyL343–423, L454–611, L116–172, L425–453, L1019–1080, L483–591
  11. 11src/kimi_cli/tools/shell/__init__.pyL22–57, L81–142, L144–219
  12. 12src/kimi_cli/background/worker.pyL87–169
  13. 13src/kimi_cli/soul/approval.pyL130–199, L200–299, L336–396, L36–67
  14. 14src/kimi_cli/approval_runtime/runtime.pyL61–152
  15. 15src/kimi_cli/agents/default/plan.yamlL14–29
  16. 16tests/core/test_subagent_builder.pyL63–86, L13–86
  17. 17src/kimi_cli/agents/default/system.mdL67–81, L67–117, L99–123, L125–148
  18. 18packages/kaos/src/kaos/local.pyL31–78, L139–176
  19. 19src/kimi_cli/hooks/engine.pyL65–91, L205–256, L287–319
  20. 20src/kimi_cli/hooks/runner.pyL27–89
  21. 21src/kimi_cli/plugin/tool.pyL37–130
  22. 22src/kimi_cli/plugin/manager.pyL54–108
  23. 23tests/core/test_plugin_tool.pyL49–153
  24. 24tests/tools/test_mcp_tool_result.pyL30–138
  25. 25src/kimi_cli/subagents/core.pyL58–69
  26. 26src/kimi_cli/config.pyL242–259, L261–264
  27. 27src/kimi_cli/subagents/builder.pyL12–36, L19–42
  28. 28src/kimi_cli/tools/agent/__init__.pyL17–60
  29. 29src/kimi_cli/subagents/store.pyL64–125, L163–196
  30. 30src/kimi_cli/subagents/runner.pyL357–391, L242–288, L393–425
  31. 31tests/core/test_subagent_resume_e2e.pyL76–148
  32. 32src/kimi_cli/telemetry/__init__.pyL52–71, L176–204
  33. 33src/kimi_cli/app.pyL329–352
  34. 34src/kimi_cli/telemetry/sink.pyL19–88
  35. 35src/kimi_cli/session.pyL84–97
  36. 36LICENSEL1–6
下一步

从课程回到报告,做一次反向核验

教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。

查看 Kimi CLI 报告 ↗