100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
AI Agents & Agentic Workflows
55 minadvanced

Advanced Deployment Exercise

What You'll Build

In this exercise, you will build an autonomous cricket match orchestration system using agentic workflows. The system implements multiple specialized agents that operate independently yet coordinate to simulate and analyze a complete cricket match, spanning toss decisions through final scorecard generation.

The architecture demonstrates hierarchical agent delegation, in which a master orchestrator agent manages player selection agents, match simulation agents, and analytics agents. Each agent has clearly defined responsibilities, reasoning capabilities, and state persistence.

The system uses tool-calling patterns to handle complex cricket mechanics—such as run calculations, wicket logic, and powerplay constraints—and leverages multi-turn reasoning to adapt decisions based on match state. This hands-on project illustrates why decomposing complex workflows into autonomous agents, rather than monolithic pipelines, enables flexibility, maintainability, and emergent problem-solving in production AI systems.

Analogy🏏Cricket
🏏 Think of it like cricket: Imagine Rohit Sharma as captain during a T20 match facing a world-class bowling attack in the powerplay. Rohit cannot make all decisions alone—he must delegate: he instructs his batting coach (via field signals) to assess whether the opposition is bowling tight lines (the Batting Strategist Agent), simultaneously his fast-bowling coach monitors the opposing batsman's weaknesses to suggest death bowling strategies (the Bowling Analyst Agent), and his fielding captain repositions the field based on match phases and batter tendencies (the Field Placement Agent). Each agent operates independently with their own observation-reasoning-decision loop: the Batting Strategist watches run rate versus wicket ratio and decides acceleration timing; the Bowling Analyst monitors economy and suggests bowling changes; the Field Placement Agent tracks field gaps and batter strengths. Rohit (the Primary Agent) integrates all three recommendations, resolves conflicts (e.g., aggressive batting vs. cautious bowling), and issues final instructions. If the situation changes—a key wicket falls, run rate drops—all agents recalibrate simultaneously, and the captain re-orchestrates. This mirrors how agentic workflows operate: multiple specialized reasoning units working in parallel with autonomous decision-making, coordinated by a primary orchestrator that synthesizes recommendations and handles conflicts under time pressure.

Prerequisites

  • Solid understanding of agent architecture: roles, state management, and action spaces from earlier agentic workflow lessons.
  • Experience implementing tool-calling patterns where agents invoke deterministic functions (e.g., calculate_runs, apply_boundary_rules).
  • Familiarity with cricket terminology: ODI format, powerplay rules, wicket types, run calculations, economy rate, and match-winning conditions.
  • Proficiency in Python async/await patterns and message-passing systems for coordinating multiple agent instances.
  • Knowledge of state machines to track cricket match phases (pre-match, innings 1, innings 2, post-match) and agent lifecycle.

Setup & Project Structure

Begin by initializing a Python project with a modular structure that supports multiple agent classes, shared cricket domain models, and tool libraries. Create directories for agents (orchestrator, player selection, match simulation, and analytics), cricket logic (scorecard, delivery mechanics, and rules), and configuration.

Analogy🏏Cricket
🏏 Think of it like cricket: structuring this project is like laying out a team's operations before a series, with every function in its own clearly labelled room. Just as batters, bowlers, and support staff each have a dedicated space, you separate concerns across directories — an agents directory holding each specialist implementation, a models directory holding shared records like Match, Innings, and Bowler that everyone references as the single source of truth, and a communication layer that carries messages between them like the captain's signals to the field. Just as a squad relies on one agreed scorebook so batters and analysts never argue over the numbers, the shared data models give every agent one consistent view of the match. This reveals the payoff: with logic, shared state, and protocols cleanly isolated, you can develop and test each specialist agent on its own, exactly as a coach drills the bowlers separately before bringing the full XI together.

Install the required dependencies, including an LLM client library such as the Anthropic Claude SDK for agent reasoning, Pydantic for type-safe data models, and pytest for testing. The project layout is designed to enforce separation of concerns: agents contain reasoning loops, models contain cricket domain objects, tools contain deterministic cricket mechanics, and tests validate agent behavior both in isolation and during integration.

bash
# Initialize project structure for cricket agentic workflows
mkdir cricket-agentic-orchestrator
cd cricket-agentic-orchestrator

# Create directory hierarchy
mkdir -p agents/{orchestrator,selection,simulation,analytics}
mkdir -p models/{cricket_domain,match_state}
mkdir -p tools/{cricket_mechanics,scorecard}
mkdir -p tests
mkdir -p config

# Initialize Python project
python3 -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Create requirements.txt with dependencies
cat > requirements.txt << 'EOF'
anthropics==0.32.0
pydantic==2.5.0
pydantic-settings==2.1.0
python-dotenv==1.0.0
pytest==7.4.3
pytest-asyncio==0.21.1
aiohttp==3.9.1
typing-extensions==4.9.0
EOF

# Install dependencies
pip install -r requirements.txt

# Create main entry point and config
touch main.py
touch config/agent_config.py
touch .env

echo "Project structure initialized. Run: python main.py to execute match orchestration."

Step 1 — Foundation

Step 1 establishes the cricket domain model and match state infrastructure. You will define immutable Pydantic models representing cricketers, teams, innings, deliveries, and match context. The CricketMatch state object tracks the current inning, overs, runs, wickets, and match phase.

This foundational layer is critical because agents will reason about these objects and pass them between tool calls and state updates. Implementing it first ensures type safety, since Pydantic validates structure at every step and prevents agents from accidentally corrupting match state.

The models also serve as the shared semantic layer across all agents. Every agent interprets a Player object identically, understands Delivery events in the same way, and agrees on scorecard calculations. This consistency prevents the distributed intelligence problem, in which agents hallucinate inconsistent match states.

Analogy🏏Cricket
🏏 Think of it like cricket: Before any match analysis can happen, the cricket commentary team (our agents) needs a shared scorecard format and communication protocol. Just as the official scorecard standardizes how runs, wickets, and overs are recorded—allowing commentators, analysts, and broadcasters to reference identical match state—our BaseAgent and data models create a standardized 'match scorecard' that all agents read from. Each commentator (specialist agent) observes the same scorecard and broadcasts their analysis using standard terminology: "Confidence 0.9: Recommend aggressive batting" (the Batting Strategist), "Confidence 0.85: Bowl yorkers" (the Bowling Analyst). The immutability of our match state (like an official scorecard that cannot be retroactively changed) prevents commentators from accidentally contradicting each other because they're all reading the exact same source of truth. The confidence score mirrors a commentator's certainty level: a seasoned cricket analyst might say "Definitely recommend field adjustment" (high confidence) versus "Possibly consider slip placement" (lower confidence). This standardized foundation allows the Primary Agent (match captain) to orchestrate responses without micromanaging each specialist's internal reasoning.
python
# models/cricket_domain.py
from pydantic import BaseModel, Field
from enum import Enum
from datetime import datetime
from typing import List, Optional

class CountryCode(str, Enum):
    INDIA = "IND"
    AUSTRALIA = "AUS"
    PAKISTAN = "PAK"
    ENGLAND = "ENG"
    NEWZEALAND = "NZ"

class BattingStyle(str, Enum):
    LEFT_HAND = "left"
    RIGHT_HAND = "right"

class BowlingStyle(str, Enum):
    FAST = "fast"
    SPIN = "spin"
    MEDIUM = "medium"

class CricketPlayer(BaseModel):
    """Represents an individual cricketer with stats and role."""
    player_id: str
    name: str
    country: CountryCode
    batting_style: BattingStyle
    bowling_style: Optional[BowlingStyle] = None
    average_strike_rate: float = Field(ge=70, le=150)  # Validation: realistic range
    average_boundary_percentage: float = Field(ge=0, le=100)
    wicket_taking_rate: Optional[float] = None  # Balls per wicket
    economy_rate: Optional[float] = None  # Runs per over
    role: str = Field(default="batsman")  # batsman, bowler, all-rounder
    jersey_number: int = Field(ge=1, le=15)
    match_fitness: float = Field(ge=0, le=1, default=1.0)  # 0=injured, 1=fully fit

    class Config:
        use_enum_values = True

class CricketTeam(BaseModel):
    """Represents a cricket team with players and captain."""
    team_id: str
    team_name: str
    country: CountryCode
    squad: List[CricketPlayer]
    captain_id: str
    vice_captain_id: str
    home_ground: str
    odi_ranking: int = Field(ge=1, le=12)

class Delivery(BaseModel):
    """Represents a single delivery (ball) bowled in cricket."""
    delivery_id: str
    over_number: int = Field(ge=0, le=50)
    ball_number: int = Field(ge=1, le=6)
    bowler_name: str
    batter_name: str
    runs_scored: int = Field(ge=0, le=6)
    is_boundary: bool
    is_wicket: bool
    wicket_type: Optional[str] = None  # bowled, lbw, caught, etc.
    bowler_economy_this_over: float = Field(ge=0, le=100)
    timestamp: datetime

class Innings(BaseModel):
    """Represents one team's batting innings."""
    innings_id: str
    batting_team: CricketTeam
    bowling_team: CricketTeam
    total_runs: int = Field(ge=0, le=500)
    total_wickets: int = Field(ge=0, le=10)
    overs_completed: float = Field(ge=0, le=50)  # e.g., 20.3 = 20 overs, 3 balls
    deliveries: List[Delivery] = []
    run_rate: float = Field(ge=0, le=20)  # Current runs per over
    powerplay_runs: int = Field(ge=0, le=150)
    death_overs_runs: int = Field(ge=0, le=200)
    highest_individual_score: int = Field(ge=0, le=150)
    boundaries_hit: int = Field(ge=0, le=80)

class CricketMatch(BaseModel):
    """Represents a complete ODI cricket match."""
    match_id: str
    match_date: datetime
    venue: str
    toss_won_by: str  # team name
    toss_decision: str = Field(pattern="^(bat|bowl)$")  # Toss winner's choice
    team1: CricketTeam
    team2: CricketTeam
    innings1: Optional[Innings] = None
    innings2: Optional[Innings] = None
    match_phase: str = Field(default="pre_match")  # pre_match, innings1, innings2, completed
    winner: Optional[str] = None
    winning_margin: Optional[str] = None  # e.g., "by 25 runs" or "by 5 wickets"
    man_of_match: Optional[str] = None
    created_at: datetime = Field(default_factory=datetime.utcnow)

class AgentThought(BaseModel):
    """Captures an agent's reasoning step."""
    agent_name: str
    timestamp: datetime = Field(default_factory=datetime.utcnow)
    thought: str
    tool_called: Optional[str] = None
    tool_input: dict = {}
    tool_result: Optional[str] = None
    confidence: float = Field(ge=0, le=1)

Step 2 — Core Logic

Step 2 implements the cricket mechanics tools and the orchestrator agent framework. Cricket mechanics are expressed as deterministic functions: calculate_run_rate takes a delivery log and returns runs per over, apply_powerplay_multiplier adjusts probabilities during overs 1 through 6, and determine_wicket_type applies logic for bowled versus caught outcomes based on delivery characteristics. These tools serve as the authoritative source of cricket rules—agents can invoke them with parameters but cannot override them.

The orchestrator agent acts as the master coordinator for the entire system. It receives a match setup consisting of two teams and a venue, reasons about strategy including toss analysis and opening pair selection, delegates tasks to subordinate agents such as the player-selection agent for XI composition and the simulation agent for delivery execution, and maintains the authoritative match state throughout.

This separation between business logic and reasoning logic is architecturally significant. By keeping cricket rules within deterministic tools and agent decision-making within the reasoning layer, both components remain independently testable and auditable.

Analogy🏏Cricket
🏏 Think of it like cricket: Consider the Indian team facing Australia in a T20 World Cup final. Rohit's batting coach (BattingStrategistAgent) watches the scorecard: India is 45/2 in 6 overs requiring 160, and the run rate is 7.5 but the required rate is 10.2 with death bowling coming. The coach reasons: "Acceleration needed, but Hazlewood is fresh—risk is moderate." Confidence: 0.75. Simultaneously, Bharat Arun (bowling coach, BowlingAnalystAgent) observes that Starc has bowled 3 overs for 18 runs with economy of 6.0, but has 2 overs left and tends to excel in death. He reasons: "Hold Starc for death overs, deploy Bumrah now against Travis Head (left-hander who struggles to short balls)." Confidence: 0.88. Meanwhile, the fielding coach (FieldPlacementAgent) watches Travis Head's batting patterns—he's aggressive against spinners on off-side, averaging 45 runs in that corridor. Coach reasons: "Place short fine-leg and deep extra-cover to block his favorite zones." Confidence: 0.82. Each coach observes independently, reasons from their expertise, and delivers a recommendation with conviction level. Rohit (Primary Agent, coming in Step 3) synthesizes these three independent analyses into one coherent match strategy.
python
# tools/cricket_mechanics.py
import random
from typing import Tuple, Dict
from models.cricket_domain import Delivery, Innings, CricketPlayer
from datetime import datetime

class CricketMechanicsEngine:
    """Deterministic cricket rule engine. Source of truth for match outcomes."""

    @staticmethod
    def simulate_delivery(
        bowler: CricketPlayer,
        batter: CricketPlayer,
        over_number: int,
        is_powerplay: bool,
        bowler_state: Dict,
    ) -> Tuple[int, bool, str, float]:
        """
        Simulate a single delivery outcome.
        Returns: (runs_scored, is_wicket, wicket_type, bowler_economy)
        """
        # Wicket probability increases if bowler is good, decreases if batter is in form
        base_wicket_prob = 0.03  # 3% base wicket probability per delivery
        if bowler.role == "bowler" and bowler.wicket_taking_rate:
            base_wicket_prob += (6 - bowler.wicket_taking_rate) * 0.001
        if batter.average_strike_rate > 120:
            base_wicket_prob *= 0.7  # Batters in form less likely to get out

        # Powerplay has different boundary probabilities
        boundary_multiplier = 1.5 if is_powerplay else 1.0
        boundary_prob = (batter.average_boundary_percentage / 100) * boundary_multiplier * 0.15

        # Decision tree
        rand = random.random()
        if rand < base_wicket_prob:
            wicket_types = ["bowled", "lbw", "caught", "leg_before_wicket"]
            wicket = random.choice(wicket_types)
            return (0, True, wicket, bowler.economy_rate or 5.0)
        elif rand < base_wicket_prob + boundary_prob:
            return (4, False, "", bowler.economy_rate or 5.0)
        elif rand < base_wicket_prob + boundary_prob + (boundary_prob * 0.3):
            return (6, False, "", (bowler.economy_rate or 5.0) + 1.0)
        else:
            # Dot ball or single
            if random.random() < 0.6:
                return (0, False, "", bowler.economy_rate or 5.0)  # Dot ball
            else:
                return (1, False, "", bowler.economy_rate or 5.0)  # Single

    @staticmethod
    def calculate_run_rate(innings: Innings) -> float:
        """Calculate current run rate (runs per over)."""
        if innings.overs_completed == 0:
            return 0.0
        return round(innings.total_runs / innings.overs_completed, 2)

    @staticmethod
    def apply_powerplay_rules(over_number: int) -> bool:
        """Check if current over is in powerplay (first 6 overs in ODI)."""
        return 0 <= over_number < 6

    @staticmethod
    def calculate_economy_rate(bowler_runs: int, overs_bowled: float) -> float:
        """Calculate bowler's economy (runs conceded per over)."""
        if overs_bowled == 0:
            return 0.0
        return round(bowler_runs / overs_bowled, 2)

    @staticmethod
    def determine_match_winner(
        innings1_runs: int,
        innings2_runs: int,
        innings1_team: str,
        innings2_team: str,
    ) -> Tuple[str, str]:
        """Determine match winner and margin."""
        if innings2_runs > innings1_runs:
            margin = f"by {innings2_runs - innings1_runs} runs"
            return (innings2_team, margin)
        else:
            margin = f"by {innings1_runs - innings2_runs} runs"
            return (innings1_team, margin)

    @staticmethod
    def validate_match_state(innings: Innings) -> Tuple[bool, str]:
        """Validate innings state for consistency."""
        if innings.total_wickets > 10:
            return (False, "Wickets cannot exceed 10")
        if innings.overs_completed > 50:
            return (False, "Overs cannot exceed 50")
        if innings.total_runs < 0:
            return (False, "Runs cannot be negative")
        return (True, "Valid innings state")

# agents/orchestrator/orchestrator_agent.py
from models.cricket_domain import CricketMatch, CricketTeam, Innings
from tools.cricket_mechanics import CricketMechanicsEngine
from datetime import datetime
import json

class OrchestratorAgent:
    """
    Master agent that coordinates entire cricket match.
    Responsibilities: toss logic, team selection, phase transitions, result determination.
    """

    def __init__(self, match: CricketMatch, llm_client):
        self.match = match
        self.llm_client = llm_client
        self.agent_id = "orchestrator-main"
        self.reasoning_log = []
        self.tools = {
            "apply_powerplay_rules": CricketMechanicsEngine.apply_powerplay_rules,
            "validate_match_state": CricketMechanicsEngine.validate_match_state,
            "determine_match_winner": CricketMechanicsEngine.determine_match_winner,
        }

    async def execute_toss_logic(self) -> str:
        """
        Reason about toss: which team won, what should they choose (bat/bowl)?
        This demonstrates agent reasoning without simulation yet.
        """
        prompt = f"""
        Simulate a cricket toss between {self.match.team1.team_name} (#{self.match.team1.odi_ranking}) 
        and {self.match.team2.team_name} (#{self.match.team2.odi_ranking}).
        Venue: {self.match.venue}
        
        Facts:
        - {self.match.team1.team_name} home ground: {self.match.team1.home_ground}
        - {self.match.team2.team_name} home ground: {self.match.team2.home_ground}
        - {self.match.team1.team_name} ODI ranking: {self.match.team1.odi_ranking}
        - {self.match.team2.team_name} ODI ranking: {self.match.team2.odi_ranking}
        
        Decide:
        1. Which team wins the toss (reason about form, recent results, captain experience)?
        2. Should the toss-winning captain choose to bat or bowl first (consider ground, weather patterns)?
        
        Respond with JSON: {{"toss_winner": "team_name", "choice": "bat|bowl", "reasoning": "explanation"}}
        """
        # In production, call self.llm_client with prompt
        # For now, return simulated decision
        return json.dumps({
            "toss_winner": self.match.team1.team_name,
            "choice": "bat",
            "reasoning": f"{self.match.team1.team_name} won toss, chose to bat first. Higher-ranked team prefers batting in favorable conditions."
        })

    def transition_phase(self, new_phase: str) -> None:
        """Safely transition match to next phase (pre_match -> innings1 -> innings2 -> completed)."""
        valid_transitions = {
            "pre_match": ["innings1"],
            "innings1": ["innings2"],
            "innings2": ["completed"],
        }
        if new_phase not in valid_transitions.get(self.match.match_phase, []):
            raise ValueError(f"Invalid phase transition: {self.match.match_phase} -> {new_phase}")
        self.match.match_phase = new_phase
        self.reasoning_log.append({
            "timestamp": datetime.utcnow().isoformat(),
            "action": "phase_transition",
            "from": self.match.match_phase,
            "to": new_phase,
        })

Step 3 — Integration & Enhancement

Step 3 builds the player selection agent and the simulation agent, both of which work in tandem with the orchestrator. The selection agent receives team rosters and match context—including venue, toss decision, and opposition—and uses LLM reasoning to construct optimal playing XIs. It reasons about player form, role balance across openers, middle order, and tail, as well as match-ups against opposition bowlers.

The simulation agent executes the match delivery by delivery. It retrieves the current state from the orchestrator, invokes mechanics tools to simulate each delivery, updates innings state, detects match-ending conditions, and feeds the updated state back to the orchestrator. This pattern of passing state immutably and invoking shared tools is fundamental to production agentic workflows.

An enhancement layer adds analytics agents that compute live metrics such as run rate progression, partnership analysis, and momentum indicators. These metrics are fed back to the orchestrator to inform decisions about player substitutions or strategy changes as the match unfolds.

Analogy🏏Cricket
🏏 Think of it like cricket: Picture the Indian war room during India's T20 World Cup final against Australia. The captain (Primary Agent/Orchestrator) simultaneously receives three specialist reports during a timeout: (1) Batting Coach (confidence 0.80): "Continue aggressive batting, we're 2 runs ahead of required rate." (2) Bowling Coach (confidence 0.88): "Starc will bowl next; field a defensive setup." (3) Field Coach (confidence 0.75): "Head has 4 boundaries this powerplay; place deep cover." The captain faces conflict: aggressive batting strategy contradicts defensive bowling field. But the captain applies domain wisdom: bowling coach's confidence (0.88) is highest, and a fresh bowler like Starc warrants caution. Additionally, a hidden rule applies—if India loses a wicket in the next over, the aggressive approach becomes too risky (conditional override). The captain synthesizes: "Batting: stay aggressive but rotate strike to avoid dot balls against Starc. Bowling: field defensively with deep cover per Head's pattern. Fielding: place that cover immediately." This unified strategy honors all three specialists' expertise while applying contextual judgment and conflict resolution rules. The same pattern occurs in our Orchestrator: gather parallel recommendations, weight by confidence, apply domain rules, and synthesize coherent output.
python
# agents/selection/player_selection_agent.py
from models.cricket_domain import CricketTeam, CricketPlayer
from typing import List
import json

class PlayerSelectionAgent:
    """
    Autonomous agent responsible for constructing playing XI from squad.
    Reasons about role balance, form, and match conditions.
    """

    def __init__(self, llm_client):
        self.agent_id = "selection-agent"
        self.llm_client = llm_client
        self.selection_log = []

    async def select_playing_xi(
        self,
        squad: CricketTeam,
        opposition: CricketTeam,
        venue: str,
        toss_decision: str,
    ) -> List[CricketPlayer]:
        """
        Select 11 players from squad, reasoning about:
        - Opposition strengths/weaknesses
        - Venue conditions (home vs away)
        - Player fitness and form
        - Role balance (openers, middle order, bowlers)
        """
        squad_summary = json.dumps([
            {
                "name": p.name,
                "role": p.role,
                "avg_strike_rate": p.average_strike_rate,
                "fitness": p.match_fitness,
                "economy_rate": p.economy_rate,
            }
            for p in squad.squad
        ])

        prompt = f"""
        You are selecting a cricket XI for {squad.team_name} against {opposition.team_name}.
        
        Squad available:
        {squad_summary}
        
        Context:
        - Venue: {venue}
        - Toss decision: {toss_decision} (your team will {'bat' if toss_decision == 'bat' else 'bowl'} first)
        - Opposition ranking: #{opposition.odi_ranking}
        - Opposition strengths: {opposition.team_name} is a strong {'bowling' if opposition.odi_ranking < 5 else 'all-round'} team
        
        Select exactly 11 players. Consider:
        1. Role balance: 3 openers, 4 middle-order, 1 wicket-keeper, 3 bowlers
        2. Player fitness: avoid any player with fitness < 0.8 unless critical
        3. Home advantage if playing at home
        
        Respond with JSON: {{
            "selected_xi": ["player_name_1", "player_name_2", ...],
            "captain": "captain_name",
            "vice_captain": "vice_captain_name",
            "reasoning": "brief explanation of selection strategy"
        }}
        """
        
        # In production: response = await self.llm_client.call(prompt)
        # For demonstration, return hardcoded selection
        selected_names = [p.name for p in squad.squad[:11]]
        return [p for p in squad.squad if p.name in selected_names]

# agents/simulation/match_simulation_agent.py
from models.cricket_domain import (
    CricketMatch, Delivery, Innings, CricketPlayer
)
from tools.cricket_mechanics import CricketMechanicsEngine
from datetime import datetime
import random

class MatchSimulationAgent:
    """
    Executes match simulation delivery-by-delivery.
    Maintains innings state, invokes mechanics tools, handles wicket logic.
    """

    def __init__(self, match: CricketMatch):
        self.match = match
        self.agent_id = "simulation-agent"
        self.mechanics = CricketMechanicsEngine()
        self.delivery_count = 0
        self.bowler_state = {}  # Track bowler fatigue, economy by bowler

    async def execute_delivery(
        self,
        innings: Innings,
        bowler: CricketPlayer,
        batter: CricketPlayer,
        over_number: int,
        ball_number: int,
    ) -> Delivery:
        """
        Simulate one delivery: invoke mechanics tool, update state, return Delivery object.
        """
        is_powerplay = self.mechanics.apply_powerplay_rules(over_number)
        
        # Invoke deterministic tool
        runs, is_wicket, wicket_type, economy = self.mechanics.simulate_delivery(
            bowler=bowler,
            batter=batter,
            over_number=over_number,
            is_powerplay=is_powerplay,
            bowler_state=self.bowler_state,
        )

        # Create Delivery record
        delivery = Delivery(
            delivery_id=f"del-{innings.innings_id}-{self.delivery_count}",
            over_number=over_number,
            ball_number=ball_number,
            bowler_name=bowler.name,
            batter_name=batter.name,
            runs_scored=runs,
            is_boundary=(runs >= 4),
            is_wicket=is_wicket,
            wicket_type=wicket_type if is_wicket else None,
            bowler_economy_this_over=economy,
            timestamp=datetime.utcnow(),
        )

        # Update innings state
        innings.total_runs += runs
        if is_wicket:
            innings.total_wickets += 1
        if ball_number == 6:
            innings.overs_completed += 1
        if is_powerplay:
            innings.powerplay_runs += runs
        if over_number >= 44:
            innings.death_overs_runs += runs
        if runs >= 4:
            innings.boundaries_hit += 1

        innings.deliveries.append(delivery)
        self.delivery_count += 1

        # Update run rate
        innings.run_rate = self.mechanics.calculate_run_rate(innings)

        return delivery

    def is_innings_complete(self, innings: Innings) -> bool:
        """Check if innings has ended (all out or 50 overs completed)."""
        return innings.total_wickets == 10 or innings.overs_completed >= 50

# agents/analytics/analytics_agent.py
from models.cricket_domain import Innings
from typing import Dict
import statistics

class AnalyticsAgent:
    """
    Computes live match analytics and insights.
    Feeds metrics to orchestrator for decision-making.
    """

    def __init__(self):
        self.agent_id = "analytics-agent"

    def compute_partnership_analysis(self, innings: Innings) -> Dict:
        """
        Analyze current batting partnership.
        """
        if len(innings.deliveries) < 5:
            return {"status": "insufficient_data", "balls_faced": len(innings.deliveries)}

        last_ten_deliveries = innings.deliveries[-10:]
        runs_last_ten = sum(d.runs_scored for d in last_ten_deliveries)
        balls_last_ten = len(last_ten_deliveries)
        strike_rate = (runs_last_ten / balls_last_ten * 100) if balls_last_ten > 0 else 0

        return {
            "runs_last_10_balls": runs_last_ten,
            "strike_rate_last_10": round(strike_rate, 2),
            "momentum": "ascending" if strike_rate > 120 else "steady" if strike_rate > 80 else "under_pressure",
        }

    def compute_match_projection(self, innings: Innings) -> Dict:
        """
        Project final score based on current run rate and overs remaining.
        """
        overs_remaining = 50 - innings.overs_completed
        if overs_remaining <= 0:
            return {"projected_total": innings.total_runs, "status": "complete"}

        projected_total = innings.total_runs + (innings.run_rate * overs_remaining)
        return {
            "current_runs": innings.total_runs,
            "overs_remaining": overs_remaining,
            "projected_total": round(projected_total, 0),
            "required_run_rate": round(9 / overs_remaining, 2) if overs_remaining > 0 else 0,
        }

    def recommend_strategy_change(self, innings: Innings, opposition_total: int) -> str:
        """
        Recommend strategy adjustments based on match state.
        """
        projection = self.compute_match_projection(innings)
        if projection["projected_total"] < opposition_total * 0.9:
            return "ACCELERATE: Increase scoring to match opposition total"
        elif projection["projected_total"] > opposition_total * 1.1:
            return "CONSOLIDATE: Maintain current strategy, target achieved"
        else:
            return "STEADY: Continue current run rate, match progressing on track"

Step 4 — Testing & Verification

With all components in place, run the integrated orchestrator system end-to-end using realistic cricket data. Execute a complete match simulation between two international teams and verify that the orchestrator transitions match phases correctly, agents coordinate without race conditions, and match state remains consistent across all 100 deliveries.

Analogy🏏Cricket
🏏 Think of it like cricket: this verification step is like a captain gathering specialist advice at a tense mid-innings India-versus-Australia T20 before committing to a move. You build a Match object frozen at a real situation — say fifteen overs gone, wickets in hand, rate climbing — then initialise three specialist agents and run the Orchestrator, exactly as a captain consults the bowling coach, the analyst, and the senior pro before acting. Each agent returns a decision with a confidence score, just as each advisor offers a recommendation with more or less conviction, and the Orchestrator synthesises them into one unified strategy the way a captain weighs the counsel and settles on a single plan. The payoff is proof that the pipeline works end to end: specialists reason independently, their confidence is quantified, and their advice is fused into a coherent decision — the same discipline that turns scattered dressing-room opinions into a match-winning call.

Validate that the final scorecard accurately reflects all deliveries, that wickets are tracked properly, and that the match winner is determined correctly. Use pytest to run both unit tests on individual agents and integration tests on multi-agent workflows to confirm the system behaves as expected at every level.

bash
# commands/run_match_simulation.sh
#!/bin/bash
echo "=== Cricket Agentic Orchestrator - Match Simulation ==="
echo ""

# Set environment
export PYTHONPATH="${PYTHONPATH}:$(pwd)"

# Run main match orchestration
echo "[1] Initializing match between India vs Australia..."
python3 -c "
import asyncio
from datetime import datetime
from models.cricket_domain import (
    CricketMatch, CricketTeam, CricketPlayer, Innings,
    CountryCode, BattingStyle, BowlingStyle
)
from agents.orchestrator.orchestrator_agent import OrchestratorAgent
from agents.simulation.match_simulation_agent import MatchSimulationAgent
from agents.analytics.analytics_agent import AnalyticsAgent

# Create sample teams with real player names
india_players = [
    CricketPlayer(player_id='IND001', name='Rohit Sharma', country=CountryCode.INDIA,
                  batting_style=BattingStyle.RIGHT_HAND, average_strike_rate=95.2,
                  average_boundary_percentage=35.0, role='batsman', jersey_number=45, match_fitness=1.0),
    CricketPlayer(player_id='IND002', name='Shubman Gill', country=CountryCode.INDIA,
                  batting_style=BattingStyle.RIGHT_HAND, average_strike_rate=92.1,
                  average_boundary_percentage=28.0, role='batsman', jersey_number=77, match_fitness=0.95),
    CricketPlayer(player_id='IND003', name='Virat Kohli', country=CountryCode.INDIA,
                  batting_style=BattingStyle.RIGHT_HAND, average_strike_rate=92.5,
                  average_boundary_percentage=32.0, role='batsman', jersey_number=18, match_fitness=1.0),
    CricketPlayer(player_id='IND004', name='Suryakumar Yadav', country=CountryCode.INDIA,
                  batting_style=BattingStyle.RIGHT_HAND, average_strike_rate=139.0,
                  average_boundary_percentage=45.0, role='batsman', jersey_number=63, match_fitness=0.9),
    CricketPlayer(player_id='IND005', name='Rishabh Pant', country=CountryCode.INDIA,
                  batting_style=BattingStyle.LEFT_HAND, average_strike_rate=115.0,
                  average_boundary_percentage=38.0, role='batsman', jersey_number=17, match_fitness=0.85),
    CricketPlayer(player_id='IND006', name='Jasprit Bumrah', country=CountryCode.INDIA,
                  bowling_style=BowlingStyle.FAST, average_strike_rate=0, average_boundary_percentage=0,
                  wicket_taking_rate=23.5, economy_rate=5.3, role='bowler', jersey_number=93, match_fitness=1.0),
    CricketPlayer(player_id='IND007', name='Mohammed Shami', country=CountryCode.INDIA,
                  bowling_style=BowlingStyle.FAST, average_strike_rate=0, average_boundary_percentage=0,
                  wicket_taking_rate=25.0, economy_rate=5.8, role='bowler', jersey_number=11, match_fitness=0.9),
]

aus_players = [
    CricketPlayer(player_id='AUS001', name='David Warner', country=CountryCode.AUSTRALIA,
                  batting_style=BattingStyle.LEFT_HAND, average_strike_rate=98.0,
                  average_boundary_percentage=40.0, role='batsman', jersey_number=31, match_fitness=1.0),
    CricketPlayer(player_id='AUS002', name='Steve Smith', country=CountryCode.AUSTRALIA,
                  batting_style=BattingStyle.RIGHT_HAND, average_strike_rate=85.0,
                  average_boundary_percentage=25.0, role='batsman', jersey_number=49, match_fitness=1.0),
    CricketPlayer(player_id='AUS003', name='Glenn Maxwell', country=CountryCode.AUSTRALIA,
                  batting_style=BattingStyle.RIGHT_HAND, average_strike_rate=135.0,
                  average_boundary_percentage=42.0, role='batsman', jersey_number=32, match_fitness=0.88),
    CricketPlayer(player_id='AUS004', name='Mitchell Starc', country=CountryCode.AUSTRALIA,
                  bowling_style=BowlingStyle.FAST, average_strike_rate=0, average_boundary_percentage=0,
                  wicket_taking_rate=24.0, economy_rate=5.5, role='bowler', jersey_number=62, match_fitness=1.0),
]

# Create teams
india = CricketTeam(
    team_id='IND', team_name='India', country=CountryCode.INDIA, squad=india_players,
    captain_id='IND001', vice_captain_id='IND003', home_ground='Delhi',
    odi_ranking=1
)

aus = CricketTeam(
    team_id='AUS', team_name='Australia', country=CountryCode.AUSTRALIA, squad=aus_players,
    captain_id='AUS001', vice_captain_id='AUS002', home_ground='Melbourne',
    odi_ranking=2
)

# Create match
match = CricketMatch(
    match_id='MATCH-IND-AUS-001',
    match_date=datetime.utcnow(),
    venue='Melbourne Cricket Ground',
    toss_won_by='India',
    toss_decision='bat',
    team1=india,
    team2=aus
)

print(f'[✓] Match created: {india.team_name} vs {aus.team_name}')
print(f'[✓] Venue: {match.venue}')
print(f'[✓] Toss: {match.toss_won_by} chose to {match.toss_decision}')
print()

# Orchestrator setup
orchestrator = OrchestratorAgent(match=match, llm_client=None)
print('[✓] Orchestrator agent initialized')
print(f'    Agent ID: {orchestrator.agent_id}')
print()

# Simulation setup
simulator = MatchSimulationAgent(match=match)
analytics = AnalyticsAgent()
print('[✓] Simulation and analytics agents initialized')
print()

print('Match is ready for simulation. Run: python main.py')
"

echo ""
echo "[2] Running unit tests on cricket mechanics..."
python3 -m pytest tests/test_mechanics.py -v --tb=short 2>/dev/null || echo "   [Note: Test file not yet created]"

echo ""
echo "[3] Expected output from full match simulation:"
echo "    - 50 overs simulated with ~300 deliveries"
echo "    - India 1st innings: 280-6 (50 overs)"
echo "    - Australia 2nd innings: 275 all out (48.3 overs)"
echo "    - Winner: India by 5 runs"
echo "    - Man of Match: Jasprit Bumrah (3 wickets, economy 5.1)"
echo ""
echo "=== Simulation Ready ==="

Warning: A common error is agents creating inconsistent match state—e.g., a simulation agent updating total_runs without calling the orchestrator's validation tool, leading to scorecard corruption. Always enforce that agents invoke CricketMechanicsEngine.validate_match_state() after state mutations and that only the orchestrator commits state changes to the authoritative match object. Use immutable patterns: agents should create new Innings objects rather than mutating existing ones. Implement a transaction-like pattern where state changes are validated before being accepted into the match record.

Extension Challenge: Enhance the system with a decision-making agent that makes in-match strategy calls: after every 10 overs, it evaluates innings.run_rate vs. projected_total and recommends field placements or bowling changes to the orchestrator. Implement DLS (Duckworth-Lewis-Stern) calculations so if rain interrupts, the target for the chasing team is recalculated dynamically and agents adapt strategy. Add a predictive agent that uses historical player vs. bowler match-ups to forecast probability of wickets in next over.

  • Agentic decomposition separates business logic (cricket rules in tools) from reasoning logic (agents making decisions), enabling parallel development and testing of each layer independently.
  • State immutability and validation are critical: agents pass state through tools that enforce cricket law constraints, preventing hallucinated impossible scenarios like 8-run deliveries.
  • The orchestrator pattern works as a coordinator managing subsidiary agents: it maintains authoritative match state, sequences phase transitions, and resolves conflicts between agents' competing recommendations.
  • Tool-calling is the synchronization mechanism: agents don't directly invoke other agents; instead, they invoke deterministic tools that apply domain rules, ensuring consistent outcomes regardless of agent reasoning drift.
  • Multi-turn reasoning in agents enables them to reason about match context, justify decisions (toss strategy, XI selection), and adapt to feedback from tools before committing to actions.
Lesson 30 of 35
0% complete