Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 24 additions & 24 deletions src/agents/hermes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ fn enable_context_engine_config(existing: &str) -> std::result::Result<String, S
.map(str::trim)
.unwrap_or_default();
match parse_yaml_scalar(current).as_deref() {
None | Some("compressor") | Some("tokensave") => {
None | Some("compressor" | "tokensave") => {
lines[engine_line] = " engine: tracedecay".to_string();
}
Some("tracedecay") => {}
Expand Down Expand Up @@ -3368,25 +3368,24 @@ def _translate_lcm_args(native_name: str, args: dict) -> dict:

_ENGINE_DEFAULT_SESSION = "__default__"

def _content_has_text(content) -> bool:
if isinstance(content, str):
return bool(content.strip())
if isinstance(content, list):
return any(_content_has_text(item) for item in content)
if isinstance(content, dict):
for key in ("text", "content"):
if _content_has_text(content.get(key)):
return True
return False

def _message_has_compressible_content(message) -> bool:
return isinstance(message, dict) and _content_has_text(message.get("content"))

def _has_compressible_message_content(messages) -> bool:
if not isinstance(messages, list):
return False

def _content_has_text(content) -> bool:
if isinstance(content, str):
return bool(content.strip())
if isinstance(content, list):
return any(_content_has_text(item) for item in content)
if isinstance(content, dict):
for key in ("text", "content"):
if _content_has_text(content.get(key)):
return True
return False

for message in messages:
if isinstance(message, dict) and _content_has_text(message.get("content")):
return True
return False
return any(_message_has_compressible_content(message) for message in messages)

class _EngineSessionState:
"""Mutable per-conversation engine state.
Expand Down Expand Up @@ -3718,14 +3717,15 @@ class TraceDecayContextEngine(ContextEngine):
def has_content_to_compress(self, messages, current_tokens=None, **kwargs):
"""Optional Hermes hook: conservatively probe only eligible content."""
del current_tokens, kwargs
if not _has_compressible_message_content(messages):
if not isinstance(messages, list):
return False
compressible = [
message
for message in messages
if isinstance(message, dict) and _has_compressible_message_content([message])
]
return len(compressible) > 1
compressible = 0
for message in messages:
if _message_has_compressible_content(message):
compressible += 1
if compressible > 1:
return True
return False

def should_defer_preflight_to_real_usage(self, rough_tokens=None, **kwargs):
"""Suppress rough-token retry loops until post-compression usage arrives."""
Expand Down
34 changes: 12 additions & 22 deletions src/mcp/tools/handlers/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,7 @@ fn lcm_compress_tool_json(project_root: Option<&Path>, value: &Value) -> ToolRes
if compact_text.len() <= MAX_RESPONSE_CHARS {
compact_text
} else {
let minimal = compact_lcm_compress_payload(value, formatted.len(), true, 8, 512);
let minimal_text = serde_json::to_string_pretty(&minimal).unwrap_or_default();
if minimal_text.len() <= MAX_RESPONSE_CHARS {
minimal_text
} else {
let floor = compact_lcm_compress_payload(value, formatted.len(), true, 1, 64);
bounded_lcm_contract_text(&floor)
}
truncated_json_envelope_with_handle(project_root, &formatted)
}
} else {
truncated_json_envelope_with_handle(project_root, &formatted)
Expand Down Expand Up @@ -215,20 +208,17 @@ fn compact_lcm_compress_payload(
compact_request.insert(key.to_string(), field.clone());
}
}
let (source_messages, source_truncated, source_compacted) = compact_replay_messages(
summary_request.get("source_messages"),
replay_limit,
replay_content_chars,
);
compact_request.insert("source_messages".to_string(), source_messages);
compact_request.insert(
"source_messages_truncated_for_mcp".to_string(),
json!(source_truncated),
);
compact_request.insert(
"source_messages_compacted_for_mcp".to_string(),
json!(source_compacted),
);
if let Some(field) = summary_request.get("source_messages") {
compact_request.insert("source_messages".to_string(), field.clone());
compact_request.insert(
"source_messages_truncated_for_mcp".to_string(),
json!(false),
);
compact_request.insert(
"source_messages_compacted_for_mcp".to_string(),
json!(false),
);
}
compact_request.insert("prompt_omitted_for_mcp".to_string(), json!(true));
compact_request.insert(
"extraction_request_omitted_for_mcp".to_string(),
Expand Down
36 changes: 24 additions & 12 deletions tests/mcp_handler_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5478,7 +5478,7 @@ async fn lcm_compress_oversized_needs_summary_preserves_bridge_contract() {
"provider": "cursor",
"session_id": "lcm-oversized-needs-summary",
"messages": [
{"id": "oversized-1", "role": "user", "content": huge_source},
{"id": "oversized-1", "role": "user", "content": huge_source.clone()},
{"id": "oversized-2", "role": "assistant", "content": "acknowledged"},
{"id": "oversized-3", "role": "user", "content": "latest objective"}
],
Expand All @@ -5495,7 +5495,26 @@ async fn lcm_compress_oversized_needs_summary_preserves_bridge_contract() {
.unwrap();

let text = extract_text(&compress.value);
let payload: Value = serde_json::from_str(text).unwrap();
let envelope: Value = serde_json::from_str(text).unwrap();
assert_eq!(envelope["truncated"], true);
assert_eq!(envelope["retrieve_tool"], "tracedecay_retrieve");
assert!(text.len() <= 15_000);

let handle = envelope["handle"]
.as_str()
.expect("oversized needs-summary payload should include retrieve handle");
let retrieved = handle_tool_call(
&cg,
"tracedecay_retrieve",
json!({ "handle": handle }),
None,
None,
)
.await
.unwrap();
let retrieved_payload: Value = serde_json::from_str(extract_text(&retrieved.value)).unwrap();
let payload: Value =
serde_json::from_str(retrieved_payload["content"].as_str().unwrap()).unwrap();
assert_eq!(payload["status"], "needs_summary");
assert_eq!(payload["reason"], "hermes_auxiliary_not_available");
assert!(
Expand All @@ -5508,20 +5527,13 @@ async fn lcm_compress_oversized_needs_summary_preserves_bridge_contract() {
payload["summary_request"].is_object(),
"bridge must retain summary request metadata, got {payload:#}"
);
assert_eq!(payload["mcp_response_truncated"], true);
assert_eq!(payload["contract_truncated"], true);
assert!(payload.get("truncated").is_none());
assert!(extract_text(&compress.value).len() <= 15_000);
let source_messages = payload["summary_request"]["source_messages"]
.as_array()
.expect("compact needs-summary payload should retain bounded source messages");
.expect("retrieved needs-summary payload should retain exact source messages");
assert!(!source_messages.is_empty());
assert!(source_messages[0]["content"]
.as_str()
.is_some_and(|content| content.len() <= 512));
assert_eq!(
payload["summary_request"]["source_messages_compacted_for_mcp"],
true
source_messages[0]["content"].as_str(),
Some(huge_source.as_str())
);
}

Expand Down
Loading