Skip to main content

codetether_agent/tool/
context_reset.rs

1//! `context_reset`: agent-callable Lu et al. reset (arXiv:2510.06727).
2//!
3//! ## What it does
4//!
5//! The agent hands the harness a compressed summary of the session so
6//! far. The tool records the reset decision in the audit log and
7//! returns a marker [`ToolResult`] that the next turn's derivation can
8//! pick up (via `DerivePolicy::Reset`) as the new anchor for the
9//! context window.
10//!
11//! Unlike [`session_recall`], this tool does **not** read disk or run
12//! RLM. The summary text is supplied by the model itself — that is the
13//! whole point of Lu et al.'s semantic: the *policy* decides what to
14//! keep, rather than an external summariser guessing.
15//!
16//! ## When the agent should call it
17//!
18//! * The transcript is getting long and the model wants to proactively
19//!   fold it before the next tool call.
20//! * The agent has just completed a sub-task and wants to hand-off a
21//!   compact state to itself for the next phase.
22//! * The user's next message is about to bring in a large payload
23//!   (large file, long web page) and the agent wants headroom.
24//!
25//! ## Invariants
26//!
27//! * The tool never mutates [`Session::messages`]. The summary enters
28//!   history naturally — as the tool's `ToolResult` on the next turn.
29//! * The returned marker text always starts with `[CONTEXT RESET]` so
30//!   Phase B's derivation policies can recognise it without an
31//!   out-of-band signal.
32//!
33//! ## Examples
34//!
35//! ```rust
36//! use codetether_agent::tool::context_reset::{ContextResetTool, format_reset_marker};
37//!
38//! // Sanity-check the stable marker format the next-turn derivation
39//! // policy can look for.
40//! let body = format_reset_marker("summary body", 42);
41//! assert!(body.starts_with("[CONTEXT RESET]"));
42//! assert!(body.contains("summary body"));
43//! assert!(body.contains("bytes=42"));
44//!
45//! let _ = ContextResetTool;
46//! ```
47
48use 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
56/// Marker prefix that identifies a `context_reset` tool output in the
57/// transcript. Phase B derivations search for this literal when
58/// deciding whether a user-visible summary has already landed.
59pub const RESET_MARKER_PREFIX: &str = "[CONTEXT RESET]";
60
61/// Shape the tool-result payload carrying the agent-authored summary.
62///
63/// Kept as a free function so the exact wire format can be unit-tested
64/// in isolation from the [`Tool`] trait impl.
65pub 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
80/// Agent-callable Lu et al. reset tool.
81pub 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}