Skip to main content

brainwires_agent/
market_allocation.rs

1//! Market-Based Resource Allocation with Priority Bidding
2//!
3//! Based on Multi-Agent Coordination Survey and Hierarchical Multi-Agent Systems
4//! research, this module implements market-based allocation where agents bid for
5//! resources with dynamic urgency scores. Higher urgency = higher chance of getting
6//! the resource.
7//!
8//! # Key Concepts
9//!
10//! - **ResourceBid**: Agent's bid with base priority and urgency multiplier
11//! - **AgentBudget**: Budget management for fair allocation
12//! - **MarketAllocator**: Manages auctions and allocations
13//! - **PricingStrategy**: How prices are calculated (first-price, second-price, etc.)
14//! - **UrgencyCalculator**: Dynamic priority based on context
15
16use std::collections::HashMap;
17use std::time::{Duration, Instant};
18
19use serde::{Deserialize, Serialize};
20use tokio::sync::RwLock;
21
22/// Market-based resource allocator
23pub struct MarketAllocator {
24    /// Resource auctions
25    auctions: RwLock<HashMap<String, ResourceAuction>>,
26    /// Agent budgets (for fair allocation)
27    budgets: RwLock<HashMap<String, AgentBudget>>,
28    /// Pricing strategy
29    pricing: PricingStrategy,
30    /// Allocation history for analysis
31    allocation_history: RwLock<Vec<AllocationRecord>>,
32    /// Maximum history entries
33    max_history: usize,
34}
35
36/// A resource auction
37pub struct ResourceAuction {
38    /// Resource being auctioned
39    pub resource_id: String,
40    /// Current bids
41    pub bids: Vec<ResourceBid>,
42    /// Current holder (if any)
43    pub current_holder: Option<CurrentHolder>,
44    /// When the auction started
45    pub auction_start: Instant,
46}
47
48/// Information about current resource holder
49#[derive(Debug, Clone)]
50pub struct CurrentHolder {
51    /// Agent holding the resource
52    pub agent_id: String,
53    /// When they acquired it
54    pub acquired_at: Instant,
55    /// Expected release time (if known)
56    pub expected_release: Option<Instant>,
57}
58
59/// Bid submitted by an agent
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ResourceBid {
62    /// Agent submitting the bid
63    pub agent_id: String,
64    /// Resource being bid on
65    pub resource_id: String,
66    /// Base priority (0-10, static)
67    pub base_priority: u8,
68    /// Urgency multiplier (1.0 = normal, 2.0 = double urgency)
69    pub urgency_multiplier: f32,
70    /// Maximum bid amount from budget
71    pub max_bid: u32,
72    /// Reason for urgency (for logging/debugging)
73    pub urgency_reason: String,
74    /// Estimated hold duration
75    #[serde(skip, default = "default_duration")]
76    pub estimated_duration: Duration,
77    /// When the bid was submitted
78    #[serde(skip, default = "Instant::now")]
79    pub submitted_at: Instant,
80}
81
82/// Default duration for serde deserialization
83fn default_duration() -> Duration {
84    Duration::from_secs(60)
85}
86
87impl ResourceBid {
88    /// Create a new bid
89    pub fn new(agent_id: impl Into<String>, resource_id: impl Into<String>) -> Self {
90        Self {
91            agent_id: agent_id.into(),
92            resource_id: resource_id.into(),
93            base_priority: 5,
94            urgency_multiplier: 1.0,
95            max_bid: 10,
96            urgency_reason: String::new(),
97            estimated_duration: Duration::from_secs(60),
98            submitted_at: Instant::now(),
99        }
100    }
101
102    /// Set base priority
103    pub fn with_priority(mut self, priority: u8) -> Self {
104        self.base_priority = priority.min(10);
105        self
106    }
107
108    /// Set urgency multiplier
109    pub fn with_urgency(mut self, multiplier: f32, reason: impl Into<String>) -> Self {
110        self.urgency_multiplier = multiplier.clamp(0.1, 10.0);
111        self.urgency_reason = reason.into();
112        self
113    }
114
115    /// Set max bid
116    pub fn with_max_bid(mut self, max_bid: u32) -> Self {
117        self.max_bid = max_bid;
118        self
119    }
120
121    /// Set estimated duration
122    pub fn with_duration(mut self, duration: Duration) -> Self {
123        self.estimated_duration = duration;
124        self
125    }
126
127    /// Calculate effective priority
128    pub fn effective_priority(&self) -> f32 {
129        self.base_priority as f32 * self.urgency_multiplier
130    }
131
132    /// Calculate bid score (for ranking)
133    pub fn score(&self) -> f32 {
134        // Combine priority, urgency, and bid amount
135        let priority_factor = self.effective_priority() / 10.0;
136        let bid_factor = (self.max_bid as f32 / 100.0).min(1.0);
137
138        0.7 * priority_factor + 0.3 * bid_factor
139    }
140}
141
142/// Agent's budget for bidding
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct AgentBudget {
145    /// Agent identifier
146    pub agent_id: String,
147    /// Total budget points
148    pub total_budget: u32,
149    /// Currently available points
150    pub available: u32,
151    /// Budget replenishment rate (points per second)
152    pub replenish_rate: f32,
153    /// Last replenishment time
154    #[serde(skip, default = "Instant::now")]
155    pub last_replenish: Instant,
156}
157
158impl AgentBudget {
159    /// Create a new budget
160    pub fn new(agent_id: impl Into<String>, total_budget: u32) -> Self {
161        Self {
162            agent_id: agent_id.into(),
163            total_budget,
164            available: total_budget,
165            replenish_rate: 1.0, // 1 point per second by default
166            last_replenish: Instant::now(),
167        }
168    }
169
170    /// Set replenishment rate
171    pub fn with_replenish_rate(mut self, rate: f32) -> Self {
172        self.replenish_rate = rate.max(0.0);
173        self
174    }
175
176    /// Replenish budget based on elapsed time
177    pub fn replenish(&mut self) {
178        let elapsed = self.last_replenish.elapsed().as_secs_f32();
179        let replenished = (elapsed * self.replenish_rate) as u32;
180        self.available = (self.available + replenished).min(self.total_budget);
181        self.last_replenish = Instant::now();
182    }
183
184    /// Check if agent can afford a bid
185    pub fn can_afford(&self, amount: u32) -> bool {
186        self.available >= amount
187    }
188
189    /// Spend budget points
190    pub fn spend(&mut self, amount: u32) -> bool {
191        if self.available >= amount {
192            self.available -= amount;
193            true
194        } else {
195            false
196        }
197    }
198
199    /// Refund budget points (e.g., if allocation failed)
200    pub fn refund(&mut self, amount: u32) {
201        self.available = (self.available + amount).min(self.total_budget);
202    }
203
204    /// Get current availability as percentage
205    pub fn availability_percent(&self) -> f32 {
206        self.available as f32 / self.total_budget as f32 * 100.0
207    }
208}
209
210/// Strategy for calculating prices
211#[derive(Debug, Clone, Default)]
212pub enum PricingStrategy {
213    /// Winner pays their bid
214    FirstPrice,
215    /// Winner pays second-highest bid + 1
216    #[default]
217    SecondPrice,
218    /// Fixed price based on resource type
219    FixedPrice(HashMap<String, u32>),
220    /// Dynamic based on demand
221    Dynamic {
222        /// Base price before demand adjustment.
223        base_price: u32,
224        /// Multiplier applied per competing bid.
225        demand_multiplier: f32,
226    },
227    /// Free (no budget consumption)
228    Free,
229}
230
231/// Result of an allocation attempt
232#[derive(Debug, Clone)]
233pub enum AllocationResult {
234    /// Resource allocated to agent
235    Allocated {
236        /// Winning agent identifier.
237        agent_id: String,
238        /// Price paid for the allocation.
239        price: u32,
240        /// Position in bid ranking.
241        position: usize,
242    },
243    /// No valid bids
244    NoBids,
245    /// Resource still held by current owner
246    StillHeld {
247        /// Agent currently holding the resource.
248        holder: String,
249        /// Estimated time until release.
250        remaining: Option<Duration>,
251    },
252    /// Agent doesn't have enough budget
253    InsufficientBudget {
254        /// Agent that could not afford.
255        agent_id: String,
256        /// Budget required.
257        required: u32,
258        /// Budget available.
259        available: u32,
260    },
261    /// Bid was outbid by another agent
262    Outbid {
263        /// Agent that was outbid.
264        agent_id: String,
265        /// Agent that won.
266        winning_agent: String,
267        /// Winning bid score.
268        winning_score: f32,
269    },
270}
271
272impl AllocationResult {
273    /// Check if allocation was successful
274    pub fn is_success(&self) -> bool {
275        matches!(self, AllocationResult::Allocated { .. })
276    }
277
278    /// Get the winning agent if allocation succeeded
279    pub fn winning_agent(&self) -> Option<&str> {
280        match self {
281            AllocationResult::Allocated { agent_id, .. } => Some(agent_id),
282            _ => None,
283        }
284    }
285}
286
287/// Record of an allocation for history
288#[derive(Debug, Clone)]
289pub struct AllocationRecord {
290    /// Resource that was allocated
291    pub resource_id: String,
292    /// Winning agent
293    pub winner: String,
294    /// Price paid
295    pub price: u32,
296    /// Number of competing bids
297    pub competing_bids: usize,
298    /// When allocated
299    pub allocated_at: Instant,
300}
301
302impl MarketAllocator {
303    /// Create a new market allocator with default settings
304    pub fn new() -> Self {
305        Self {
306            auctions: RwLock::new(HashMap::new()),
307            budgets: RwLock::new(HashMap::new()),
308            pricing: PricingStrategy::SecondPrice,
309            allocation_history: RwLock::new(Vec::new()),
310            max_history: 1000,
311        }
312    }
313
314    /// Create with a specific pricing strategy
315    pub fn with_pricing(pricing: PricingStrategy) -> Self {
316        Self {
317            auctions: RwLock::new(HashMap::new()),
318            budgets: RwLock::new(HashMap::new()),
319            pricing,
320            allocation_history: RwLock::new(Vec::new()),
321            max_history: 1000,
322        }
323    }
324
325    /// Set maximum history size
326    pub fn with_max_history(mut self, max: usize) -> Self {
327        self.max_history = max;
328        self
329    }
330
331    /// Initialize budget for an agent
332    pub async fn register_agent(&self, agent_id: &str, total_budget: u32, replenish_rate: f32) {
333        self.budgets.write().await.insert(
334            agent_id.to_string(),
335            AgentBudget::new(agent_id, total_budget).with_replenish_rate(replenish_rate),
336        );
337    }
338
339    /// Get an agent's current budget
340    pub async fn get_budget(&self, agent_id: &str) -> Option<AgentBudget> {
341        let mut budgets = self.budgets.write().await;
342        if let Some(budget) = budgets.get_mut(agent_id) {
343            budget.replenish();
344            Some(budget.clone())
345        } else {
346            None
347        }
348    }
349
350    /// Submit a bid for a resource
351    pub async fn submit_bid(&self, bid: ResourceBid) -> Result<(), String> {
352        // Check budget
353        let mut budgets = self.budgets.write().await;
354        let budget = budgets
355            .get_mut(&bid.agent_id)
356            .ok_or_else(|| "Agent not registered".to_string())?;
357
358        budget.replenish();
359        if !budget.can_afford(bid.max_bid) {
360            return Err(format!(
361                "Insufficient budget: have {}, need {}",
362                budget.available, bid.max_bid
363            ));
364        }
365
366        // Add bid to auction
367        let mut auctions = self.auctions.write().await;
368        let auction = auctions
369            .entry(bid.resource_id.clone())
370            .or_insert_with(|| ResourceAuction {
371                resource_id: bid.resource_id.clone(),
372                bids: Vec::new(),
373                current_holder: None,
374                auction_start: Instant::now(),
375            });
376
377        // Remove existing bid from same agent
378        auction.bids.retain(|b| b.agent_id != bid.agent_id);
379        auction.bids.push(bid);
380
381        Ok(())
382    }
383
384    /// Cancel a bid
385    pub async fn cancel_bid(&self, agent_id: &str, resource_id: &str) -> bool {
386        let mut auctions = self.auctions.write().await;
387        if let Some(auction) = auctions.get_mut(resource_id) {
388            let len_before = auction.bids.len();
389            auction.bids.retain(|b| b.agent_id != agent_id);
390            return auction.bids.len() < len_before;
391        }
392        false
393    }
394
395    /// Allocate resource to highest bidder
396    pub async fn allocate(&self, resource_id: &str) -> AllocationResult {
397        let mut auctions = self.auctions.write().await;
398        let auction = match auctions.get_mut(resource_id) {
399            Some(a) => a,
400            None => return AllocationResult::NoBids,
401        };
402
403        // Check if currently held
404        if let Some(ref holder) = auction.current_holder {
405            let remaining = holder
406                .expected_release
407                .map(|r| r.saturating_duration_since(Instant::now()));
408            return AllocationResult::StillHeld {
409                holder: holder.agent_id.clone(),
410                remaining,
411            };
412        }
413
414        if auction.bids.is_empty() {
415            return AllocationResult::NoBids;
416        }
417
418        // Sort by score (highest first)
419        auction.bids.sort_by(|a, b| {
420            b.score()
421                .partial_cmp(&a.score())
422                .unwrap_or(std::cmp::Ordering::Equal)
423        });
424
425        // Calculate price
426        let price = self.calculate_price(&auction.bids);
427
428        // Try to charge the winner
429        let mut budgets = self.budgets.write().await;
430        for (position, bid) in auction.bids.iter().enumerate() {
431            if let Some(budget) = budgets.get_mut(&bid.agent_id) {
432                budget.replenish();
433                if budget.spend(price) {
434                    let winner_id = bid.agent_id.clone();
435                    let expected_release = Some(Instant::now() + bid.estimated_duration);
436
437                    // Record holder
438                    auction.current_holder = Some(CurrentHolder {
439                        agent_id: winner_id.clone(),
440                        acquired_at: Instant::now(),
441                        expected_release,
442                    });
443
444                    // Record allocation
445                    drop(budgets);
446                    let competing_bids = auction.bids.len();
447                    auction.bids.clear();
448                    drop(auctions);
449
450                    self.record_allocation(resource_id, &winner_id, price, competing_bids)
451                        .await;
452
453                    return AllocationResult::Allocated {
454                        agent_id: winner_id,
455                        price,
456                        position,
457                    };
458                } else {
459                    // Winner can't afford, will try next bidder
460                    continue;
461                }
462            }
463        }
464
465        // No one could afford the price
466        let first_bid = &auction.bids[0];
467        AllocationResult::InsufficientBudget {
468            agent_id: first_bid.agent_id.clone(),
469            required: price,
470            available: budgets
471                .get(&first_bid.agent_id)
472                .map(|b| b.available)
473                .unwrap_or(0),
474        }
475    }
476
477    /// Calculate price based on strategy
478    fn calculate_price(&self, bids: &[ResourceBid]) -> u32 {
479        match &self.pricing {
480            PricingStrategy::FirstPrice => bids.first().map(|b| b.max_bid).unwrap_or(0),
481            PricingStrategy::SecondPrice => {
482                if bids.len() >= 2 {
483                    bids[1].max_bid.min(bids[0].max_bid)
484                } else {
485                    1 // Minimum price
486                }
487            }
488            PricingStrategy::FixedPrice(prices) => bids
489                .first()
490                .and_then(|b| prices.get(&b.resource_id))
491                .copied()
492                .unwrap_or(1),
493            PricingStrategy::Dynamic {
494                base_price,
495                demand_multiplier,
496            } => {
497                let demand = bids.len() as f32;
498                (*base_price as f32 * (1.0 + demand * demand_multiplier)) as u32
499            }
500            PricingStrategy::Free => 0,
501        }
502    }
503
504    /// Release a resource (current holder done)
505    pub async fn release(&self, resource_id: &str, agent_id: &str) -> bool {
506        let mut auctions = self.auctions.write().await;
507        if let Some(auction) = auctions.get_mut(resource_id)
508            && let Some(ref holder) = auction.current_holder
509            && holder.agent_id == agent_id
510        {
511            auction.current_holder = None;
512            return true;
513        }
514        false
515    }
516
517    /// Get current market state for a resource
518    pub async fn market_status(&self, resource_id: &str) -> Option<MarketStatus> {
519        let auctions = self.auctions.read().await;
520        auctions.get(resource_id).map(|a| MarketStatus {
521            resource_id: resource_id.to_string(),
522            current_holder: a.current_holder.as_ref().map(|h| h.agent_id.clone()),
523            pending_bids: a.bids.len(),
524            highest_score: a.bids.first().map(|b| b.score()),
525            auction_age: a.auction_start.elapsed(),
526        })
527    }
528
529    /// Get all active auctions
530    pub async fn list_auctions(&self) -> Vec<MarketStatus> {
531        let auctions = self.auctions.read().await;
532        auctions
533            .iter()
534            .map(|(resource_id, a)| MarketStatus {
535                resource_id: resource_id.clone(),
536                current_holder: a.current_holder.as_ref().map(|h| h.agent_id.clone()),
537                pending_bids: a.bids.len(),
538                highest_score: a.bids.first().map(|b| b.score()),
539                auction_age: a.auction_start.elapsed(),
540            })
541            .collect()
542    }
543
544    /// Record an allocation in history
545    async fn record_allocation(
546        &self,
547        resource_id: &str,
548        winner: &str,
549        price: u32,
550        competing_bids: usize,
551    ) {
552        let mut history = self.allocation_history.write().await;
553        history.push(AllocationRecord {
554            resource_id: resource_id.to_string(),
555            winner: winner.to_string(),
556            price,
557            competing_bids,
558            allocated_at: Instant::now(),
559        });
560
561        // Trim history
562        while history.len() > self.max_history {
563            history.remove(0);
564        }
565    }
566
567    /// Get allocation history
568    pub async fn get_history(&self) -> Vec<AllocationRecord> {
569        self.allocation_history.read().await.clone()
570    }
571
572    /// Get market statistics
573    pub async fn get_stats(&self) -> MarketStats {
574        let history = self.allocation_history.read().await;
575        let auctions = self.auctions.read().await;
576        let budgets = self.budgets.read().await;
577
578        let total_allocations = history.len();
579        let total_revenue: u32 = history.iter().map(|r| r.price).sum();
580        let avg_price = if total_allocations > 0 {
581            total_revenue as f32 / total_allocations as f32
582        } else {
583            0.0
584        };
585        let avg_competition = if total_allocations > 0 {
586            history.iter().map(|r| r.competing_bids).sum::<usize>() as f32
587                / total_allocations as f32
588        } else {
589            0.0
590        };
591
592        MarketStats {
593            active_auctions: auctions.len(),
594            total_pending_bids: auctions.values().map(|a| a.bids.len()).sum(),
595            registered_agents: budgets.len(),
596            total_allocations,
597            total_revenue,
598            avg_price,
599            avg_competition,
600        }
601    }
602}
603
604impl Default for MarketAllocator {
605    fn default() -> Self {
606        Self::new()
607    }
608}
609
610/// Status of a specific resource's market
611#[derive(Debug, Clone)]
612pub struct MarketStatus {
613    /// Resource identifier
614    pub resource_id: String,
615    /// Current holder (if any)
616    pub current_holder: Option<String>,
617    /// Number of pending bids
618    pub pending_bids: usize,
619    /// Highest bid score (if any bids)
620    pub highest_score: Option<f32>,
621    /// How long the auction has been running
622    pub auction_age: Duration,
623}
624
625/// Overall market statistics
626#[derive(Debug, Clone)]
627pub struct MarketStats {
628    /// Number of active auctions
629    pub active_auctions: usize,
630    /// Total pending bids across all auctions
631    pub total_pending_bids: usize,
632    /// Number of registered agents
633    pub registered_agents: usize,
634    /// Total allocations made
635    pub total_allocations: usize,
636    /// Total revenue (budget points collected)
637    pub total_revenue: u32,
638    /// Average price per allocation
639    pub avg_price: f32,
640    /// Average number of competing bids
641    pub avg_competition: f32,
642}
643
644/// Urgency factors for dynamic priority calculation
645pub struct UrgencyCalculator;
646
647impl UrgencyCalculator {
648    /// Calculate urgency multiplier based on context
649    pub fn calculate(context: &UrgencyContext) -> f32 {
650        let mut multiplier = 1.0;
651
652        // User is actively waiting
653        if context.user_waiting {
654            multiplier *= 2.0;
655        }
656
657        // Deadline approaching
658        if let Some(deadline) = context.deadline {
659            let remaining = deadline.saturating_duration_since(Instant::now());
660            if remaining < Duration::from_secs(60) {
661                multiplier *= 3.0;
662            } else if remaining < Duration::from_secs(300) {
663                multiplier *= 2.0;
664            } else if remaining < Duration::from_secs(600) {
665                multiplier *= 1.5;
666            }
667        }
668
669        // Part of critical path
670        if context.critical_path {
671            multiplier *= 1.5;
672        }
673
674        // Holding other resources (avoid starvation)
675        multiplier *= 1.0 + (context.resources_held as f32 * 0.2);
676
677        // Wait time factor (longer wait = higher urgency)
678        if let Some(wait_time) = context.wait_time {
679            let wait_secs = wait_time.as_secs();
680            if wait_secs > 60 {
681                multiplier *= 1.0 + (wait_secs as f32 / 120.0).min(2.0);
682            }
683        }
684
685        multiplier.min(10.0) // Cap at 10x
686    }
687
688    /// Create an urgency context builder
689    pub fn builder() -> UrgencyContextBuilder {
690        UrgencyContextBuilder::new()
691    }
692}
693
694/// Context for calculating urgency
695#[derive(Debug, Clone, Default)]
696pub struct UrgencyContext {
697    /// User is actively waiting for the result
698    pub user_waiting: bool,
699    /// Deadline for the operation (if any)
700    pub deadline: Option<Instant>,
701    /// Operation is on the critical path
702    pub critical_path: bool,
703    /// Number of other resources currently held
704    pub resources_held: usize,
705    /// How long the agent has been waiting for this resource
706    pub wait_time: Option<Duration>,
707}
708
709/// Builder for UrgencyContext
710pub struct UrgencyContextBuilder {
711    context: UrgencyContext,
712}
713
714impl UrgencyContextBuilder {
715    /// Create a new builder
716    pub fn new() -> Self {
717        Self {
718            context: UrgencyContext::default(),
719        }
720    }
721
722    /// Set user waiting flag
723    pub fn user_waiting(mut self, waiting: bool) -> Self {
724        self.context.user_waiting = waiting;
725        self
726    }
727
728    /// Set deadline
729    pub fn deadline(mut self, deadline: Instant) -> Self {
730        self.context.deadline = Some(deadline);
731        self
732    }
733
734    /// Set deadline from duration
735    pub fn deadline_in(mut self, duration: Duration) -> Self {
736        self.context.deadline = Some(Instant::now() + duration);
737        self
738    }
739
740    /// Set critical path flag
741    pub fn critical_path(mut self, critical: bool) -> Self {
742        self.context.critical_path = critical;
743        self
744    }
745
746    /// Set resources held count
747    pub fn resources_held(mut self, count: usize) -> Self {
748        self.context.resources_held = count;
749        self
750    }
751
752    /// Set wait time
753    pub fn wait_time(mut self, duration: Duration) -> Self {
754        self.context.wait_time = Some(duration);
755        self
756    }
757
758    /// Build the context
759    pub fn build(self) -> UrgencyContext {
760        self.context
761    }
762
763    /// Calculate urgency from the built context
764    pub fn calculate(self) -> f32 {
765        UrgencyCalculator::calculate(&self.context)
766    }
767}
768
769impl Default for UrgencyContextBuilder {
770    fn default() -> Self {
771        Self::new()
772    }
773}
774
775#[cfg(test)]
776mod tests {
777    use super::*;
778
779    #[tokio::test]
780    async fn test_agent_registration() {
781        let allocator = MarketAllocator::new();
782
783        allocator.register_agent("agent-1", 100, 1.0).await;
784
785        let budget = allocator.get_budget("agent-1").await.unwrap();
786        assert_eq!(budget.total_budget, 100);
787        assert_eq!(budget.available, 100);
788    }
789
790    #[tokio::test]
791    async fn test_submit_bid() {
792        let allocator = MarketAllocator::new();
793
794        allocator.register_agent("agent-1", 100, 1.0).await;
795
796        let bid = ResourceBid::new("agent-1", "resource-a")
797            .with_priority(8)
798            .with_urgency(1.5, "user waiting")
799            .with_max_bid(20);
800
801        let result = allocator.submit_bid(bid).await;
802        assert!(result.is_ok());
803
804        let status = allocator.market_status("resource-a").await.unwrap();
805        assert_eq!(status.pending_bids, 1);
806    }
807
808    #[tokio::test]
809    async fn test_allocation() {
810        let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
811
812        allocator.register_agent("agent-1", 100, 1.0).await;
813        allocator.register_agent("agent-2", 100, 1.0).await;
814
815        // Agent 1 bids with lower priority
816        let bid1 = ResourceBid::new("agent-1", "resource-a").with_priority(5);
817        allocator.submit_bid(bid1).await.unwrap();
818
819        // Agent 2 bids with higher priority
820        let bid2 = ResourceBid::new("agent-2", "resource-a").with_priority(8);
821        allocator.submit_bid(bid2).await.unwrap();
822
823        // Allocate - agent-2 should win
824        let result = allocator.allocate("resource-a").await;
825        match result {
826            AllocationResult::Allocated { agent_id, .. } => {
827                assert_eq!(agent_id, "agent-2");
828            }
829            _ => panic!("Expected allocation"),
830        }
831    }
832
833    #[tokio::test]
834    async fn test_urgency_affects_allocation() {
835        let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
836
837        allocator.register_agent("agent-1", 100, 1.0).await;
838        allocator.register_agent("agent-2", 100, 1.0).await;
839
840        // Agent 1 has higher base priority but lower urgency
841        let bid1 = ResourceBid::new("agent-1", "resource-a")
842            .with_priority(8)
843            .with_urgency(1.0, "normal");
844        allocator.submit_bid(bid1).await.unwrap();
845
846        // Agent 2 has lower base priority but higher urgency
847        let bid2 = ResourceBid::new("agent-2", "resource-a")
848            .with_priority(5)
849            .with_urgency(2.5, "deadline approaching");
850        allocator.submit_bid(bid2).await.unwrap();
851
852        // Agent 1 effective: 8 * 1.0 = 8
853        // Agent 2 effective: 5 * 2.5 = 12.5
854        // Agent 2 should win
855
856        let result = allocator.allocate("resource-a").await;
857        match result {
858            AllocationResult::Allocated { agent_id, .. } => {
859                assert_eq!(agent_id, "agent-2");
860            }
861            _ => panic!("Expected allocation"),
862        }
863    }
864
865    #[tokio::test]
866    async fn test_second_price_auction() {
867        let allocator = MarketAllocator::with_pricing(PricingStrategy::SecondPrice);
868
869        allocator.register_agent("agent-1", 100, 1.0).await;
870        allocator.register_agent("agent-2", 100, 1.0).await;
871
872        // Agent 1 bids 30
873        let bid1 = ResourceBid::new("agent-1", "resource-a")
874            .with_priority(8)
875            .with_max_bid(30);
876        allocator.submit_bid(bid1).await.unwrap();
877
878        // Agent 2 bids 20
879        let bid2 = ResourceBid::new("agent-2", "resource-a")
880            .with_priority(5)
881            .with_max_bid(20);
882        allocator.submit_bid(bid2).await.unwrap();
883
884        // Agent 1 should win but pay agent-2's bid (20)
885        let result = allocator.allocate("resource-a").await;
886        match result {
887            AllocationResult::Allocated { price, .. } => {
888                assert_eq!(price, 20);
889            }
890            _ => panic!("Expected allocation"),
891        }
892    }
893
894    #[tokio::test]
895    async fn test_insufficient_budget() {
896        let allocator = MarketAllocator::with_pricing(PricingStrategy::FirstPrice);
897
898        allocator.register_agent("agent-1", 10, 1.0).await;
899
900        // Try to bid more than budget allows
901        let bid = ResourceBid::new("agent-1", "resource-a").with_max_bid(20);
902        let result = allocator.submit_bid(bid).await;
903
904        assert!(result.is_err());
905        assert!(result.unwrap_err().contains("Insufficient budget"));
906    }
907
908    #[tokio::test]
909    async fn test_release_resource() {
910        let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
911
912        allocator.register_agent("agent-1", 100, 1.0).await;
913
914        let bid = ResourceBid::new("agent-1", "resource-a");
915        allocator.submit_bid(bid).await.unwrap();
916        allocator.allocate("resource-a").await;
917
918        // Resource is held
919        let status = allocator.market_status("resource-a").await.unwrap();
920        assert!(status.current_holder.is_some());
921
922        // Release
923        let released = allocator.release("resource-a", "agent-1").await;
924        assert!(released);
925
926        // Resource is free
927        let status = allocator.market_status("resource-a").await.unwrap();
928        assert!(status.current_holder.is_none());
929    }
930
931    #[tokio::test]
932    async fn test_cannot_allocate_held_resource() {
933        let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
934
935        allocator.register_agent("agent-1", 100, 1.0).await;
936        allocator.register_agent("agent-2", 100, 1.0).await;
937
938        // Agent 1 gets the resource
939        let bid1 = ResourceBid::new("agent-1", "resource-a");
940        allocator.submit_bid(bid1).await.unwrap();
941        allocator.allocate("resource-a").await;
942
943        // Agent 2 tries to bid and allocate
944        let bid2 = ResourceBid::new("agent-2", "resource-a");
945        allocator.submit_bid(bid2).await.unwrap();
946
947        let result = allocator.allocate("resource-a").await;
948        match result {
949            AllocationResult::StillHeld { holder, .. } => {
950                assert_eq!(holder, "agent-1");
951            }
952            _ => panic!("Expected StillHeld result"),
953        }
954    }
955
956    #[test]
957    fn test_urgency_calculator() {
958        // Normal context
959        let context = UrgencyContext::default();
960        let urgency = UrgencyCalculator::calculate(&context);
961        assert!((urgency - 1.0).abs() < 0.01);
962
963        // User waiting
964        let context = UrgencyContext {
965            user_waiting: true,
966            ..Default::default()
967        };
968        let urgency = UrgencyCalculator::calculate(&context);
969        assert!((urgency - 2.0).abs() < 0.01);
970
971        // Critical path
972        let context = UrgencyContext {
973            critical_path: true,
974            ..Default::default()
975        };
976        let urgency = UrgencyCalculator::calculate(&context);
977        assert!((urgency - 1.5).abs() < 0.01);
978
979        // Both
980        let context = UrgencyContext {
981            user_waiting: true,
982            critical_path: true,
983            ..Default::default()
984        };
985        let urgency = UrgencyCalculator::calculate(&context);
986        assert!((urgency - 3.0).abs() < 0.01);
987    }
988
989    #[test]
990    fn test_urgency_builder() {
991        let urgency = UrgencyCalculator::builder()
992            .user_waiting(true)
993            .critical_path(true)
994            .resources_held(2)
995            .calculate();
996
997        // 1.0 * 2.0 (user waiting) * 1.5 (critical) * 1.4 (2 resources held)
998        // = 4.2
999        assert!(urgency > 4.0 && urgency < 4.5);
1000    }
1001
1002    #[tokio::test]
1003    async fn test_budget_replenishment() {
1004        let mut budget = AgentBudget::new("agent-1", 100).with_replenish_rate(10.0);
1005
1006        // Spend some
1007        budget.spend(50);
1008        assert_eq!(budget.available, 50);
1009
1010        // Simulate time passing (we can't actually wait in tests)
1011        // But we can verify the replenish logic
1012        budget.last_replenish = Instant::now() - Duration::from_secs(5);
1013        budget.replenish();
1014
1015        // Should have replenished 50 points (5 seconds * 10 per second)
1016        assert_eq!(budget.available, 100); // Capped at total
1017    }
1018
1019    #[tokio::test]
1020    async fn test_market_stats() {
1021        let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
1022
1023        allocator.register_agent("agent-1", 100, 1.0).await;
1024        allocator.register_agent("agent-2", 100, 1.0).await;
1025
1026        // Make some allocations
1027        for i in 0..5 {
1028            let bid = ResourceBid::new("agent-1", format!("resource-{}", i));
1029            allocator.submit_bid(bid).await.unwrap();
1030            allocator.allocate(&format!("resource-{}", i)).await;
1031        }
1032
1033        let stats = allocator.get_stats().await;
1034        assert_eq!(stats.registered_agents, 2);
1035        assert_eq!(stats.total_allocations, 5);
1036    }
1037
1038    #[test]
1039    fn test_bid_scoring() {
1040        // Higher priority = higher score
1041        let bid1 = ResourceBid::new("agent-1", "resource").with_priority(8);
1042        let bid2 = ResourceBid::new("agent-2", "resource").with_priority(5);
1043        assert!(bid1.score() > bid2.score());
1044
1045        // Higher urgency = higher effective priority
1046        let bid3 = ResourceBid::new("agent-3", "resource")
1047            .with_priority(5)
1048            .with_urgency(2.0, "urgent");
1049        assert!(bid3.effective_priority() > bid2.effective_priority());
1050    }
1051
1052    #[tokio::test]
1053    async fn test_cancel_bid() {
1054        let allocator = MarketAllocator::new();
1055
1056        allocator.register_agent("agent-1", 100, 1.0).await;
1057
1058        let bid = ResourceBid::new("agent-1", "resource-a");
1059        allocator.submit_bid(bid).await.unwrap();
1060
1061        let status = allocator.market_status("resource-a").await.unwrap();
1062        assert_eq!(status.pending_bids, 1);
1063
1064        let cancelled = allocator.cancel_bid("agent-1", "resource-a").await;
1065        assert!(cancelled);
1066
1067        let status = allocator.market_status("resource-a").await.unwrap();
1068        assert_eq!(status.pending_bids, 0);
1069    }
1070}