Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- README.md +6 -4
- inference.py +3 -2
- requirements.txt +2 -1
- server/__init__.py +0 -4
- server/app.py +3 -0
- server/graders.py +19 -5
- server/pipeline_engine.py +3 -0
- server/pipeline_environment.py +14 -1
- server/rewards.py +45 -26
README.md
CHANGED
|
@@ -114,15 +114,17 @@ Failure space: 4 services x 3 failure types x 2 severities = 24 distinct failure
|
|
| 114 |
|
| 115 |
## Reward Design
|
| 116 |
|
| 117 |
-
Dense per-step reward that creates a learnable gradient for RL training. Investigation rewards
|
| 118 |
|
| 119 |
| Signal | Reward | Condition |
|
| 120 |
|--------|--------|-----------|
|
| 121 |
| Service deployed to production | +0.15 | Service reaches prod successfully |
|
| 122 |
| Service verified in staging | +0.05 | Staging health check passes |
|
| 123 |
-
|
|
| 124 |
-
|
|
| 125 |
-
|
|
|
|
|
|
|
|
| 126 |
| Health improvement | +0.005/1% | System health delta |
|
| 127 |
| Broke healthy service | -0.30 | Service went from healthy to degraded/down |
|
| 128 |
| Repeated investigation | -0.01 | Same view action on same target |
|
|
|
|
| 114 |
|
| 115 |
## Reward Design
|
| 116 |
|
| 117 |
+
Dense per-step reward that creates a learnable gradient for RL training. Investigation rewards use **diminishing-returns exploration** — first investigation of an unhealthy service gives +0.04, with decay as more services are investigated. Health improvements give proportional reward via system health delta (+0.005 per 1% improvement). **Sub-goal milestones** reward intermediate progress: config fixes (+0.08), migrations (+0.06), and alert resolution (+0.03). Breaking healthy services is heavily penalized (-0.30). All grading is outcome-based — no procedure-based criteria. Rewards are **task-adaptive** — harder tasks with time pressure get steeper gradients (1.0x–1.5x urgency scaling), creating a curriculum-aware reward landscape. Rewards are bounded [-0.35, +0.20] per step to prevent training instability.
|
| 118 |
|
| 119 |
| Signal | Reward | Condition |
|
| 120 |
|--------|--------|-----------|
|
| 121 |
| Service deployed to production | +0.15 | Service reaches prod successfully |
|
| 122 |
| Service verified in staging | +0.05 | Staging health check passes |
|
| 123 |
+
| Config error fixed | +0.08 | Service health improved after config change |
|
| 124 |
+
| Migration completed | +0.06 | Pending migration count decreased |
|
| 125 |
+
| Alert resolved | +0.03 | Alert count decreased |
|
| 126 |
+
| Investigation (degraded svc) | +0.04× | First-time view on unhealthy service (with decay) |
|
| 127 |
+
| Investigation (healthy svc) | +0.01× | First-time view on healthy service (with decay) |
|
| 128 |
| Health improvement | +0.005/1% | System health delta |
|
| 129 |
| Broke healthy service | -0.30 | Service went from healthy to degraded/down |
|
| 130 |
| Repeated investigation | -0.01 | Same view action on same target |
|
inference.py
CHANGED
|
@@ -4,6 +4,7 @@ import asyncio
|
|
| 4 |
import json
|
| 5 |
import os
|
| 6 |
import textwrap
|
|
|
|
| 7 |
|
| 8 |
from openai import OpenAI
|
| 9 |
|
|
@@ -33,7 +34,7 @@ def log_start(task: str, env: str, model: str) -> None:
|
|
| 33 |
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 34 |
|
| 35 |
|
| 36 |
-
def log_step(step: int, action: str, reward: float, done: bool, error: str
|
| 37 |
error_val = error if error else "null"
|
| 38 |
done_val = str(done).lower()
|
| 39 |
print(
|
|
@@ -43,7 +44,7 @@ def log_step(step: int, action: str, reward: float, done: bool, error: str | Non
|
|
| 43 |
)
|
| 44 |
|
| 45 |
|
| 46 |
-
def log_end(success: bool, steps: int, score: float, rewards:
|
| 47 |
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 48 |
print(
|
| 49 |
f"[END] success={str(success).lower()} steps={steps} "
|
|
|
|
| 4 |
import json
|
| 5 |
import os
|
| 6 |
import textwrap
|
| 7 |
+
from typing import List, Optional
|
| 8 |
|
| 9 |
from openai import OpenAI
|
| 10 |
|
|
|
|
| 34 |
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 35 |
|
| 36 |
|
| 37 |
+
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 38 |
error_val = error if error else "null"
|
| 39 |
done_val = str(done).lower()
|
| 40 |
print(
|
|
|
|
| 44 |
)
|
| 45 |
|
| 46 |
|
| 47 |
+
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
|
| 48 |
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 49 |
print(
|
| 50 |
f"[END] success={str(success).lower()} steps={steps} "
|
requirements.txt
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
-
openenv-core>=0.2.
|
| 2 |
pydantic>=2.0
|
| 3 |
fastapi>=0.104.0
|
| 4 |
uvicorn>=0.24.0
|
|
|
|
|
|
| 1 |
+
openenv-core[core]>=0.2.2
|
| 2 |
pydantic>=2.0
|
| 3 |
fastapi>=0.104.0
|
| 4 |
uvicorn>=0.24.0
|
| 5 |
+
openai>=1.0.0
|
server/__init__.py
CHANGED
|
@@ -5,7 +5,3 @@
|
|
| 5 |
# LICENSE file in the root directory of this source tree.
|
| 6 |
|
| 7 |
"""DevOps Pipeline environment server components."""
|
| 8 |
-
|
| 9 |
-
from server.pipeline_environment import PipelineEnvironment
|
| 10 |
-
|
| 11 |
-
__all__ = ["PipelineEnvironment"]
|
|
|
|
| 5 |
# LICENSE file in the root directory of this source tree.
|
| 6 |
|
| 7 |
"""DevOps Pipeline environment server components."""
|
|
|
|
|
|
|
|
|
|
|
|
server/app.py
CHANGED
|
@@ -106,6 +106,9 @@ async def run_grader(task_name: str = "clean_deploy"):
|
|
| 106 |
return {"task": task_name, "score": 0.0, "error": "No active session. Call /reset first."}
|
| 107 |
if not env.get_episode_history():
|
| 108 |
return {"task": env.get_task_name(), "score": 0.0, "error": "No steps taken. Call /step first."}
|
|
|
|
|
|
|
|
|
|
| 109 |
score = _grade_task(
|
| 110 |
env.get_task_name(),
|
| 111 |
env.get_episode_history(),
|
|
|
|
| 106 |
return {"task": task_name, "score": 0.0, "error": "No active session. Call /reset first."}
|
| 107 |
if not env.get_episode_history():
|
| 108 |
return {"task": env.get_task_name(), "score": 0.0, "error": "No steps taken. Call /step first."}
|
| 109 |
+
active_task = env.get_task_name()
|
| 110 |
+
if task_name and task_name != "clean_deploy" and task_name != active_task:
|
| 111 |
+
return {"task": task_name, "score": 0.0, "error": f"Task mismatch: active session is '{active_task}', not '{task_name}'."}
|
| 112 |
score = _grade_task(
|
| 113 |
env.get_task_name(),
|
| 114 |
env.get_episode_history(),
|
server/graders.py
CHANGED
|
@@ -133,12 +133,18 @@ def grade_judgment_call(episode_history, engine):
|
|
| 133 |
elif api_gw.latency_ms < 500:
|
| 134 |
score += 0.10 # Partial improvement
|
| 135 |
|
| 136 |
-
# web-frontend collateral damage
|
| 137 |
if web_fe:
|
| 138 |
-
if web_fe.health.value == "healthy" and web_fe.error_rate <
|
| 139 |
score += 0.25
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
elif web_fe.error_rate < 10.0:
|
| 141 |
score += 0.10
|
|
|
|
|
|
|
| 142 |
|
| 143 |
# Time to resolution — when api-gateway actually became healthy (outcome-based)
|
| 144 |
resolution_step = len(episode_history)
|
|
@@ -255,8 +261,12 @@ def grade_capacity_crisis(episode_history, engine):
|
|
| 255 |
# Root cause: database protected (0.30)
|
| 256 |
if db:
|
| 257 |
max_conn = int(db.config.get("max_connections", "50"))
|
| 258 |
-
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
elif max_conn >= 75 and db.cpu_percent < 85:
|
| 261 |
score += 0.20
|
| 262 |
elif max_conn >= 75:
|
|
@@ -320,7 +330,11 @@ def grade_random_incident(episode_history, engine):
|
|
| 320 |
if not scenario.check_config_error(failing_name, failing_svc.config):
|
| 321 |
score += 0.20
|
| 322 |
elif getattr(scenario, '_failure_type', '') == 'degraded_performance':
|
| 323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
|
| 325 |
# No collateral damage (0.10) — outcome-based, not procedure-based
|
| 326 |
any_broke = any(entry.get("broke_healthy", False) for entry in episode_history)
|
|
|
|
| 133 |
elif api_gw.latency_ms < 500:
|
| 134 |
score += 0.10 # Partial improvement
|
| 135 |
|
| 136 |
+
# web-frontend collateral damage (smooth gradient)
|
| 137 |
if web_fe:
|
| 138 |
+
if web_fe.health.value == "healthy" and web_fe.error_rate < 0.5:
|
| 139 |
score += 0.25
|
| 140 |
+
elif web_fe.health.value == "healthy" and web_fe.error_rate < 2.0:
|
| 141 |
+
score += 0.20
|
| 142 |
+
elif web_fe.health.value == "healthy" and web_fe.error_rate < 5.0:
|
| 143 |
+
score += 0.15
|
| 144 |
elif web_fe.error_rate < 10.0:
|
| 145 |
score += 0.10
|
| 146 |
+
elif web_fe.error_rate < 20.0:
|
| 147 |
+
score += 0.05
|
| 148 |
|
| 149 |
# Time to resolution — when api-gateway actually became healthy (outcome-based)
|
| 150 |
resolution_step = len(episode_history)
|
|
|
|
| 261 |
# Root cause: database protected (0.30)
|
| 262 |
if db:
|
| 263 |
max_conn = int(db.config.get("max_connections", "50"))
|
| 264 |
+
shared_buf = db.config.get("shared_buffers", "4GB")
|
| 265 |
+
shared_gb = int(shared_buf.replace("GB", "")) if "GB" in str(shared_buf) else 4
|
| 266 |
+
if max_conn >= 100 and db.cpu_percent < 85 and shared_gb >= 6:
|
| 267 |
+
score += 0.30 # Both configs optimized
|
| 268 |
+
elif max_conn >= 100 and db.cpu_percent < 85:
|
| 269 |
+
score += 0.25 # Connections fixed, buffers not
|
| 270 |
elif max_conn >= 75 and db.cpu_percent < 85:
|
| 271 |
score += 0.20
|
| 272 |
elif max_conn >= 75:
|
|
|
|
| 330 |
if not scenario.check_config_error(failing_name, failing_svc.config):
|
| 331 |
score += 0.20
|
| 332 |
elif getattr(scenario, '_failure_type', '') == 'degraded_performance':
|
| 333 |
+
# No config error — check if agent actually improved the service
|
| 334 |
+
if failing_svc.error_rate < 5.0 and failing_svc.health.value == "healthy":
|
| 335 |
+
score += 0.20 # Fully recovered
|
| 336 |
+
elif failing_svc.error_rate < 10.0:
|
| 337 |
+
score += 0.10 # Partially improved
|
| 338 |
|
| 339 |
# No collateral damage (0.10) — outcome-based, not procedure-based
|
| 340 |
any_broke = any(entry.get("broke_healthy", False) for entry in episode_history)
|
server/pipeline_engine.py
CHANGED
|
@@ -620,11 +620,14 @@ class PipelineEngine:
|
|
| 620 |
"latency_ms": s.latency_ms,
|
| 621 |
"prod_deployed": s.prod_deployed,
|
| 622 |
"staging_verified": s.staging_verified,
|
|
|
|
| 623 |
}
|
| 624 |
for name, s in self.services.items()
|
| 625 |
},
|
| 626 |
"system_health": self.get_system_health(),
|
| 627 |
"pipeline_stage": self.pipeline_stage.value,
|
|
|
|
|
|
|
| 628 |
}
|
| 629 |
|
| 630 |
def get_system_health(self):
|
|
|
|
| 620 |
"latency_ms": s.latency_ms,
|
| 621 |
"prod_deployed": s.prod_deployed,
|
| 622 |
"staging_verified": s.staging_verified,
|
| 623 |
+
"config": dict(s.config),
|
| 624 |
}
|
| 625 |
for name, s in self.services.items()
|
| 626 |
},
|
| 627 |
"system_health": self.get_system_health(),
|
| 628 |
"pipeline_stage": self.pipeline_stage.value,
|
| 629 |
+
"migrations_pending": list(self.migrations_pending),
|
| 630 |
+
"alerts": list(self.alerts),
|
| 631 |
}
|
| 632 |
|
| 633 |
def get_system_health(self):
|
server/pipeline_environment.py
CHANGED
|
@@ -31,7 +31,7 @@ TASK_SEEDS = {
|
|
| 31 |
"judgment_call": 3003,
|
| 32 |
"cascading_failure": 4004,
|
| 33 |
"capacity_crisis": 5005,
|
| 34 |
-
"random_incident": 6006,
|
| 35 |
}
|
| 36 |
|
| 37 |
TASK_MAX_STEPS = {
|
|
@@ -232,6 +232,19 @@ class PipelineEnvironment(Environment):
|
|
| 232 |
)
|
| 233 |
elif svc_state.cpu_percent > 80:
|
| 234 |
alerts.append(f"CAUTION: {name} CPU high ({svc_state.cpu_percent:.0f}%)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
summary = "; ".join(alerts) if alerts else "All services nominal."
|
| 236 |
|
| 237 |
return PipelineObservation(
|
|
|
|
| 31 |
"judgment_call": 3003,
|
| 32 |
"cascading_failure": 4004,
|
| 33 |
"capacity_crisis": 5005,
|
| 34 |
+
"random_incident": int(os.environ.get("DEVOPS_SEED", "6006")),
|
| 35 |
}
|
| 36 |
|
| 37 |
TASK_MAX_STEPS = {
|
|
|
|
| 232 |
)
|
| 233 |
elif svc_state.cpu_percent > 80:
|
| 234 |
alerts.append(f"CAUTION: {name} CPU high ({svc_state.cpu_percent:.0f}%)")
|
| 235 |
+
# Add dependency chain hints for degraded services
|
| 236 |
+
for name, svc_state in self._engine.services.items():
|
| 237 |
+
if svc_state.health in (ServiceHealth.DEGRADED, ServiceHealth.DOWN):
|
| 238 |
+
upstream_issues = [
|
| 239 |
+
d for d in svc_state.dependencies
|
| 240 |
+
if d in self._engine.services
|
| 241 |
+
and self._engine.services[d].health in (ServiceHealth.DEGRADED, ServiceHealth.DOWN)
|
| 242 |
+
]
|
| 243 |
+
if upstream_issues:
|
| 244 |
+
alerts.append(
|
| 245 |
+
f"HINT: {name} depends on {', '.join(upstream_issues)} "
|
| 246 |
+
f"(also unhealthy — root cause likely upstream)"
|
| 247 |
+
)
|
| 248 |
summary = "; ".join(alerts) if alerts else "All services nominal."
|
| 249 |
|
| 250 |
return PipelineObservation(
|
server/rewards.py
CHANGED
|
@@ -23,58 +23,77 @@ TASK_URGENCY = {
|
|
| 23 |
def calculate_reward(prev_snapshot, current_snapshot, action, viewed_actions,
|
| 24 |
last_action_key=None, task_name="clean_deploy"):
|
| 25 |
"""
|
| 26 |
-
Outcome-based reward. No procedure bonuses.
|
| 27 |
-
|
| 28 |
-
last_action_key: string key of previous action for repeat detection
|
| 29 |
-
task_name: current task for urgency scaling
|
| 30 |
Returns a float bounded to [-0.35, +0.20].
|
| 31 |
"""
|
| 32 |
reward = 0.0
|
| 33 |
|
| 34 |
-
# 1. System health delta (
|
| 35 |
health_delta = current_snapshot["system_health"] - prev_snapshot["system_health"]
|
| 36 |
-
reward += health_delta * 0.005
|
| 37 |
|
| 38 |
-
# 2. Deployment progress
|
| 39 |
for name, curr_svc in current_snapshot["services"].items():
|
| 40 |
prev_svc = prev_snapshot["services"].get(name, {})
|
| 41 |
if curr_svc["prod_deployed"] and not prev_svc.get("prod_deployed"):
|
| 42 |
-
reward += 0.15
|
| 43 |
if curr_svc["staging_verified"] and not prev_svc.get("staging_verified"):
|
| 44 |
-
reward += 0.05
|
| 45 |
|
| 46 |
-
# 3. Broke
|
| 47 |
for name, curr_svc in current_snapshot["services"].items():
|
| 48 |
prev_svc = prev_snapshot["services"].get(name, {})
|
| 49 |
if prev_svc.get("health") == "healthy" and curr_svc["health"] in ("degraded", "down"):
|
| 50 |
-
reward -= 0.30
|
| 51 |
|
| 52 |
-
# 4.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
if action.action_type in (ActionType.VIEW_PIPELINE, ActionType.VIEW_LOGS, ActionType.VIEW_CONFIG):
|
| 54 |
action_key = f"{action.action_type.value}:{action.service_name or 'global'}"
|
| 55 |
if action_key not in viewed_actions:
|
| 56 |
viewed_actions.add(action_key)
|
| 57 |
-
|
|
|
|
| 58 |
if action.service_name:
|
| 59 |
svc_data = current_snapshot["services"].get(action.service_name, {})
|
| 60 |
if svc_data.get("health") in ("degraded", "down"):
|
| 61 |
-
reward += 0.04
|
| 62 |
else:
|
| 63 |
-
reward += 0.01
|
| 64 |
else:
|
| 65 |
-
reward += 0.02
|
| 66 |
else:
|
| 67 |
-
reward -= 0.01
|
| 68 |
-
|
| 69 |
-
# Apply urgency scaling and bound
|
| 70 |
-
reward *= TASK_URGENCY.get(task_name, 1.0)
|
| 71 |
-
return max(min(reward, 0.20), -0.35)
|
| 72 |
|
| 73 |
-
#
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
| 77 |
|
| 78 |
-
#
|
| 79 |
reward *= TASK_URGENCY.get(task_name, 1.0)
|
| 80 |
return max(min(reward, 0.20), -0.35)
|
|
|
|
| 23 |
def calculate_reward(prev_snapshot, current_snapshot, action, viewed_actions,
|
| 24 |
last_action_key=None, task_name="clean_deploy"):
|
| 25 |
"""
|
| 26 |
+
Outcome-based reward. No procedure bonuses. No early returns.
|
| 27 |
+
ALL actions go through the full reward pipeline.
|
|
|
|
|
|
|
| 28 |
Returns a float bounded to [-0.35, +0.20].
|
| 29 |
"""
|
| 30 |
reward = 0.0
|
| 31 |
|
| 32 |
+
# 1. System health delta (ALL actions)
|
| 33 |
health_delta = current_snapshot["system_health"] - prev_snapshot["system_health"]
|
| 34 |
+
reward += health_delta * 0.005
|
| 35 |
|
| 36 |
+
# 2. Deployment progress (ALL actions — recovery ticks can change state)
|
| 37 |
for name, curr_svc in current_snapshot["services"].items():
|
| 38 |
prev_svc = prev_snapshot["services"].get(name, {})
|
| 39 |
if curr_svc["prod_deployed"] and not prev_svc.get("prod_deployed"):
|
| 40 |
+
reward += 0.15
|
| 41 |
if curr_svc["staging_verified"] and not prev_svc.get("staging_verified"):
|
| 42 |
+
reward += 0.05
|
| 43 |
|
| 44 |
+
# 3. Broke healthy service penalty (ALL actions)
|
| 45 |
for name, curr_svc in current_snapshot["services"].items():
|
| 46 |
prev_svc = prev_snapshot["services"].get(name, {})
|
| 47 |
if prev_svc.get("health") == "healthy" and curr_svc["health"] in ("degraded", "down"):
|
| 48 |
+
reward -= 0.30
|
| 49 |
|
| 50 |
+
# 4. Sub-goal completion rewards (intermediate milestones for RL)
|
| 51 |
+
# Config change detected — check if it fixed an error
|
| 52 |
+
for name, curr_svc in current_snapshot["services"].items():
|
| 53 |
+
prev_svc = prev_snapshot["services"].get(name, {})
|
| 54 |
+
prev_config = prev_svc.get("config", {})
|
| 55 |
+
curr_config = curr_svc.get("config", {})
|
| 56 |
+
if prev_config and curr_config and prev_config != curr_config:
|
| 57 |
+
# Config changed — reward if health improved on this service
|
| 58 |
+
if prev_svc.get("health") in ("degraded", "down") and curr_svc["health"] == "healthy":
|
| 59 |
+
reward += 0.08
|
| 60 |
+
|
| 61 |
+
# Migration completed
|
| 62 |
+
prev_pending = len(prev_snapshot.get("migrations_pending", []))
|
| 63 |
+
curr_pending = len(current_snapshot.get("migrations_pending", []))
|
| 64 |
+
if curr_pending < prev_pending:
|
| 65 |
+
reward += 0.06
|
| 66 |
+
|
| 67 |
+
# Alert resolved
|
| 68 |
+
prev_alerts = len(prev_snapshot.get("alerts", []))
|
| 69 |
+
curr_alerts = len(current_snapshot.get("alerts", []))
|
| 70 |
+
if curr_alerts < prev_alerts:
|
| 71 |
+
reward += 0.03
|
| 72 |
+
|
| 73 |
+
# 5. Investigation bonus with diminishing returns (view_* actions only)
|
| 74 |
if action.action_type in (ActionType.VIEW_PIPELINE, ActionType.VIEW_LOGS, ActionType.VIEW_CONFIG):
|
| 75 |
action_key = f"{action.action_type.value}:{action.service_name or 'global'}"
|
| 76 |
if action_key not in viewed_actions:
|
| 77 |
viewed_actions.add(action_key)
|
| 78 |
+
investigation_count = len(viewed_actions)
|
| 79 |
+
decay_factor = 1.0 / (1 + (investigation_count - 1) * 0.3)
|
| 80 |
if action.service_name:
|
| 81 |
svc_data = current_snapshot["services"].get(action.service_name, {})
|
| 82 |
if svc_data.get("health") in ("degraded", "down"):
|
| 83 |
+
reward += 0.04 * decay_factor
|
| 84 |
else:
|
| 85 |
+
reward += 0.01 * decay_factor
|
| 86 |
else:
|
| 87 |
+
reward += 0.02 * decay_factor
|
| 88 |
else:
|
| 89 |
+
reward -= 0.01
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
+
# 6. Repeated exact action penalty (non-view actions)
|
| 92 |
+
if action.action_type not in (ActionType.VIEW_PIPELINE, ActionType.VIEW_LOGS, ActionType.VIEW_CONFIG):
|
| 93 |
+
current_action_key = f"{action.action_type.value}:{action.service_name or ''}"
|
| 94 |
+
if last_action_key and current_action_key == last_action_key:
|
| 95 |
+
reward -= 0.02
|
| 96 |
|
| 97 |
+
# 7. Apply task urgency scaling and bound
|
| 98 |
reward *= TASK_URGENCY.get(task_name, 1.0)
|
| 99 |
return max(min(reward, 0.20), -0.35)
|