kartikmandar commited on
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 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 three real-world scenarios: software development, CI/CD deployment, and production incident response.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- | Task | Score | Model |
105
- |------|-------|-------|
106
- | Easy | 0.900 | Qwen/Qwen3-32B |
107
- | Medium | 0.633 | Qwen/Qwen3-32B |
108
- | Hard | 0.808 | Qwen/Qwen3-32B |
109
- | Expert | 0.802 | Qwen/Qwen3-32B |
 
 
 
 
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/ # 137 passing 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.6325, "hard": 0.6625, "expert": 0.7467}
 
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 = 4 # Last 4 turns — enough for context without bloating input tokens
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
- RULES:
67
- 1. delegate: Assign a READY subtask to an IDLE agent whose capabilities include the subtask type.
68
- 2. retry: Re-assign a FAILED subtask. If error says "permanent failure" or "lacks required tooling", you MUST pick a DIFFERENT agent.
69
- 3. wait: ONLY use when no READY subtasks exist OR all capable agents are busy.
70
- 4. synthesize: ONLY when ALL subtasks are COMPLETED.
71
- 5. NEVER wait when READY subtasks and idle capable agents exist — delegate instead.
72
- 6. Maximize parallelism: delegate multiple ready subtasks across steps before waiting.
73
- 7. Prefer cheaper agents (lower cost_per_step) when multiple can handle the same task type.
74
- 8. After a fix is validated, wait 2 steps to monitor stability before synthesizing.
 
 
 
 
 
 
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
- try:
264
- completion = llm.chat.completions.create(
265
- model=MODEL_NAME,
266
- messages=messages,
267
- temperature=TEMPERATURE,
268
- max_tokens=MAX_TOKENS,
269
- stream=False,
270
- )
271
- return completion.choices[0].message.content or ""
272
- except Exception as exc:
273
- print(f"[DEBUG] LLM call failed: {exc}", flush=True)
274
- return ""
 
 
 
 
 
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
- # Even doing nothing scores 0.3: error_classification(0.1) +
483
- # capacity_discipline(0.1) + cost_efficiency(0.1) from "no harm done"
484
- assert result.score <= 0.35
 
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