use super::*;
impl HarnessTurnState {
pub(crate) fn budget_recovery_reason(&self) -> Option<&str> {
budget_exhaustion_cause(self.recovery_prompt_reason()?)
}
}
pub(crate) fn budget_exhaustion_cause(reason: &str) -> Option<&str> {
[
"Tool loop budget exhausted",
"Tool-call budget exhausted for this turn",
"Tool wall-clock budget exhausted for this turn",
]
.iter()
.any(|prefix| reason.starts_with(prefix))
.then(|| reason.split_once(". ").map_or(reason, |(cause, _)| cause))
}
#[derive(serde::Deserialize)]
pub(super) struct PreviewBudgetExhaustedProbe {
#[serde(default)]
pub(super) preview_budget_exhausted: Option<bool>,
}
impl ToolBudgetWarning {
pub(crate) fn system_message(self) -> String {
format!(
"Tool-call budget warning: {}/{} used; {} remaining for this turn. Use targeted extraction/batching before additional tool calls.",
self.used, self.max, self.remaining
)
}
pub(crate) fn log_threshold_reached(self, path: &'static str) {
tracing::info!(used = self.used, max = self.max, remaining = self.remaining, "{path}");
}
}
pub(crate) const BUDGET_EXHAUSTED_SYNTHESIS_NOTE: &str = "Tools are disabled for the rest of this turn, so further tool calls are \
skipped. Synthesize your final answer now from the tool outputs already gathered in this conversation.";
impl ToolBudgetExhaustion {
pub(crate) fn policy_violation_message(self) -> String {
format!("Policy violation: exceeded max tool calls per turn ({})", self.max)
}
pub(crate) fn skipped_call_message(self) -> String {
"Tool-call budget exhausted for this turn; call skipped.".to_string()
}
pub(crate) fn synthesis_directive_message(self) -> String {
debug_assert!(self.max > 0, "disabled tool-call caps must not emit exhaustion");
format!(
"Tool-call budget exhausted for this turn ({}/{}). {BUDGET_EXHAUSTED_SYNTHESIS_NOTE}",
self.used, self.max
)
}
}
pub(crate) mod budget_kind {
pub(crate) const TOOL_CALLS: &str = "tool_calls";
pub(crate) const TOOL_LOOP: &str = "tool_loop";
pub(crate) const SESSION_CALLS: &str = "session_calls";
pub(crate) const WALL_CLOCK: &str = "wall_clock_secs";
}
#[derive(Copy, Clone)]
pub(crate) struct BudgetExhaustedMetrics {
pub budget: &'static str,
pub used: usize,
pub max: usize,
pub step_count: Option<usize>,
pub planning_active: bool,
pub tool_calls: usize,
}
pub(crate) fn emit_budget_exhausted_metric(traj: &TrajectoryLogger, metrics: BudgetExhaustedMetrics) {
traj.log(&budget_exhausted_record(metrics));
}
#[derive(serde::Serialize)]
pub(crate) struct BudgetExhaustedRecord {
kind: &'static str,
budget: &'static str,
used: usize,
max: usize,
#[serde(skip_serializing_if = "Option::is_none")]
step_count: Option<usize>,
planning_active: bool,
tool_calls: usize,
ts: i64,
}
pub(crate) fn budget_exhausted_record(metrics: BudgetExhaustedMetrics) -> BudgetExhaustedRecord {
BudgetExhaustedRecord {
kind: "budget_exhausted",
budget: metrics.budget,
used: metrics.used,
max: metrics.max,
step_count: metrics.step_count,
planning_active: metrics.planning_active,
tool_calls: metrics.tool_calls,
ts: chrono::Utc::now().timestamp(),
}
}
impl ToolWallClockExhaustion {
pub(crate) fn policy_violation_message(self) -> String {
format!("Policy violation: exceeded tool wall clock budget ({}s)", self.max_secs)
}
pub(crate) fn skipped_call_message(self) -> String {
"Tool wall-clock budget exhausted for this turn; call skipped.".to_string()
}
pub(crate) fn synthesis_directive_message(self) -> String {
format!(
"Tool wall-clock budget exhausted for this turn ({}s). {BUDGET_EXHAUSTED_SYNTHESIS_NOTE}",
self.max_secs
)
}
}