Replace ContextWindowWarning with generic Warning event

Introduce a reusable Warning { kind, message, details } variant in
AgentEvent so non-fatal warnings (context window usage, deprecation,
etc.) share a single event shape. The context_window warning preserves
all original fields inside the JSON details object.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-23 13:48:26 -04:00
parent dd75880cdd
commit 2a7aea5950
No known key found for this signature in database
8 changed files with 36 additions and 35 deletions

View file

@ -253,7 +253,7 @@ All agent events include `node_id` and `node_label`.
| `Agent.ToolCallOutputDelta` | `delta` |
| `Agent.ToolCallCompleted` | `tool_name`, `tool_call_id`, `output`, `is_error` |
| `Agent.Error` | `error` |
| `Agent.ContextWindowWarning` | `estimated_tokens`, `context_window_size`, `usage_percent` |
| `Agent.Warning` | `kind`, `message`, `details` |
| `Agent.LoopDetected` | _(none)_ |
| `Agent.TurnLimitReached` | `max_turns` |
| `Agent.SkillExpanded` | `skill_name` |

View file

@ -240,6 +240,6 @@ Compaction emits three events to the [event stream](/execution/observability#eve
| Event | When | Key fields |
|---|---|---|
| `Agent.ContextWindowWarning` | Token estimate exceeds 80% of context window | `estimated_tokens`, `context_window_size`, `usage_percent` |
| `Agent.Warning` (kind: `context_window`) | Token estimate exceeds 80% of context window | `kind`, `message`, `details` (contains `estimated_tokens`, `context_window_size`, `usage_percent`) |
| `Agent.CompactionStarted` | Compaction begins | `estimated_tokens`, `context_window_size` |
| `Agent.CompactionCompleted` | Summary generated and history replaced | `original_turn_count`, `preserved_turn_count`, `summary_token_estimate`, `tracked_file_count` |

View file

@ -45,7 +45,7 @@ Events fall into several categories:
| `Agent.Error` | `stage`, `error` | Agent-level error |
| `Agent.LoopDetected` | `stage` | Repeated tool call pattern detected |
| `Agent.SteeringInjected` | `stage`, `text` | Human steering message injected |
| `Agent.ContextWindowWarning` | `stage`, `estimated_tokens`, `context_window_size`, `usage_percent` | Token usage exceeds context window threshold |
| `Agent.Warning` | `stage`, `kind`, `message`, `details` | Non-fatal warning (e.g. context window usage) |
| `Agent.CompactionStarted` | `stage`, `estimated_tokens`, `context_window_size` | Context compaction triggered |
| `Agent.CompactionCompleted` | `stage`, `original_turn_count`, `preserved_turn_count`, `summary_token_estimate`, `tracked_file_count` | Context compaction finished |
| `Agent.LlmRetry` | `stage`, `provider`, `model`, `attempt`, `delay_secs` | LLM API call retried |

View file

@ -227,7 +227,7 @@ profile.register_subagent_tools(manager, factory, 0);
## Safety Features
- **Loop detection** -- Detects repeating tool call patterns (period 1, 2, or 3) and injects a steering warning
- **Context window monitoring** -- Emits `ContextWindowWarning` events when estimated usage exceeds 80%
- **Context window monitoring** -- Emits `Warning` events (kind `"context_window"`) when estimated usage exceeds 80%
- **Tool argument validation** -- Validates arguments against JSON Schema before execution
- **Tool output truncation** -- Per-tool character and line limits with head/tail or tail-only truncation modes
- **Environment variable filtering** -- `LocalSandbox` strips secrets (`*_API_KEY`, `*_SECRET`, `*_TOKEN`, `*_PASSWORD`, `*_CREDENTIAL`) from subprocess environments

View file

@ -9,7 +9,7 @@ use fabro_llm::types::{Message, Request};
use tracing::debug;
/// Check whether the context window usage exceeds the configured threshold.
/// Emits a `ContextWindowWarning` event when over the threshold.
/// Emits a `Warning` event with kind `"context_window"` when over the threshold.
/// Returns `true` if the threshold is exceeded.
pub fn check_context_usage(
system_prompt: &str,
@ -26,10 +26,17 @@ pub fn check_context_usage(
if estimated_tokens > threshold {
emitter.emit(
session_id.to_owned(),
AgentEvent::ContextWindowWarning {
estimated_tokens,
context_window_size: context_window,
usage_percent: estimated_tokens * 100 / context_window,
AgentEvent::Warning {
kind: "context_window".into(),
message: format!(
"Context window usage: {}%",
estimated_tokens * 100 / context_window
),
details: serde_json::json!({
"estimated_tokens": estimated_tokens,
"context_window_size": context_window,
"usage_percent": estimated_tokens * 100 / context_window,
}),
},
);
true
@ -345,11 +352,8 @@ mod tests {
let over = check_context_usage("prompt", &history, &profile, 80, &emitter, "sess");
assert!(over);
// Should have emitted a ContextWindowWarning
// Should have emitted a Warning
let event = rx.try_recv().unwrap();
assert!(matches!(
event.event,
AgentEvent::ContextWindowWarning { .. }
));
assert!(matches!(event.event, AgentEvent::Warning { .. }));
}
}

View file

@ -1608,13 +1608,9 @@ mod tests {
let mut found_warning = false;
while let Ok(event) = rx.try_recv() {
if let AgentEvent::ContextWindowWarning {
context_window_size,
..
} = &event.event
{
if let AgentEvent::Warning { details, .. } = &event.event {
found_warning = true;
assert_eq!(*context_window_size, 100);
assert_eq!(details["context_window_size"], 100);
}
}
assert!(found_warning);
@ -1660,7 +1656,7 @@ mod tests {
let mut found_warning = false;
while let Ok(event) = rx.try_recv() {
if matches!(event.event, AgentEvent::ContextWindowWarning { .. }) {
if matches!(event.event, AgentEvent::Warning { .. }) {
found_warning = true;
}
}

View file

@ -130,10 +130,10 @@ pub enum AgentEvent {
Error {
error: crate::error::AgentError,
},
ContextWindowWarning {
estimated_tokens: usize,
context_window_size: usize,
usage_percent: usize,
Warning {
kind: String,
message: String,
details: serde_json::Value,
},
LoopDetected,
TurnLimitReached {
@ -264,17 +264,12 @@ impl AgentEvent {
Self::Error { error } => {
error!(session_id, error = %error, "Agent error");
}
Self::ContextWindowWarning {
estimated_tokens,
context_window_size,
usage_percent,
} => {
Self::Warning { kind, message, .. } => {
warn!(
session_id,
estimated_tokens,
context_window_size,
usage_percent,
"Context window usage high"
kind = kind.as_str(),
message = message.as_str(),
"Warning"
);
}
Self::LoopDetected => {

View file

@ -1380,7 +1380,13 @@ impl ProgressUI {
}
self.on_tool_call_completed(stage_node_id, tool_call_id, *is_error);
}
AgentEvent::ContextWindowWarning { usage_percent, .. } if self.verbose => {
AgentEvent::Warning { kind, details, .. }
if kind == "context_window" && self.verbose =>
{
let usage_percent = details
.get("usage_percent")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let yellow = Style::new().yellow();
self.insert_info_line_for_stage(
stage_node_id,