codetether_agent/tool/
context_reset.rs1use super::{Tool, ToolResult};
49use anyhow::Result;
50use async_trait::async_trait;
51use serde_json::{Value, json};
52
53#[path = "context_reset_audit.rs"]
54mod audit;
55
56pub const RESET_MARKER_PREFIX: &str = "[CONTEXT RESET]";
60
61pub fn format_reset_marker(summary: &str, bytes: usize) -> String {
66 format!(
67 "{RESET_MARKER_PREFIX}\n\
68 The agent requested a Lu et al. (arXiv:2510.06727) context \
69 reset. Everything older than this point should be treated as \
70 summarised by the text below. The canonical transcript is \
71 preserved on disk; call `session_recall` for dropped \
72 details.\n\
73 \n\
74 bytes={bytes}\n\
75 \n\
76 {summary}"
77 )
78}
79
80pub struct ContextResetTool;
82
83#[async_trait]
84impl Tool for ContextResetTool {
85 fn id(&self) -> &str {
86 "context_reset"
87 }
88
89 fn name(&self) -> &str {
90 "ContextReset"
91 }
92
93 fn description(&self) -> &str {
94 "REQUEST A CONTEXT RESET (Lu et al., arXiv:2510.06727). Call this \
95 when the active transcript is approaching the model's context \
96 window and you want to compress it in your own words before \
97 continuing. Pass a `summary` string distilling the task state, \
98 decisions, open questions, and any must-remember details. The \
99 tool records the reset and returns a `[CONTEXT RESET]` marker \
100 that will become the anchor for the next turn. The canonical \
101 chat history is preserved on disk — call `session_recall` if \
102 you later need a specific detail you dropped."
103 }
104
105 fn parameters(&self) -> Value {
106 json!({
107 "type": "object",
108 "properties": {
109 "summary": {
110 "type": "string",
111 "description": "Compressed task state in your own words: \
112 goal, key decisions, open questions, \
113 must-remember details. This replaces \
114 the earlier transcript as the anchor \
115 for subsequent turns."
116 }
117 },
118 "required": ["summary"]
119 })
120 }
121
122 async fn execute(&self, args: Value) -> Result<ToolResult> {
123 let summary = match args["summary"].as_str() {
124 Some(s) if !s.trim().is_empty() => s.trim().to_string(),
125 _ => {
126 return Ok(ToolResult::error(
127 "`summary` is required and must be non-empty",
128 ));
129 }
130 };
131 let bytes = summary.len();
132 audit::log(bytes).await;
133 Ok(ToolResult::success(format_reset_marker(&summary, bytes)))
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[tokio::test]
142 async fn execute_requires_non_empty_summary() {
143 let tool = ContextResetTool;
144 for summary in ["", " "] {
145 let result = tool.execute(json!({"summary": summary})).await.unwrap();
146 assert!(!result.success);
147 }
148 }
149
150 #[tokio::test]
151 async fn execute_wraps_summary_with_marker() {
152 let tool = ContextResetTool;
153 let result = tool
154 .execute(json!({"summary": "goal: ship it; open: review"}))
155 .await
156 .unwrap();
157 assert!(result.success);
158 assert!(result.output.starts_with(RESET_MARKER_PREFIX));
159 assert!(result.output.contains("goal: ship it"));
160 }
161
162 #[test]
163 fn format_reset_marker_has_stable_prefix_and_body() {
164 let body = format_reset_marker("hello", 5);
165 assert_eq!(RESET_MARKER_PREFIX, "[CONTEXT RESET]");
166 assert!(body.starts_with(RESET_MARKER_PREFIX));
167 assert!(body.contains("hello"));
168 assert!(body.contains("bytes=5"));
169 assert!(body.contains("session_recall"));
170 }
171}