| |
| |
| |
|
|
| """ |
| Organization Simulation Environment. |
| |
| A multi-agent environment simulating hierarchical organization with |
| teams, escalation, and resource management. |
| """ |
|
|
| import uuid |
| from typing import Any |
|
|
| from openenv.core.env_server.interfaces import Environment |
|
|
| from ..models import OrgAction, OrgObservation, OrgState |
|
|
|
|
| TASK_SCENARIOS = { |
| "solo_bug_fix": { |
| "description": "Fix a high-priority bug in the engineering team", |
| "tasks": [ |
| { |
| "id": "task_bug", |
| "team": "engineering", |
| "type": "fix_bug", |
| "priority": "high", |
| "deadline": 10, |
| "difficulty": 1, |
| } |
| ], |
| }, |
| "cross_team_launch": { |
| "description": "Engineering and Sales coordinate a product launch", |
| "tasks": [ |
| { |
| "id": "task_eng", |
| "team": "engineering", |
| "type": "implement_feature", |
| "priority": "high", |
| "deadline": 15, |
| "difficulty": 2, |
| }, |
| { |
| "id": "task_sales", |
| "team": "sales", |
| "type": "prepare_proposal", |
| "priority": "medium", |
| "deadline": 12, |
| "difficulty": 2, |
| }, |
| ], |
| }, |
| "startup_crisis": { |
| "description": "Simultaneous incident + resource-gated feature + cross-team client meeting", |
| "tasks": [ |
| { |
| "id": "task_incident", |
| "team": "engineering", |
| "type": "fix_bug", |
| "priority": "critical", |
| "deadline": 5, |
| "difficulty": 3, |
| }, |
| { |
| "id": "task_feature", |
| "team": "engineering", |
| "type": "implement_feature", |
| "priority": "high", |
| "deadline": 20, |
| "difficulty": 2, |
| "requires_resource": "senior_engineer", |
| }, |
| { |
| "id": "task_client", |
| "team": "sales", |
| "type": "client_meeting", |
| "priority": "critical", |
| "deadline": 8, |
| "difficulty": 2, |
| }, |
| ], |
| }, |
| } |
|
|
| TASK_IDS = list(TASK_SCENARIOS.keys()) |
|
|
|
|
| class OrgSimEnvironment(Environment): |
| """Organization simulation environment.""" |
|
|
| def __init__(self, max_steps: int = 50): |
| super().__init__() |
| self.max_steps = max_steps |
| self._current_task_id = "cross_team_launch" |
|
|
| self._state = OrgState(episode_id="", step_count=0) |
| self._current_agent = "eng_swe1" |
| self._tasks: dict[str, dict] = {} |
| self._messages: list[dict] = [] |
| self._episode_resources: dict[str, bool] = {} |
|
|
| self._init_organization() |
|
|
| def _init_organization(self): |
| """Initialize static organization structure.""" |
| self.teams = { |
| "engineering": { |
| "lead": "eng_lead", |
| "members": ["eng_swe1", "eng_swe2", "eng_swe3"], |
| }, |
| "sales": { |
| "lead": "sales_mgr", |
| "members": ["sales_rep1", "sales_rep2", "sales_rep3"], |
| }, |
| "operations": { |
| "lead": "ops_lead", |
| "members": ["ops_analyst1", "ops_analyst2", "ops_analyst3"], |
| }, |
| } |
|
|
| def reset(self, agent_id: str = "eng_swe1", task_id: str = "cross_team_launch", **kwargs: Any) -> OrgObservation: |
| """Reset environment for new episode. |
| |
| Args: |
| agent_id: The agent playing this episode. |
| task_id: One of 'solo_bug_fix', 'cross_team_launch', 'startup_crisis'. |
| """ |
| self._current_task_id = task_id if task_id in TASK_SCENARIOS else "cross_team_launch" |
| self._state.episode_id = str(uuid.uuid4()) |
| self._state.step_count = 0 |
| self._current_agent = agent_id |
| self._messages = [] |
|
|
| scenario = TASK_SCENARIOS[self._current_task_id] |
|
|
| self._tasks = { |
| t["id"]: { |
| **t, |
| "status": "pending", |
| "assignee": None, |
| "progress": 0.0, |
| } |
| for t in scenario["tasks"] |
| } |
|
|
| |
| self._episode_resources = { |
| "senior_engineer": True, |
| "cloud_instances": True, |
| "budget_approval": True, |
| } |
|
|
| return self._get_observation() |
|
|
| def step(self, action: OrgAction) -> OrgObservation: |
| """Execute action and return observation.""" |
| self._state.step_count += 1 |
|
|
| reward = 0.0 |
|
|
| if action.action_type == "REQUEST_TASK": |
| reward = self._handle_request_task(action) |
| elif action.action_type == "ACCEPT_TASK": |
| reward = self._handle_accept_task(action) |
| elif action.action_type == "COMPLETE_TASK": |
| reward = self._handle_complete_task(action) |
| elif action.action_type == "REQUEST_HELP": |
| reward = self._handle_request_help(action) |
| elif action.action_type == "PROVIDE_HELP": |
| reward = self._handle_provide_help(action) |
| elif action.action_type == "ESCALATE": |
| reward = self._handle_escalate(action) |
| elif action.action_type == "REQUEST_RESOURCE": |
| reward = self._handle_request_resource(action) |
| elif action.action_type == "REPORT_STATUS": |
| reward = 0.0 |
|
|
| done = self._check_done() |
|
|
| return self._get_observation(reward=reward, done=done) |
|
|
| def _handle_request_task(self, action: OrgAction) -> float: |
| my_team = self._get_team_for_agent(self._current_agent) |
|
|
| available = [ |
| t |
| for t in self._tasks.values() |
| if t["team"] == my_team |
| and t["status"] == "pending" |
| and t.get("assignee") is None |
| ] |
|
|
| if available: |
| task = available[0] |
| task["status"] = "in_progress" |
| task["assignee"] = self._current_agent |
| return 0.1 |
|
|
| return -0.05 |
|
|
| def _handle_accept_task(self, action: OrgAction) -> float: |
| task_id = action.payload.get("task_id") |
| if not task_id or task_id not in self._tasks: |
| return -0.1 |
|
|
| task = self._tasks[task_id] |
| if task["status"] != "pending": |
| return -0.1 |
|
|
| task["status"] = "in_progress" |
| task["assignee"] = self._current_agent |
| return 0.1 |
|
|
| def _handle_complete_task(self, action: OrgAction) -> float: |
| task_id = action.payload.get("task_id") |
| if not task_id or task_id not in self._tasks: |
| return -0.2 |
|
|
| task = self._tasks[task_id] |
| if task.get("assignee") != self._current_agent: |
| return -0.1 |
|
|
| if task["status"] != "in_progress": |
| return -0.1 |
|
|
| |
| required = task.get("requires_resource") |
| if required and self._episode_resources.get(required, True): |
| |
| return -0.2 |
|
|
| deadline = task.get("deadline", 20) |
| time_taken = self._state.step_count |
|
|
| if time_taken <= deadline: |
| base_reward = 1.0 |
| time_bonus = max(0.0, (deadline - time_taken) / deadline) * 0.3 |
| priority_scores = {"critical": 0.3, "high": 0.2, "medium": 0.1, "low": 0.0} |
| priority_bonus = priority_scores.get(task.get("priority", "medium"), 0.1) |
| task["status"] = "completed" |
| task["completed_at"] = self._state.step_count |
| return base_reward + time_bonus + priority_bonus |
| else: |
| task["status"] = "failed" |
| return -0.3 |
|
|
| def _handle_request_help(self, action: OrgAction) -> float: |
| task_id = action.payload.get("task_id") |
| if not task_id or task_id not in self._tasks: |
| return -0.1 |
|
|
| task = self._tasks[task_id] |
| if task.get("assignee") != self._current_agent: |
| return -0.1 |
|
|
| task["progress"] = min(1.0, task.get("progress", 0.0) + 0.2) |
|
|
| lead = self._get_team_lead(self._get_team_for_agent(self._current_agent)) |
| self._messages.append({ |
| "from": self._current_agent, |
| "to": lead, |
| "type": "help_request", |
| "task_id": task_id, |
| }) |
|
|
| |
| self._messages.append({ |
| "from": lead, |
| "to": self._current_agent, |
| "type": "help_response", |
| "task_id": task_id, |
| "content": f"Acknowledged. Assigned eng_swe2 to assist with {task_id}. Check internal docs for context.", |
| }) |
|
|
| return 0.1 |
|
|
| def _handle_provide_help(self, action: OrgAction) -> float: |
| task_id = action.payload.get("task_id") |
| if not task_id or task_id not in self._tasks: |
| return -0.1 |
|
|
| task = self._tasks[task_id] |
| task["progress"] = min(1.0, task.get("progress", 0.0) + 0.3) |
| return 0.2 |
|
|
| def _handle_escalate(self, action: OrgAction) -> float: |
| task_id = action.payload.get("task_id") |
| if not task_id or task_id not in self._tasks: |
| return -0.2 |
|
|
| task = self._tasks[task_id] |
|
|
| if task["status"] in ("completed", "failed", "escalated"): |
| return -0.1 |
|
|
| |
| my_team = self._get_team_for_agent(self._current_agent) |
| is_cross_team = task["team"] != my_team |
| is_hard_stuck = task.get("difficulty", 1) >= 2 and task.get("progress", 0.0) < 0.5 |
|
|
| if is_cross_team or is_hard_stuck: |
| task["status"] = "escalated" |
| task["escalated_at"] = self._state.step_count |
| return 0.3 |
| else: |
| return -0.2 |
|
|
| def _handle_request_resource(self, action: OrgAction) -> float: |
| resource_id = action.payload.get("resource_id") |
|
|
| if not resource_id or resource_id not in self._episode_resources: |
| return -0.2 |
|
|
| if self._episode_resources[resource_id]: |
| self._episode_resources[resource_id] = False |
| return 0.2 |
|
|
| return -0.1 |
|
|
| def _check_done(self) -> bool: |
| """Episode ends when all tasks are in a terminal state OR max_steps reached. |
| |
| Checks for terminal states (completed/failed/escalated), NOT just non-pending. |
| A task in 'in_progress' is NOT done — episode must continue. |
| """ |
| if self._state.step_count >= self.max_steps: |
| return True |
|
|
| active = sum( |
| 1 for t in self._tasks.values() |
| if t["status"] in ("pending", "in_progress") |
| ) |
| return active == 0 |
|
|
| def grade_episode(self) -> float: |
| """Grade the completed episode. Returns 0.0–1.0. |
| |
| Called after done=True. Score varies with timing, escalation quality, |
| and resource management — never identical across different trajectories. |
| """ |
| if self._current_task_id == "solo_bug_fix": |
| return self._grade_solo_bug_fix() |
| elif self._current_task_id == "cross_team_launch": |
| return self._grade_cross_team_launch() |
| elif self._current_task_id == "startup_crisis": |
| return self._grade_startup_crisis() |
| return 0.0 |
|
|
| def _grade_solo_bug_fix(self) -> float: |
| task = self._tasks.get("task_bug", {}) |
| if task.get("status") == "completed": |
| completed_at = task.get("completed_at", self._state.step_count) |
| deadline = task.get("deadline", 10) |
| time_bonus = max(0.0, (deadline - completed_at) / deadline) * 0.30 |
| return min(1.0, 0.50 + time_bonus + 0.15) |
| elif task.get("status") == "failed": |
| return 0.15 |
| return 0.0 |
|
|
| def _grade_cross_team_launch(self) -> float: |
| score = 0.0 |
| eng = self._tasks.get("task_eng", {}) |
| sales = self._tasks.get("task_sales", {}) |
|
|
| if eng.get("status") == "completed": |
| completed_at = eng.get("completed_at", self._state.step_count) |
| deadline = eng.get("deadline", 15) |
| time_bonus = max(0.0, (deadline - completed_at) / deadline) * 0.20 |
| score += 0.40 + time_bonus |
|
|
| if sales.get("status") == "escalated": |
| score += 0.25 |
| elif sales.get("status") == "completed": |
| score += 0.20 |
|
|
| return min(1.0, score) |
|
|
| def _grade_startup_crisis(self) -> float: |
| score = 0.0 |
| incident = self._tasks.get("task_incident", {}) |
| feature = self._tasks.get("task_feature", {}) |
| client = self._tasks.get("task_client", {}) |
|
|
| |
| if incident.get("status") == "completed": |
| completed_at = incident.get("completed_at", self._state.step_count) |
| if completed_at <= 5: |
| score += 0.40 |
| else: |
| score += 0.10 |
|
|
| |
| resource_locked = not self._episode_resources.get("senior_engineer", True) |
| if feature.get("status") == "completed" and resource_locked: |
| score += 0.30 |
| elif feature.get("status") == "completed": |
| score += 0.10 |
|
|
| |
| if client.get("status") == "escalated": |
| score += 0.20 |
| elif client.get("status") == "completed": |
| score += 0.10 |
|
|
| |
| if resource_locked: |
| score += 0.10 |
|
|
| return min(1.0, score) |
|
|
| def _get_observation(self, reward: float = 0.0, done: bool = False) -> OrgObservation: |
| my_team = self._get_team_for_agent(self._current_agent) |
|
|
| available = [ |
| t for t in self._tasks.values() |
| if t["team"] == my_team |
| and t["status"] == "pending" |
| and t.get("assignee") is None |
| ] |
|
|
| active = next( |
| (t for t in self._tasks.values() |
| if t.get("assignee") == self._current_agent and t["status"] == "in_progress"), |
| None, |
| ) |
|
|
| team_status = { |
| team_name: { |
| "busy": sum(1 for t in self._tasks.values() if t["team"] == team_name and t.get("assignee")), |
| "total": len(team_data["members"]), |
| } |
| for team_name, team_data in self.teams.items() |
| } |
|
|
| return OrgObservation( |
| my_agent_id=self._current_agent, |
| my_team=my_team, |
| my_role="lead" if self._current_agent == self.teams[my_team]["lead"] else "member", |
| available_tasks=available, |
| active_task=active, |
| inbox=[m for m in self._messages if m.get("to") == self._current_agent], |
| team_status=team_status, |
| resources=self._episode_resources.copy(), |
| metrics={ |
| "tasks_completed": sum(1 for t in self._tasks.values() if t["status"] == "completed"), |
| "tasks_failed": sum(1 for t in self._tasks.values() if t["status"] == "failed"), |
| "tasks_escalated": sum(1 for t in self._tasks.values() if t["status"] == "escalated"), |
| "current_task_id": self._current_task_id, |
| "step_count": self._state.step_count, |
| }, |
| done=done, |
| reward=reward, |
| metadata={"step_count": self._state.step_count, "task_id": self._current_task_id}, |
| ) |
|
|
| def _get_team_for_agent(self, agent_id: str) -> str: |
| """Get team for agent — checks both lead and members.""" |
| for team_name, team_data in self.teams.items(): |
| if agent_id == team_data["lead"] or agent_id in team_data["members"]: |
| return team_name |
| return "engineering" |
|
|
| def _get_team_lead(self, team_name: str) -> str: |
| return self.teams.get(team_name, {}).get("lead", "") |
|
|
| @property |
| def state(self) -> OrgState: |
| self._state.tasks = self._tasks.copy() |
| self._state.team_members = {k: v["members"] for k, v in self.teams.items()} |
| self._state.resource_status = self._episode_resources.copy() |
| return self._state |
|
|