Initial implementation of the agent framework

This commit is contained in:
Illia Polosukhin
2026-02-02 20:41:05 -08:00
commit 8c38566378
63 changed files with 14099 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
//! Cost estimation.
use std::collections::HashMap;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
/// Estimates costs for tools and operations.
pub struct CostEstimator {
/// Base costs per tool.
tool_costs: HashMap<String, Decimal>,
/// LLM cost per 1K tokens.
llm_cost_per_1k: Decimal,
}
impl CostEstimator {
/// Create a new cost estimator.
pub fn new() -> Self {
let mut tool_costs = HashMap::new();
// Default tool costs (in USD or equivalent)
tool_costs.insert("http".to_string(), dec!(0.0001)); // API call
tool_costs.insert("marketplace".to_string(), dec!(0.01)); // Gas costs
tool_costs.insert("ecommerce".to_string(), dec!(0.001)); // API call
tool_costs.insert("taskrabbit".to_string(), dec!(0.0)); // Cost comes from task itself
tool_costs.insert("restaurant".to_string(), dec!(0.001)); // API call
tool_costs.insert("echo".to_string(), dec!(0.0)); // Free
tool_costs.insert("time".to_string(), dec!(0.0)); // Free
tool_costs.insert("json".to_string(), dec!(0.0)); // Free
Self {
tool_costs,
llm_cost_per_1k: dec!(0.01), // Approximate
}
}
/// Estimate cost for a tool call.
pub fn estimate_tool(&self, tool_name: &str) -> Decimal {
self.tool_costs
.get(tool_name)
.copied()
.unwrap_or(dec!(0.001)) // Default for unknown tools
}
/// Estimate LLM cost for tokens.
pub fn estimate_llm_tokens(&self, input_tokens: u32, output_tokens: u32) -> Decimal {
let total_tokens = Decimal::from(input_tokens + output_tokens);
(total_tokens / dec!(1000)) * self.llm_cost_per_1k
}
/// Set a tool's base cost.
pub fn set_tool_cost(&mut self, tool_name: impl Into<String>, cost: Decimal) {
self.tool_costs.insert(tool_name.into(), cost);
}
/// Get all tool costs.
pub fn all_tool_costs(&self) -> &HashMap<String, Decimal> {
&self.tool_costs
}
}
impl Default for CostEstimator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_cost_estimation() {
let estimator = CostEstimator::new();
assert_eq!(estimator.estimate_tool("echo"), dec!(0.0));
assert_eq!(estimator.estimate_tool("marketplace"), dec!(0.01));
assert!(estimator.estimate_tool("unknown") > dec!(0.0));
}
#[test]
fn test_llm_cost_estimation() {
let estimator = CostEstimator::new();
let cost = estimator.estimate_llm_tokens(1000, 500);
assert!(cost > dec!(0.0));
}
}
+228
View File
@@ -0,0 +1,228 @@
//! Statistical learning for estimation improvement.
use std::collections::HashMap;
use std::time::Duration;
use rust_decimal::Decimal;
/// Learning model for estimation adjustments.
#[derive(Debug, Clone)]
pub struct LearningModel {
/// Cost adjustment factor (multiplier).
pub cost_factor: f64,
/// Time adjustment factor (multiplier).
pub time_factor: f64,
/// Number of samples.
pub sample_count: u64,
/// Running error rate for cost.
pub cost_error_rate: f64,
/// Running error rate for time.
pub time_error_rate: f64,
}
impl Default for LearningModel {
fn default() -> Self {
Self {
cost_factor: 1.0,
time_factor: 1.0,
sample_count: 0,
cost_error_rate: 0.0,
time_error_rate: 0.0,
}
}
}
/// Learner that improves estimates over time.
pub struct EstimationLearner {
/// Models per category.
models: HashMap<String, LearningModel>,
/// Exponential moving average alpha.
alpha: f64,
/// Minimum samples before adjusting.
min_samples: u64,
}
impl EstimationLearner {
/// Create a new estimation learner.
pub fn new() -> Self {
Self {
models: HashMap::new(),
alpha: 0.1, // EMA smoothing factor
min_samples: 5,
}
}
/// Record actual results and update the model.
pub fn record(
&mut self,
category: &str,
estimated_cost: Decimal,
actual_cost: Decimal,
estimated_time: Duration,
actual_time: Duration,
) {
let model = self.models.entry(category.to_string()).or_default();
model.sample_count += 1;
// Calculate errors
let cost_ratio = if !estimated_cost.is_zero() {
(actual_cost / estimated_cost)
.to_string()
.parse::<f64>()
.unwrap_or(1.0)
} else {
1.0
};
let time_ratio = if !estimated_time.is_zero() {
actual_time.as_secs_f64() / estimated_time.as_secs_f64()
} else {
1.0
};
// Update factors using exponential moving average
model.cost_factor = model.cost_factor * (1.0 - self.alpha) + cost_ratio * self.alpha;
model.time_factor = model.time_factor * (1.0 - self.alpha) + time_ratio * self.alpha;
// Update error rates
let cost_error = (cost_ratio - 1.0).abs();
let time_error = (time_ratio - 1.0).abs();
model.cost_error_rate =
model.cost_error_rate * (1.0 - self.alpha) + cost_error * self.alpha;
model.time_error_rate =
model.time_error_rate * (1.0 - self.alpha) + time_error * self.alpha;
}
/// Adjust estimates based on learned factors.
pub fn adjust(&self, category: &str, cost: Decimal, time: Duration) -> (Decimal, Duration) {
let model = self.models.get(category);
match model {
Some(m) if m.sample_count >= self.min_samples => {
let adjusted_cost = cost * Decimal::try_from(m.cost_factor).unwrap_or(Decimal::ONE);
let adjusted_time = Duration::from_secs_f64(time.as_secs_f64() * m.time_factor);
(adjusted_cost, adjusted_time)
}
_ => (cost, time), // Not enough data, use original estimates
}
}
/// Get confidence for a category (based on sample count and error rate).
pub fn confidence(&self, category: &str) -> f64 {
match self.models.get(category) {
Some(m) if m.sample_count >= self.min_samples => {
// Higher samples and lower error = higher confidence
let sample_factor = (m.sample_count as f64 / 100.0).min(1.0);
let error_factor = 1.0 - ((m.cost_error_rate + m.time_error_rate) / 2.0).min(1.0);
0.5 + (sample_factor * 0.3) + (error_factor * 0.2)
}
Some(_) => 0.3, // Some data but not enough
None => 0.2, // No data
}
}
/// Get the model for a category.
pub fn get_model(&self, category: &str) -> Option<&LearningModel> {
self.models.get(category)
}
/// Get all models.
pub fn all_models(&self) -> &HashMap<String, LearningModel> {
&self.models
}
/// Set the EMA alpha.
pub fn set_alpha(&mut self, alpha: f64) {
self.alpha = alpha.clamp(0.01, 0.5);
}
/// Set minimum samples.
pub fn set_min_samples(&mut self, min: u64) {
self.min_samples = min;
}
/// Clear all learned data.
pub fn clear(&mut self) {
self.models.clear();
}
}
impl Default for EstimationLearner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_learning_model_update() {
let mut learner = EstimationLearner::new();
learner.set_min_samples(2);
// Record some results where actuals are 20% higher than estimates
for _ in 0..5 {
learner.record(
"test",
dec!(100.0),
dec!(120.0),
Duration::from_secs(60),
Duration::from_secs(72),
);
}
let model = learner.get_model("test").unwrap();
assert!(model.cost_factor > 1.0);
assert!(model.time_factor > 1.0);
}
#[test]
fn test_adjustment() {
let mut learner = EstimationLearner::new();
learner.set_min_samples(2);
// Train with consistent 50% underestimation
for _ in 0..10 {
learner.record(
"test",
dec!(100.0),
dec!(150.0),
Duration::from_secs(60),
Duration::from_secs(90),
);
}
let (adjusted_cost, adjusted_time) =
learner.adjust("test", dec!(100.0), Duration::from_secs(60));
// Should adjust upward
assert!(adjusted_cost > dec!(100.0));
assert!(adjusted_time > Duration::from_secs(60));
}
#[test]
fn test_confidence() {
let mut learner = EstimationLearner::new();
// No data = low confidence
assert!(learner.confidence("unknown") < 0.5);
// Add data
for _ in 0..20 {
learner.record(
"known",
dec!(100.0),
dec!(100.0), // Perfect estimates
Duration::from_secs(60),
Duration::from_secs(60),
);
}
// More data with good accuracy = higher confidence
assert!(learner.confidence("known") > 0.5);
}
}
+139
View File
@@ -0,0 +1,139 @@
//! Cost, time, and value estimation with continuous learning.
//!
//! Estimates are based on:
//! - Historical data from similar jobs
//! - Tool cost/time characteristics
//! - Statistical models that improve over time
mod cost;
mod learner;
mod time;
mod value;
pub use cost::CostEstimator;
pub use learner::{EstimationLearner, LearningModel};
pub use time::TimeEstimator;
pub use value::ValueEstimator;
use rust_decimal::Decimal;
use std::time::Duration;
/// Combined estimation for a job.
#[derive(Debug, Clone)]
pub struct JobEstimate {
/// Estimated cost to complete the job.
pub cost: Decimal,
/// Estimated time to complete.
pub duration: Duration,
/// Estimated value/earnings.
pub value: Decimal,
/// Confidence in the estimate (0-1).
pub confidence: f64,
/// Breakdown by tool.
pub tool_breakdown: Vec<ToolEstimate>,
}
/// Estimate for a single tool usage.
#[derive(Debug, Clone)]
pub struct ToolEstimate {
pub tool_name: String,
pub cost: Decimal,
pub duration: Duration,
pub confidence: f64,
}
/// Combined estimator.
pub struct Estimator {
cost: CostEstimator,
time: TimeEstimator,
value: ValueEstimator,
learner: EstimationLearner,
}
impl Estimator {
/// Create a new estimator.
pub fn new() -> Self {
Self {
cost: CostEstimator::new(),
time: TimeEstimator::new(),
value: ValueEstimator::new(),
learner: EstimationLearner::new(),
}
}
/// Estimate for a job.
pub fn estimate_job(
&self,
description: &str,
category: Option<&str>,
tools: &[String],
) -> JobEstimate {
let tool_estimates: Vec<ToolEstimate> = tools
.iter()
.map(|t| ToolEstimate {
tool_name: t.clone(),
cost: self.cost.estimate_tool(t),
duration: self.time.estimate_tool(t),
confidence: 0.7, // Default confidence
})
.collect();
let total_cost: Decimal = tool_estimates.iter().map(|e| e.cost).sum();
let total_duration: Duration = tool_estimates.iter().map(|e| e.duration).sum();
// Apply learned adjustments
let (adjusted_cost, adjusted_time) =
self.learner
.adjust(category.unwrap_or("general"), total_cost, total_duration);
let value = self.value.estimate(description, adjusted_cost);
let confidence = self.learner.confidence(category.unwrap_or("general"));
JobEstimate {
cost: adjusted_cost,
duration: adjusted_time,
value,
confidence,
tool_breakdown: tool_estimates,
}
}
/// Record actual results for learning.
pub fn record_actuals(
&mut self,
category: &str,
estimated_cost: Decimal,
actual_cost: Decimal,
estimated_time: Duration,
actual_time: Duration,
) {
self.learner.record(
category,
estimated_cost,
actual_cost,
estimated_time,
actual_time,
);
}
/// Get the cost estimator.
pub fn cost(&self) -> &CostEstimator {
&self.cost
}
/// Get the time estimator.
pub fn time(&self) -> &TimeEstimator {
&self.time
}
/// Get the value estimator.
pub fn value(&self) -> &ValueEstimator {
&self.value
}
}
impl Default for Estimator {
fn default() -> Self {
Self::new()
}
}
+81
View File
@@ -0,0 +1,81 @@
//! Time estimation.
use std::collections::HashMap;
use std::time::Duration;
/// Estimates time for tools and operations.
pub struct TimeEstimator {
/// Base durations per tool.
tool_durations: HashMap<String, Duration>,
}
impl TimeEstimator {
/// Create a new time estimator.
pub fn new() -> Self {
let mut tool_durations = HashMap::new();
// Default tool durations
tool_durations.insert("http".to_string(), Duration::from_secs(5));
tool_durations.insert("marketplace".to_string(), Duration::from_secs(10));
tool_durations.insert("ecommerce".to_string(), Duration::from_secs(8));
tool_durations.insert("taskrabbit".to_string(), Duration::from_secs(30)); // Just API, not task itself
tool_durations.insert("restaurant".to_string(), Duration::from_secs(5));
tool_durations.insert("echo".to_string(), Duration::from_millis(10));
tool_durations.insert("time".to_string(), Duration::from_millis(1));
tool_durations.insert("json".to_string(), Duration::from_millis(5));
Self { tool_durations }
}
/// Estimate duration for a tool call.
pub fn estimate_tool(&self, tool_name: &str) -> Duration {
self.tool_durations
.get(tool_name)
.copied()
.unwrap_or(Duration::from_secs(5)) // Default for unknown tools
}
/// Estimate LLM response time.
pub fn estimate_llm_response(&self, estimated_tokens: u32) -> Duration {
// Rough estimate: ~50 tokens/second
let seconds = estimated_tokens as f64 / 50.0;
Duration::from_secs_f64(seconds.max(1.0))
}
/// Set a tool's base duration.
pub fn set_tool_duration(&mut self, tool_name: impl Into<String>, duration: Duration) {
self.tool_durations.insert(tool_name.into(), duration);
}
/// Get all tool durations.
pub fn all_tool_durations(&self) -> &HashMap<String, Duration> {
&self.tool_durations
}
}
impl Default for TimeEstimator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_time_estimation() {
let estimator = TimeEstimator::new();
assert!(estimator.estimate_tool("echo") < Duration::from_secs(1));
assert!(estimator.estimate_tool("http") >= Duration::from_secs(1));
}
#[test]
fn test_llm_time_estimation() {
let estimator = TimeEstimator::new();
let duration = estimator.estimate_llm_response(500);
assert!(duration >= Duration::from_secs(1));
}
}
+107
View File
@@ -0,0 +1,107 @@
//! Value/earnings estimation.
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
/// Estimates the value/earnings potential of jobs.
pub struct ValueEstimator {
/// Minimum profit margin to aim for.
min_margin: Decimal,
/// Target profit margin.
target_margin: Decimal,
}
impl ValueEstimator {
/// Create a new value estimator.
pub fn new() -> Self {
Self {
min_margin: dec!(0.1), // 10% minimum
target_margin: dec!(0.3), // 30% target
}
}
/// Estimate value for a job based on description and cost.
pub fn estimate(&self, _description: &str, estimated_cost: Decimal) -> Decimal {
// Simple formula: value = cost + margin
// In practice, this would analyze the description to estimate complexity
let margin = estimated_cost * self.target_margin;
estimated_cost + margin
}
/// Calculate minimum acceptable bid.
pub fn minimum_bid(&self, estimated_cost: Decimal) -> Decimal {
estimated_cost + (estimated_cost * self.min_margin)
}
/// Calculate ideal bid.
pub fn ideal_bid(&self, estimated_cost: Decimal) -> Decimal {
estimated_cost + (estimated_cost * self.target_margin)
}
/// Check if a job is profitable at a given price.
pub fn is_profitable(&self, price: Decimal, estimated_cost: Decimal) -> bool {
let margin = (price - estimated_cost) / price;
margin >= self.min_margin
}
/// Calculate profit for a completed job.
pub fn calculate_profit(&self, earnings: Decimal, actual_cost: Decimal) -> Decimal {
earnings - actual_cost
}
/// Calculate profit margin.
pub fn calculate_margin(&self, earnings: Decimal, actual_cost: Decimal) -> Decimal {
if earnings.is_zero() {
return Decimal::ZERO;
}
(earnings - actual_cost) / earnings
}
/// Set minimum margin.
pub fn set_min_margin(&mut self, margin: Decimal) {
self.min_margin = margin;
}
/// Set target margin.
pub fn set_target_margin(&mut self, margin: Decimal) {
self.target_margin = margin;
}
}
impl Default for ValueEstimator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_value_estimation() {
let estimator = ValueEstimator::new();
let cost = dec!(10.0);
let value = estimator.estimate("test job", cost);
assert!(value > cost);
}
#[test]
fn test_profitability() {
let estimator = ValueEstimator::new();
let cost = dec!(10.0);
assert!(estimator.is_profitable(dec!(15.0), cost));
assert!(!estimator.is_profitable(dec!(10.5), cost)); // Only 5% margin
}
#[test]
fn test_margin_calculation() {
let estimator = ValueEstimator::new();
let margin = estimator.calculate_margin(dec!(100.0), dec!(70.0));
assert_eq!(margin, dec!(0.30)); // 30%
}
}