1use anyhow::Result;
4use std::sync::atomic::Ordering;
5use tokio::sync::mpsc;
6
7use crate::event::AgentEvent;
8use crate::providers::{ContentBlock, MessageContent, Role, Usage};
9use crate::truncate::truncate_chars;
10
11use super::types::Agent;
12
13pub struct ContextInfo {
15 pub message_count: usize,
17 pub estimated_input_tokens: u64,
19 pub total_input_tokens: u64,
21 pub total_output_tokens: u64,
23 pub system_prompt_preview: String,
25 pub memory_summary: Option<String>,
27 pub project_overview_preview: Option<String>,
29 pub recent_messages_preview: Vec<String>,
31 pub model_name: String,
33 pub max_tokens: u32,
35}
36
37impl Agent {
38 pub fn get_context_info(&self) -> ContextInfo {
40 let estimated_tokens = self.messages.iter()
42 .map(|m| {
43 let content = match &m.content {
44 MessageContent::Text(t) => t.len(),
45 MessageContent::Blocks(blocks) => {
46 blocks.iter()
47 .filter_map(|b| {
48 if let ContentBlock::Text { text } = b {
49 Some(text.len())
50 } else {
51 None
52 }
53 })
54 .sum::<usize>()
55 }
56 };
57 (content / 3 + 50) as u64
59 })
60 .sum();
61
62 let system_preview = truncate_chars(&self.system_prompt, 500);
64
65 let project_preview = self.project_overview.as_ref()
67 .map(|o| truncate_chars(o, 300));
68
69 let recent_preview = self.messages.iter().rev().take(5).rev()
71 .map(|m| {
72 let role = match m.role {
73 Role::User => "User",
74 Role::Assistant => "Assistant",
75 Role::System => "System",
76 Role::Tool => "Tool",
77 };
78 let content_preview = match &m.content {
79 MessageContent::Text(t) => truncate_chars(t, 100),
80 MessageContent::Blocks(blocks) => {
81 let text = blocks.iter()
82 .filter_map(|b| {
83 if let ContentBlock::Text { text } = b {
84 Some(text.clone())
85 } else {
86 None
87 }
88 })
89 .collect::<Vec<_>>()
90 .join(" ");
91 truncate_chars(&text, 100)
92 }
93 };
94 format!("{}: {}", role, content_preview)
95 })
96 .collect();
97
98 ContextInfo {
99 message_count: self.messages.len(),
100 estimated_input_tokens: estimated_tokens,
101 total_input_tokens: self.total_input_tokens.load(Ordering::Relaxed),
102 total_output_tokens: self.total_output_tokens.load(Ordering::Relaxed),
103 system_prompt_preview: system_preview,
104 memory_summary: self.memory_summary.clone(),
105 project_overview_preview: project_preview,
106 recent_messages_preview: recent_preview,
107 model_name: self.model_name.clone(),
108 max_tokens: self.max_tokens,
109 }
110 }
111
112 pub fn get_full_context_preview(&self) -> String {
114 let mut preview = String::new();
115
116 preview.push_str("=== SYSTEM PROMPT ===\n");
118 preview.push_str(&self.system_prompt);
119 preview.push_str("\n\n");
120
121 if let Some(memory) = &self.memory_summary {
123 preview.push_str("=== MEMORY SUMMARY ===\n");
124 preview.push_str(memory);
125 preview.push_str("\n\n");
126 }
127
128 if let Some(overview) = &self.project_overview {
130 preview.push_str("=== PROJECT OVERVIEW ===\n");
131 preview.push_str(overview);
132 preview.push_str("\n\n");
133 }
134
135 preview.push_str("=== MESSAGES ===\n");
137 for (i, msg) in self.messages.iter().enumerate() {
138 let role = match msg.role {
139 Role::User => "User",
140 Role::Assistant => "Assistant",
141 Role::System => "System",
142 Role::Tool => "Tool",
143 };
144 preview.push_str(&format!("\n[{}] {}:\n", i + 1, role));
145
146 match &msg.content {
147 MessageContent::Text(t) => {
148 preview.push_str(t);
149 }
150 MessageContent::Blocks(blocks) => {
151 for block in blocks {
152 match block {
153 ContentBlock::Text { text } => {
154 preview.push_str(text);
155 preview.push_str("\n");
156 }
157 ContentBlock::ToolUse { name, input, .. } => {
158 preview.push_str(&format!("[Tool: {}]\n", name));
159 preview.push_str(&serde_json::to_string_pretty(input).unwrap_or_default());
160 preview.push_str("\n");
161 }
162 ContentBlock::ToolResult { tool_use_id, content } => {
163 preview.push_str(&format!("[Tool Result: {}]\n", tool_use_id));
164 preview.push_str(content);
165 preview.push_str("\n");
166 }
167 ContentBlock::Thinking { thinking, .. } => {
169 preview.push_str("[Thinking]\n");
170 preview.push_str(thinking);
171 preview.push_str("\n");
172 }
173 ContentBlock::ServerToolUse { name, .. } => {
174 preview.push_str(&format!("[Server Tool: {}]\n", name));
175 }
176 ContentBlock::ServerToolResult { tool_use_id, content, .. } => {
177 preview.push_str(&format!("[Server Tool Result: {}]\n", tool_use_id));
178 preview.push_str(content);
179 preview.push_str("\n");
180 }
181 _ => {
182 preview.push_str("[Other Content]\n");
183 }
184 }
185 }
186 }
187 }
188 }
189
190 preview
191 }
192 pub(crate) fn track_usage(&self, usage: &Usage) {
194 self.total_input_tokens
195 .fetch_add(usage.input_tokens as u64, Ordering::Relaxed);
196 self.total_output_tokens
197 .fetch_add(usage.output_tokens as u64, Ordering::Relaxed);
198 self.last_input_tokens
199 .store(usage.input_tokens as u64, Ordering::Relaxed);
200
201 crate::debug::debug_log().log(
202 "usage",
203 &format!(
204 "tracked: input_tokens={}, output_tokens={}, cache_read={}, cache_created={}",
205 usage.input_tokens,
206 usage.output_tokens,
207 usage.cache_read_input_tokens,
208 usage.cache_creation_input_tokens
209 ),
210 );
211
212 let _ = self.event_tx.try_send(AgentEvent::usage_with_cache(
213 self.total_input_tokens.load(Ordering::Relaxed),
214 usage.output_tokens as u64,
215 usage.cache_read_input_tokens as u64,
216 usage.cache_creation_input_tokens as u64,
217 ));
218 }
219
220 pub(crate) fn emit(&self, event: AgentEvent) -> Result<()> {
222 log::debug!("Agent emit: event_type={:?}", event.event_type);
223 match self.event_tx.try_send(event) {
224 Ok(_) => {
225 log::debug!("Agent emit: sent successfully");
226 Ok(())
227 }
228 Err(mpsc::error::TrySendError::Full(_)) => {
229 log::warn!("Agent emit: channel full, skipping event");
230 Ok(())
231 }
232 Err(mpsc::error::TrySendError::Closed(_)) => {
233 log::error!("Agent emit: channel closed");
234 Err(anyhow::anyhow!("Event channel closed"))
235 }
236 }
237 }
238
239 pub(crate) fn get_pending_todos_with_limit(
250 &self,
251 todo_reminder_count: &std::collections::HashMap<String, usize>,
252 max_reminders: usize,
253 ) -> (Vec<(String, String)>, bool) {
254 for msg in self.messages.iter().rev().take(10) {
256 if let MessageContent::Blocks(blocks) = &msg.content {
257 for block in blocks {
258 if let ContentBlock::ToolUse { name, input, .. } = block
259 && name == "todo_write"
260 {
261 if let Some(todos) = input.get("todos").and_then(|t| t.as_array()) {
263 let pending: Vec<(String, String)> = todos
264 .iter()
265 .filter_map(|todo| {
266 let status = todo.get("status").and_then(|s| s.as_str())?;
267 let content = todo.get("content").and_then(|c| c.as_str())?;
268 if status != "completed" {
269 Some((status.to_string(), content.to_string()))
270 } else {
271 None
272 }
273 })
274 .collect();
275
276 let mut filtered_pending = Vec::new();
278 let mut all_at_limit = true;
279
280 for (status, content) in pending {
281 let count = todo_reminder_count.get(&content).copied().unwrap_or(0);
282 if count < max_reminders {
283 filtered_pending.push((status, content));
284 all_at_limit = false;
285 }
286 }
287
288 return (filtered_pending, all_at_limit); }
290 }
291 }
292 }
293 }
294 (Vec::new(), true)
295 }
296
297 pub(crate) fn last_message_was_todo_reminder(&self) -> bool {
300 for msg in self.messages.iter().rev().take(3) {
302 if msg.role == Role::User {
303 if let MessageContent::Text(text) = &msg.content {
304 if text.contains("任务尚未完成") && text.contains("待办项需要处理") {
305 return true;
306 }
307 }
308 }
309 }
310 false
311 }
312
313 #[allow(dead_code)]
315 pub(crate) fn get_pending_todos(&self) -> Vec<(String, String)> {
316 for msg in self.messages.iter().rev().take(10) {
318 if let MessageContent::Blocks(blocks) = &msg.content {
319 for block in blocks {
320 if let ContentBlock::ToolUse { name, input, .. } = block
321 && name == "todo_write"
322 {
323 if let Some(todos) = input.get("todos").and_then(|t| t.as_array()) {
325 let pending: Vec<(String, String)> = todos
326 .iter()
327 .filter_map(|todo| {
328 let status = todo.get("status").and_then(|s| s.as_str())?;
329 let content = todo.get("content").and_then(|c| c.as_str())?;
330 if status != "completed" {
331 Some((status.to_string(), content.to_string()))
332 } else {
333 None
334 }
335 })
336 .collect();
337 return pending; }
339 }
340 }
341 }
342 }
343 Vec::new()
344 }
345}
346
347pub(crate) fn extract_tool_detail(tool_name: &str, input: &serde_json::Value) -> Option<String> {
349 match tool_name.to_lowercase().as_str() {
350 "read" => input
351 .get("path")
352 .and_then(|v| v.as_str())
353 .map(|s| truncate_str(s, 50)),
354 "write" => input
355 .get("path")
356 .and_then(|v| v.as_str())
357 .map(|s| truncate_str(s, 50)),
358 "edit" | "multi_edit" => {
359 let path = input.get("path").and_then(|v| v.as_str());
360 let old = input.get("old_string").and_then(|v| v.as_str());
361 match (path, old) {
362 (Some(p), Some(o)) => Some(format!(
363 "{}: \"{}\"",
364 truncate_str(p, 30),
365 truncate_str(o, 20)
366 )),
367 (Some(p), None) => Some(truncate_str(p, 50)),
368 _ => None,
369 }
370 }
371 "bash" => input
372 .get("command")
373 .and_then(|v| v.as_str())
374 .map(|s| truncate_str(s, 60)),
375 "search" | "grep" => input
376 .get("pattern")
377 .and_then(|v| v.as_str())
378 .map(|s| format!("\"{}\"", truncate_str(s, 30))),
379 "glob" => input
380 .get("pattern")
381 .and_then(|v| v.as_str())
382 .map(|s| truncate_str(s, 40)),
383 "ls" => input
384 .get("path")
385 .and_then(|v| v.as_str())
386 .map(|s| truncate_str(s, 50)),
387 "websearch" => input
388 .get("query")
389 .and_then(|v| v.as_str())
390 .map(|s| truncate_str(s, 40)),
391 "webfetch" => input
392 .get("url")
393 .and_then(|v| v.as_str())
394 .map(|s| truncate_str(s, 50)),
395 "task" => input
396 .get("description")
397 .and_then(|v| v.as_str())
398 .map(|s| truncate_str(s, 40)),
399 "task_create" => input
400 .get("description")
401 .and_then(|v| v.as_str())
402 .map(|s| truncate_str(s, 40)),
403 "task_get" | "task_stop" => input
404 .get("task_id")
405 .and_then(|v| v.as_str())
406 .map(|s| s.to_string()),
407 _ => None,
408 }
409}
410
411pub(crate) fn truncate_str(s: &str, max: usize) -> String {
413 truncate_chars(s, max)
414}