use std::time::Duration;
use crate::{
budget::BudgetPool,
run::{Effort, TurnOptions},
};
pub(crate) const DEFAULT_SESSION_NAME: &str = "basis run";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RunSpec {
pub prompt: String,
pub session_name: String,
pub effort: Option<Effort>,
pub deadline: Option<Duration>,
pub tool_budget: Option<usize>,
pub token_budget: Option<u64>,
pub budget: Option<BudgetPool>,
}
impl RunSpec {
pub fn new(prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
session_name: DEFAULT_SESSION_NAME.to_string(),
effort: None,
deadline: None,
tool_budget: None,
token_budget: None,
budget: None,
}
}
pub fn with_prompt(self, prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
..self
}
}
pub fn with_session_name(self, session_name: impl Into<String>) -> Self {
Self {
session_name: session_name.into(),
..self
}
}
pub fn with_effort(self, effort: Effort) -> Self {
Self {
effort: Some(effort),
..self
}
}
pub fn with_deadline(self, deadline: Duration) -> Self {
Self {
deadline: Some(deadline),
..self
}
}
pub fn with_tool_budget(self, tool_budget: usize) -> Self {
Self {
tool_budget: Some(tool_budget),
..self
}
}
pub fn with_token_budget(self, token_budget: u64) -> Self {
Self {
token_budget: Some(token_budget),
..self
}
}
pub fn with_budget(self, budget: BudgetPool) -> Self {
Self {
budget: Some(budget),
..self
}
}
pub fn turn_options(&self) -> TurnOptions {
TurnOptions {
deadline: self.deadline,
tool_budget: self.tool_budget,
token_budget: self.token_budget,
budget: self.budget.clone(),
..TurnOptions::default()
}
}
}
impl Default for RunSpec {
fn default() -> Self {
Self::new("")
}
}
impl From<&str> for RunSpec {
fn from(prompt: &str) -> Self {
Self::new(prompt)
}
}
impl From<String> for RunSpec {
fn from(prompt: String) -> Self {
Self::new(prompt)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bare_prompt_is_a_whole_spec() {
let spec: RunSpec = "review this diff".into();
assert_eq!(spec.prompt, "review this diff");
assert_eq!(spec.session_name, DEFAULT_SESSION_NAME);
assert_eq!(spec.effort, None);
}
#[test]
fn a_spec_with_nothing_said_yet_is_the_default() {
assert_eq!(RunSpec::default().prompt, "");
}
#[test]
fn builders_return_new_values() {
let base = RunSpec::new("prompt");
let derived = base
.clone()
.with_session_name("named")
.with_effort(Effort::High);
assert_eq!(base.session_name, DEFAULT_SESSION_NAME);
assert_eq!(base.effort, None, "the original must be untouched");
assert_eq!(derived.session_name, "named");
assert_eq!(derived.effort, Some(Effort::High));
}
#[test]
fn a_run_is_unbounded_unless_the_spec_asks_for_a_bound() {
let options = RunSpec::new("prompt").turn_options();
assert_eq!(options.deadline, None);
assert_eq!(options.tool_budget, None);
assert_eq!(options.token_budget, None);
}
#[test]
fn every_bound_reaches_the_turn_as_configured() {
let options = RunSpec::new("prompt")
.with_deadline(Duration::from_secs(3_600))
.with_tool_budget(12)
.with_token_budget(50_000)
.turn_options();
assert_eq!(options.deadline, Some(Duration::from_secs(3_600)));
assert_eq!(options.tool_budget, Some(12));
assert_eq!(options.token_budget, Some(50_000));
}
#[test]
fn a_spec_carries_no_stop_signal_of_its_own() {
let options = RunSpec::new("prompt")
.with_deadline(Duration::from_secs(60))
.turn_options();
assert!(options.cancel.is_none());
assert!(options.stop.is_none());
}
#[test]
fn a_shared_allowance_reaches_the_turn_through_the_spec() {
use crate::budget::BudgetPool;
let pool = BudgetPool::new(500_000);
let options = RunSpec::new("prompt")
.with_budget(pool.clone())
.turn_options();
assert_eq!(options.budget, Some(pool));
}
#[test]
fn deriving_a_spec_shares_the_pool_but_copies_everything_else() {
use crate::budget::BudgetPool;
let pool = BudgetPool::new(1_000);
let template = RunSpec::new("").with_budget(pool.clone());
let first = template.clone().with_prompt("review the tests");
let second = template.with_prompt("review the docs");
assert_eq!(first.budget, second.budget, "one allowance, two runs");
assert_eq!(first.budget, Some(pool));
assert_ne!(first.prompt, second.prompt);
}
}