ml-debug-env / server /adversarial_scheduler.py
rak2315's picture
Block A B C: partial observability, LLM judge, adversarial scheduler
49aa3ca
Raw
History Blame Contribute Delete
2.9 kB
"""
Block C β€” Adversarial Variant Generator
Tracks agent performance per bug type across episodes.
After 3+ attempts on a bug type, if mean score < 0.6, that type is "weak".
reset() skews toward weak types: 70% chance to pick a weak task if any exist.
Weak tasks also get random seeds to generate novel code variants.
This mirrors the curriculum + adversarial self-play pattern from Kube SRE Gym.
"""
import random
from collections import defaultdict
from typing import Optional, List, Dict
class AdversarialScheduler:
"""
Tracks per-task scores and schedules harder variants of weak tasks.
Thread-safe enough for single-agent use; for concurrent sessions,
each environment instance gets its own scheduler.
"""
WEAK_THRESHOLD = 0.6
MIN_ATTEMPTS_BEFORE_WEAK = 3
WEAK_TASK_PROB = 0.70
def __init__(self, all_tasks: List[str]):
self._all_tasks = all_tasks
self._scores: Dict[str, List[float]] = defaultdict(list)
self._episode_count = 0
self._rng = random.Random()
def record(self, task_id: str, score: float) -> None:
"""Call after each episode ends with the final score."""
self._scores[task_id].append(score)
def weak_tasks(self) -> List[str]:
"""Tasks with mean score < threshold after MIN_ATTEMPTS episodes."""
weak = []
for task_id, scores in self._scores.items():
if len(scores) >= self.MIN_ATTEMPTS_BEFORE_WEAK:
if (sum(scores) / len(scores)) < self.WEAK_THRESHOLD:
weak.append(task_id)
return weak
def next_task(self) -> str:
"""
Pick next task. If weak tasks exist, pick one with WEAK_TASK_PROB probability.
Otherwise round-robin through all tasks.
"""
weak = self.weak_tasks()
if weak and self._rng.random() < self.WEAK_TASK_PROB:
task = self._rng.choice(weak)
else:
task = self._all_tasks[self._episode_count % len(self._all_tasks)]
self._episode_count += 1
return task
def next_seed(self, task_id: str) -> int:
"""
Weak tasks get random seeds β†’ novel code variants.
Strong tasks get deterministic seeds β†’ consistent baseline.
"""
weak = self.weak_tasks()
if task_id in weak:
return self._rng.randint(0, 99999)
return 42
def stats(self) -> Dict[str, dict]:
"""Return per-task performance stats for logging/debugging."""
result = {}
for task_id in self._all_tasks:
scores = self._scores.get(task_id, [])
result[task_id] = {
"attempts": len(scores),
"mean_score": round(sum(scores) / len(scores), 4) if scores else None,
"is_weak": task_id in self.weak_tasks(),
"scores": scores[-5:],
}
return result