Skip to content

Commit 440f29d

Browse files
husterL9chengshuyi
authored andcommitted
refactor(sight): derive session_id and conversation_id from response_id instead of message content
1 parent 7012be7 commit 440f29d

5 files changed

Lines changed: 592 additions & 141 deletions

File tree

src/agentsight/src/genai/builder.rs

Lines changed: 112 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ use super::semantic::{
1818
GenAISemanticEvent, LLMCall, LLMRequest, LLMResponse,
1919
InputMessage, OutputMessage, MessagePart, TokenUsage,
2020
};
21+
use super::id_resolver::IdResolver;
2122
use std::collections::HashMap;
2223
use std::sync::atomic::{AtomicU64, Ordering};
2324
use std::time::{SystemTime, UNIX_EPOCH};
24-
use sha2::{Sha256, Digest};
2525

2626
/// Output from `GenAIBuilder::build()`, containing built events and deferred resolution info.
2727
pub struct BuildOutput {
@@ -39,6 +39,9 @@ pub struct GenAIBuilder {
3939
session_prefix: String,
4040
/// Counter for generating unique IDs within a session
4141
call_counter: AtomicU64,
42+
/// Resolver for `session_id` fallback / `conversation_id` based on the
43+
/// earliest `response_id` observed within a session / conversation.
44+
id_resolver: IdResolver,
4245
}
4346

4447
impl Default for GenAIBuilder {
@@ -58,6 +61,7 @@ impl GenAIBuilder {
5861
GenAIBuilder {
5962
session_prefix: format!("{:x}_{:x}", ts, pid),
6063
call_counter: AtomicU64::new(0),
64+
id_resolver: IdResolver::new(),
6165
}
6266
}
6367

@@ -179,9 +183,19 @@ impl GenAIBuilder {
179183
.and_then(|v| v.as_bool())
180184
.unwrap_or(false);
181185

182-
// Parse messages from body to compute session_id, conversation_id,
183-
// user_query, and serialise input_messages / system_instructions
184-
let (session_id, conversation_id, user_query, input_messages, system_instructions) =
186+
// Parse messages from body to extract user_query / input_messages /
187+
// system_instructions / first_user_text / last_user_text。session_id 与
188+
// conversation_id 在 request 阶段采用双层兑底:
189+
// 1. 优先走 IdResolver::peek_*(同 PID 之前有过正常完成的调用 →
190+
// LRU 已 anchor 首个 response_id,复用后与正常路径完全对齐)。
191+
// 2. 未命中时 → `crash_fallback_id`以 (agent_name, pid, user_text) 作为
192+
// 兑底 ID 输入,保证 crash-drain 路径同 PID 同 user_query 的
193+
// crash 记录归一桶,不同 user_query 分桶。
194+
//
195+
// 正常响应到达后 `complete_pending` 仍会用 `IdResolver::resolve_*`
196+
// 重新计算并 UPDATE 正常 ID,只有 crash 路径才会保留这里写入的
197+
// peek/fallback 值。
198+
let (user_query, input_messages, system_instructions, first_user_text, last_user_text) =
185199
if let Some(ref v) = body {
186200
if let Some(messages) = v.get("messages").and_then(|m| m.as_array()) {
187201
// Helper: extract text from "content" which can be either
@@ -210,33 +224,20 @@ impl GenAIBuilder {
210224
None
211225
};
212226

213-
// session_id: SHA256 of first user message text (same logic
214-
// as compute_session_id but operating on raw JSON values)
227+
// First user message raw text — used as `session_key` material
228+
// for IdResolver peek / crash fallback.
215229
let first_user_text = messages.iter()
216230
.filter(|m| m.get("role").and_then(|r| r.as_str()) == Some("user"))
217231
.find_map(|m| extract_text(m))
218232
.unwrap_or_default();
219233

220-
let session_id = if !first_user_text.is_empty() {
221-
let hash = Sha256::digest(first_user_text.as_bytes());
222-
Some(format!("{:x}", hash)[..32].to_string())
223-
} else {
224-
None
225-
};
226-
227-
// Last user message raw text — used for both conversation_id
228-
// (fingerprint hash) and user_query (display text)
234+
// Last user message raw text — used for user_query (display text)
235+
// 以及 conversation_key (peek / crash fallback)。
229236
let last_user_raw = messages.iter()
230237
.rev()
231238
.filter(|m| m.get("role").and_then(|r| r.as_str()) == Some("user"))
232239
.find_map(|m| extract_text(m));
233-
234-
// conversation_id: SHA256 of last user message raw text
235-
// (same logic as compute_user_query_fingerprint)
236-
let conversation_id = last_user_raw.as_deref().map(|text| {
237-
let hash = Sha256::digest(text.as_bytes());
238-
format!("{:x}", hash)[..32].to_string()
239-
});
240+
let last_user_text = last_user_raw.clone().unwrap_or_default();
240241

241242
// user_query: last user message text, stripped of metadata prefix
242243
let user_query = last_user_raw.as_deref()
@@ -261,13 +262,13 @@ impl GenAIBuilder {
261262
serde_json::to_string(&sys).ok()
262263
};
263264

264-
(session_id, conversation_id, user_query, input_messages, system_instructions)
265+
(user_query, input_messages, system_instructions, first_user_text, last_user_text)
265266
} else {
266267
// messages key missing or not an array
267-
(None, None, None, None, None)
268+
(None, None, None, String::new(), String::new())
268269
}
269270
} else {
270-
(None, None, None, None, None)
271+
(None, None, None, String::new(), String::new())
271272
};
272273

273274
// Extract model from request body JSON "model" field
@@ -284,13 +285,48 @@ impl GenAIBuilder {
284285
let agent_name = Self::resolve_agent_name_from_comm(&request.source_event.comm, conn_id.pid as u32, pid_agent_name_cache)
285286
.or_else(|| Some(request.source_event.comm_str()));
286287

288+
// 双层兑底计算 session_id / conversation_id(详见上方注释)。
289+
// 这里不使用 unwrap_or_else(|| “”) 是为了让“同 PID 同 agent”上下文下
290+
// crash_fallback_id 输入始终相同。
291+
let agent_name_str = agent_name.as_deref().unwrap_or("");
292+
let pid_i32 = conn_id.pid as i32;
293+
294+
let session_id = self
295+
.id_resolver
296+
.peek_session_id(agent_name_str, pid_i32, &first_user_text)
297+
.or_else(|| {
298+
Some(super::id_resolver::crash_fallback_id(
299+
"session",
300+
agent_name_str,
301+
pid_i32,
302+
&first_user_text,
303+
))
304+
});
305+
let conversation_id = self
306+
.id_resolver
307+
.peek_conversation_id(agent_name_str, pid_i32, &last_user_text)
308+
.or_else(|| {
309+
Some(super::id_resolver::crash_fallback_id(
310+
"conversation",
311+
agent_name_str,
312+
pid_i32,
313+
&last_user_text,
314+
))
315+
});
316+
287317
Some(PendingCallInfo {
288318
call_id,
289319
trace_id: None, // LLM API response_id, not available until response
290-
conversation_id, // User query fingerprint hash (from request body)
320+
// session_id / conversation_id 在请求阶段采用双层兑底:
321+
// 1) IdResolver::peek_* 复用同 PID 之前正常完成调用的 anchor,
322+
// 响应到达后 `complete_pending` 会用同样的值覆盖;
323+
// 2) LRU miss 时走 `crash_fallback_id`(`crash-` 前缀与正常 ID 隔离),
324+
// 进程崩溃不会走到 complete_pending 时该值会保留,供
325+
// handle_agent_crash_detection 按 (sid, cid) 分组。
326+
conversation_id,
291327
session_id,
292328
start_timestamp_ns: request.source_event.timestamp_ns,
293-
pid: conn_id.pid as i32,
329+
pid: pid_i32,
294330
process_name: request.source_event.comm.clone(),
295331
agent_name,
296332
http_method: Some(request.method.clone()),
@@ -457,17 +493,10 @@ impl GenAIBuilder {
457493
.or_else(|| Self::extract_model_from_body(&http.request_body, &http.response_body))
458494
.unwrap_or_else(|| "unknown".to_string());
459495

460-
// 在 request move 之前提取用户查询、fingerprint 和 session_id
461-
let query_fp = Self::compute_user_query_fingerprint(&request);
496+
// 在 request move 之前提取用户查询 / first&last user message 原文
462497
let user_query = Self::extract_last_user_query(&request);
463-
// session_id: 优先从 agent 自身的 session 获取(通过 response ID → .jsonl UUID 映射),
464-
// fallback 到基于首条 user message 的 hash 计算
465-
let response_id_val = parsed_message.as_ref().and_then(|m| m.response_id()).map(|s| s.to_string());
466-
let mapper_session = response_id_val.as_deref()
467-
.and_then(|rid| response_mapper.get_session_by_response_id(rid))
468-
.map(|s| s.to_string());
469-
let session_id = mapper_session.clone()
470-
.unwrap_or_else(|| Self::compute_session_id(&request));
498+
let first_user_raw = Self::extract_first_user_raw(&request).unwrap_or_default();
499+
let last_user_raw = Self::extract_last_user_raw(&request).unwrap_or_default();
471500

472501
// 提取 LLM API 的 response_id(如 chatcmpl-xxx),用作 trace_id
473502
// 同时作为 call_id 的首选值:trace_id 有值时直接复用,避免两套 ID;
@@ -476,6 +505,39 @@ impl GenAIBuilder {
476505
let call_id = response_id.clone().unwrap_or_else(|| internal_id.clone());
477506
let response_id = response_id.unwrap_or_else(|| call_id.clone());
478507

508+
// 提前解析 agent_name,后面调用 IdResolver 需要它作为 LRU key 维度,
509+
// 避免同机不同 agent 在同一 user query 下撞同一 session_id。
510+
let agent_name = Self::resolve_agent_name(&http.comm, http.pid, pid_agent_name_cache)
511+
.unwrap_or_else(|| http.comm.clone());
512+
let pid_i32 = http.pid as i32;
513+
514+
// session_id: 优先从 agent 自身的 session 获取(通过 response ID → .jsonl UUID 映射),
515+
// 未命中时 fallback 到 `SHA256("session" + 该 session 内最早 response_id)`。
516+
let parsed_response_id = parsed_message.as_ref()
517+
.and_then(|m| m.response_id())
518+
.map(|s| s.to_string());
519+
let mapper_session = parsed_response_id.as_deref()
520+
.and_then(|rid| response_mapper.get_session_by_response_id(rid))
521+
.map(|s| s.to_string());
522+
let session_id = match mapper_session {
523+
Some(uuid) => Some(uuid),
524+
None => self.id_resolver.resolve_session_id(
525+
&agent_name,
526+
pid_i32,
527+
&first_user_raw,
528+
&response_id,
529+
),
530+
};
531+
532+
// conversation_id: SHA256("conversation" + 该 conversation 内最早 response_id)
533+
let conversation_id = self.id_resolver.resolve_conversation_id(
534+
&agent_name,
535+
pid_i32,
536+
&last_user_raw,
537+
&response_id,
538+
);
539+
540+
479541
// Extract error message from response body when status_code >= 400
480542
let error = if http.status_code >= 400 {
481543
http.response_body.as_ref().and_then(|body| {
@@ -542,10 +604,9 @@ impl GenAIBuilder {
542604
response,
543605
token_usage,
544606
error,
545-
pid: http.pid as i32,
607+
pid: pid_i32,
546608
process_name: http.comm.clone(),
547-
agent_name: Self::resolve_agent_name(&http.comm, http.pid, pid_agent_name_cache)
548-
.or_else(|| Some(http.comm.clone())),
609+
agent_name: Some(agent_name.clone()),
549610
metadata: {
550611
let mut meta = HashMap::new();
551612
meta.insert("method".to_string(), http.method);
@@ -573,15 +634,19 @@ impl GenAIBuilder {
573634
meta.insert("operation_name".to_string(), "chat".to_string());
574635
}
575636
// conversation_id: 对话ID,同一 user query 触发的所有调用共享
576-
meta.insert("conversation_id".to_string(), query_fp);
637+
if let Some(ref cid) = conversation_id {
638+
meta.insert("conversation_id".to_string(), cid.clone());
639+
}
577640
// response_id: LLM API 返回的响应 ID,用作 trace_id
578641
meta.insert("response_id".to_string(), response_id);
579642
// user_query: 用户实际输入的原文
580643
if let Some(ref q) = user_query {
581644
meta.insert("user_query".to_string(), q.clone());
582645
}
583646
// session_id: 同一 agent 进程的完整会话标识
584-
meta.insert("session_id".to_string(), session_id);
647+
if let Some(ref sid) = session_id {
648+
meta.insert("session_id".to_string(), sid.clone());
649+
}
585650
meta
586651
},
587652
})
@@ -1124,15 +1189,12 @@ impl GenAIBuilder {
11241189
format!("{}_{}", self.session_prefix, seq)
11251190
}
11261191

1127-
/// 生成 session_id(32 位 hex)
1192+
/// 提取第一条有实际文本内容的 user message 的原始文本
11281193
///
1129-
/// 基于第一条 user message 原文生成,原文包含时间戳前缀如
1130-
/// `[Tue 2026-03-31 17:19 GMT+8] 用户输入`,天然唯一。
1131-
/// - 同一会话(含退出重进):第一条 user message 不变 → session_id 稳定
1132-
/// - 新会话:时间戳不同 → session_id 不同
1133-
fn compute_session_id(request: &LLMRequest) -> String {
1134-
// 找第一条有实际文本的 user message(原始文本,含时间戳)
1135-
let first_user_raw: String = request.messages.iter()
1194+
/// 仅返回含非空 `Text` 片段的首条 user message,供 `IdResolver`
1195+
/// 生成 session_key 使用。跳过只含 tool_result 等的 user message。
1196+
fn extract_first_user_raw(request: &LLMRequest) -> Option<String> {
1197+
request.messages.iter()
11361198
.filter(|m| m.role == "user")
11371199
.find_map(|m| {
11381200
let text: String = m.parts.iter()
@@ -1144,10 +1206,6 @@ impl GenAIBuilder {
11441206
.join("\n");
11451207
if text.is_empty() { None } else { Some(text) }
11461208
})
1147-
.unwrap_or_default();
1148-
1149-
let hash = Sha256::digest(first_user_raw.as_bytes());
1150-
format!("{:x}", hash)[..32].to_string()
11511209
}
11521210

11531211
/// 提取最后一条有实际文本内容的 user message 的原始文本
@@ -1204,20 +1262,6 @@ impl GenAIBuilder {
12041262
}
12051263
text.to_string()
12061264
}
1207-
1208-
/// 计算 user query 的 fingerprint,用于关联同一个请求的调用链
1209-
///
1210-
/// 使用原始文本(包含时间戳前缀)计算 hash,
1211-
/// 这样相同命令在不同时间发送也会产生不同的 fingerprint
1212-
fn compute_user_query_fingerprint(request: &LLMRequest) -> String {
1213-
match Self::extract_last_user_raw(request) {
1214-
Some(content) => {
1215-
let hash = Sha256::digest(content.as_bytes());
1216-
format!("{:x}", hash)[..32].to_string()
1217-
}
1218-
None => "no_user_query".to_string(),
1219-
}
1220-
}
12211265

12221266
/// Resolve agent name from comm string only (no /proc access).
12231267
/// Used for dead-PID drain where the process is already gone.
@@ -1597,75 +1641,6 @@ mod tests {
15971641
assert_eq!(GenAIBuilder::strip_user_query_prefix(text), "[not a timestamp] content");
15981642
}
15991643

1600-
#[test]
1601-
fn test_compute_session_id_deterministic() {
1602-
let req = LLMRequest {
1603-
messages: vec![InputMessage {
1604-
role: "user".to_string(),
1605-
parts: vec![MessagePart::Text { content: "hello".to_string() }],
1606-
name: None,
1607-
}],
1608-
temperature: None, max_tokens: None, frequency_penalty: None,
1609-
presence_penalty: None, top_p: None, top_k: None, seed: None,
1610-
stop_sequences: None, stream: false, tools: None, raw_body: None,
1611-
};
1612-
let id1 = GenAIBuilder::compute_session_id(&req);
1613-
let id2 = GenAIBuilder::compute_session_id(&req);
1614-
assert_eq!(id1, id2);
1615-
assert_eq!(id1.len(), 32);
1616-
}
1617-
1618-
#[test]
1619-
fn test_compute_session_id_no_user() {
1620-
let req = LLMRequest {
1621-
messages: vec![InputMessage {
1622-
role: "system".to_string(),
1623-
parts: vec![MessagePart::Text { content: "sys".to_string() }],
1624-
name: None,
1625-
}],
1626-
temperature: None, max_tokens: None, frequency_penalty: None,
1627-
presence_penalty: None, top_p: None, top_k: None, seed: None,
1628-
stop_sequences: None, stream: false, tools: None, raw_body: None,
1629-
};
1630-
let id = GenAIBuilder::compute_session_id(&req);
1631-
assert_eq!(id.len(), 32); // still produces 32-char hash of empty string
1632-
}
1633-
1634-
#[test]
1635-
fn test_compute_user_query_fingerprint() {
1636-
let req = LLMRequest {
1637-
messages: vec![
1638-
InputMessage {
1639-
role: "user".to_string(),
1640-
parts: vec![MessagePart::Text { content: "first".to_string() }],
1641-
name: None,
1642-
},
1643-
InputMessage {
1644-
role: "user".to_string(),
1645-
parts: vec![MessagePart::Text { content: "second".to_string() }],
1646-
name: None,
1647-
},
1648-
],
1649-
temperature: None, max_tokens: None, frequency_penalty: None,
1650-
presence_penalty: None, top_p: None, top_k: None, seed: None,
1651-
stop_sequences: None, stream: false, tools: None, raw_body: None,
1652-
};
1653-
let fp = GenAIBuilder::compute_user_query_fingerprint(&req);
1654-
assert_eq!(fp.len(), 32);
1655-
// fingerprint uses last user message
1656-
let req2 = LLMRequest {
1657-
messages: vec![InputMessage {
1658-
role: "user".to_string(),
1659-
parts: vec![MessagePart::Text { content: "second".to_string() }],
1660-
name: None,
1661-
}],
1662-
temperature: None, max_tokens: None, frequency_penalty: None,
1663-
presence_penalty: None, top_p: None, top_k: None, seed: None,
1664-
stop_sequences: None, stream: false, tools: None, raw_body: None,
1665-
};
1666-
assert_eq!(fp, GenAIBuilder::compute_user_query_fingerprint(&req2));
1667-
}
1668-
16691644
#[test]
16701645
fn test_extract_last_user_query() {
16711646
let req = LLMRequest {

0 commit comments

Comments
 (0)