# 一条消息的生命周期

> 从敲下回车到屏幕上出现回复——横穿 TUI、SDK、klient、Agent 级服务、human 状态机与工具层。

- 作者：David（道雾轩）
- 专栏：Kimi Code 源码解读（https://daiw.org/manual/kimi-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.org/manual/kimi-code/message-lifecycle
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

这一篇把所有层**串起来走一遍**。细节留给后面各章，这里只建立全局地图，每一跳都给出 v0.43.0 里的落点。

## 全景

```mermaid
sequenceDiagram
    participant U as 用户
    participant T as TUI
    participant S as SDK
    participant K as klient
    participant P as prompt 通道
    participant L as AgentLoopService
    participant M as turn 状态机
    participant R as LLM 请求
    participant X as 工具执行

    U->>T: 输入 + 回车
    T->>S: session.prompt(input)
    S->>K: agent.prompt(...)
    K->>P: agentPromptService.submit
    P->>L: loop.submit(UserEntry)
    L->>M: 启动 turn
    loop 每个 step
        M->>R: generate
        R-->>M: 流式 part + usage + finish
        alt 有 toolCalls
            M->>X: 执行工具批次
            X-->>M: 工具结果
        else 没有 toolCalls
            M-->>L: turn 结束
        end
    end
    L-->>S: 领域事件（IEventBus）
    S-->>T: Session.onEvent
    T-->>U: pi-tui 差分渲染
```

## 逐层拆开

### 1. TUI 收输入

入口链写在 `apps/kimi-code/AGENTS.md`：`src/main.ts` → `src/cli/commands.ts` → `src/cli/run-shell.ts` → SDK `KimiHarness` → `src/tui/kimi-tui.ts`。`run-shell.ts` 用 `createKimiHarness(harnessOptions)` 建好 harness 交给 `KimiTUI`；`/` 开头的 slash 命令在本地处理，普通输入由 `sendMessageInternal` 发出（`kimi-tui.ts:1847`）：

```ts
this.staging.trackDispatch(stagingLease, session.prompt(sdkInput, { promptId: submissionId }), (error) => {
  this.failSessionRequest(`Failed to send: ${formatErrorMessage(error)}`);
});
```

goal 正在跑时改调 `session.steer`，把输入并进当前回合。`kimi -p` 不走这条线：`cli/v2/run-v2-print.ts` 自己 `bootstrap()` 引擎后直接 `loop.submit`。

### 2. SDK：进程内起引擎

`createKimiHarness`（`packages/node-sdk/src/sdk-rpc-client-v2.ts:2728`）先 `new SDKRpcClientV2(options)`，再构造 `KimiHarness`。`SDKRpcClientV2` 的构造函数调引擎的 `bootstrap()`（`app/bootstrap/bootstrap.ts:143`），拿到 App scope 后包一层 klient：

```ts
this.app = app;
this.klient = createKlient({ scope: app });
```

`Session.prompt`（`session.ts:138`）校验后调 `this.rpc.prompt(...)`；`SDKRpcClientV2.prompt`（第 1968 行）取会话的交互 agent——`this.klient.session(sessionId).agent(this.interactiveAgentId)`——调它的 `prompt`。SDK 注释逐处对照 v1：v1 在回合进行中会丢掉新 prompt，v2 按 FIFO 排队。

<Callout type="info">
  「RPC 是解耦缝，不必然是进程边界」在 v2 里换了形式：klient 内存传输从不出进程，但每个值都做一次 `JSON.parse(JSON.stringify(...))` 往返（`klient/src/transports/memory/dispatcher.ts:67`），与 ipc 传输返回字节一致的数据。换个传输就能跨进程，行为不变。
</Callout>

### 3. klient：契约与路由

门面把 `prompt` 映射成一次「服务名 + 方法」调用（`klient/src/core/facade/agent.ts:108`）：

```ts
prompt: (input) =>
  call(scope, 'agentPromptService', 'submit', [input]) as Promise<PromptLaunchResult>,
```

契约 `agentPromptContract`（`contract/agent/services.ts:31`）给出入参与出参的 zod schema，两头都校验。内存分发器的 `resolveScope`（`dispatcher.ts:290`）把 `ScopeRef` 解析到具体的 Session / Agent scope，与 kap-server 的同名函数同构，必要时用 `ensureMainAgent` 补建主 agent。

### 4. 引擎入口：Agent 级的 prompt 通道

`'agentPromptService'` 是 `IAgentPromptChannel` 的 DI 标识（`agent-core-v2/src/agent/loop/promptChannel.ts`），以 `LifecycleScope.Agent`、`ScopeActivation.OnDemand` 注册。`submit`（第 40 行）先为主 agent 更新会话标题与最后一条 prompt，再把输入交给 loop：

```ts
const { id } = this.loop.submit({
  message: { role: 'user', content: [...payload.input] },
  meta: { promptId: payload.promptId, origin: { kind: 'user' }, tracked: true },
});
if (status.state === 'running' || status.paused || status.queue.length > 0) return undefined;
```

loop 忙时 prompt 进队列、返回 `undefined`；空闲时等回合真正启动，返回 `turn_id`。

### 5. AgentLoopService 与 human 状态机

`IAgentLoopService`（`agent/loop/loop.ts:204`）是 Agent 级服务，管 prompt 队列、取消、快照与 step 钩子。循环本身不在它身上，而在 `src/human/` 的 agent / turn / tool 三台纯状态机里；`agent/loop/machine/engine.ts:329` 把后两台接到引擎服务上：

```ts
turnLogic: createTurnMachine(requester.requester, {
  retry: { maxAttemptsPerStep: options.maxAttemptsPerStep },
  recovery: {
    propose: (ctx) => credentialsRecovery.propose(ctx) ?? options.recovery?.propose(ctx),
  },
}),
toolLogic: createToolMachine(tools.executor),
```

`createMachineRequester` 把 `generate` 接到 `IAgentLLMRequesterService`，`createMachineTools` 接到 `IAgentToolExecutorService`；状态机基于 xstate。turn 状态机在 `thinking` 收到 `llm.done` 时决定下一步（`human/agent/turn.ts:572`，有删减）：

```ts
'llm.done': [
  {
    guard: ({ context }) =>
      context.accumulator.finish().message.toolCalls.length > 0,
    target: 'acting',
  },
  { target: 'done' },
],
```

有工具调用就进 `acting`，否则回合结束；空回复转成 `llm.failed.remote`，重试走 `retrying`，中止走 `aborted`。回合结果 `LoopRunResult`（`loop.ts:101`）是 `completed`、`failed`（带 `error`）、`cancelled`（带 `reason`）三选一。

### 6. 调用模型

`createMachineRequester`（`machine/requester.ts:49`）调 `IAgentLLMRequesterService.start`，把流式 part 转成事件；请求一结束先发用量（第 93 行起）：

```ts
const finish = await task.result;
lastFinish = finish;
control.onEvent?.({ type: 'llm.streaming.usage', usage: finish.usage });
```

`engine.ts` 在 `llm.done` 时就带着 usage 发出 `stepCompleted`，工具要等进入 `acting` 才执行。请求层在 `src/human/llm/`，`llm-adapter/protocol/protocolAdapterRegistry.ts` 按模型的 `protocol` 路由到 `openai`、`openai_responses`、`anthropic`、`google-genai` 四种协议基座。

### 7. 工具执行

`acting` 把工具调用交给 tool 状态机，经 `createMachineTools` 进入 `IAgentToolExecutorService`，由 `ToolScheduler` 排队：与正在跑或排在前面的任务有资源冲突就等，否则并行。冲突规则在 `tool/toolContract.ts:191`：

```ts
function resourceAccessesConflict(left: ToolResourceAccess, right: ToolResourceAccess): boolean {
  if (left.kind === 'all' || right.kind === 'all') return true;
  if (!fileOperationsConflict(left.operation, right.operation)) return false;
  return fileAccessesOverlap(left, right);
}
```

`all` 与一切冲突；两边都不写就不冲突；否则看路径是否重叠——比较前统一斜杠并小写折叠。

### 8. 事件回流与落盘

loop 经 `IEventDispatcher` 发出 `turn.started`、`turn.step.*`、`assistant.delta` 等领域事件（`agent/loop/turnEvents.ts`），分发器再 `publish` 到 `IEventBus`（`state/eventDispatcherService.ts:623`）。SDK 的 `SessionEventWiring` 从每个活着的 agent 取 `IEventBus` 订阅（`node-sdk/src/v2/session-wiring.ts:162`，中途新建的子代理也算），经 `translateDomainEvent` 译成 SDK 事件交给 `receiveEvent`；TUI 挂在 `session.onEvent` 上的 `SessionEventHandler.handleEvent`（`controllers/session-event-handler.ts:269`）按 `type` 分派，最后由 pi-tui 差分重绘。可见调用经 klient，事件却直接挂在 SDK 自己 bootstrap 出的 scope 上。

审批与提问是反方向：v2 把待办交互停在进程级的交互内核里，`SessionEventWiring` 监听 `onDidChangePending`，交给 `setApprovalHandler` 等注册的处理器，TUI 的 `reverse-rpc/` 把它们变成面板，用户的选择经内核的 `respond` 写回。

落盘这一侧，loop 用 `wireStoreJournal`（`loopService.ts:203`）把状态机的事件存储接到 wire 服务，记录键是 `wire.jsonl`；wire 协议当前为 `1.5`，旧记录经 `wire/migration/` 逐级迁移。

## 两条值得记住的约定

1. **prompt 不丢**：loop 忙时新 prompt 按 FIFO 排队；想插进当前回合要显式 `steer`。
2. **用量先于工具记账**：模型请求一结束，usage 就随 `stepCompleted` 发出，工具被中止也不影响。

## 接下来

第 1 部分从[Agent 循环：三台状态机](https://daiw.org/manual/kimi-code/agent-loop)开始，把这里一笔带过的 loop 与状态机拆开细看。
