use async_trait::async_trait;
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 SellerNode;
impl SellerNode {
fn new() -> Self {
Self
}
fn calculate_price_reduction() -> u32 {
let mut rng = rand::rng();
rng.random_range(500..=2000)
}
}
#[async_trait]
impl Node<NegotiationAction> for SellerNode {
type PrepResult = NegotiationState;
type ExecResult = NegotiationState;
fn config(&self) -> TaskConfig {
TaskConfig::minimal()
}
async fn prep(&self, store: &MemoryStore) -> Result<Self::PrepResult, CanoError> {
match store.get::<NegotiationState>("negotiation_state") {
Ok(state) => {
println!(
"🏷️ Seller: Round {} - Current offer on table: ${}",
state.round, state.current_offer
);
Ok(state)
}
Err(_) => {
let initial_price = 10000;
let buyer_budget = 1000;
let state = 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));
Ok(state)
}
}
}
async fn exec(&self, prep_res: Self::PrepResult) -> Self::ExecResult {
let mut state = prep_res;
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);
}
state
}
async fn post(
&self,
store: &MemoryStore,
exec_res: Self::ExecResult,
) -> Result<NegotiationAction, CanoError> {
store.put("negotiation_state", exec_res.clone())?;
println!("🏷️ Seller: Waiting for buyer's response...");
println!("{}", "-".repeat(30));
Ok(NegotiationAction::BuyerEvaluate)
}
}
#[derive(Clone)]
struct BuyerNode;
impl BuyerNode {
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
}
}
#[async_trait]
impl Node<NegotiationAction> for BuyerNode {
type PrepResult = NegotiationState;
type ExecResult = (NegotiationState, bool);
fn config(&self) -> TaskConfig {
TaskConfig::minimal()
}
async fn prep(&self, store: &MemoryStore) -> Result<Self::PrepResult, CanoError> {
let state: NegotiationState = store.get("negotiation_state").map_err(|e| {
CanoError::preparation(format!("Failed to load negotiation state: {e}"))
})?;
println!(
"💰 Buyer: Evaluating seller's offer of ${}",
state.current_offer
);
Ok(state)
}
async fn exec(&self, prep_res: Self::PrepResult) -> Self::ExecResult {
let state = prep_res;
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?");
}
}
(state, acceptable)
}
async fn post(
&self,
store: &MemoryStore,
exec_res: Self::ExecResult,
) -> Result<NegotiationAction, CanoError> {
let (mut state, acceptable) = exec_res;
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(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(NegotiationAction::NoDeal);
}
state.round += 1;
store.put("negotiation_state", state)?;
println!("{}", "-".repeat(30));
Ok(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(store.clone())
.register(NegotiationAction::StartSelling, SellerNode::new())
.register(NegotiationAction::BuyerEvaluate, BuyerNode::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!("\n✅ Negotiation workflow completed!");
}
Err(e) => {
eprintln!("\n❌ Negotiation workflow failed: {e}");
std::process::exit(1);
}
}
println!("\n🎭 This example demonstrates:");
println!(" • Inter-node 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_node_initialization() {
let seller = SellerNode::new();
let store = MemoryStore::new();
let result = seller.run(&store).await.unwrap();
assert_eq!(result, 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_node_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 = BuyerNode::new();
let result = buyer.run(&store).await.unwrap();
assert_eq!(result, NegotiationAction::Deal);
}
#[tokio::test]
async fn test_buyer_node_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 = BuyerNode::new();
let result = buyer.run(&store).await.unwrap();
assert_eq!(result, NegotiationAction::StartSelling);
}
#[tokio::test]
async fn test_buyer_node_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 = BuyerNode::new();
let result = buyer.run(&store).await.unwrap();
assert_eq!(result, 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 = SellerNode::new();
let result = seller.run(&store).await.unwrap();
assert_eq!(result, 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 = SellerNode::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());
}
}