Skip to main content

j_agent/storage/
types.rs

1use serde::{Deserialize, Serialize};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4/// 消息角色(API 层 + 存储层共用)
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum MessageRole {
8    User,
9    Assistant,
10    Tool,
11    System,
12}
13
14impl MessageRole {
15    /// 返回对应的字符串表示(用于日志、外部协议等)
16    pub const fn as_str(self) -> &'static str {
17        match self {
18            MessageRole::User => "user",
19            MessageRole::Assistant => "assistant",
20            MessageRole::Tool => "tool",
21            MessageRole::System => "system",
22        }
23    }
24}
25
26impl std::fmt::Display for MessageRole {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.write_str(self.as_str())
29    }
30}
31
32/// 显示类型(渲染层专用,面向 UI 语义细分)
33///
34/// 将 `role` + `tool_calls` 组合映射为精确的渲染语义,
35/// 渲染层只需 `match msg.display_type()` 即可,无需二次判断。
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum DisplayType {
38    /// 用户消息(右对齐气泡)
39    User,
40    /// AI 文本回复(左对齐气泡 + Markdown)
41    AssistantText,
42    /// 工具调用请求(折叠/展开参数)
43    ToolCallRequest,
44    /// 工具执行结果(带状态图标 + 摘要)
45    ToolResult,
46    /// 系统消息(灰色缩进)
47    System,
48}
49
50/// 单次工具调用请求(序列化到历史记录)
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ToolCallItem {
53    pub id: String,
54    pub name: String,
55    pub arguments: String,
56}
57
58/// 图片数据(用于多模态消息,序列化时跳过以节省存储空间)
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ImageData {
61    /// base64 编码的图片数据
62    pub base64: String,
63    /// MIME 类型(如 "image/png", "image/jpeg")
64    pub media_type: String,
65}
66
67/// 消息显示提示(运行时 UI 渲染层专用,不持久化)
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
69pub enum DisplayHint {
70    /// 正常消息(默认)
71    #[default]
72    Normal,
73    /// 内部思考(teammate 未通过 SendMessage 发出的纯文本,用户可见但 agent 不可见)
74    Draft,
75}
76
77/// 对话消息
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ChatMessage {
80    pub role: MessageRole,
81    /// 消息内容(tool_call 类消息可为空)
82    #[serde(default)]
83    pub content: String,
84    /// LLM 发起的工具调用列表(仅 assistant 角色且有 tool_calls 时非 None)
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub tool_calls: Option<Vec<ToolCallItem>>,
87    /// 工具执行结果对应的 tool_call_id(仅 tool 角色时非 None)
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub tool_call_id: Option<String>,
90    /// 图片数据(用于多模态 user message,不持久化到 session 文件)
91    #[serde(skip)]
92    pub images: Option<Vec<ImageData>>,
93    /// LLM 思考内容(thinking mode 返回的 reasoning_content,需传回下一轮请求)
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub reasoning_content: Option<String>,
96    /// 消息发送者名称(如 `Teammate@Frontend`、`SubAgent@search`)。
97    /// 仅由 teammate/subagent 消息设置;主 agent 消息为 None。
98    /// 不持久化到 session 文件,仅用于运行时 UI 渲染。
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub sender_name: Option<String>,
101    /// 消息目标接收者名称(如 `Counter2`、`Main`)。
102    /// 仅当消息有明确 @目标时设置;广播给所有人时为 None。
103    /// 不持久化到 session 文件,仅用于运行时 UI 渲染。
104    #[serde(skip)]
105    pub recipient_name: Option<String>,
106    /// 显示提示(运行时 UI 渲染层专用,不持久化)。
107    /// Draft 表示 teammate 内部思考,用户可见但其他 agent 不可见。
108    #[serde(skip)]
109    pub display_hint: DisplayHint,
110}
111
112impl ChatMessage {
113    /// 创建普通文本消息
114    pub fn text(role: MessageRole, content: impl Into<String>) -> Self {
115        Self {
116            role,
117            content: content.into(),
118            tool_calls: None,
119            tool_call_id: None,
120            images: None,
121            reasoning_content: None,
122            sender_name: None,
123            recipient_name: None,
124            display_hint: DisplayHint::Normal,
125        }
126    }
127
128    /// 设置消息发送者名称(Builder 模式)。
129    ///
130    /// 用于 teammate/subagent 消息标记发送者,UI 渲染层据此显示气泡标签。
131    pub fn with_sender(mut self, name: impl Into<String>) -> Self {
132        self.sender_name = Some(name.into());
133        self
134    }
135
136    /// 设置消息目标接收者名称(Builder 模式)。
137    ///
138    /// 仅当消息有明确 @目标时设置,UI 渲染层据此显示 → Target 标识。
139    pub fn with_recipient(mut self, name: impl Into<String>) -> Self {
140        self.recipient_name = Some(name.into());
141        self
142    }
143
144    /// 设置显示提示(Builder 模式)。
145    pub fn with_display_hint(mut self, hint: DisplayHint) -> Self {
146        self.display_hint = hint;
147        self
148    }
149
150    /// 推断显示类型(渲染层入口)
151    ///
152    /// 将 `role` + `tool_calls` 组合映射为精确的 `DisplayType`,
153    /// 渲染层无需再做 `role == "assistant" && tool_calls.is_some()` 的判断。
154    pub fn display_type(&self) -> DisplayType {
155        match self.role {
156            MessageRole::User => DisplayType::User,
157            MessageRole::System => DisplayType::System,
158            MessageRole::Assistant => {
159                if self.tool_calls.is_some() {
160                    DisplayType::ToolCallRequest
161                } else {
162                    DisplayType::AssistantText
163                }
164            }
165            MessageRole::Tool => DisplayType::ToolResult,
166        }
167    }
168}
169
170pub(super) fn is_zero_u64(v: &u64) -> bool {
171    *v == 0
172}
173
174/// 当前时刻(epoch milliseconds)
175pub fn current_millis() -> u64 {
176    SystemTime::now()
177        .duration_since(UNIX_EPOCH)
178        .map(|d| d.as_millis() as u64)
179        .unwrap_or(0)
180}
181
182/// Session JSONL 事件类型(每行一个事件,append-only)
183#[derive(Debug, Clone, Serialize, Deserialize)]
184#[serde(tag = "type", rename_all = "snake_case")]
185pub enum SessionEvent {
186    /// 新增一条消息
187    Msg {
188        #[serde(flatten)]
189        message: ChatMessage,
190        /// 消息产生时刻(epoch milliseconds);老数据反序列化为 0。
191        #[serde(default, skip_serializing_if = "is_zero_u64")]
192        timestamp_ms: u64,
193    },
194    /// 对话清空
195    Clear,
196    /// 归档还原(messages 为还原后的完整消息列表)
197    Restore { messages: Vec<ChatMessage> },
198    /// 会话级性能指标(session 结束时追加一次)
199    Metrics { metrics: SessionMetrics },
200}
201
202/// 会话级性能/质量指标(session 结束时写入)
203#[derive(Debug, Clone, Serialize, Deserialize, Default)]
204pub struct SessionMetrics {
205    /// LLM API 调用次数(每轮 round 一次)
206    pub total_llm_calls: u32,
207    /// 工具调用次数(tool_calls 数组元素总数)
208    pub total_tool_calls: u32,
209    /// 真实 input tokens(来自 API usage,0 表示未获取到)
210    pub total_input_tokens: u64,
211    /// 真实 output tokens(来自 API usage,0 表示未获取到)
212    pub total_output_tokens: u64,
213    /// 上下文 token 峰值(estimated_context_tokens 各轮最大值)
214    pub estimated_context_tokens_peak: usize,
215    /// auto_compact(含 CompactTool)触发次数
216    pub auto_compact_count: u32,
217    /// micro_compact 触发次数
218    pub micro_compact_count: u32,
219    /// 本次 session 加载的 skill 名称列表
220    pub skill_loads: Vec<String>,
221    /// 每次 LLM 调用的首字延迟(毫秒);流式路径精确,fallback 路径为整个调用耗时
222    pub ttft_ms_per_call: Vec<u64>,
223    /// LLM 调用总耗时(毫秒)—— 仅计算 LLM API 等待时间(含流式读取),不含工具执行时间
224    #[serde(default, skip_serializing_if = "is_zero_u64")]
225    pub total_llm_elapsed_ms: u64,
226    /// 工具执行总耗时(毫秒)—— 仅计算工具调用执行时间
227    #[serde(default, skip_serializing_if = "is_zero_u64")]
228    pub total_tool_elapsed_ms: u64,
229    /// session 开始时间(epoch ms)
230    pub session_start_ms: u64,
231    /// session 结束时间(epoch ms)
232    pub session_end_ms: u64,
233}
234
235/// Session 操作审计记录,追加到 sessions/<id>/ops.jsonl
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct SessionOp {
238    /// 操作类型
239    pub op: SessionOpKind,
240    /// 时间戳(epoch ms)
241    pub timestamp_ms: u64,
242    /// 是否执行失败
243    pub is_error: bool,
244}
245
246/// 操作类型
247#[derive(Debug, Clone, Serialize, Deserialize)]
248#[serde(tag = "kind", rename_all = "snake_case")]
249pub enum SessionOpKind {
250    /// 文件编辑(Edit 工具)
251    Edit {
252        /// 被编辑的文件路径
253        path: String,
254    },
255    /// 文件写入(Write 工具)
256    Write {
257        /// 被写入的文件路径
258        path: String,
259    },
260    /// Shell 命令执行(Shell 工具,变体名保留 bash 以兼容已持久化的 session 数据)
261    Bash {
262        /// 执行的命令
263        command: String,
264    },
265}
266
267impl SessionEvent {
268    /// 构造一条带当前时间戳的 Msg 事件
269    pub fn msg(message: ChatMessage) -> Self {
270        Self::Msg {
271            message,
272            timestamp_ms: current_millis(),
273        }
274    }
275}