Skip to main content

agent_context/context/
types.rs

1//! 后端 trait 定义。
2//!
3//! [`ContextBackend`] 封装 LLM 后端的消息工厂、格式转换、模型对话和配置信息。
4//! [`ContextBackendResponse`] 约束后端 Response 类型,提供统一的响应访问接口。
5
6use crate::error::AgentError;
7use crate::message::ContextMessage;
8
9// ---------------------------------------------------------------------------
10// ContextBackendResponse — Response 类型约束
11// ---------------------------------------------------------------------------
12
13/// 工具调用信息。从后端 Response 中提取,供 consumer 执行工具并构造 Tool 角色消息。
14#[derive(Debug, Clone)]
15pub struct ToolCallInfo {
16    /// 工具调用唯一标识,对应 [`ContextBackend::tool_message`] 的 `tool_call_id`。
17    pub id: String,
18    /// 函数名。
19    pub name: String,
20    /// 函数参数(JSON 字符串)。
21    pub arguments: String,
22}
23
24/// 后端 Response 类型约束,流式/非流式 Response 均需实现。
25///
26/// 提供:
27/// - [`response_type`](Self::response_type):内容分类,供 [`ContextBackend::classify_chunk`] 默认实现
28/// - [`reasoning_content`](Self::reasoning_content):思考链文本
29/// - [`content`](Self::content):正文文本
30/// - [`tool_calls`](Self::tool_calls):工具调用信息
31pub trait ContextBackendResponse {
32    /// 返回响应包含的内容类型。
33    fn response_type(&self) -> ResponseType;
34
35    /// 提取思考链文本(流式为 delta,非流式为完整内容)。
36    ///
37    /// `None` 表示字段不存在/null,`Some("")` 表示空字符串。
38    fn reasoning_content(&self) -> Option<String>;
39
40    /// 提取正文文本(流式为 delta,非流式为完整内容)。
41    ///
42    /// `None` 表示字段不存在/null,`Some("")` 表示空字符串。
43    fn content(&self) -> Option<String>;
44
45    /// 提取工具调用信息(流式为 delta,非流式为完整列表)。
46    fn tool_calls(&self) -> Vec<ToolCallInfo>;
47}
48
49/// 响应内容类型枚举。
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum ResponseType {
52    /// 无增量内容
53    Empty,
54    /// 仅思考链
55    Reasoning,
56    /// 仅正文
57    Content,
58    /// 同时包含思考链和正文
59    ReasoningAndContent,
60}
61
62// ---------------------------------------------------------------------------
63// 流式输出事件
64// ---------------------------------------------------------------------------
65
66/// 流式输出事件,由 [`ContextBackend::classify_chunk`] 产出。
67///
68/// 每个事件持有原始后端响应引用,用户可从中提取任意数据(content、reasoning、usage 等)。
69/// 事件类型仅做阶段标记,不做数据裁剪。
70#[derive(Debug, Clone)]
71pub enum StreamEvent<R> {
72    /// 思考链增量响应
73    Thinking(R),
74    /// 第一个正文增量响应(思考链→正文的过渡点)
75    ContentFirst(R),
76    /// 后续正文增量响应
77    Content(R),
78    /// 工具调用增量响应
79    ToolCalls(R),
80}
81
82// ---------------------------------------------------------------------------
83// ScratchOpts trait
84// ---------------------------------------------------------------------------
85
86/// 每轮刷新的临时元数据,通过 Opts 配置,发送时追加到消息列表末尾。
87///
88/// 实现此 trait 的 Opts 类型提供一个 `scratch` 字段,内容在每次 [`SendMsg`](crate::SendMsg)/[`SendStreamMsg`](crate::SendStreamMsg)
89/// 时作为 system 消息拼接到对话末尾。Scratch 不存储到任何区,每轮由调用方重新设置。
90pub trait ScratchOpts {
91    /// 返回 scratch 内容。`Some` 时作为 system 消息追加;`None` 则不追加。
92    fn scratch(&self) -> Option<&str>;
93}
94
95// ---------------------------------------------------------------------------
96// ContextBackend trait
97// ---------------------------------------------------------------------------
98
99/// 后端 trait:抽象 LLM 后端的完整接口。
100///
101/// 实现此 trait 即可让 [`AgentContext`](crate::AgentContext) 对接任意 LLM 后端(DeepSeek、智谱、OpenAI 等)。
102///
103/// ## 方法分类
104///
105/// | 类别 | 方法 | 类型 |
106/// |------|------|------|
107/// | 消息工厂 | [`user_message`](Self::user_message)、[`system_message`](Self::system_message)、[`tool_message`](Self::tool_message) | 关联函数 |
108/// | 格式转换 | [`to_system_message`](Self::to_system_message)、[`to_request_messages`](Self::to_request_messages) | 关联函数(默认实现) |
109/// | 请求选项 | 通过 [`Default::default()`] 获取 [`Self::Opts`] 默认值 | 类型约束 |
110/// | 响应解析 | [`extract_messages_from_backend_response`](Self::extract_messages_from_backend_response) | 实例方法 |
111/// | 模型对话 | [`estimate_tokens`](Self::estimate_tokens)、[`send`](Self::send)、[`send_stream`](Self::send_stream) | 实例方法 |
112/// | 配置信息 | [`context_window`](Self::context_window) | 实例方法 |
113pub trait ContextBackend: Send + Sync + Clone + 'static {
114    /// 后端消息类型,必须实现 [`ContextMessage`]。
115    type Message: ContextMessage;
116    /// 后端自定义的请求选项类型,AC 不感知字段含义,只做透传。
117    ///
118    /// 典型用途:传递 `temperature`、`max_tokens`、`thinking` 等模型参数。
119    /// 实现 [`Default`] trait 获取默认值,CLI 可在此基础上覆盖。
120    type Opts: ScratchOpts + Clone + Default + Send + Sync;
121    /// 后端完整的 API 响应类型。
122    ///
123    /// - 非流式:ChatCompletion(含 choices + usage)
124    /// - 流式:ChatCompletionChunk(含 delta content / reasoning_content)
125    type Response: Clone + Send + Sync + ContextBackendResponse;
126
127    // 消息工厂(实例方法)
128    /// 构造一条 User 角色消息。
129    fn user_message(&self, content: impl Into<String> + Send) -> Self::Message;
130    /// 构造一条 System 角色消息。
131    fn system_message(&self, content: impl Into<String> + Send) -> Self::Message;
132    /// 构造一条 Tool 角色消息(工具调用结果)。
133    fn tool_message(
134        &self,
135        tool_call_id: impl Into<String> + Send,
136        content: impl Into<String> + Send,
137    ) -> Self::Message;
138
139    // 格式转换(实例方法,含默认实现)
140    /// 将消息转换为 System 角色(用于压缩摘要等场景)。
141    ///
142    /// 默认实现调用 [`ContextMessage::with_role`]。
143    fn to_system_message(&self, msg: Self::Message) -> Self::Message {
144        msg.with_role(crate::Role::System)
145    }
146
147    /// 将后端响应消息转换为请求格式。
148    ///
149    /// 对 `!preserve_reasoning()` 的消息剥离 `reasoning_content`,减少网络传输和 token 消耗。
150    ///
151    /// 默认实现调用 [`ContextMessage::without_reasoning`]。
152    fn to_request_messages(
153        &self,
154        messages: Vec<Self::Message>,
155    ) -> Result<Vec<Self::Message>, AgentError> {
156        Ok(messages
157            .into_iter()
158            .map(|m| {
159                if m.preserve_reasoning() {
160                    m
161                } else {
162                    m.without_reasoning()
163                }
164            })
165            .collect())
166    }
167
168    // 响应解析(实例方法)
169    /// 将流式分块合并为单条消息。
170    ///
171    /// 累加 `content`、`reasoning_content`、`tool_calls`,构造完整的 assistant 消息。
172    fn merge_chunks(&self, responses: &[Self::Response]) -> Option<Self::Message>;
173
174    /// 从后端原始响应中提取消息列表。非流式传 `&[单个 Response]`,流式传 `&[所有累积 chunk]`。
175    fn extract_messages_from_backend_response(
176        &self,
177        responses: &[Self::Response],
178    ) -> Result<Vec<Self::Message>, AgentError>;
179
180    // 模型对话(实例方法)
181    /// 估算消息列表的 token 数量。I/O 操作(可能需要调用远程 tokenizer API)。
182    fn estimate_tokens(
183        &self,
184        messages: &[Self::Message],
185    ) -> impl std::future::Future<Output = Result<usize, AgentError>> + Send;
186
187    /// 模型上下文窗口大小(token 数),用于 [`IsFullMsg`](crate::IsFullMsg) 检测。
188    fn context_window(&self) -> usize;
189
190    /// 非流式对话。发送全部消息,返回完整 Response(含 usage 等元数据)。
191    fn send(
192        &self,
193        messages: &[Self::Message],
194        opts: &Self::Opts,
195    ) -> impl std::future::Future<Output = Result<Self::Response, AgentError>> + Send;
196
197    /// 流式对话。参数为 owned(数据已移动),返回 `'static` 流。
198    fn send_stream(
199        &self,
200        messages: Vec<Self::Message>,
201        opts: Self::Opts,
202    ) -> impl futures_core::Stream<Item = Result<Self::Response, AgentError>> + Send + 'static;
203
204    /// 将流式分块分类为结构化事件,同时更新阶段状态。
205    ///
206    /// 默认实现基于 [`ContextBackendResponse::response_type`] 判断阶段:
207    /// - 含 `reasoning_content` → [`StreamEvent::Thinking`]
208    /// - 第一个 `content`(且之前有思考链)→ [`StreamEvent::ContentFirst`]
209    /// - 后续 `content` → [`StreamEvent::Content`]
210    fn classify_chunk(
211        &self,
212        response: &Self::Response,
213        saw_thinking: &mut bool,
214    ) -> Vec<StreamEvent<Self::Response>> {
215        let mut events = Vec::new();
216        if !response.tool_calls().is_empty() {
217            events.push(StreamEvent::ToolCalls(response.clone()));
218        }
219        match response.response_type() {
220            ResponseType::Empty => return events,
221            ResponseType::Reasoning => {
222                events.push(StreamEvent::Thinking(response.clone()));
223                *saw_thinking = true;
224            }
225            ResponseType::Content => {
226                if *saw_thinking {
227                    events.push(StreamEvent::ContentFirst(response.clone()));
228                    *saw_thinking = false;
229                } else {
230                    events.push(StreamEvent::Content(response.clone()));
231                }
232            }
233            ResponseType::ReasoningAndContent => {
234                events.push(StreamEvent::Thinking(response.clone()));
235                *saw_thinking = true;
236                events.push(StreamEvent::ContentFirst(response.clone()));
237                *saw_thinking = false;
238            }
239        }
240        events
241    }
242
243    /// 将消息序列化为 JSONL 行(供应商原生格式)。
244    ///
245    /// 默认实现直接序列化 `Self::Message`。多供应商 enum 后端需覆写,
246    /// 提取内层供应商原生消息序列化,跳过 enum 变体名包装。
247    fn message_to_jsonl(&self, msg: &Self::Message) -> Result<String, AgentError> {
248        serde_json::to_string(msg).map_err(|e| AgentError::Context(e.to_string()))
249    }
250
251    /// 从 JSONL 行反序列化为消息。
252    ///
253    /// 默认实现直接反序列化为 `Self::Message`。多供应商 enum 后端需覆写,
254    /// 按当前供应商类型解析后包装为 enum 变体。
255    fn message_from_jsonl(&self, line: &str) -> Result<Self::Message, AgentError> {
256        serde_json::from_str(line).map_err(|e| AgentError::Context(format!("JSONL 解析失败: {e}")))
257    }
258}