use serde_json::Value;
pub(crate) const REPEAT_NUDGE_AT: u32 = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RepeatVerdict {
Fresh,
Repeating(u32),
NudgeNow(u32),
}
pub(crate) fn signature(name: &str, input: &Value) -> String {
format!("{name}|{input}")
}
#[derive(Debug, Default)]
pub(crate) struct ToolRepeatTracker {
last: Option<String>,
consecutive: u32,
nudged: bool,
}
impl ToolRepeatTracker {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn observe(&mut self, signature: &str) -> RepeatVerdict {
if self.last.as_deref() != Some(signature) {
self.last = Some(signature.to_string());
self.consecutive = 1;
self.nudged = false;
return RepeatVerdict::Fresh;
}
self.consecutive += 1;
if self.consecutive >= REPEAT_NUDGE_AT && !self.nudged {
self.nudged = true;
return RepeatVerdict::NudgeNow(self.consecutive);
}
RepeatVerdict::Repeating(self.consecutive)
}
pub(crate) fn reset(&mut self) {
self.last = None;
self.consecutive = 0;
self.nudged = false;
}
pub(crate) fn consecutive(&self) -> u32 {
self.consecutive
}
}
pub(crate) fn repeat_nudge(tool_name: &str, count: u32) -> String {
format!(
"[System: you have now called `{tool_name}` {count} times in a row with identical \
arguments. The result above is the same one you already have; calling it again with the \
same arguments cannot return anything different. Change the arguments, use a different \
approach, or if you are blocked, say so and explain what you need instead of repeating \
the call.]"
)
}
pub(crate) fn observe_round(
tracker: &mut ToolRepeatTracker,
round_signature: &str,
first_tool_name: Option<String>,
) -> Option<String> {
if round_signature.is_empty() {
return None;
}
match tracker.observe(round_signature) {
RepeatVerdict::NudgeNow(count) => Some(repeat_nudge(
first_tool_name.as_deref().unwrap_or("that tool"),
count,
)),
RepeatVerdict::Fresh | RepeatVerdict::Repeating(_) => None,
}
}