speccoder_runtime/substrate/attempt/
loop_spec.rs1use super::*;
5
6pub(crate) struct LoopSpec {
8 pub tools: Vec<ToolDeclaration>,
10 pub policy: Policy,
12 pub max_turns: u32,
14 pub max_tool_calls: u32,
16 pub reasoning: bool,
18 pub schema: Option<Value>,
20 pub routes: Vec<(String, String, String)>,
22 pub root: Option<String>,
25 pub frozen_additions: bool,
27 pub finish: fn(&Value) -> &'static str,
29 pub ask: Option<Asker>,
31 pub run_approved: Vec<Object>,
33 pub once: Vec<Object>,
36 pub observer: Option<(String, std::sync::Arc<dyn crate::progress::Progress>)>,
38 pub run: Option<String>,
40 pub asking: Asking,
43 pub parts: Vec<crate::approvals::derived::Part>,
46 pub mcp_digests: std::collections::BTreeMap<String, String>,
48 pub stop: StopGate,
50 pub max_tokens: Option<u64>,
52 pub left: Option<Left>,
54 pub reads: Option<u32>,
56 pub halt: Option<agentplane::model::Stop>,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct Asking {
65 pub asked: u32,
67 pub budget: u32,
69 pub prompts: bool,
72}
73
74impl Default for Asking {
75 fn default() -> Self {
76 Self {
77 asked: 0,
78 budget: DEFAULT_BUDGET,
79 prompts: false,
80 }
81 }
82}
83
84pub use crate::substrate::run_config::DEFAULT_BUDGET;
86
87#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
90pub struct Left {
91 pub cap: crate::substrate::run_config::Cap,
93 pub prices: Option<crate::substrate::model_config::Prices>,
95}
96
97impl Left {
98 #[must_use]
100 pub fn spent(&self, usage: &agentplane::model::Usage) -> u64 {
101 match self.cap {
102 crate::substrate::run_config::Cap::Tokens(_) => {
103 usage.input_tokens + usage.output_tokens
104 }
105 crate::substrate::run_config::Cap::Usd(_) => self.prices.map_or(0, |p| {
106 micros(p.usd(
107 usage.input_tokens,
108 usage.output_tokens,
109 usage.cache_read_tokens,
110 usage.cache_write_tokens,
111 ))
112 }),
113 }
114 }
115
116 #[must_use]
118 pub fn reached(&self, usage: &agentplane::model::Usage) -> bool {
119 self.spent(usage) >= self.cap.amount()
120 }
121
122 #[must_use]
124 pub fn reason(&self, usage: &agentplane::model::Usage) -> String {
125 format!(
126 "the run's cap: this attempt used {} of the {} left (`run --cap`, `[budget] cap`)",
127 self.cap.with(self.spent(usage)).rounded(),
128 self.cap.rounded()
129 )
130 }
131}
132
133#[must_use]
135pub fn usage_of(v: Option<&Value>) -> agentplane::model::Usage {
136 let n = |k: &str| v.and_then(|v| v[k].as_u64()).unwrap_or(0);
137 agentplane::model::Usage {
138 input_tokens: n("input_tokens"),
139 output_tokens: n("output_tokens"),
140 cache_read_tokens: n("cache_read_tokens"),
141 cache_write_tokens: n("cache_write_tokens"),
142 ..agentplane::model::Usage::default()
143 }
144}
145
146#[must_use]
148#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
149pub fn micros(usd: f64) -> u64 {
150 (usd * 1e6).round().max(0.0) as u64
151}
152
153pub(super) fn relativise(arguments: &mut Value, root: Option<&str>) {
158 let (Some(root), Some(path)) = (root, arguments.get("path").and_then(Value::as_str)) else {
159 return;
160 };
161 let root = root.trim_end_matches('/');
162 if let Some(rel) = path.strip_prefix(root).and_then(|r| r.strip_prefix('/')) {
163 let rel = if rel.is_empty() {
164 ".".to_owned()
165 } else {
166 rel.to_owned()
167 };
168 arguments["path"] = Value::String(rel);
169 }
170}
171
172pub(super) fn emit(
174 observer: Option<&(String, std::sync::Arc<dyn crate::progress::Progress>)>,
175 turn: u32,
176 entry: &Value,
177) {
178 emit_with_tail(observer, turn, entry, Vec::new());
179}
180
181pub(super) fn emit_with_tail(
184 observer: Option<&(String, std::sync::Arc<dyn crate::progress::Progress>)>,
185 turn: u32,
186 entry: &Value,
187 tail: Vec<String>,
188) {
189 if let Some((task, p)) = observer {
190 let tool = entry["tool"].as_str().unwrap_or_default();
191 p.event(&crate::progress::Event::ToolCall {
192 task: task.clone(),
193 turn,
194 tool: tool.to_owned(),
195 target: crate::progress::target(tool, &entry["arguments"]),
196 decision: entry["decision"].as_str().unwrap_or_default().to_owned(),
197 ok: entry["ok"].as_bool(),
198 tail,
199 contained: entry["contained"] == true,
200 });
201 }
202}