use cano::prelude::*;
use rand::RngExt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum NegotiationAction {
StartSelling,
BuyerEvaluate,
Deal,
NoDeal,
Error,
}
#[derive(Debug, Clone)]
struct NegotiationState {
current_offer: u32,
buyer_budget: u32,
round: u32,
seller_initial_price: u32,
}
impl NegotiationState {
fn new(initial_price: u32, buyer_budget: u32) -> Self {
Self {
current_offer: initial_price,
buyer_budget,
round: 1,
seller_initial_price: initial_price,
}
}
}
#[derive(Clone)]
struct SellerTask;
impl SellerTask {
fn new() -> Self {
Self
}
fn calculate_price_reduction() -> u32 {
let mut rng = rand::rng();
rng.random_range(500..=2000)
}
}
#[task(state = NegotiationAction)]
impl SellerTask {
fn config(&self) -> TaskConfig {
TaskConfig::minimal()
}
async fn run(&self, res: &Resources) -> Result<TaskResult<NegotiationAction>, CanoError> {
let store = res.get::<MemoryStore, _>("store")?;
let mut state = match store.get::<NegotiationState>("negotiation_state") {
Ok(s) => {
println!(
"Seller: Round {} - Current offer on table: ${}",
s.round, s.current_offer
);
s
}
Err(_) => {
let initial_price = 10000;
let buyer_budget = 1000;
let s = NegotiationState::new(initial_price, buyer_budget);
println!("Seller: Starting negotiation!");
println!("Seller: Initial asking price: ${initial_price}");
println!("Buyer budget: ${buyer_budget}");
println!("{}", "=".repeat(50));
s
}
};
if state.round > 1 {
let reduction = Self::calculate_price_reduction();
let new_offer = state.current_offer.saturating_sub(reduction);
let minimum_price = 100;
state.current_offer = std::cmp::max(new_offer, minimum_price);
println!(
"Seller: Round {} - Reducing price by ${}",
state.round, reduction
);
println!("Seller: New offer: ${}", state.current_offer);
if state.current_offer == minimum_price {
println!("Seller: This is my final offer! Can't go any lower.");
}
} else {
println!("Seller: My asking price is ${}", state.current_offer);
}
store.put("negotiation_state", state)?;
println!("Seller: Waiting for buyer's response...");
println!("{}", "-".repeat(30));
Ok(TaskResult::Single(NegotiationAction::BuyerEvaluate))
}
}
#[derive(Clone)]
struct BuyerTask;
impl BuyerTask {
fn new() -> Self {
Self
}
fn evaluate_offer(state: &NegotiationState) -> bool {
let offer_ratio = state.current_offer as f64 / state.buyer_budget as f64;
if state.current_offer <= state.buyer_budget {
return true;
}
if state.round >= 10 && offer_ratio > 3.0 {
return false; }
false
}
}
#[task(state = NegotiationAction)]
impl BuyerTask {
fn config(&self) -> TaskConfig {
TaskConfig::minimal()
}
async fn run(&self, res: &Resources) -> Result<TaskResult<NegotiationAction>, CanoError> {
let store = res.get::<MemoryStore, _>("store")?;
let mut state: NegotiationState = store.get("negotiation_state").map_err(|e| {
CanoError::task_execution(format!("Failed to load negotiation state: {e}"))
})?;
println!(
"Buyer: Evaluating seller's offer of ${}",
state.current_offer
);
let acceptable = Self::evaluate_offer(&state);
if acceptable {
if state.current_offer <= state.buyer_budget {
println!(
"Buyer: Great! This offer (${}) fits my budget (${})",
state.current_offer, state.buyer_budget
);
println!("Buyer: I accept this deal!");
}
} else {
let offer_ratio = state.current_offer as f64 / state.buyer_budget as f64;
if state.round >= 10 && offer_ratio > 3.0 {
println!(
"Buyer: This is taking too long and the offer (${}) is still {}x my budget.",
state.current_offer,
offer_ratio.round() as u32
);
println!("Buyer: I'm walking away from this negotiation.");
} else {
println!(
"Buyer: ${} is still above my budget of ${}.",
state.current_offer, state.buyer_budget
);
println!("Buyer: Can you do better?");
}
}
if acceptable && state.current_offer <= state.buyer_budget {
store.put("final_deal", state.clone())?;
store.remove("negotiation_state")?;
println!("Deal reached in round {}!", state.round);
return Ok(TaskResult::Single(NegotiationAction::Deal));
}
let offer_ratio = state.current_offer as f64 / state.buyer_budget as f64;
if state.round >= 10 && offer_ratio > 3.0 {
store.put("failed_negotiation", state)?;
store.remove("negotiation_state")?;
return Ok(TaskResult::Single(NegotiationAction::NoDeal));
}
state.round += 1;
store.put("negotiation_state", state)?;
println!("{}", "-".repeat(30));
Ok(TaskResult::Single(NegotiationAction::StartSelling))
}
}
async fn run_negotiation_workflow() -> Result<(), CanoError> {
println!("Starting Negotiation Workflow");
println!("================================");
println!("Seller starts at $10,000");
println!("Buyer has a budget of $1,000");
println!("Let's see if they can make a deal!");
println!();
let store = MemoryStore::new();
let workflow = Workflow::new(Resources::new().insert("store", store.clone()))
.register(NegotiationAction::StartSelling, SellerTask::new())
.register(NegotiationAction::BuyerEvaluate, BuyerTask::new())
.add_exit_states(vec![
NegotiationAction::Deal,
NegotiationAction::NoDeal,
NegotiationAction::Error,
]);
match workflow.orchestrate(NegotiationAction::StartSelling).await {
Ok(final_state) => {
println!("{}", "=".repeat(50));
match final_state {
NegotiationAction::Deal => {
println!("NEGOTIATION SUCCESSFUL!");
if let Ok(deal) = store.get::<NegotiationState>("final_deal") {
println!("Final Deal Summary:");
println!(" Final price: ${}", deal.current_offer);
println!(" Buyer budget: ${}", deal.buyer_budget);
println!(" Rounds of negotiation: {}", deal.round);
println!(
" Savings from initial price: ${}",
deal.seller_initial_price - deal.current_offer
);
let savings_percent = ((deal.seller_initial_price - deal.current_offer)
as f64
/ deal.seller_initial_price as f64)
* 100.0;
println!(" Discount achieved: {savings_percent:.1}%");
}
}
NegotiationAction::NoDeal => {
println!("NEGOTIATION FAILED!");
if let Ok(failed) = store.get::<NegotiationState>("failed_negotiation") {
println!("Negotiation Summary:");
println!(" Final offer: ${}", failed.current_offer);
println!(" Buyer budget: ${}", failed.buyer_budget);
println!(" Rounds attempted: {}", failed.round);
println!(
" Gap remaining: ${}",
failed.current_offer - failed.buyer_budget
);
let gap_ratio = failed.current_offer as f64 / failed.buyer_budget as f64;
println!(" Offer was {gap_ratio:.1}x the buyer's budget");
}
println!("The buyer walked away - no deal was reached.");
}
NegotiationAction::Error => {
eprintln!("Negotiation terminated due to an error");
return Err(CanoError::workflow(
"Negotiation terminated with error state",
));
}
other => {
eprintln!("Negotiation ended in unexpected state: {other:?}");
return Err(CanoError::workflow(format!(
"Negotiation ended in unexpected state: {other:?}"
)));
}
}
}
Err(e) => {
eprintln!("Negotiation workflow failed: {e}");
return Err(e);
}
}
Ok(())
}
#[tokio::main]
async fn main() {
println!("Negotiation Workflow Example");
println!("===============================");
match run_negotiation_workflow().await {
Ok(()) => {
println!("\nNegotiation workflow completed!");
}
Err(e) => {
eprintln!("\nNegotiation workflow failed: {e}");
std::process::exit(1);
}
}
println!("\nThis example demonstrates:");
println!(" Inter-task communication via shared store");
println!(" Iterative workflow with conditional routing");
println!(" Random business logic (price reductions)");
println!(" Multiple exit conditions (deal/no deal)");
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_seller_task_initialization() {
let seller = SellerTask::new();
let store = MemoryStore::new();
let res = Resources::new().insert("store", store.clone());
let result = seller.run(&res).await.unwrap();
assert_eq!(result, TaskResult::Single(NegotiationAction::BuyerEvaluate));
let state: NegotiationState = store.get("negotiation_state").unwrap();
assert_eq!(state.current_offer, 10000);
assert_eq!(state.buyer_budget, 1000);
assert_eq!(state.round, 1);
}
#[tokio::test]
async fn test_buyer_task_evaluation() {
let store = MemoryStore::new();
let affordable_state = NegotiationState::new(10000, 1000);
let mut affordable_state_modified = affordable_state.clone();
affordable_state_modified.current_offer = 800; store
.put("negotiation_state", affordable_state_modified.clone())
.unwrap();
let buyer = BuyerTask::new();
let res = Resources::new().insert("store", store.clone());
let result = buyer.run(&res).await.unwrap();
assert_eq!(result, TaskResult::Single(NegotiationAction::Deal));
}
#[tokio::test]
async fn test_buyer_task_rejection() {
let store = MemoryStore::new();
let expensive_state = NegotiationState::new(10000, 1000);
let mut expensive_state_modified = expensive_state.clone();
expensive_state_modified.current_offer = 5000; expensive_state_modified.round = 2; store
.put("negotiation_state", expensive_state_modified)
.unwrap();
let buyer = BuyerTask::new();
let res = Resources::new().insert("store", store.clone());
let result = buyer.run(&res).await.unwrap();
assert_eq!(result, TaskResult::Single(NegotiationAction::StartSelling));
}
#[tokio::test]
async fn test_buyer_task_gives_up() {
let store = MemoryStore::new();
let expensive_state = NegotiationState::new(10000, 1000);
let mut expensive_state_modified = expensive_state.clone();
expensive_state_modified.current_offer = 5000; expensive_state_modified.round = 10; store
.put("negotiation_state", expensive_state_modified)
.unwrap();
let buyer = BuyerTask::new();
let res = Resources::new().insert("store", store.clone());
let result = buyer.run(&res).await.unwrap();
assert_eq!(result, TaskResult::Single(NegotiationAction::NoDeal));
}
#[tokio::test]
async fn test_seller_price_reduction() {
let store = MemoryStore::new();
let initial_state = NegotiationState::new(10000, 1000);
let mut ongoing_state = initial_state.clone();
ongoing_state.round = 2; ongoing_state.current_offer = 8000;
store
.put("negotiation_state", ongoing_state.clone())
.unwrap();
let seller = SellerTask::new();
let res = Resources::new().insert("store", store.clone());
let result = seller.run(&res).await.unwrap();
assert_eq!(result, TaskResult::Single(NegotiationAction::BuyerEvaluate));
let updated_state: NegotiationState = store.get("negotiation_state").unwrap();
assert!(updated_state.current_offer < ongoing_state.current_offer);
assert!(updated_state.current_offer >= 100); }
#[tokio::test]
async fn test_negotiation_state_structure() {
let state = NegotiationState::new(5000, 2000);
assert_eq!(state.current_offer, 5000);
assert_eq!(state.buyer_budget, 2000);
assert_eq!(state.round, 1);
assert_eq!(state.seller_initial_price, 5000);
}
#[tokio::test]
async fn test_price_reduction_range() {
for _ in 0..10 {
let reduction = SellerTask::calculate_price_reduction();
assert!(reduction >= 500);
assert!(reduction <= 2000);
}
}
#[tokio::test]
async fn test_full_negotiation_workflow() {
let result = run_negotiation_workflow().await;
assert!(result.is_ok());
}
}