Run 生命周期

`ChatClientAgent::run()` 是 Agent 的核心执行方法,采用严格的三阶段生命周期。理解每个阶段的触发条件和数据流对于编写 ContextProvider 和调试 Agent 行为至关重要。

三阶段全景

sequenceDiagram
    participant User
    participant Agent
    participant CP as ContextProvider 链
    participant Compress as 压缩策略
    participant Client as IChatClient 管道
    participant LLM
    participant Converter as AgentResponseConverter

    User->>Agent: run(messages, session, options)

    Note over Agent: ═══ Phase 1: Pre-invocation ═══
    loop 每个 ContextProvider
        Agent->>CP: on_invoking(agent, session, messages, options)
        CP-->>Agent: ContextResult{instructions, messages, tools}
    end

    Note over Agent: 组装消息列表
    Agent->>Agent: merged = [system] + [provider_messages] + [user_messages]

    Note over Agent: ═══ Phase 1.5: Compression ═══
    alt compression_strategy 存在 且 token 超预算
        Agent->>Compress: compress(full_messages, budget, counter)
        Compress-->>Agent: compressed_messages
    end

    Note over Agent: KV 缓存追踪
    Agent->>Agent: session.touch_request_hash(&full_messages)

    Note over Agent: ═══ Phase 2: LLM 调用 ═══
    Agent->>Agent: 合并工具定义(registry + provider_tools)
    Agent->>Client: run(full_messages, options)
    Client->>LLM: POST /v1/chat/completions (SSE)
    LLM-->>Client: SSE Stream
    Client-->>Agent: BoxStream<AgentResponseUpdate>
    Agent->>Converter: consume(update) × N
    Converter-->>Agent: AgentResponseResult 块

    alt 工具调用
        Note over Client: FunctionInvokingChatClient 循环
        Client->>Client: execute(tool_call)
        Client->>LLM: 继续调用(工具结果注入)
        LLM-->>Client: 新的 SSE Stream
    end

    Note over Agent: ═══ Phase 3: Post-invocation ═══
    Agent->>Agent: Channel 分叉(非阻塞)
    par 流式输出给用户
        Agent-->>User: BoxStream<AgentResponseResult>
    and 后台执行 post-invocation
        loop 每个 ContextProvider
            Agent->>CP: on_invoked(agent, session, request, response, error)
        end
        Agent->>Agent: 持久化 assistant + tool 消息到 Session
    end

Phase 1: Pre-invocation(预调用)

**时机**:Agent 收到 `run()` 调用后,LLM 调用之前。

**目的**:让 ContextProvider 有机会注入额外的指令、消息和工具。

执行流程

// Phase 1: Pre-invocation
let mut merged_instructions = String::new();
let mut merged_provider_messages = Vec::new();
let mut merged_provider_tools = Vec::new();

if let Some(ref sess) = session {
    for provider in &self.context_providers {
        let injection = provider
            .on_invoking(self, sess.as_ref(), &messages, &run_options)
            .await
            .unwrap_or_default();  // 单个 Provider 失败不影响整体

        // 合并指令
        if let Some(inst) = injection.instructions {
            if !merged_instructions.is_empty() {
                merged_instructions.push_str("\n\n");
            }
            merged_instructions.push_str(&inst);
        }

        // 合并消息(支持替换模式)
        if injection.replace_messages {
            merged_provider_messages = injection.messages;
        } else {
            merged_provider_messages.extend(injection.messages);
        }

        // 合并工具
        merged_provider_tools.extend(injection.tools);
    }
}

消息组装顺序

[system (instructions + merged_instructions)]
  + [provider_messages (过滤 System 角色)]
  + [user_messages (过滤 System 角色)]

最终的消息顺序:系统指令在最前,然后是 ContextProvider 注入的上下文,最后是用户消息。这确保用户输入在上下文的末尾,LLM 能直接响应。

输入消息处理

- 调用方传入的 `System` 角色消息被过滤掉——Agent 的系统指令由 `instructions` 字段统一管理。 - `user_messages` 部分(非 System)被保留用于构建 turn transcript。

Phase 1.5: Compression(压缩)

**时机**:消息组装完成后,LLM 调用之前。

**触发条件**(三个条件同时满足):

1. `compression_strategy` 已配置 2. `token_counter` 已配置 3. 当前消息的 Token 数超过了 `model_metadata.input_budget()`

// Phase 1.5: Compression
if let (Some(ref strategy), Some(ref counter)) =
    (&self.compression_strategy, &self.token_counter)
{
    if let Some(model_metadata) = self.chat_client.model_metadata() {
        let budget = model_metadata.input_budget();
        let current_tokens = counter.count_tokens(&full_messages);
        if current_tokens > budget {
            // 执行压缩
            match strategy.compress(full_messages.clone(), budget, counter.as_ref()) {
                Ok(compressed) => full_messages = compressed,
                Err(e) => {
                    // 压缩失败不中断流程,使用原始消息
                    tracing::warn!("Compression failed, using original messages");
                }
            }
        }
    }
}

**注意事项**:

- 如果 `chat_client.model_metadata()` 返回 `None`,压缩不会触发(因为无法确定预算) - 压缩失败时使用原始消息继续——防止压缩策略 bug 导致 Agent 不可用 - `model_metadata` 需由具体的 `IChatClient` 实现提供(如 `ChatClient` 的 `model_metadata()` 方法)

Phase 2: LLM 调用

**目的**:向 LLM 发送消息并处理流式响应。

2.1 工具定义合并

// 从 ToolRegistry 和 Provider 中收集工具定义
let registry = self.tools.read().await;
let mut tool_defs: Vec<serde_json::Value> = Vec::new();

// 1. Agent 注册的工具
for tool in registry.list() {
    tool_defs.push(serde_json::json!({
        "type": "function",
        "function": {
            "name": tool.name(),
            "description": tool.description(),
            "parameters": tool.parameters(),
        }
    }));
}

// 2. Provider 注入的工具(去重)
let mut seen: HashSet<String> = tool_defs.iter()
    .filter_map(|d| d["function"]["name"].as_str().map(String::from))
    .collect();

for tool in &merged_provider_tools {
    let name = tool.name().to_string();
    if seen.insert(name) {
        tool_defs.push(/* ... */);
    }
}

// 注入到 ChatClientRunOptions
client_opts.tools = tool_defs;
client_opts.provider_tools = merged_provider_tools;

**去重策略**:Agent 注册的工具优先。如果 Provider 注入了同名工具,会被跳过。

2.2 LLM 流式调用

let stream = self.chat_client.run(&full_messages, client_opts).await?;

这里的 `self.chat_client` 已是管道包装后的客户端(如 `FunctionInvokingChatClient` → `DeepSeekChatClient`),所以工具调用循环对 Agent 是透明的。

2.3 流转换

`AgentResponseConverter` 将 `AgentResponseUpdate` 流转换为 `AgentResponseResult` 流:

let converter = AgentResponseConverter::new(agent_id, executor_id, &run_options);

let converted = futures_util::stream::unfold(
    (stream, converter, None::<FinishReason>, None::<Usage>, false),
    |(mut stream, mut converter, mut pf, mut pu, done)| async move {
        if done { return None; }
        loop {
            match stream.next().await {
                Some(Ok(update)) => {
                    // 跟踪 Finish 事件
                    if let AgentResponseUpdate::Finish { ref finish_reason, ref usage } = update {
                        pf = Some(finish_reason.clone());
                        if usage.is_some() { pu = usage.clone(); }
                    }
                    // 消费更新
                    let output = converter.consume(update);
                    if !output.contents.is_empty() || !output.events.is_empty() {
                        return Some((Ok(AgentResponseResult { ... }), ...));
                    }
                }
                Some(Err(e)) => return Some((Err(e), ...)),
                None => {
                    // 流结束:finalize 刷新缓冲
                    let fr = converter.finalize(pf.clone(), pu.clone());
                    return Some((Ok(fr), (stream, converter, pf, pu, true)));
                }
            }
        }
    },
);

**关键行为**:

- `converter.consume(update)` 可能返回空(如中间状态被缓存),此时继续循环读取下一个 SSE 事件 - `converter.finalize()` 在流结束时调用,刷新所有缓冲的工具调用(`flush_tool_calls()`) - `Finish` 事件在流中传递,最终由 `finalize()` 附加到最后的 `AgentResponseResult` 块

Phase 3: Post-invocation(后调用)

**时机**:Agent 收集完流式响应后,与输出流并行执行。

**目的**:通知 ContextProvider 调用已完成,持久化消息,更新会话。

> **代码组织**:Phase 3 的核心逻辑已提取为 `ChatClientAgent::spawn_post_invocation_handler()` 方法,将 `run()` 从约 330 行缩减至约 150 行。详见 [ChatClientAgent 文档](./chat-client-agent.md#方法提取spawn_post_invocation_handler)。

3.1 Channel 分叉

Phase 3 通过 `tokio::sync::mpsc::unbounded_channel` 实现非阻塞执行:

let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();

// 后台任务
tokio::spawn(async move {
    let mut collected: Vec<Result<AgentResponseResult>> = Vec::new();
    while let Some(chunk) = rx.recv().await {
        collected.push(chunk);
    }
    if collected.is_empty() { return; }

    // 收集完整响应
    let mut text = String::new();
    let mut tool_calls = Vec::new();
    // ... 处理 collected 中的所有块

    // 构建 AgentResponse + 构建 turn transcript
    let response = AgentResponse { ... };

    // 调用每个 ContextProvider 的 on_invoked
    for provider in providers.iter() {
        if let Err(e) = provider.on_invoked(&proxy, sess.as_ref(), ...).await {
            tracing::warn!(provider = %provider.name(), error = %e, "on_invoked failed");
        }
    }

    // 持久化 assistant + tool 消息到 Session
    if !response.turn_transcript.is_empty() {
        let non_user: Vec<ChatMessage> = response.turn_transcript
            .iter()
            .filter(|m| m.role != MessageRole::User)
            .cloned()
            .collect();
        if let Err(e) = sess.add_messages_batch(&non_user).await {
            tracing::warn!("Failed to persist turn transcript");
        }
    }
    // ... 备选持久化路径
});

// 输出流通过 inspect 分叉数据到 channel
let stream = converted.inspect(move |chunk| {
    if let Ok(ref c) = chunk {
        if tx.send(Ok(c.clone())).is_err() {
            tracing::warn!("Post-invocation channel closed");
        }
    }
});
return Ok(Box::pin(stream));

3.2 持久化优先级

1. **完整 turn transcript** 非空 → 批量持久化所有非用户消息 2. **有工具调用** → 持久化 `assistant_with_tools` + 逐个 `tool` 消息 3. **纯文本回复** → 持久化 `assistant` 消息

3.3 错误隔离

Phase 3 中的所有错误都会被 `tracing::warn!` 记录,但不会中断输出流或传播给调用方。这确保了持久化失败不影响 Agent 响应。

生命周期中的关键设计决策

为什么 Phase 3 要分叉?

- **响应延迟**:Phase 3 的持久化操作可能是 IO 密集的(尤其是 `FileSystemSessionStore`) - **用户体验**:用户应在 Agent 完成推理后立即看到响应,而不必等待持久化 - **错误隔离**:持久化失败不影响已生成的响应

为什么只过滤 System 消息?

调用方传入的 `System` 消息与 Agent 自身的 `instructions` 冲突。Agent 始终用自己的系统指令覆盖任何外部系统指令。如果需要在单次调用中覆盖指令,应使用 `AgentRunOptions::with_instructions()`。

为什么 Provider 失败不中断流程?

每个 Provider 的 `on_invoking()` 错误都会通过 `unwrap_or_default()` 吞没——返回空的 `ContextResult`。这确保单个 Provider 的 bug 不会导致整个 Agent 不可用。

下一步

理解生命周期后,请阅读 **[流式管道](./streaming.md)**,深入了解 `BoxStream` 的消费模式和 `AgentResponseConverter` 的工作原理。