| """
|
| CarbonSchedulerEnv โ Action and Observation models.
|
|
|
| Both inherit from OpenEnv base types so the framework can
|
| auto-generate schemas, WebSocket handlers, and the web UI.
|
|
|
| Action:
|
| Structured list of scheduling decisions โ one per pending job.
|
| Each decision is either an assignment (region + start_hour) or a defer.
|
|
|
| Observation:
|
| Full state of the scheduling problem โ text prompt included so
|
| the LLM can consume it directly via `obs.prompt`.
|
| """
|
|
|
| from typing import Dict, List, Optional
|
| from pydantic import Field
|
| from openenv.core.env_server.types import Action, Observation
|
|
|
|
|
| class ScheduleDecision(Action):
|
| """One scheduling decision for a single job."""
|
| job_id: str = Field(..., description="ID of the job being scheduled")
|
| region: str = Field(
|
| default="",
|
| description="Target data centre region (e.g. 'us-west-2'). Empty if deferring.",
|
| )
|
| start_hour: int = Field(
|
| default=-1,
|
| description="UTC hour to start the job (0โ23). -1 if deferring.",
|
| )
|
| defer: bool = Field(
|
| default=False,
|
| description="True to defer this job to the next scheduling step.",
|
| )
|
|
|
|
|
| class CarbonSchedulerAction(Action):
|
| """
|
| Agent's full scheduling action for one step.
|
|
|
| The agent outputs a list of decisions โ one per pending job.
|
| Each job must be either assigned (region + start_hour) or deferred.
|
|
|
| Example:
|
| {
|
| "assignments": [
|
| {"job_id": "job_01", "region": "us-west-2", "start_hour": 14},
|
| {"job_id": "job_02", "defer": true},
|
| {"job_id": "job_03", "region": "eu-west-1", "start_hour": 16}
|
| ],
|
| "reasoning": "Deferring job_02 to the solar window at 14:00 in Oregon"
|
| }
|
| """
|
| assignments: List[ScheduleDecision] = Field(
|
| default_factory=list,
|
| description="List of scheduling decisions โ one per pending job.",
|
| )
|
| reasoning: str = Field(
|
| default="",
|
| description="Optional: agent's reasoning for this schedule (for interpretability).",
|
| )
|
|
|
|
|
| class CarbonSchedulerObservation(Observation):
|
| """
|
| Full observation of the scheduling environment.
|
|
|
| Key fields for the LLM agent:
|
| prompt โ formatted text prompt ready for LLM consumption
|
| current_hour โ current UTC hour (0โ23)
|
| jobs_pending โ list of jobs awaiting scheduling
|
| datacenters โ list of data centres with carbon forecasts
|
| carbon_saved_so_far โ gCO2 saved vs naive baseline this episode
|
|
|
| Key fields for training:
|
| reward โ reward from the last step
|
| done โ True if episode is over
|
| reward_breakdown โ per-component reward for interpretability
|
| """
|
|
|
|
|
| current_hour: int = Field(default=0, description="Current UTC hour (0โ23)")
|
| step_number: int = Field(default=0, description="Step index within episode")
|
| done: bool = Field(default=False)
|
| reward: float = Field(default=0.0)
|
|
|
|
|
| jobs_pending: List[dict] = Field(
|
| default_factory=list,
|
| description="Jobs awaiting a scheduling decision this step.",
|
| )
|
| jobs_completed: int = Field(default=0)
|
| jobs_failed: int = Field(default=0)
|
| total_jobs: int = Field(default=0)
|
|
|
|
|
| datacenters: List[dict] = Field(
|
| default_factory=list,
|
| description="Data centre state including carbon forecast for next 12 hours.",
|
| )
|
|
|
|
|
| carbon_saved_so_far: float = Field(
|
| default=0.0,
|
| description="gCO2 saved vs naive run-immediately baseline this episode.",
|
| )
|
| naive_carbon_so_far: float = Field(
|
| default=0.0,
|
| description="Carbon that naive scheduler would have emitted.",
|
| )
|
| actual_carbon_so_far: float = Field(
|
| default=0.0,
|
| description="Actual carbon emitted by agent's schedule so far.",
|
| )
|
| completion_rate: float = Field(
|
| default=0.0,
|
| description="Fraction of jobs completed (0.0โ1.0).",
|
| )
|
|
|
|
|
| curriculum_stage: int = Field(
|
| default=1,
|
| description="Current curriculum level (1=easy, 2=medium, 3=hard).",
|
| )
|
|
|
|
|
| reward_breakdown: Dict[str, float] = Field(
|
| default_factory=dict,
|
| description="Per-component reward breakdown for this step.",
|
| )
|
| episode_summary: Dict[str, float] = Field(
|
| default_factory=dict,
|
| description="End-of-episode metrics (populated when done=True).",
|
| )
|
|
|
|
|
| prompt: str = Field(
|
| default="",
|
| description="Formatted text prompt for LLM consumption โ ready to use directly.",
|
| )
|
|
|