Spaces:
Sleeping
Sleeping
Commit ·
fa6f97b
1
Parent(s): 5f19e92
fix: add activity gate to graders, expand README, improve inference
Browse files- Add activity gate to medium/hard/expert graders so do-nothing policies
score 0.0 instead of getting free points from "no harm" dimensions
- Expand README with research foundation (5 papers), 22 capabilities
matrix, grader design section, and activity gate documentation
- Improve inference system prompt with structured decision procedure,
permanent failure awareness, and parallelism emphasis
- Increase HISTORY_WINDOW from 4 to 6 for better expert task context
- Add retry logic for transient LLM API failures
- Add 7 degenerate policy tests proving grader calibration
- Add 12 expert task walkthrough tests (0.948 grader score)
- Update baseline scores from fresh inference run
- Add debug_*.py to .gitignore
Tests: 160 passing (was 139)
- .gitignore +3 -0
- README.md +71 -9
- baseline_scores.json +1 -1
- inference.py +34 -23
- server/graders.py +23 -9
- tests/test_environment.py +245 -3
- tests/test_graders.py +103 -0
.gitignore
CHANGED
|
@@ -40,3 +40,6 @@ htmlcov/
|
|
| 40 |
# Distribution
|
| 41 |
*.whl
|
| 42 |
*.tar.gz
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
# Distribution
|
| 41 |
*.whl
|
| 42 |
*.tar.gz
|
| 43 |
+
|
| 44 |
+
# Debug scripts
|
| 45 |
+
debug_*.py
|
README.md
CHANGED
|
@@ -17,7 +17,65 @@ An OpenEnv environment where an LLM agent acts as a **project coordinator**, man
|
|
| 17 |
|
| 18 |
## Motivation
|
| 19 |
|
| 20 |
-
Agent orchestration is the #1 enterprise AI trend — yet LLMs are terrible at it. Research documents 14+ failure modes in multi-agent systems (MAST taxonomy), up to 17x error amplification in unstructured networks (Spark to Fire), and only 25% baseline correctness with GPT-4o (ChatDev). This environment provides a controlled, deterministic testbed for training and evaluating LLM orchestration capabilities across
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
## Action Space
|
| 23 |
|
|
@@ -101,12 +159,16 @@ End-of-episode: +0.20 (all complete + synthesized), +0.10 * time_efficiency, +0.
|
|
| 101 |
|
| 102 |
## Baseline Scores
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
| 107 |
-
|
|
| 108 |
-
|
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
## Setup
|
| 112 |
|
|
@@ -169,7 +231,7 @@ workflow_orchestrator/
|
|
| 169 |
│ ├── agent_pool.py # Simulated agent state machines
|
| 170 |
│ ├── reward_calculator.py # Dense reward computation
|
| 171 |
│ ├── graders.py # Per-task grading functions
|
| 172 |
-
│ ├── task_registry.py # Easy/medium/hard task configurations
|
| 173 |
│ └── observation_formatter.py # Text rendering for LLM consumption
|
| 174 |
-
└── tests/ #
|
| 175 |
```
|
|
|
|
| 17 |
|
| 18 |
## Motivation
|
| 19 |
|
| 20 |
+
Agent orchestration is the #1 enterprise AI trend — yet LLMs are terrible at it. Research documents 14+ failure modes in multi-agent systems (MAST taxonomy), up to 17x error amplification in unstructured networks (Spark to Fire), and only 25% baseline correctness with GPT-4o (ChatDev). This environment provides a controlled, deterministic testbed for training and evaluating LLM orchestration capabilities across four real-world scenarios forming a narrative arc: **build** a feature (Easy), **ship** it (Medium), **fix** the outage (Hard), and **orchestrate** your day (Expert).
|
| 21 |
+
|
| 22 |
+
**Novel domain:** Zero orchestration environments exist in OpenEnv today. This fills the biggest gap in the ecosystem — coordination, delegation, parallelism, failure recovery, and cost management are untested by any existing environment.
|
| 23 |
+
|
| 24 |
+
## Research Foundation
|
| 25 |
+
|
| 26 |
+
This environment is grounded in 5 recent research papers:
|
| 27 |
+
|
| 28 |
+
| Paper | Key Finding | How It Shaped Our Design |
|
| 29 |
+
|-------|-------------|--------------------------|
|
| 30 |
+
| **MAST Taxonomy** ([arxiv 2503.13657](https://arxiv.org/abs/2503.13657)) | 14 failure modes across 150+ multi-agent traces; GPT-4o achieves only 25% on ChatDev | Our tasks test 8 of 14 MAST failure modes (spec violation, role violation, step repetition, info withholding, premature termination, incomplete verification) |
|
| 31 |
+
| **Spark to Fire** ([arxiv 2603.04474](https://arxiv.org/abs/2603.04474)) | 17.2x error amplification in unstructured agent networks; hub injection → 100% infection | Hard task DAG topology creates realistic cascade potential; failure recovery rewards incentivize early intervention |
|
| 32 |
+
| **AgentErrorBench** ([arxiv 2509.25370](https://arxiv.org/abs/2509.25370)) | Targeted RL feedback improves error recovery by up to 26% across 5 failure categories | Dense per-step rewards target each error category; permanent vs. transient failure classification forces root-cause reasoning |
|
| 33 |
+
| **MARBLE / MultiAgentBench** ([ACL 2025](https://aclanthology.org/2025.acl-long.421/)) | 3-agent teams optimize coordination-performance balance; excessive iterations degrade coordination | Capacity limit of 3 concurrent tasks; milestone-based grading; DAG-based task structure |
|
| 34 |
+
| **DAAO** ([arxiv 2509.11079](https://arxiv.org/html/2509.11079v1)) | Task difficulty should dynamically determine orchestration strategy; 11% accuracy improvement | Our 4 tasks require fundamentally different strategies, not just more nodes |
|
| 35 |
+
|
| 36 |
+
## Capabilities Tested (22 Total)
|
| 37 |
+
|
| 38 |
+
Each difficulty level introduces **qualitatively different reasoning**, not just more nodes:
|
| 39 |
+
|
| 40 |
+
| # | Capability | Easy | Med | Hard | Expert | What It Tests |
|
| 41 |
+
|---|---|:---:|:---:|:---:|:---:|---|
|
| 42 |
+
| 1 | Dependency comprehension | ✓ | ✓ | ✓ | ✓ | Read DAG, understand what blocks what |
|
| 43 |
+
| 2 | Correct delegation | ✓ | ✓ | ✓ | ✓ | Match subtask type → agent capability |
|
| 44 |
+
| 3 | Sequential ordering | ✓ | ✓ | ✓ | ✓ | Don't delegate before prerequisites complete |
|
| 45 |
+
| 4 | Wait discipline | ✓ | ✓ | ✓ | ✓ | Wait when nothing is delegatable |
|
| 46 |
+
| 5 | Output synthesis | ✓ | ✓ | ✓ | ✓ | Combine outputs into final deliverable |
|
| 47 |
+
| 6 | Parallelism detection | | ✓ | ✓ | ✓ | Run independent subtasks concurrently |
|
| 48 |
+
| 7 | Failure recovery | | ✓ | ✓ | ✓ | Retry after agent failure |
|
| 49 |
+
| 8 | Capacity management | | ✓ | ✓ | ✓ | Stay within max concurrent task limit |
|
| 50 |
+
| 9 | Time pressure planning | | ✓ | ✓ | ✓ | Must parallelize to meet deadline |
|
| 51 |
+
| 10 | Cost awareness | | ✓ | ✓ | ✓ | Don't blindly retry expensive agents |
|
| 52 |
+
| 11 | Agent selection under overlap | | | ✓ | ✓ | Multiple agents for same task — pick best |
|
| 53 |
+
| 12 | Adaptation to agent dropout | | | ✓ | ✓ | Re-plan when agent goes offline |
|
| 54 |
+
| 13 | Conflicting info aggregation | | | ✓ | ✓ | Two tracks produce different findings |
|
| 55 |
+
| 14 | SLA milestone awareness | | | ✓ | ✓ | Hit deadlines or face escalating penalties |
|
| 56 |
+
| 15 | Patience under pressure | | | ✓ | | Wait for monitoring — resist premature synthesis |
|
| 57 |
+
| 16 | Priority reasoning | | | ✓ | ✓ | Side-channel tasks mustn't block critical path |
|
| 58 |
+
| 17 | Error classification | | | ✓ | ✓ | Distinguish permanent vs. transient failure |
|
| 59 |
+
| 18 | Multi-objective optimization | | | | ✓ | Balance competing pillar scores |
|
| 60 |
+
| 19 | Cross-domain conflict resolution | | | | ✓ | Reconcile health vs career vs personal |
|
| 61 |
+
| 20 | Agent cost-benefit analysis | | | ✓ | ✓ | When is the cheap agent actually more expensive? |
|
| 62 |
+
| 21 | Cascading delay awareness | | | | ✓ | Speed degradation means earlier delegation = critical |
|
| 63 |
+
| 22 | Multi-conflict episodes | | | | ✓ | Two distinct conflict resolution points |
|
| 64 |
+
|
| 65 |
+
**Difficulty progression:** Easy = DAG comprehension (1-5). Medium = parallelism + recovery + budgets (6-10). Hard = chaos adaptation + error classification (11-17). Expert = multi-objective optimization across life domains (18-22).
|
| 66 |
+
|
| 67 |
+
## Grader Design
|
| 68 |
+
|
| 69 |
+
Each task has a multi-dimensional grader returning a score in [0.0, 1.0] with a detailed breakdown. Graders analyze the **episode event log** (process matters, not just outcome) and are fully **deterministic** — same actions = same score.
|
| 70 |
+
|
| 71 |
+
| Task | Dimensions | Key Metrics |
|
| 72 |
+
|------|-----------|-------------|
|
| 73 |
+
| Easy | 4 | completion (85%), parallelism bonus (10%), episode complete (5%), invalid penalty |
|
| 74 |
+
| Medium | 6 | completion (40%), parallelism (20%), failure recovery (20%), time efficiency (10%), cost efficiency (10%) |
|
| 75 |
+
| Hard | 10 | completion (20%), recovery (15%), error classification (10%), capacity (10%), parallelism (10%), cost (10%), conflict resolution (10%), SLA compliance (10%), monitoring patience (5%) |
|
| 76 |
+
| Expert | 10 | completion (15%), health pillar (12%), career pillar (10%), conflict resolution (20%), cost (8%), parallelism (10%), time (5%), error classification (8%), SLA (8%), communication (4%) |
|
| 77 |
+
|
| 78 |
+
**Activity-gated scoring:** Dimensions that reward "no harm" (error classification, capacity discipline, cost efficiency) scale with actual activity via `min(1.0, completed / threshold)`. A do-nothing policy scores 0.0, not free points.
|
| 79 |
|
| 80 |
## Action Space
|
| 81 |
|
|
|
|
| 159 |
|
| 160 |
## Baseline Scores
|
| 161 |
|
| 162 |
+
Scores from running the baseline inference script with Qwen3-32B via OpenRouter:
|
| 163 |
+
|
| 164 |
+
| Task | Score | Subtasks | Key Challenge |
|
| 165 |
+
|------|-------|----------|---------------|
|
| 166 |
+
| Easy | 0.900 | 6/6 | DAG comprehension + optional parallelism |
|
| 167 |
+
| Medium | 0.626 | 9/9 | 3-way fan-out + security scan failure recovery |
|
| 168 |
+
| Hard | 0.724 | 10/10 | Permanent failure trap + agent dropout + SLA pressure |
|
| 169 |
+
| Expert | 0.747 | 14/14 | Multi-objective optimization + 2 permanent failure traps |
|
| 170 |
+
|
| 171 |
+
Scores reflect the LLM's orchestration ability: easy tasks are near-perfect, while medium/hard/expert require parallelism planning, failure recovery, and cost optimization that challenge even frontier models.
|
| 172 |
|
| 173 |
## Setup
|
| 174 |
|
|
|
|
| 231 |
│ ├── agent_pool.py # Simulated agent state machines
|
| 232 |
│ ├── reward_calculator.py # Dense reward computation
|
| 233 |
│ ├── graders.py # Per-task grading functions
|
| 234 |
+
│ ├── task_registry.py # Easy/medium/hard/expert task configurations
|
| 235 |
│ └── observation_formatter.py # Text rendering for LLM consumption
|
| 236 |
+
└── tests/ # 160+ passing tests
|
| 237 |
```
|
baseline_scores.json
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
{"easy": 0.9, "medium": 0.
|
|
|
|
| 1 |
+
{"easy": 0.9, "medium": 0.6263, "hard": 0.7239, "expert": 0.7467}
|
inference.py
CHANGED
|
@@ -45,7 +45,7 @@ MAX_TOKENS: int = 4096 # High ceiling for verbose models; concise models just u
|
|
| 45 |
MAX_STEPS: int = 50
|
| 46 |
SUCCESS_SCORE_THRESHOLD: float = 0.1
|
| 47 |
TASK_TIMEOUT_S: int = 600 # 10 min per task; total 4 tasks fits in 20 min hackathon limit
|
| 48 |
-
HISTORY_WINDOW: int =
|
| 49 |
|
| 50 |
VALID_ACTIONS: set[str] = {"delegate", "retry", "wait", "synthesize", "abort"}
|
| 51 |
|
|
@@ -63,15 +63,21 @@ ACTIONS (as JSON):
|
|
| 63 |
{"action_type": "synthesize"}
|
| 64 |
{"action_type": "abort", "subtask_id": "<id>"}
|
| 65 |
|
| 66 |
-
|
| 67 |
-
1.
|
| 68 |
-
2.
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
Respond with a single JSON object. Nothing else."""
|
| 77 |
|
|
@@ -259,19 +265,24 @@ def parse_llm_action(response_text: str) -> Dict[str, Any]:
|
|
| 259 |
|
| 260 |
|
| 261 |
def _call_llm(messages: List[Dict[str, str]]) -> str:
|
| 262 |
-
"""Synchronous LLM call — designed to be run via asyncio.to_thread()."""
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
|
| 277 |
# ── Task runner ──
|
|
|
|
| 45 |
MAX_STEPS: int = 50
|
| 46 |
SUCCESS_SCORE_THRESHOLD: float = 0.1
|
| 47 |
TASK_TIMEOUT_S: int = 600 # 10 min per task; total 4 tasks fits in 20 min hackathon limit
|
| 48 |
+
HISTORY_WINDOW: int = 6 # Last 6 turns — expert task needs longer context to avoid repeating mistakes
|
| 49 |
|
| 50 |
VALID_ACTIONS: set[str] = {"delegate", "retry", "wait", "synthesize", "abort"}
|
| 51 |
|
|
|
|
| 63 |
{"action_type": "synthesize"}
|
| 64 |
{"action_type": "abort", "subtask_id": "<id>"}
|
| 65 |
|
| 66 |
+
DECISION PROCEDURE (follow this exact order):
|
| 67 |
+
1. If ALL subtasks are COMPLETED → synthesize immediately (unless you need monitoring waits).
|
| 68 |
+
2. If any subtask is FAILED → retry it NOW with a capable IDLE agent.
|
| 69 |
+
CRITICAL: If error says "permanent failure" or "lacks required tooling", the SAME agent will ALWAYS fail. You MUST pick a DIFFERENT agent with the same capability. Never retry the same agent on a permanent failure.
|
| 70 |
+
3. If any subtask is READY and a capable IDLE agent exists → delegate it.
|
| 71 |
+
- When MULTIPLE subtasks are READY, delegate them ALL across consecutive steps before waiting.
|
| 72 |
+
- Prefer cheaper agents (lower cost_per_step) when multiple agents can do the same task.
|
| 73 |
+
4. ONLY wait when no subtasks are READY or all capable agents are busy working.
|
| 74 |
+
5. After validate_fix completes, wait 2 steps for monitoring before synthesizing.
|
| 75 |
+
|
| 76 |
+
KEY PRINCIPLES:
|
| 77 |
+
- MAXIMIZE PARALLELISM: If 3 subtasks are ready and 3 agents are idle, delegate all 3 in 3 consecutive steps.
|
| 78 |
+
- NEVER wait when there is a READY subtask and an IDLE capable agent — this wastes a step.
|
| 79 |
+
- Check the HINT line — it tells you what to do next.
|
| 80 |
+
- Match subtask TYPE to agent CAPABILITIES (not just any agent).
|
| 81 |
|
| 82 |
Respond with a single JSON object. Nothing else."""
|
| 83 |
|
|
|
|
| 265 |
|
| 266 |
|
| 267 |
def _call_llm(messages: List[Dict[str, str]]) -> str:
|
| 268 |
+
"""Synchronous LLM call with retry — designed to be run via asyncio.to_thread()."""
|
| 269 |
+
import time
|
| 270 |
+
|
| 271 |
+
for attempt in range(2):
|
| 272 |
+
try:
|
| 273 |
+
completion = llm.chat.completions.create(
|
| 274 |
+
model=MODEL_NAME,
|
| 275 |
+
messages=messages,
|
| 276 |
+
temperature=TEMPERATURE,
|
| 277 |
+
max_tokens=MAX_TOKENS,
|
| 278 |
+
stream=False,
|
| 279 |
+
)
|
| 280 |
+
return completion.choices[0].message.content or ""
|
| 281 |
+
except Exception as exc:
|
| 282 |
+
print(f"[DEBUG] LLM call failed (attempt {attempt + 1}): {exc}", flush=True)
|
| 283 |
+
if attempt == 0:
|
| 284 |
+
time.sleep(2)
|
| 285 |
+
return ""
|
| 286 |
|
| 287 |
|
| 288 |
# ── Task runner ──
|
server/graders.py
CHANGED
|
@@ -203,6 +203,10 @@ def grade_medium(log: EpisodeLog) -> GradeResult:
|
|
| 203 |
completed = count_completed_subtasks(log)
|
| 204 |
completion = (completed / 9) * 0.40
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
parallelism = 0.20 if fan_out_parallelism_detected(
|
| 207 |
log, ["run_linter", "run_unit_tests", "run_security_scan"]
|
| 208 |
) else 0.0
|
|
@@ -217,7 +221,7 @@ def grade_medium(log: EpisodeLog) -> GradeResult:
|
|
| 217 |
budget_total = get_total_budget(log)
|
| 218 |
cost_eff = 0.0
|
| 219 |
if budget_total and budget_total > 0:
|
| 220 |
-
cost_eff = 0.10 * max(0.0, 1.0 - budget_used / budget_total)
|
| 221 |
|
| 222 |
invalid_count = count_invalid_actions(log)
|
| 223 |
penalty = min(0.10, invalid_count * 0.03)
|
|
@@ -247,6 +251,12 @@ def grade_hard(log: EpisodeLog) -> GradeResult:
|
|
| 247 |
completed = count_completed_subtasks(log)
|
| 248 |
completion = (completed / 10) * 0.20
|
| 249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
# Recovery: 0.15 split across two designed failure scenarios
|
| 251 |
# - enrich_logs: investigator_alpha permanently fails (must use different agent)
|
| 252 |
# - deploy_hotfix: deployer goes offline at step 12 (must use senior_engineer)
|
|
@@ -258,9 +268,9 @@ def grade_hard(log: EpisodeLog) -> GradeResult:
|
|
| 258 |
recovery = 0.15 * (recovery_count / 2) if recovery_count > 0 else 0.0
|
| 259 |
|
| 260 |
perm_retries = count_retries_on_permanent_failure(log)
|
| 261 |
-
error_class = 0.10 * max(0.0, 1.0 - 0.5 * perm_retries)
|
| 262 |
|
| 263 |
-
capacity = 0.10 if zero_capacity_violations(log) else 0.0
|
| 264 |
|
| 265 |
parallelism = 0.10 * compute_parallel_efficiency(log)
|
| 266 |
|
|
@@ -268,7 +278,7 @@ def grade_hard(log: EpisodeLog) -> GradeResult:
|
|
| 268 |
budget_total = get_total_budget(log)
|
| 269 |
cost_eff = 0.0
|
| 270 |
if budget_total and budget_total > 0:
|
| 271 |
-
cost_eff = 0.10 * max(0.0, 1.0 - budget_used / budget_total)
|
| 272 |
|
| 273 |
conflict = 0.10 if both_findings_aggregated(log) else 0.0
|
| 274 |
|
|
@@ -382,6 +392,10 @@ def grade_expert(log: EpisodeLog) -> GradeResult:
|
|
| 382 |
completed = count_completed_subtasks(log)
|
| 383 |
breakdown["completion"] = (completed / 14) * 0.15
|
| 384 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
# 2. Health pillar: 12%
|
| 386 |
health_score = 0.0
|
| 387 |
if subtask_completed_check(log, "assess_sleep_energy"):
|
|
@@ -412,13 +426,13 @@ def grade_expert(log: EpisodeLog) -> GradeResult:
|
|
| 412 |
conflict_score += 0.3
|
| 413 |
breakdown["conflict_resolution"] = conflict_score * 0.20
|
| 414 |
|
| 415 |
-
# 5. Cost efficiency: 8%
|
| 416 |
budget_used = get_total_budget_used(log)
|
| 417 |
cost_ratio = budget_used / 55.0 if 55.0 > 0 else 0
|
| 418 |
if cost_ratio <= 0.75:
|
| 419 |
-
breakdown["cost_efficiency"] = 0.08
|
| 420 |
elif cost_ratio <= 1.0:
|
| 421 |
-
breakdown["cost_efficiency"] = (1.0 - cost_ratio) / 0.25 * 0.08
|
| 422 |
else:
|
| 423 |
breakdown["cost_efficiency"] = 0.0
|
| 424 |
|
|
@@ -441,9 +455,9 @@ def grade_expert(log: EpisodeLog) -> GradeResult:
|
|
| 441 |
else:
|
| 442 |
breakdown["time_efficiency"] = 0.0
|
| 443 |
|
| 444 |
-
# 8. Error classification: 8%
|
| 445 |
perm_retries = count_retries_on_permanent_failure(log)
|
| 446 |
-
breakdown["error_classification"] = max(0.0, 1.0 - 0.5 * perm_retries) * 0.08
|
| 447 |
|
| 448 |
# 9. SLA compliance: 8%
|
| 449 |
sla_milestones = {"plan_day_schedule": 8, "resolve_priority_conflict": 16, "synthesize_day_report": 23}
|
|
|
|
| 203 |
completed = count_completed_subtasks(log)
|
| 204 |
completion = (completed / 9) * 0.40
|
| 205 |
|
| 206 |
+
# Activity gate: cost efficiency should scale with actual activity.
|
| 207 |
+
# Reaching 3+ completions (33% of subtasks) earns full credit.
|
| 208 |
+
activity = min(1.0, completed / 3)
|
| 209 |
+
|
| 210 |
parallelism = 0.20 if fan_out_parallelism_detected(
|
| 211 |
log, ["run_linter", "run_unit_tests", "run_security_scan"]
|
| 212 |
) else 0.0
|
|
|
|
| 221 |
budget_total = get_total_budget(log)
|
| 222 |
cost_eff = 0.0
|
| 223 |
if budget_total and budget_total > 0:
|
| 224 |
+
cost_eff = 0.10 * max(0.0, 1.0 - budget_used / budget_total) * activity
|
| 225 |
|
| 226 |
invalid_count = count_invalid_actions(log)
|
| 227 |
penalty = min(0.10, invalid_count * 0.03)
|
|
|
|
| 251 |
completed = count_completed_subtasks(log)
|
| 252 |
completion = (completed / 10) * 0.20
|
| 253 |
|
| 254 |
+
# Activity gate: dimensions that reward "no harm" (error classification,
|
| 255 |
+
# capacity discipline, cost efficiency) should scale with actual activity.
|
| 256 |
+
# A do-nothing policy (0 completions) gets 0 on these dimensions.
|
| 257 |
+
# Reaching 3+ completions (30% of subtasks) earns full credit.
|
| 258 |
+
activity = min(1.0, completed / 3)
|
| 259 |
+
|
| 260 |
# Recovery: 0.15 split across two designed failure scenarios
|
| 261 |
# - enrich_logs: investigator_alpha permanently fails (must use different agent)
|
| 262 |
# - deploy_hotfix: deployer goes offline at step 12 (must use senior_engineer)
|
|
|
|
| 268 |
recovery = 0.15 * (recovery_count / 2) if recovery_count > 0 else 0.0
|
| 269 |
|
| 270 |
perm_retries = count_retries_on_permanent_failure(log)
|
| 271 |
+
error_class = 0.10 * max(0.0, 1.0 - 0.5 * perm_retries) * activity
|
| 272 |
|
| 273 |
+
capacity = (0.10 if zero_capacity_violations(log) else 0.0) * activity
|
| 274 |
|
| 275 |
parallelism = 0.10 * compute_parallel_efficiency(log)
|
| 276 |
|
|
|
|
| 278 |
budget_total = get_total_budget(log)
|
| 279 |
cost_eff = 0.0
|
| 280 |
if budget_total and budget_total > 0:
|
| 281 |
+
cost_eff = 0.10 * max(0.0, 1.0 - budget_used / budget_total) * activity
|
| 282 |
|
| 283 |
conflict = 0.10 if both_findings_aggregated(log) else 0.0
|
| 284 |
|
|
|
|
| 392 |
completed = count_completed_subtasks(log)
|
| 393 |
breakdown["completion"] = (completed / 14) * 0.15
|
| 394 |
|
| 395 |
+
# Activity gate: "no harm" dimensions scale with actual activity.
|
| 396 |
+
# Reaching 4+ completions (29% of subtasks) earns full credit.
|
| 397 |
+
activity = min(1.0, completed / 4)
|
| 398 |
+
|
| 399 |
# 2. Health pillar: 12%
|
| 400 |
health_score = 0.0
|
| 401 |
if subtask_completed_check(log, "assess_sleep_energy"):
|
|
|
|
| 426 |
conflict_score += 0.3
|
| 427 |
breakdown["conflict_resolution"] = conflict_score * 0.20
|
| 428 |
|
| 429 |
+
# 5. Cost efficiency: 8% (gated by activity)
|
| 430 |
budget_used = get_total_budget_used(log)
|
| 431 |
cost_ratio = budget_used / 55.0 if 55.0 > 0 else 0
|
| 432 |
if cost_ratio <= 0.75:
|
| 433 |
+
breakdown["cost_efficiency"] = 0.08 * activity
|
| 434 |
elif cost_ratio <= 1.0:
|
| 435 |
+
breakdown["cost_efficiency"] = (1.0 - cost_ratio) / 0.25 * 0.08 * activity
|
| 436 |
else:
|
| 437 |
breakdown["cost_efficiency"] = 0.0
|
| 438 |
|
|
|
|
| 455 |
else:
|
| 456 |
breakdown["time_efficiency"] = 0.0
|
| 457 |
|
| 458 |
+
# 8. Error classification: 8% (gated by activity)
|
| 459 |
perm_retries = count_retries_on_permanent_failure(log)
|
| 460 |
+
breakdown["error_classification"] = max(0.0, 1.0 - 0.5 * perm_retries) * 0.08 * activity
|
| 461 |
|
| 462 |
# 9. SLA compliance: 8%
|
| 463 |
sla_milestones = {"plan_day_schedule": 8, "resolve_priority_conflict": 16, "synthesize_day_report": 23}
|
tests/test_environment.py
CHANGED
|
@@ -479,9 +479,10 @@ class TestHardTaskEdgeCases:
|
|
| 479 |
_wait(env)
|
| 480 |
log = _episode_store["hard"]
|
| 481 |
result = grade_hard(log)
|
| 482 |
-
#
|
| 483 |
-
#
|
| 484 |
-
|
|
|
|
| 485 |
assert result.breakdown["completion"] == 0.0
|
| 486 |
assert result.breakdown["recovery"] == 0.0
|
| 487 |
assert result.breakdown["sla_compliance"] == 0.0
|
|
@@ -500,3 +501,244 @@ class TestHardTaskEdgeCases:
|
|
| 500 |
assert state.subtask_statuses["root_cause_analysis"] != "completed"
|
| 501 |
# Total reward should be negative due to SLA + unnecessary_wait penalties
|
| 502 |
assert env._total_reward < 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
_wait(env)
|
| 480 |
log = _episode_store["hard"]
|
| 481 |
result = grade_hard(log)
|
| 482 |
+
# With activity gate, doing nothing scores 0.0: "no harm" dimensions
|
| 483 |
+
# (error_classification, capacity_discipline, cost_efficiency) are
|
| 484 |
+
# gated by min(1.0, completed/3) which is 0.0 when nothing is completed.
|
| 485 |
+
assert result.score <= 0.05
|
| 486 |
assert result.breakdown["completion"] == 0.0
|
| 487 |
assert result.breakdown["recovery"] == 0.0
|
| 488 |
assert result.breakdown["sla_compliance"] == 0.0
|
|
|
|
| 501 |
assert state.subtask_statuses["root_cause_analysis"] != "completed"
|
| 502 |
# Total reward should be negative due to SLA + unnecessary_wait penalties
|
| 503 |
assert env._total_reward < 0
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
# ── Expert task walkthrough ──
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
class TestExpertTaskWalkthrough:
|
| 510 |
+
"""Full walkthrough of the expert task (Life OS Daily Orchestration).
|
| 511 |
+
|
| 512 |
+
The 18-step sequence was verified against the seeded RNG (seed=45) to
|
| 513 |
+
produce deterministic outcomes. It completes all 14 subtasks, achieves
|
| 514 |
+
full parallelism across all 3 fan-out points, meets all 3 SLA milestones,
|
| 515 |
+
avoids permanent failure traps, and routes around the personal_agent
|
| 516 |
+
dropout at step 10.
|
| 517 |
+
|
| 518 |
+
Grader score: 0.948 (8/10 dimensions at 100%).
|
| 519 |
+
"""
|
| 520 |
+
|
| 521 |
+
def _run_known_good_sequence(self):
|
| 522 |
+
"""Execute the known-good 18-step expert task walkthrough.
|
| 523 |
+
|
| 524 |
+
Returns (env, final_obs) so callers can make assertions.
|
| 525 |
+
|
| 526 |
+
Step-by-step plan:
|
| 527 |
+
- S0: morning_check_in -> personal_agent (speed=1, completes immediately)
|
| 528 |
+
- S1: assess_career_deadlines -> career_agent (speed=2, stays in_progress)
|
| 529 |
+
- S2: assess_sleep_energy -> health_agent (speed=1)
|
| 530 |
+
PARALLELISM: [assess_career_deadlines, assess_sleep_energy]
|
| 531 |
+
Both complete; career_agent finishes career_deadlines
|
| 532 |
+
- S3: assess_personal_commitments -> personal_agent (speed=1, completes)
|
| 533 |
+
All 3 assessments done -> plan_day_schedule ready
|
| 534 |
+
- S4: plan_day_schedule -> companion (speed=2, stays in_progress)
|
| 535 |
+
- S5: wait (companion completes plan_day_schedule at step 5; SLA met)
|
| 536 |
+
- S6: process_inbox -> executive_assistant (speed=2, stays in_progress)
|
| 537 |
+
- S7: start_focus_session -> focus_agent (speed=1)
|
| 538 |
+
PARALLELISM: [process_inbox, start_focus_session]
|
| 539 |
+
Both complete; career_agent degrades (speed 2->4) this step
|
| 540 |
+
- S8: deep_work_block -> companion (speed=2, stays in_progress)
|
| 541 |
+
- S9: handle_urgent_request -> executive_assistant (speed=2)
|
| 542 |
+
PARALLELISM: [deep_work_block, handle_urgent_request]
|
| 543 |
+
companion completes deep_work_block; exec_asst still working
|
| 544 |
+
- S10: midday_health_check -> health_agent (speed=1)
|
| 545 |
+
PARALLELISM: [handle_urgent_request, midday_health_check]
|
| 546 |
+
Both complete; personal_agent drops out (idle, no impact)
|
| 547 |
+
-> resolve_priority_conflict ready
|
| 548 |
+
- S11: resolve_priority_conflict -> companion (speed=2, stays in_progress)
|
| 549 |
+
- S12: wait (companion completes; SLA met at step 12 < 16)
|
| 550 |
+
- S13: afternoon_execution -> companion (speed=2, stays in_progress)
|
| 551 |
+
- S14: notify_stakeholders -> mail_agent (speed=1, FAILS: roll > reliability)
|
| 552 |
+
PARALLELISM: [afternoon_execution, notify_stakeholders]
|
| 553 |
+
companion completes afternoon_execution; mail_agent fails
|
| 554 |
+
- S15: retry notify_stakeholders -> mail_agent (attempt=1, succeeds)
|
| 555 |
+
- S16: synthesize_day_report -> mail_agent (speed=1, completes)
|
| 556 |
+
SLA met at step 16 < 23
|
| 557 |
+
- S17: synthesize (all 14 done, episode ends with bonus)
|
| 558 |
+
"""
|
| 559 |
+
env, obs = _make_env("expert")
|
| 560 |
+
|
| 561 |
+
# Phase 1: Morning check-in
|
| 562 |
+
_delegate(env, "morning_check_in", "personal_agent")
|
| 563 |
+
|
| 564 |
+
# Phase 2: Three assessments with parallelism
|
| 565 |
+
_delegate(env, "assess_career_deadlines", "career_agent")
|
| 566 |
+
_delegate(env, "assess_sleep_energy", "health_agent")
|
| 567 |
+
_delegate(env, "assess_personal_commitments", "personal_agent")
|
| 568 |
+
|
| 569 |
+
# Phase 3: Plan day schedule (companion only viable first-attempt agent)
|
| 570 |
+
_delegate(env, "plan_day_schedule", "companion")
|
| 571 |
+
_wait(env)
|
| 572 |
+
|
| 573 |
+
# Phase 4: Focus + Inbox with parallelism
|
| 574 |
+
_delegate(env, "process_inbox", "executive_assistant")
|
| 575 |
+
_delegate(env, "start_focus_session", "focus_agent")
|
| 576 |
+
|
| 577 |
+
# Phase 5: Deep work + Urgent request with parallelism
|
| 578 |
+
_delegate(env, "deep_work_block", "companion")
|
| 579 |
+
_delegate(env, "handle_urgent_request", "executive_assistant")
|
| 580 |
+
|
| 581 |
+
# Phase 6: Midday health check (avoid wellness_monitor!)
|
| 582 |
+
_delegate(env, "midday_health_check", "health_agent")
|
| 583 |
+
|
| 584 |
+
# Phase 7: Conflict resolution (only companion viable)
|
| 585 |
+
_delegate(env, "resolve_priority_conflict", "companion")
|
| 586 |
+
_wait(env)
|
| 587 |
+
|
| 588 |
+
# Phase 8: Afternoon + Notify with parallelism
|
| 589 |
+
_delegate(env, "afternoon_execution", "companion")
|
| 590 |
+
_delegate(env, "notify_stakeholders", "mail_agent") # fails attempt 0
|
| 591 |
+
|
| 592 |
+
# Phase 9: Retry notify, then final report
|
| 593 |
+
_retry(env, "notify_stakeholders", "mail_agent") # attempt 1 succeeds
|
| 594 |
+
_delegate(env, "synthesize_day_report", "mail_agent")
|
| 595 |
+
|
| 596 |
+
# Phase 10: Synthesize
|
| 597 |
+
obs = _synthesize(env)
|
| 598 |
+
|
| 599 |
+
return env, obs
|
| 600 |
+
|
| 601 |
+
def test_all_14_subtasks_complete(self) -> None:
|
| 602 |
+
"""All 14 subtasks should be completed after the walkthrough."""
|
| 603 |
+
env, obs = self._run_known_good_sequence()
|
| 604 |
+
assert obs.done is True
|
| 605 |
+
assert len(obs.completed_outputs) == 14
|
| 606 |
+
assert env._dag.is_all_completed()
|
| 607 |
+
|
| 608 |
+
def test_episode_terminates_with_positive_reward(self) -> None:
|
| 609 |
+
"""Synthesize should trigger done and large positive total reward."""
|
| 610 |
+
env, obs = self._run_known_good_sequence()
|
| 611 |
+
assert obs.done is True
|
| 612 |
+
assert obs.reward > 0 # end bonus included
|
| 613 |
+
assert env._total_reward > 2.5 # verified: ~2.988
|
| 614 |
+
|
| 615 |
+
def test_time_and_budget(self) -> None:
|
| 616 |
+
"""Verify time remaining and budget match expected values."""
|
| 617 |
+
env, obs = self._run_known_good_sequence()
|
| 618 |
+
assert obs.time_remaining == 7 # 25 - 18 steps
|
| 619 |
+
assert env._pool.get_budget_used() == pytest.approx(44.0, abs=0.1)
|
| 620 |
+
|
| 621 |
+
def test_failure_and_recovery(self) -> None:
|
| 622 |
+
"""mail_agent fails notify_stakeholders attempt 0, recovers on retry."""
|
| 623 |
+
env, obs = self._run_known_good_sequence()
|
| 624 |
+
assert env._failures_occurred == 1
|
| 625 |
+
assert env._failures_recovered == 1
|
| 626 |
+
|
| 627 |
+
def test_parallelism_detected(self) -> None:
|
| 628 |
+
"""Should detect parallelism across multiple fan-out points."""
|
| 629 |
+
env, obs = self._run_known_good_sequence()
|
| 630 |
+
# Morning assessments, focus+inbox, deep+urgent, urgent+health,
|
| 631 |
+
# afternoon+notify = 5+ parallelism events
|
| 632 |
+
assert env._parallelism_events >= 5
|
| 633 |
+
|
| 634 |
+
def test_personal_agent_offline_after_dropout(self) -> None:
|
| 635 |
+
"""personal_agent should be offline after step 10 dropout event."""
|
| 636 |
+
env, obs = self._run_known_good_sequence()
|
| 637 |
+
assert env._pool.is_online("personal_agent") is False
|
| 638 |
+
|
| 639 |
+
def test_career_agent_degraded(self) -> None:
|
| 640 |
+
"""career_agent speed should be 4 after degradation at step 7."""
|
| 641 |
+
env, obs = self._run_known_good_sequence()
|
| 642 |
+
agent_infos = env._pool.get_agent_infos()
|
| 643 |
+
career = [a for a in agent_infos if a.name == "career_agent"][0]
|
| 644 |
+
assert career.speed == 4
|
| 645 |
+
|
| 646 |
+
def test_zero_capacity_violations(self) -> None:
|
| 647 |
+
"""No capacity violations in the known-good sequence."""
|
| 648 |
+
env, obs = self._run_known_good_sequence()
|
| 649 |
+
assert env._capacity_violations == 0
|
| 650 |
+
|
| 651 |
+
def test_grader_score_above_threshold(self) -> None:
|
| 652 |
+
"""Grader should score >= 0.94 for the known-good walkthrough."""
|
| 653 |
+
from server.graders import grade_expert
|
| 654 |
+
|
| 655 |
+
env, obs = self._run_known_good_sequence()
|
| 656 |
+
log = _episode_store["expert"]
|
| 657 |
+
result = grade_expert(log)
|
| 658 |
+
assert result.score >= 0.94
|
| 659 |
+
assert result.score <= 1.0
|
| 660 |
+
|
| 661 |
+
def test_grader_all_dimensions_present(self) -> None:
|
| 662 |
+
"""All 10 grader dimensions should be present and non-negative."""
|
| 663 |
+
from server.graders import grade_expert
|
| 664 |
+
|
| 665 |
+
env, obs = self._run_known_good_sequence()
|
| 666 |
+
log = _episode_store["expert"]
|
| 667 |
+
result = grade_expert(log)
|
| 668 |
+
|
| 669 |
+
expected_keys = [
|
| 670 |
+
"completion", "health_pillar", "career_pillar",
|
| 671 |
+
"conflict_resolution", "cost_efficiency", "parallelism",
|
| 672 |
+
"time_efficiency", "error_classification", "sla_compliance",
|
| 673 |
+
"communication",
|
| 674 |
+
]
|
| 675 |
+
for key in expected_keys:
|
| 676 |
+
assert key in result.breakdown, f"Missing grader dimension: {key}"
|
| 677 |
+
assert result.breakdown[key] >= 0.0, f"{key} is negative"
|
| 678 |
+
|
| 679 |
+
def test_grader_perfect_dimensions(self) -> None:
|
| 680 |
+
"""Verify the known-good walkthrough scores perfectly on 8 of 10 dimensions."""
|
| 681 |
+
from server.graders import grade_expert
|
| 682 |
+
|
| 683 |
+
env, obs = self._run_known_good_sequence()
|
| 684 |
+
log = _episode_store["expert"]
|
| 685 |
+
result = grade_expert(log)
|
| 686 |
+
|
| 687 |
+
# 100% dimensions
|
| 688 |
+
assert result.breakdown["completion"] == pytest.approx(0.15, abs=0.001)
|
| 689 |
+
assert result.breakdown["health_pillar"] == pytest.approx(0.12, abs=0.001)
|
| 690 |
+
assert result.breakdown["career_pillar"] == pytest.approx(0.10, abs=0.001)
|
| 691 |
+
assert result.breakdown["conflict_resolution"] == pytest.approx(0.20, abs=0.001)
|
| 692 |
+
assert result.breakdown["parallelism"] == pytest.approx(0.10, abs=0.001)
|
| 693 |
+
assert result.breakdown["error_classification"] == pytest.approx(0.08, abs=0.001)
|
| 694 |
+
assert result.breakdown["sla_compliance"] == pytest.approx(0.08, abs=0.001)
|
| 695 |
+
assert result.breakdown["communication"] == pytest.approx(0.04, abs=0.001)
|
| 696 |
+
|
| 697 |
+
# Partial dimensions (cost efficiency ~0.064, time efficiency ~0.014)
|
| 698 |
+
assert result.breakdown["cost_efficiency"] > 0.05
|
| 699 |
+
assert result.breakdown["time_efficiency"] > 0.01
|
| 700 |
+
|
| 701 |
+
def test_sla_milestones_all_met(self) -> None:
|
| 702 |
+
"""All 3 SLA milestones should be met."""
|
| 703 |
+
from server.graders import count_sla_milestones_met
|
| 704 |
+
|
| 705 |
+
env, obs = self._run_known_good_sequence()
|
| 706 |
+
log = _episode_store["expert"]
|
| 707 |
+
|
| 708 |
+
sla_milestones = {
|
| 709 |
+
"plan_day_schedule": 8,
|
| 710 |
+
"resolve_priority_conflict": 16,
|
| 711 |
+
"synthesize_day_report": 23,
|
| 712 |
+
}
|
| 713 |
+
met = count_sla_milestones_met(log, sla_milestones)
|
| 714 |
+
assert met == 3
|
| 715 |
+
|
| 716 |
+
def test_permanent_failure_traps_avoided(self) -> None:
|
| 717 |
+
"""Sequence never assigns wellness_monitor to health_alert or
|
| 718 |
+
executive_assistant to conflict_resolution."""
|
| 719 |
+
from server.graders import count_retries_on_permanent_failure
|
| 720 |
+
|
| 721 |
+
env, obs = self._run_known_good_sequence()
|
| 722 |
+
log = _episode_store["expert"]
|
| 723 |
+
|
| 724 |
+
assert count_retries_on_permanent_failure(log) == 0
|
| 725 |
+
|
| 726 |
+
def test_conflict_resolution_by_companion(self) -> None:
|
| 727 |
+
"""resolve_priority_conflict should be completed by companion."""
|
| 728 |
+
from server.graders import subtask_completed_by_agent
|
| 729 |
+
|
| 730 |
+
env, obs = self._run_known_good_sequence()
|
| 731 |
+
log = _episode_store["expert"]
|
| 732 |
+
|
| 733 |
+
assert subtask_completed_by_agent(log, "resolve_priority_conflict", "companion")
|
| 734 |
+
|
| 735 |
+
def test_midday_health_not_by_wellness_monitor(self) -> None:
|
| 736 |
+
"""midday_health_check should NOT be completed by wellness_monitor."""
|
| 737 |
+
from server.graders import subtask_not_completed_by_agent
|
| 738 |
+
|
| 739 |
+
env, obs = self._run_known_good_sequence()
|
| 740 |
+
log = _episode_store["expert"]
|
| 741 |
+
|
| 742 |
+
assert subtask_not_completed_by_agent(
|
| 743 |
+
log, "midday_health_check", ["wellness_monitor"]
|
| 744 |
+
)
|
tests/test_graders.py
CHANGED
|
@@ -14,6 +14,7 @@ from server.graders import (
|
|
| 14 |
fan_out_parallelism_detected,
|
| 15 |
grade,
|
| 16 |
grade_easy,
|
|
|
|
| 17 |
grade_hard,
|
| 18 |
grade_medium,
|
| 19 |
monitoring_completed,
|
|
@@ -233,3 +234,105 @@ class TestGradeDispatcher:
|
|
| 233 |
log = _make_log("unknown")
|
| 234 |
with pytest.raises(KeyError):
|
| 235 |
grade("unknown", log)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
fan_out_parallelism_detected,
|
| 15 |
grade,
|
| 16 |
grade_easy,
|
| 17 |
+
grade_expert,
|
| 18 |
grade_hard,
|
| 19 |
grade_medium,
|
| 20 |
monitoring_completed,
|
|
|
|
| 234 |
log = _make_log("unknown")
|
| 235 |
with pytest.raises(KeyError):
|
| 236 |
grade("unknown", log)
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
# ── Degenerate policy tests ──
|
| 240 |
+
# Prove that graders are well-calibrated: doing nothing scores near 0,
|
| 241 |
+
# and known-good walkthroughs always outscore degenerate policies.
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
class TestDegeneratePolicies:
|
| 245 |
+
"""Verify that a do-nothing policy (all waits) scores near 0 on every task.
|
| 246 |
+
|
| 247 |
+
This validates the activity gate mechanism: dimensions that reward "no harm"
|
| 248 |
+
(error classification, capacity discipline, cost efficiency) scale with
|
| 249 |
+
actual task completion, preventing free points for inaction.
|
| 250 |
+
"""
|
| 251 |
+
|
| 252 |
+
@staticmethod
|
| 253 |
+
def _run_do_nothing(task_id: str) -> GradeResult:
|
| 254 |
+
"""Run a do-nothing policy (all waits) and return grader result."""
|
| 255 |
+
from server.environment import OrchestratorEnvironment, _episode_store
|
| 256 |
+
from models import OrchestratorAction
|
| 257 |
+
|
| 258 |
+
time_budgets = {"easy": 15, "medium": 16, "hard": 22, "expert": 25}
|
| 259 |
+
env = OrchestratorEnvironment()
|
| 260 |
+
env.reset(task_id=task_id)
|
| 261 |
+
for _ in range(time_budgets[task_id]):
|
| 262 |
+
env.step(OrchestratorAction(action_type="wait"))
|
| 263 |
+
return grade(task_id, _episode_store[task_id])
|
| 264 |
+
|
| 265 |
+
def test_do_nothing_easy_scores_zero(self) -> None:
|
| 266 |
+
result = self._run_do_nothing("easy")
|
| 267 |
+
assert result.score == 0.0
|
| 268 |
+
|
| 269 |
+
def test_do_nothing_medium_scores_near_zero(self) -> None:
|
| 270 |
+
result = self._run_do_nothing("medium")
|
| 271 |
+
assert result.score <= 0.05
|
| 272 |
+
|
| 273 |
+
def test_do_nothing_hard_scores_near_zero(self) -> None:
|
| 274 |
+
result = self._run_do_nothing("hard")
|
| 275 |
+
assert result.score <= 0.05
|
| 276 |
+
|
| 277 |
+
def test_do_nothing_expert_scores_near_zero(self) -> None:
|
| 278 |
+
result = self._run_do_nothing("expert")
|
| 279 |
+
assert result.score <= 0.05
|
| 280 |
+
|
| 281 |
+
def test_known_good_easy_outscores_degenerate(self) -> None:
|
| 282 |
+
"""Known-good easy walkthrough must score higher than do-nothing."""
|
| 283 |
+
from server.environment import OrchestratorEnvironment, _episode_store
|
| 284 |
+
from models import OrchestratorAction
|
| 285 |
+
|
| 286 |
+
degenerate = self._run_do_nothing("easy")
|
| 287 |
+
|
| 288 |
+
# Run known-good easy walkthrough
|
| 289 |
+
env = OrchestratorEnvironment()
|
| 290 |
+
env.reset(task_id="easy")
|
| 291 |
+
seq = [
|
| 292 |
+
("delegate", "technical_design", "tech_lead"),
|
| 293 |
+
("delegate", "implement_backend", "backend_dev"),
|
| 294 |
+
("delegate", "implement_frontend", "frontend_dev"),
|
| 295 |
+
("delegate", "write_tests", "qa_engineer"),
|
| 296 |
+
("delegate", "run_tests", "backend_dev"),
|
| 297 |
+
("delegate", "review_and_merge", "tech_lead"),
|
| 298 |
+
("synthesize", None, None),
|
| 299 |
+
]
|
| 300 |
+
for action_type, sid, agent in seq:
|
| 301 |
+
env.step(OrchestratorAction(
|
| 302 |
+
action_type=action_type, subtask_id=sid, agent_name=agent,
|
| 303 |
+
))
|
| 304 |
+
good = grade("easy", _episode_store["easy"])
|
| 305 |
+
assert good.score > degenerate.score + 0.5
|
| 306 |
+
|
| 307 |
+
def test_known_good_hard_outscores_degenerate(self) -> None:
|
| 308 |
+
"""Known-good hard walkthrough must score higher than do-nothing."""
|
| 309 |
+
from server.environment import OrchestratorEnvironment, _episode_store
|
| 310 |
+
from models import OrchestratorAction
|
| 311 |
+
|
| 312 |
+
degenerate = self._run_do_nothing("hard")
|
| 313 |
+
|
| 314 |
+
# Run known-good hard walkthrough (14 steps)
|
| 315 |
+
env = OrchestratorEnvironment()
|
| 316 |
+
env.reset(task_id="hard")
|
| 317 |
+
steps = [
|
| 318 |
+
("delegate", "alert_triage", "triage_analyst"),
|
| 319 |
+
("delegate", "enrich_logs", "investigator_alpha"),
|
| 320 |
+
("delegate", "check_dashboards", "monitor"),
|
| 321 |
+
("retry", "enrich_logs", "investigator_beta"),
|
| 322 |
+
("delegate", "check_dependencies", "investigator_alpha"),
|
| 323 |
+
("delegate", "notify_stakeholders", "communicator"),
|
| 324 |
+
("delegate", "root_cause_analysis", "senior_engineer"),
|
| 325 |
+
("delegate", "deploy_hotfix", "deployer"),
|
| 326 |
+
("delegate", "update_status_page", "communicator"),
|
| 327 |
+
("delegate", "validate_fix", "senior_engineer"),
|
| 328 |
+
("delegate", "monitor_recovery", "monitor"),
|
| 329 |
+
("wait", None, None),
|
| 330 |
+
("wait", None, None),
|
| 331 |
+
("synthesize", None, None),
|
| 332 |
+
]
|
| 333 |
+
for action_type, sid, agent in steps:
|
| 334 |
+
env.step(OrchestratorAction(
|
| 335 |
+
action_type=action_type, subtask_id=sid, agent_name=agent,
|
| 336 |
+
))
|
| 337 |
+
good = grade("hard", _episode_store["hard"])
|
| 338 |
+
assert good.score > degenerate.score + 0.5
|