use super::{Tool, ToolResult};
use anyhow::Result;
use async_trait::async_trait;
use serde_json::{Value, json};
pub const RESET_MARKER_PREFIX: &str = "[CONTEXT RESET]";
pub fn format_reset_marker(summary: &str, bytes: usize) -> String {
format!(
"{RESET_MARKER_PREFIX}\n\
The agent requested a Lu et al. (arXiv:2510.06727) context \
reset. Everything older than this point should be treated as \
summarised by the text below. The canonical transcript is \
preserved on disk; call `session_recall` for dropped \
details.\n\
\n\
bytes={bytes}\n\
\n\
{summary}"
)
}
pub struct ContextResetTool;
#[async_trait]
impl Tool for ContextResetTool {
fn id(&self) -> &str {
"context_reset"
}
fn name(&self) -> &str {
"ContextReset"
}
fn description(&self) -> &str {
"REQUEST A CONTEXT RESET (Lu et al., arXiv:2510.06727). Call this \
when the active transcript is approaching the model's context \
window and you want to compress it in your own words before \
continuing. Pass a `summary` string distilling the task state, \
decisions, open questions, and any must-remember details. The \
tool records the reset and returns a `[CONTEXT RESET]` marker \
that will become the anchor for the next turn. The canonical \
chat history is preserved on disk — call `session_recall` if \
you later need a specific detail you dropped."
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "Compressed task state in your own words: \
goal, key decisions, open questions, \
must-remember details. This replaces \
the earlier transcript as the anchor \
for subsequent turns."
}
},
"required": ["summary"]
})
}
async fn execute(&self, args: Value) -> Result<ToolResult> {
let summary = match args["summary"].as_str() {
Some(s) if !s.trim().is_empty() => s.trim().to_string(),
_ => {
return Ok(ToolResult::error(
"`summary` is required and must be non-empty",
));
}
};
let bytes = summary.len();
tracing::info!(bytes, "context_reset: agent-authored summary accepted");
Ok(ToolResult::success(format_reset_marker(&summary, bytes)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn execute_requires_non_empty_summary() {
let tool = ContextResetTool;
let result = tool.execute(json!({"summary": ""})).await.unwrap();
assert!(!result.success);
let result = tool.execute(json!({"summary": " "})).await.unwrap();
assert!(!result.success);
}
#[tokio::test]
async fn execute_wraps_summary_with_marker() {
let tool = ContextResetTool;
let result = tool
.execute(json!({"summary": "goal: ship it; open: review"}))
.await
.unwrap();
assert!(result.success);
assert!(result.output.starts_with(RESET_MARKER_PREFIX));
assert!(result.output.contains("goal: ship it"));
}
#[test]
fn format_reset_marker_has_stable_prefix_and_body() {
let body = format_reset_marker("hello", 5);
assert!(body.starts_with(RESET_MARKER_PREFIX));
assert!(body.contains("hello"));
assert!(body.contains("bytes=5"));
assert!(body.contains("session_recall"));
}
#[test]
fn marker_prefix_is_the_documented_constant() {
assert_eq!(RESET_MARKER_PREFIX, "[CONTEXT RESET]");
}
}