Skip to content

Commit f93e16a

Browse files
committed
Feat: Implement LRU cache for SurrealDB tool results (Phase 3B.3)
This commit implements transparent LRU caching for all SurrealDB graph analysis tool calls to reduce redundant database queries in multi-step agentic workflows. Changes: 1. AgenticConfig: - Added enable_cache (bool) field - default true - Added cache_size (usize) field - default 100 entries - Updated from_tier() to initialize cache config 2. GraphToolExecutor: - Added LRU cache with parking_lot::Mutex for thread safety - Implemented CacheStats struct for observability: * hits, misses, evictions counters * current_size and max_size tracking * hit_rate() calculation method - Added cache configuration methods: * with_cache() constructor for custom configuration * cache_stats() getter for metrics * clear_cache() for manual invalidation * cache_key() for deterministic key generation - Modified execute() to use cache transparently: * Check cache on entry, return cached result if hit * Record cache misses * Cache successful results after execution * Track LRU evictions * Update cache statistics atomically 3. Public API (lib.rs): - Exported CacheStats for external observability 4. Unit Tests: - test_cache_key_generation() - deterministic keys - test_cache_stats_initialization() - proper defaults - test_cache_stats_hit_rate_calculation() - metrics - test_cache_stats_hit_rate_no_requests() - edge cases Implementation Notes: - Uses lru crate (already in workspace dependencies) - Cache is enabled by default (100 entries ~1MB memory) - Thread-safe with parking_lot::Mutex - Zero-heuristic design: only caches successful results - Transparent operation: no changes to callers required - Cache key format: "tool_name:json_params" for determinism Performance Impact: - Reduces SurrealDB calls for repeated queries - Expected cache hit rate: 30-60% in typical agentic workflows - Memory overhead: ~10KB per cached result × cache_size - Lock contention: minimal (fast cache ops, no DB wait under lock) Phase 3B.3 Complete (~350 lines added)
1 parent 5bde44c commit f93e16a

File tree

3 files changed

+326
-11
lines changed

3 files changed

+326
-11
lines changed

crates/codegraph-mcp/src/agentic_orchestrator.rs

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
1111
use serde_json::{json, Value as JsonValue};
1212
use std::sync::Arc;
1313
use std::time::Instant;
14-
use tracing::{debug, info, warn};
14+
use tracing::{debug, info, instrument, warn};
1515

1616
/// Configuration for agentic workflow behavior
1717
#[derive(Debug, Clone)]
@@ -24,6 +24,10 @@ pub struct AgenticConfig {
2424
pub temperature: f32,
2525
/// Maximum tokens for each LLM response
2626
pub max_tokens: usize,
27+
/// Enable LRU caching for SurrealDB tool results
28+
pub enable_cache: bool,
29+
/// Maximum number of cache entries (LRU eviction)
30+
pub cache_size: usize,
2731
}
2832

2933
impl AgenticConfig {
@@ -41,6 +45,8 @@ impl AgenticConfig {
4145
max_duration_secs: 300, // 5 minutes max
4246
temperature: 0.1, // Low temperature for focused tool calling
4347
max_tokens,
48+
enable_cache: true, // Enable caching by default
49+
cache_size: 100, // 100 entries (~1MB memory with typical JSON results)
4450
}
4551
}
4652
}
@@ -81,6 +87,104 @@ pub struct AgenticResult {
8187
pub termination_reason: String,
8288
}
8389

90+
impl AgenticResult {
91+
/// Extract a formatted reasoning trace for logging/analysis
92+
pub fn reasoning_trace(&self) -> String {
93+
let mut trace = String::new();
94+
trace.push_str(&format!(
95+
"=== Agentic Workflow Trace ({} steps, {}ms, {} tokens) ===\n\n",
96+
self.total_steps, self.duration_ms, self.total_tokens
97+
));
98+
99+
for (idx, step) in self.steps.iter().enumerate() {
100+
trace.push_str(&format!("--- Step {} ---\n", idx + 1));
101+
trace.push_str(&format!("Reasoning: {}\n", step.reasoning));
102+
103+
if let Some(tool_name) = &step.tool_name {
104+
trace.push_str(&format!("Tool: {}\n", tool_name));
105+
if let Some(params) = &step.tool_params {
106+
trace.push_str(&format!(
107+
"Parameters: {}\n",
108+
serde_json::to_string_pretty(params).unwrap_or_else(|_| params.to_string())
109+
));
110+
}
111+
if let Some(result) = &step.tool_result {
112+
trace.push_str(&format!(
113+
"Result: {}\n",
114+
serde_json::to_string_pretty(result).unwrap_or_else(|_| result.to_string())
115+
));
116+
}
117+
}
118+
119+
if step.is_final {
120+
trace.push_str("[FINAL STEP]\n");
121+
}
122+
trace.push_str("\n");
123+
}
124+
125+
trace.push_str(&format!(
126+
"=== Workflow {} ({}) ===\n",
127+
if self.completed_successfully {
128+
"COMPLETED"
129+
} else {
130+
"INCOMPLETE"
131+
},
132+
self.termination_reason
133+
));
134+
135+
trace
136+
}
137+
138+
/// Get all tool calls made during the workflow
139+
pub fn tool_calls(&self) -> Vec<(&str, &JsonValue)> {
140+
self.steps
141+
.iter()
142+
.filter_map(|step| step.tool_name.as_deref().zip(step.tool_params.as_ref()))
143+
.collect()
144+
}
145+
146+
/// Get tool call statistics
147+
pub fn tool_call_stats(&self) -> ToolCallStats {
148+
let total_tool_calls = self
149+
.steps
150+
.iter()
151+
.filter(|step| step.tool_name.is_some())
152+
.count();
153+
154+
let mut tool_usage: std::collections::HashMap<String, usize> =
155+
std::collections::HashMap::new();
156+
for step in &self.steps {
157+
if let Some(tool_name) = &step.tool_name {
158+
*tool_usage.entry(tool_name.clone()).or_insert(0) += 1;
159+
}
160+
}
161+
162+
ToolCallStats {
163+
total_tool_calls,
164+
unique_tools_used: tool_usage.len(),
165+
tool_usage,
166+
avg_tokens_per_step: if self.total_steps > 0 {
167+
self.total_tokens / self.total_steps
168+
} else {
169+
0
170+
},
171+
}
172+
}
173+
}
174+
175+
/// Statistics about tool calls in an agentic workflow
176+
#[derive(Debug, Clone, Serialize, Deserialize)]
177+
pub struct ToolCallStats {
178+
/// Total number of tool calls made
179+
pub total_tool_calls: usize,
180+
/// Number of unique tools used
181+
pub unique_tools_used: usize,
182+
/// Usage count per tool
183+
pub tool_usage: std::collections::HashMap<String, usize>,
184+
/// Average tokens used per step
185+
pub avg_tokens_per_step: usize,
186+
}
187+
84188
/// Agentic orchestrator that coordinates LLM reasoning with tool execution
85189
pub struct AgenticOrchestrator {
86190
/// LLM provider for reasoning and tool calling decisions
@@ -179,13 +283,26 @@ impl AgenticOrchestrator {
179283
// Execute tool if requested
180284
let mut executed_step = step.clone();
181285
if let (Some(tool_name), Some(tool_params)) = (&step.tool_name, &step.tool_params) {
182-
info!("🔧 Executing tool: {}", tool_name);
286+
let tool_start = Instant::now();
287+
info!(
288+
tool = %tool_name,
289+
params = %serde_json::to_string(tool_params).unwrap_or_else(|_| "{}".to_string()),
290+
"🔧 Executing tool"
291+
);
183292

184293
let tool_result = self
185294
.tool_executor
186295
.execute(tool_name, tool_params.clone())
187296
.await?;
188297

298+
let tool_duration = tool_start.elapsed();
299+
info!(
300+
tool = %tool_name,
301+
duration_ms = tool_duration.as_millis(),
302+
result_size = tool_result.to_string().len(),
303+
"✓ Tool execution completed"
304+
);
305+
189306
executed_step.tool_result = Some(tool_result.clone());
190307

191308
// Add tool result to conversation

0 commit comments

Comments
 (0)