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