Skip to content

Commit af1310c

Browse files
committed
feat: Add progress reporting and context overflow protection for agentic tools
- Add per-step progress notifications via MCP protocol during agent execution - Thread ProgressCallback through Builder → Executor → Factory → Server chain - Emit "Step N: Calling tool_name" notifications for user visibility - Add per-tool result truncation in GraphToolExecutor (max_result_bytes) - Intelligently truncates large results with _truncated metadata for agent awareness - Add context accumulation guard in CodeGraphChatAdapter (max_context_bytes) - Fail fast with clear error when accumulated context exceeds safe threshold - Reduce tier-based max steps from 5/10/15/20 to 3/5/6/8 - Add hard cap at 8 steps (10 with env override) to prevent runaway costs - Fix env var: check CODEGRAPH_CONTEXT_WINDOW before CODEGRAPH_LLM_CONTEXT_WINDOW - Update README with new step limits table and context overflow protection docs
1 parent 8d136a6 commit af1310c

9 files changed

Lines changed: 485 additions & 40 deletions

File tree

README.md

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,24 +104,52 @@ Running a small local model? Get focused, efficient queries.
104104

105105
Using GPT-5.1 or Claude with 200K context? Get comprehensive, exploratory analysis.
106106

107-
Using grok-4-1-fast-reasoning with 2M context? Get incredibly comprehensive up-to 40 turns spanning in-depth analyses.
107+
Using grok-4-1-fast-reasoning with 2M context? Get detailed analysis with intelligent result management.
108108

109109
The Agent only uses the amount of steps that it requires to produce the answer so tool execution times vary based on the query and amount of data indexed in the database.
110110

111-
During development the agent used 3-10 steps on average to produce answers for test scenarios.
111+
During development the agent used 3-6 steps on average to produce answers for test scenarios.
112112

113113
The Agent is stateless it only has conversational memory for the span of tool execution it does not accumulate context/memory over multiple chained tool calls this is already handled by your client of choice, it accumulates that context so codegraph needs to just provide answers.
114114

115115
| Your Model | CodeGraph's Behavior |
116116
|------------|---------------------|
117-
| < 50K tokens | Terse prompts, max 5 steps |
118-
| 50K-150K | Balanced analysis, max 10 steps |
119-
| 150K-500K | Detailed exploration, max 15 steps |
120-
| > 500K (Grok, etc.) | Full monty, max 20 steps |
117+
| < 50K tokens | Terse prompts, max 3 steps |
118+
| 50K-150K | Balanced analysis, max 5 steps |
119+
| 150K-500K | Detailed exploration, max 6 steps |
120+
| > 500K (Grok, etc.) | Comprehensive analysis, max 8 steps |
121+
122+
**Hard cap:** Maximum 8 steps regardless of tier (10 with env override). This prevents runaway costs and context overflow while still allowing thorough analysis.
121123

122124
**Same tool, automatically optimized for your setup.**
123125

124-
### 4. Hybrid Search That Actually Works
126+
### 4. Context Overflow Protection
127+
128+
CodeGraph includes multi-layer protection against context overflow—preventing expensive failures when tool results exceed your model's limits.
129+
130+
**Per-Tool Result Truncation:**
131+
- Each tool result is limited based on your configured context window
132+
- Large results (e.g., dependency trees with 1000+ nodes) are intelligently truncated
133+
- Truncated results include `_truncated: true` metadata so the agent knows data was cut
134+
- Array results keep the most relevant items that fit within limits
135+
136+
**Context Accumulation Guard:**
137+
- Monitors total accumulated context across multi-step reasoning
138+
- Fails fast with clear error message if accumulated tool results exceed safe threshold
139+
- Threshold: 80% of context window × 4 (conservative estimate for token overhead)
140+
141+
**Configure via environment:**
142+
```bash
143+
# CRITICAL: Set this to match your agent's LLM context window
144+
CODEGRAPH_CONTEXT_WINDOW=128000 # Default: 128K
145+
146+
# Per-tool result limit derived automatically: context_window × 2 bytes
147+
# Accumulation limit derived automatically: context_window × 4 × 0.8 bytes
148+
```
149+
150+
**Why this matters:** Without these guards, a single `agentic_dependency_analysis` on a large codebase could return 6M+ tokens—far exceeding most models' limits and causing expensive failures.
151+
152+
### 5. Hybrid Search That Actually Works
125153

126154
We don't pick sides in the "embeddings vs keywords" debate. CodeGraph combines:
127155

crates/codegraph-mcp-autoagents/src/autoagents/agent_builder.rs

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use autoagents::llm::{FunctionCall, ToolCall};
1515
use codegraph_ai::llm_provider::{LLMProvider as CodeGraphLLM, Message, MessageRole};
1616
use codegraph_mcp_core::debug_logger::DebugLogger;
1717
use serde::Deserialize;
18-
use std::sync::atomic::{AtomicU64, Ordering};
18+
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
1919
use std::sync::Arc;
2020

2121
/// Convert CodeGraph Message to AutoAgents ChatMessage
@@ -50,15 +50,76 @@ pub(crate) fn read_memory_window_config() -> usize {
5050
.unwrap_or(DEFAULT_MEMORY_WINDOW)
5151
}
5252

53+
use crate::autoagents::progress_notifier::ProgressCallback;
54+
5355
/// Adapter that bridges codegraph_ai::LLMProvider to AutoAgents ChatProvider
5456
pub struct CodeGraphChatAdapter {
5557
provider: Arc<dyn CodeGraphLLM>,
5658
tier: ContextTier,
59+
progress_callback: Option<ProgressCallback>,
60+
step_counter: Arc<AtomicUsize>,
61+
/// Maximum context size in bytes (derived from CODEGRAPH_CONTEXT_WINDOW)
62+
/// Used as safety valve to prevent accumulated tool results from exceeding model limits
63+
max_context_bytes: usize,
5764
}
5865

5966
impl CodeGraphChatAdapter {
67+
/// Calculate max context bytes from environment or tier
68+
/// Uses ~80% of context window (reserves 20% for response) at ~4 bytes/token
69+
fn calculate_max_context_bytes(tier: ContextTier) -> usize {
70+
// Check env var first
71+
if let Ok(context_window) = std::env::var("CODEGRAPH_CONTEXT_WINDOW")
72+
.ok()
73+
.and_then(|v| v.parse::<usize>().ok())
74+
.ok_or(())
75+
{
76+
// 80% of context window * 4 bytes/token
77+
return context_window * 4 * 8 / 10;
78+
}
79+
80+
// Fall back to tier-based defaults (tokens * 4 bytes * 80%)
81+
match tier {
82+
ContextTier::Small => 50_000 * 4 * 8 / 10, // ~160KB
83+
ContextTier::Medium => 128_000 * 4 * 8 / 10, // ~410KB
84+
ContextTier::Large => 200_000 * 4 * 8 / 10, // ~640KB
85+
ContextTier::Massive => 2_000_000 * 4 * 8 / 10, // ~6.4MB
86+
}
87+
}
88+
6089
pub fn new(provider: Arc<dyn CodeGraphLLM>, tier: ContextTier) -> Self {
61-
Self { provider, tier }
90+
let max_context_bytes = Self::calculate_max_context_bytes(tier);
91+
tracing::debug!(
92+
"CodeGraphChatAdapter initialized with max_context_bytes: {} ({:.1}MB)",
93+
max_context_bytes,
94+
max_context_bytes as f64 / 1_000_000.0
95+
);
96+
Self {
97+
provider,
98+
tier,
99+
progress_callback: None,
100+
step_counter: Arc::new(AtomicUsize::new(1)),
101+
max_context_bytes,
102+
}
103+
}
104+
105+
pub fn with_progress_callback(
106+
provider: Arc<dyn CodeGraphLLM>,
107+
tier: ContextTier,
108+
callback: ProgressCallback,
109+
) -> Self {
110+
let max_context_bytes = Self::calculate_max_context_bytes(tier);
111+
tracing::debug!(
112+
"CodeGraphChatAdapter initialized with max_context_bytes: {} ({:.1}MB)",
113+
max_context_bytes,
114+
max_context_bytes as f64 / 1_000_000.0
115+
);
116+
Self {
117+
provider,
118+
tier,
119+
progress_callback: Some(callback),
120+
step_counter: Arc::new(AtomicUsize::new(1)),
121+
max_context_bytes,
122+
}
62123
}
63124

64125
/// Convert AutoAgents Tool to CodeGraph ToolDefinition
@@ -165,6 +226,46 @@ impl ChatProvider for CodeGraphChatAdapter {
165226
})
166227
.collect();
167228

229+
// Safety valve: Check accumulated context size before sending to LLM
230+
let total_context_bytes: usize = cg_messages.iter().map(|m| m.content.len()).sum();
231+
if total_context_bytes > self.max_context_bytes {
232+
let overflow_ratio = total_context_bytes as f64 / self.max_context_bytes as f64;
233+
tracing::error!(
234+
total_bytes = total_context_bytes,
235+
max_bytes = self.max_context_bytes,
236+
overflow_ratio = format!("{:.1}x", overflow_ratio),
237+
message_count = cg_messages.len(),
238+
"CONTEXT OVERFLOW: Accumulated messages exceed max_context_bytes limit"
239+
);
240+
241+
// Return error instead of letting the API reject with a cryptic message
242+
return Err(LLMError::Generic(format!(
243+
"Context overflow: {} bytes exceeds {} byte limit ({:.1}x). \
244+
Tool results accumulated too much data. Try reducing result limits or query scope.",
245+
total_context_bytes,
246+
self.max_context_bytes,
247+
overflow_ratio
248+
)));
249+
}
250+
251+
// Log context usage for monitoring
252+
let usage_percent = (total_context_bytes as f64 / self.max_context_bytes as f64) * 100.0;
253+
if usage_percent > 70.0 {
254+
tracing::warn!(
255+
total_bytes = total_context_bytes,
256+
max_bytes = self.max_context_bytes,
257+
usage_percent = format!("{:.1}%", usage_percent),
258+
"Context usage above 70% - approaching limit"
259+
);
260+
} else {
261+
tracing::debug!(
262+
total_bytes = total_context_bytes,
263+
max_bytes = self.max_context_bytes,
264+
usage_percent = format!("{:.1}%", usage_percent),
265+
"Context size within limits"
266+
);
267+
}
268+
168269
// Convert AutoAgents tools to CodeGraph ToolDefinitions
169270
let cg_tools = tools.map(Self::convert_tools);
170271

@@ -212,6 +313,32 @@ impl ChatProvider for CodeGraphChatAdapter {
212313
response.finish_reason
213314
);
214315

316+
// Emit step progress notification if callback is configured
317+
if let Some(ref callback) = self.progress_callback {
318+
let step = self.step_counter.fetch_add(1, Ordering::SeqCst);
319+
let tool_names = response
320+
.tool_calls
321+
.as_ref()
322+
.map(|tc| {
323+
tc.iter()
324+
.map(|t| t.function.name.as_str())
325+
.collect::<Vec<_>>()
326+
.join(", ")
327+
})
328+
.filter(|s| !s.is_empty());
329+
330+
let message = match tool_names {
331+
Some(names) => format!("Step {}: Calling {}", step, names),
332+
None => format!("Step {}: Agent reasoning...", step),
333+
};
334+
335+
// Fire-and-forget progress notification (non-blocking)
336+
let cb = callback.clone();
337+
tokio::spawn(async move {
338+
cb(step as f64, Some(message)).await;
339+
});
340+
}
341+
215342
// Wrap response in AutoAgents ChatResponse with native tool calls
216343
Ok(Box::new(CodeGraphChatResponse {
217344
content: response.content,
@@ -675,6 +802,26 @@ impl CodeGraphAgentBuilder {
675802
}
676803
}
677804

805+
/// Create builder with progress callback for step-by-step notifications
806+
pub fn with_progress_callback(
807+
llm_provider: Arc<dyn codegraph_ai::llm_provider::LLMProvider>,
808+
tool_executor: Arc<GraphToolExecutor>,
809+
tier: ContextTier,
810+
analysis_type: AnalysisType,
811+
callback: ProgressCallback,
812+
) -> Self {
813+
Self {
814+
llm_adapter: Arc::new(CodeGraphChatAdapter::with_progress_callback(
815+
llm_provider,
816+
tier,
817+
callback,
818+
)),
819+
tool_factory: GraphToolFactory::new(tool_executor),
820+
tier,
821+
analysis_type,
822+
}
823+
}
824+
678825
pub async fn build(self) -> Result<AgentHandle, AutoAgentsError> {
679826
// Get tier-aware configuration and system prompt
680827
let tier_plugin = TierAwarePromptPlugin::new(self.analysis_type, self.tier);

crates/codegraph-mcp-autoagents/src/autoagents/executor.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// ABOUTME: Orchestrates architecture detection, factory-based executor creation, and delegation
33

44
use crate::autoagents::codegraph_agent::CodeGraphAgentOutput;
5+
use crate::autoagents::progress_notifier::ProgressCallback;
56
use crate::autoagents::startup_context::{build_startup_context, StartupContextRender};
67
use codegraph_ai::llm_provider::LLMProvider;
78
use codegraph_graph::{GraphFunctions, HubNode};
@@ -116,11 +117,37 @@ impl CodeGraphExecutor {
116117
llm_provider: Arc<dyn LLMProvider>,
117118
tool_executor: Arc<GraphToolExecutor>,
118119
config: Arc<codegraph_mcp_core::config_manager::CodeGraphConfig>,
120+
) -> Self {
121+
Self::with_optional_progress_callback(llm_provider, tool_executor, config, None)
122+
}
123+
124+
pub fn with_progress_callback(
125+
llm_provider: Arc<dyn LLMProvider>,
126+
tool_executor: Arc<GraphToolExecutor>,
127+
config: Arc<codegraph_mcp_core::config_manager::CodeGraphConfig>,
128+
callback: ProgressCallback,
129+
) -> Self {
130+
Self::with_optional_progress_callback(llm_provider, tool_executor, config, Some(callback))
131+
}
132+
133+
fn with_optional_progress_callback(
134+
llm_provider: Arc<dyn LLMProvider>,
135+
tool_executor: Arc<GraphToolExecutor>,
136+
config: Arc<codegraph_mcp_core::config_manager::CodeGraphConfig>,
137+
progress_callback: Option<ProgressCallback>,
119138
) -> Self {
120139
use crate::autoagents::executor_factory::AgentExecutorFactory;
121140

122141
// Create factory for architecture-specific executors
123-
let factory = AgentExecutorFactory::new(llm_provider, tool_executor, config.clone());
142+
let factory = match progress_callback {
143+
Some(callback) => AgentExecutorFactory::with_progress_callback(
144+
llm_provider,
145+
tool_executor,
146+
config.clone(),
147+
callback,
148+
),
149+
None => AgentExecutorFactory::new(llm_provider, tool_executor, config.clone()),
150+
};
124151

125152
// Detect architecture from environment or config
126153
let architecture = AgentExecutorFactory::detect_architecture();
@@ -293,6 +320,7 @@ pub struct CodeGraphExecutorBuilder {
293320
llm_provider: Option<Arc<dyn LLMProvider>>,
294321
tool_executor: Option<Arc<GraphToolExecutor>>,
295322
config: Option<Arc<codegraph_mcp_core::config_manager::CodeGraphConfig>>,
323+
progress_callback: Option<ProgressCallback>,
296324
}
297325

298326
impl CodeGraphExecutorBuilder {
@@ -301,6 +329,7 @@ impl CodeGraphExecutorBuilder {
301329
llm_provider: None,
302330
tool_executor: None,
303331
config: None,
332+
progress_callback: None,
304333
}
305334
}
306335

@@ -322,6 +351,12 @@ impl CodeGraphExecutorBuilder {
322351
self
323352
}
324353

354+
/// Set progress callback for step-by-step notifications
355+
pub fn progress_callback(mut self, callback: ProgressCallback) -> Self {
356+
self.progress_callback = Some(callback);
357+
self
358+
}
359+
325360
pub fn build(self) -> Result<CodeGraphExecutor, ExecutorError> {
326361
let llm_provider = self
327362
.llm_provider
@@ -337,7 +372,15 @@ impl CodeGraphExecutorBuilder {
337372
Arc::new(codegraph_mcp_core::config_manager::CodeGraphConfig::default())
338373
});
339374

340-
Ok(CodeGraphExecutor::new(llm_provider, tool_executor, config))
375+
// Create executor with or without progress callback
376+
let executor = match self.progress_callback {
377+
Some(callback) => {
378+
CodeGraphExecutor::with_progress_callback(llm_provider, tool_executor, config, callback)
379+
}
380+
None => CodeGraphExecutor::new(llm_provider, tool_executor, config),
381+
};
382+
383+
Ok(executor)
341384
}
342385
}
343386

0 commit comments

Comments
 (0)