use super::*;
pub(crate) struct LoopSpec {
pub tools: Vec<ToolDeclaration>,
pub policy: Policy,
pub max_turns: u32,
pub max_tool_calls: u32,
pub reasoning: bool,
pub schema: Option<Value>,
pub routes: Vec<(String, String, String)>,
pub root: Option<String>,
pub frozen_additions: bool,
pub finish: fn(&Value) -> &'static str,
pub ask: Option<Asker>,
pub run_approved: Vec<Object>,
pub once: Vec<Object>,
pub observer: Option<(String, std::sync::Arc<dyn crate::progress::Progress>)>,
pub run: Option<String>,
pub asking: Asking,
pub parts: Vec<crate::approvals::derived::Part>,
pub mcp_digests: std::collections::BTreeMap<String, String>,
pub stop: StopGate,
pub max_tokens: Option<u64>,
pub left: Option<Left>,
pub reads: Option<u32>,
pub halt: Option<agentplane::model::Stop>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Asking {
pub asked: u32,
pub budget: u32,
pub prompts: bool,
}
impl Default for Asking {
fn default() -> Self {
Self {
asked: 0,
budget: DEFAULT_BUDGET,
prompts: false,
}
}
}
pub use crate::substrate::run_config::DEFAULT_BUDGET;
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Left {
pub cap: crate::substrate::run_config::Cap,
pub prices: Option<crate::substrate::model_config::Prices>,
}
impl Left {
#[must_use]
pub fn spent(&self, usage: &agentplane::model::Usage) -> u64 {
match self.cap {
crate::substrate::run_config::Cap::Tokens(_) => {
usage.input_tokens + usage.output_tokens
}
crate::substrate::run_config::Cap::Usd(_) => self.prices.map_or(0, |p| {
micros(p.usd(
usage.input_tokens,
usage.output_tokens,
usage.cache_read_tokens,
usage.cache_write_tokens,
))
}),
}
}
#[must_use]
pub fn reached(&self, usage: &agentplane::model::Usage) -> bool {
self.spent(usage) >= self.cap.amount()
}
#[must_use]
pub fn reason(&self, usage: &agentplane::model::Usage) -> String {
format!(
"the run's cap: this attempt used {} of the {} left (`run --cap`, `[budget] cap`)",
self.cap.with(self.spent(usage)).rounded(),
self.cap.rounded()
)
}
}
#[must_use]
pub fn usage_of(v: Option<&Value>) -> agentplane::model::Usage {
let n = |k: &str| v.and_then(|v| v[k].as_u64()).unwrap_or(0);
agentplane::model::Usage {
input_tokens: n("input_tokens"),
output_tokens: n("output_tokens"),
cache_read_tokens: n("cache_read_tokens"),
cache_write_tokens: n("cache_write_tokens"),
..agentplane::model::Usage::default()
}
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn micros(usd: f64) -> u64 {
(usd * 1e6).round().max(0.0) as u64
}
pub(super) fn relativise(arguments: &mut Value, root: Option<&str>) {
let (Some(root), Some(path)) = (root, arguments.get("path").and_then(Value::as_str)) else {
return;
};
let root = root.trim_end_matches('/');
if let Some(rel) = path.strip_prefix(root).and_then(|r| r.strip_prefix('/')) {
let rel = if rel.is_empty() {
".".to_owned()
} else {
rel.to_owned()
};
arguments["path"] = Value::String(rel);
}
}
pub(super) fn emit(
observer: Option<&(String, std::sync::Arc<dyn crate::progress::Progress>)>,
turn: u32,
entry: &Value,
) {
emit_with_tail(observer, turn, entry, Vec::new());
}
pub(super) fn emit_with_tail(
observer: Option<&(String, std::sync::Arc<dyn crate::progress::Progress>)>,
turn: u32,
entry: &Value,
tail: Vec<String>,
) {
if let Some((task, p)) = observer {
let tool = entry["tool"].as_str().unwrap_or_default();
p.event(&crate::progress::Event::ToolCall {
task: task.clone(),
turn,
tool: tool.to_owned(),
target: crate::progress::target(tool, &entry["arguments"]),
decision: entry["decision"].as_str().unwrap_or_default().to_owned(),
ok: entry["ok"].as_bool(),
tail,
contained: entry["contained"] == true,
});
}
}