supercode_harness/tools/
context_budget.rs1use std::sync::Mutex;
31
32use async_trait::async_trait;
33use serde::Deserialize;
34use serde_json::{json, Value};
35
36use crate::error::{Error, Result};
37use crate::tools::{Tool, ToolContext};
38
39pub const GET_CONTEXT_REMAINING: &str = "get_context_remaining";
41
42pub const NEW_CONTEXT: &str = "new_context";
44
45#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct NewContextRequest {
50 pub objective: String,
52 pub keep_recent: Option<usize>,
57}
58
59#[derive(Debug, Default)]
61pub struct ContextBudget {
62 usage: Mutex<Option<Value>>,
64 pending: Mutex<Option<NewContextRequest>>,
65}
66
67impl ContextBudget {
68 pub fn new() -> Self {
70 Self::default()
71 }
72
73 pub fn publish(&self, usage: Value) {
76 if let Ok(mut slot) = self.usage.lock() {
77 *slot = Some(usage);
78 }
79 }
80
81 pub fn snapshot(&self) -> Option<Value> {
83 self.usage.lock().ok().and_then(|slot| slot.clone())
84 }
85
86 pub fn request_new_context(&self, request: NewContextRequest) {
88 if let Ok(mut pending) = self.pending.lock() {
89 *pending = Some(request);
90 }
91 }
92
93 pub fn take_new_context(&self) -> Option<NewContextRequest> {
96 self.pending.lock().ok().and_then(|mut p| p.take())
97 }
98}
99
100#[derive(Debug, Default)]
102pub struct GetContextRemainingTool;
103
104#[async_trait]
105impl Tool for GetContextRemainingTool {
106 fn name(&self) -> &str {
107 GET_CONTEXT_REMAINING
108 }
109 fn description(&self) -> &str {
110 "Report how much of the model's context window this conversation is using and how many \
111 tokens remain (messages, tool schemas, the reply reserve, and whether the next request \
112 would still fit). Use it before starting something long, or to decide whether to call \
113 new_context."
114 }
115 fn parameters(&self) -> Value {
116 json!({"type": "object", "properties": {}, "additionalProperties": false})
117 }
118 fn structured_output(&self) -> bool {
119 true
120 }
121 async fn execute(&self, _args: Value, ctx: &ToolContext) -> Result<String> {
122 let Some(usage) = ctx.context_budget.snapshot() else {
123 return Err(Error::tool(
124 self.name(),
125 "no context accounting is available for this session (this tool reports the \
126 running agent's own figures, and none have been published)",
127 ));
128 };
129 Ok(usage.to_string())
130 }
131}
132
133#[derive(Debug, Deserialize)]
134struct NewContextArgs {
135 objective: String,
136 #[serde(default)]
137 keep_recent: Option<usize>,
138}
139
140#[derive(Debug, Default)]
143pub struct NewContextTool;
144
145#[async_trait]
146impl Tool for NewContextTool {
147 fn name(&self) -> &str {
148 NEW_CONTEXT
149 }
150 fn description(&self) -> &str {
151 "Continue this session in a fresh context window: state the objective the new window \
152 opens with. The system prompt, a handoff marker carrying that objective, and the most \
153 recent messages are kept; everything earlier is set aside (and stays in the session's \
154 transcript). Takes effect immediately after this tool round."
155 }
156 fn parameters(&self) -> Value {
157 json!({
158 "type": "object",
159 "properties": {
160 "objective": {
161 "type": "string",
162 "description": "What the fresh window is for — the one paragraph the new \
163 context opens with."
164 },
165 "keep_recent": {
166 "type": "integer",
167 "minimum": 0,
168 "description": "How many of the most recent messages to keep. Omit to use \
169 the session's own keep-recent token budget."
170 }
171 },
172 "required": ["objective"],
173 "additionalProperties": false
174 })
175 }
176 async fn execute(&self, args: Value, ctx: &ToolContext) -> Result<String> {
177 let a: NewContextArgs =
178 serde_json::from_value(args).map_err(|e| Error::InvalidArguments {
179 tool: self.name().to_string(),
180 message: e.to_string(),
181 })?;
182 let objective = a.objective.trim().to_string();
183 if objective.is_empty() {
184 return Err(Error::InvalidArguments {
185 tool: self.name().to_string(),
186 message: "state an objective for the fresh context window".to_string(),
187 });
188 }
189 ctx.context_budget.request_new_context(NewContextRequest {
190 objective: objective.clone(),
191 keep_recent: a.keep_recent,
192 });
193 Ok(format!(
194 "A fresh context window is queued and takes effect before your next turn. \
195 Objective: {objective}. The system prompt, a handoff marker with that objective, \
196 and the recent tail are kept; earlier turns are set aside."
197 ))
198 }
199}