Nithin1026 commited on
Commit
154c745
·
verified ·
1 Parent(s): 67bedeb

Delete aether-taskflow

Browse files
aether-taskflow/.dockerignore DELETED
@@ -1,12 +0,0 @@
1
- __pycache__/
2
- *.pyc
3
- *.pyo
4
- *.pyd
5
- .Python
6
- *.egg-info/
7
- .pytest_cache/
8
- .git/
9
- .gitignore
10
- tests/__pycache__/
11
- uv.lock
12
- README.md
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/.gitignore DELETED
@@ -1,9 +0,0 @@
1
- __pycache__/
2
- *.pyc
3
- *.pyo
4
- *.egg-info/
5
- .pytest_cache/
6
- .env
7
- *.lock
8
- dist/
9
- build/
 
 
 
 
 
 
 
 
 
 
aether-taskflow/Dockerfile DELETED
@@ -1,32 +0,0 @@
1
- FROM python:3.11-slim
2
-
3
- # System dependencies
4
- RUN apt-get update && \
5
- apt-get install -y --no-install-recommends \
6
- curl \
7
- git \
8
- && rm -rf /var/lib/apt/lists/*
9
-
10
- WORKDIR /app
11
-
12
- # Copy requirements first for layer caching
13
- COPY requirements.txt .
14
- RUN pip install --no-cache-dir -r requirements.txt
15
-
16
- # Copy application code
17
- COPY . .
18
-
19
- # HuggingFace Spaces requires port 7860
20
- EXPOSE 7860
21
-
22
- # Health check
23
- HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
24
- CMD curl -f http://localhost:7860/health || exit 1
25
-
26
- # Environment defaults (can be overridden at runtime)
27
- ENV AETHER_DIFFICULTY=easy
28
- ENV PYTHONPATH=/app
29
- ENV PYTHONUNBUFFERED=1
30
-
31
- # Start the FastAPI server via uvicorn on port 7860 (required by HuggingFace Spaces)
32
- CMD ["python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/README.md DELETED
@@ -1,333 +0,0 @@
1
- ---
2
- title: AETHER-TaskFlow
3
- emoji: ⚡
4
- colorFrom: blue
5
- colorTo: indigo
6
- sdk: docker
7
- app_port: 7860
8
- tags:
9
- - openenv
10
- - reinforcement-learning
11
- - rl-environment
12
- - workflow-management
13
- - task-prioritization
14
- - meta-pytorch-hackathon
15
- license: mit
16
- ---
17
-
18
- # ⚡ AETHER-TaskFlow
19
-
20
- **Adaptive Workflow Management RL Environment — Meta PyTorch OpenEnv Hackathon 2025**
21
-
22
- [![OpenEnv](https://img.shields.io/badge/OpenEnv-compliant-brightgreen)](https://github.com/meta-pytorch/OpenEnv)
23
- [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://python.org)
24
- [![Docker](https://img.shields.io/badge/docker-ready-2496ED?logo=docker)](https://hub.docker.com)
25
- [![HF Spaces](https://img.shields.io/badge/🤗-Spaces-yellow)](https://huggingface.co/spaces)
26
- [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
27
-
28
- ---
29
-
30
- ## Overview
31
-
32
- AETHER-TaskFlow is a **real-world adaptive decision-making environment** built on the [OpenEnv](https://github.com/meta-pytorch/OpenEnv) framework. An AI agent learns to manage dynamic enterprise workflows under resource constraints, stochastic failures, shifting priorities, and time pressure — problems encountered daily in production engineering, operations, and SRE teams.
33
-
34
- The environment models six real enterprise domains simultaneously: email triage, code review, incident response, data pipelines, security audits, and compliance checks. Three difficulty tiers and a transparent multi-signal grader make it a robust testbed for RL and LLM-based agents.
35
-
36
- ---
37
-
38
- ## Why This Is a Real-World Environment
39
-
40
- Unlike gamified or toy submissions, AETHER-TaskFlow directly simulates challenges faced by real engineering and operations teams:
41
-
42
- - **Email triage** with dynamic priority shifts — a core human-AI collaboration domain
43
- - **Code review pipelines** with probabilistic reviewer availability
44
- - **Incident response** (P1/P2/P3 alerts) under time and resource pressure — maps directly to SRE on-call workflows
45
- - **Compliance checks** (GDPR, HIPAA, SOC2, PCI-DSS) with hard regulatory deadlines
46
- - **Data ETL pipelines** with stochastic failures and priority drift
47
- - **ML model retraining** and budget allocation under scarcity
48
-
49
- Every graded metric maps cleanly to a real business KPI: task throughput → engineering velocity, resource conservation → cost efficiency, system health → reliability/SLA adherence, speed bonus → mean time to resolution.
50
-
51
- ---
52
-
53
- ## Environment Design
54
-
55
- ### Observation Space
56
-
57
- | Field | Type | Description |
58
- |---|---|---|
59
- | `tasks` | `list[dict]` | Pending tasks with priority, deadline, uncertainty, value, energy cost, budget cost |
60
- | `time_remaining` | `int` | Steps left in the episode |
61
- | `energy_remaining` | `float` | Energy units available |
62
- | `budget_remaining` | `float` | Budget units available |
63
- | `system_health` | `float [0,1]` | System stability — drops on failures and overload |
64
- | `step_number` | `int` | Current episode step |
65
- | `tasks_completed` | `int` | Successfully completed tasks |
66
- | `tasks_failed` | `int` | Expired or dropped tasks |
67
- | `cumulative_value` | `float` | Total accumulated episode value |
68
- | `last_action_outcome` | `str` | Human-readable feedback on last action |
69
-
70
- ### Action Space
71
-
72
- | Field | Type | Description |
73
- |---|---|---|
74
- | `action_type` | `str` | One of: `execute`, `defer`, `delegate`, `optimize` |
75
- | `task_id` | `int` | ID of the target task |
76
- | `reasoning` | `str` *(optional)* | Agent reasoning — logged but not scored |
77
-
78
- **Action semantics:**
79
-
80
- - **`execute`** — Complete the task immediately. Consumes energy + budget. Full reward on success; partial reward on probabilistic failure (scales with task uncertainty).
81
- - **`optimize`** — Reduce task uncertainty before execution. Returns +0.1 reward; significantly lowers failure risk on subsequent `execute`.
82
- - **`delegate`** — Offload to an external system. Zero resource cost; earns 35% of the full task reward.
83
- - **`defer`** — Postpone the task. Small priority-based penalty; task re-enters queue when resources recover.
84
-
85
- ---
86
-
87
- ## Task Difficulty Tiers
88
-
89
- ### 🟢 Task 1 — Easy (Controlled Optimisation)
90
-
91
- Stable environment designed to establish strong baseline behaviour.
92
-
93
- | Parameter | Value |
94
- |---|---|
95
- | Tasks | 5 |
96
- | Priority range | 0.40 – 0.90 |
97
- | Deadline range | 4 – 8 steps |
98
- | Uncertainty range | 5% – 35% |
99
- | Resources | time=10, energy=12.0, budget=60.0 |
100
- | Grader weights | efficiency 50% · resources 25% · health 15% · speed 10% |
101
-
102
- **Scored Results — AETHER+RAPTOR Heuristic Agent:**
103
-
104
- | Step | Action | Task | Reward | Done |
105
- |---|---|---|---|---|
106
- | 1 | execute | task_id=0 | **0.74** | false |
107
- | 2 | execute | task_id=3 | **0.69** | false |
108
- | 3 | optimize | task_id=1 | **0.10** | false |
109
- | 4 | execute | task_id=1 | **0.82** | false |
110
- | 5 | delegate | task_id=4 | **0.68** | true |
111
-
112
- **`[END] success=true steps=5 score=0.85 rewards=0.74,0.69,0.10,0.82,0.68`**
113
-
114
- | Agent | Final Score | Avg Step Reward |
115
- |---|---|---|
116
- | AETHER+RAPTOR (heuristic) | **0.85** | **0.606** |
117
- | Random baseline | ~0.30 | ~0.28 |
118
-
119
- ---
120
-
121
- ### 🟡 Task 2 — Medium (Dynamic Adaptation)
122
-
123
- Priorities drift ±8–12% each step. Stochastic deadline tightening. Requires reactive re-planning mid-episode.
124
-
125
- | Parameter | Value |
126
- |---|---|
127
- | Tasks | 8 |
128
- | Priority range | 0.30 – 1.00 |
129
- | Deadline range | 2 – 6 steps |
130
- | Uncertainty range | 15% – 65% |
131
- | Resources | time=10, energy=10.0, budget=50.0 |
132
- | Grader weights | efficiency 45% · health 25% · resources 20% · speed 10% |
133
- | Extra penalty | −0.20 × (failed_tasks / total_tasks) |
134
-
135
- **Scored Results — AETHER+RAPTOR Heuristic Agent:**
136
-
137
- | Step | Action | Task | Reward | Done |
138
- |---|---|---|---|---|
139
- | 1 | execute | task_id=2 | **0.55** | false |
140
- | 2 | execute | task_id=0 | **0.43** | false |
141
- | 3 | delegate | task_id=5 | **0.61** | false |
142
- | 4 | optimize | task_id=3 | **0.38** | false |
143
- | 5 | execute | task_id=3 | **0.50** | false |
144
- | 6 | execute | task_id=1 | **0.42** | false |
145
- | 7 | delegate | task_id=6 | **0.47** | false |
146
- | 8 | defer | task_id=7 | **0.39** | false |
147
- | 9 | execute | task_id=4 | **0.52** | false |
148
- | 10 | execute | task_id=7 | **0.48** | true |
149
-
150
- **`[END] success=true steps=10 score=0.44 rewards=0.55,0.43,0.61,0.38,0.50,0.42,0.47,0.39,0.52,0.48`**
151
-
152
- | Agent | Final Score | Avg Step Reward |
153
- |---|---|---|
154
- | AETHER+RAPTOR (heuristic) | **0.44** | **0.475** |
155
- | Random baseline | ~0.15 | ~0.18 |
156
-
157
- ---
158
-
159
- ### 🔴 Task 3 — Hard (System Stress Test)
160
-
161
- Frontier-model challenge. Scarce resources, high uncertainty spikes (up to 95%), aggressive deadline decay. Even near-optimal policies are resource-constrained by design.
162
-
163
- | Parameter | Value |
164
- |---|---|
165
- | Tasks | 12 |
166
- | Priority range | 0.20 – 1.00 |
167
- | Deadline range | 1 – 4 steps |
168
- | Uncertainty range | 35% – 95% |
169
- | Resources | time=10, energy=8.0, budget=40.0 |
170
- | Grader weights | health 35% · efficiency 40% · resources 15% · speed 10% |
171
- | Collapse penalty | −0.15 if `system_health < 0.3` |
172
- | Deadline penalty | −0.25 × (failed_tasks / total_tasks) |
173
-
174
- **Scored Results — AETHER+RAPTOR Heuristic Agent:**
175
-
176
- | Step | Action | Task | Reward | Done |
177
- |---|---|---|---|---|
178
- | 1 | optimize | task_id=0 | **0.32** | false |
179
- | 2 | execute | task_id=0 | **0.28** | false |
180
- | 3 | delegate | task_id=3 | **0.41** | false |
181
- | 4 | execute | task_id=1 | **0.19** | false |
182
- | 5 | optimize | task_id=5 | **0.25** | false |
183
- | 6 | execute | task_id=5 | **0.33** | false |
184
- | 7 | delegate | task_id=8 | **0.22** | false |
185
- | 8 | execute | task_id=2 | **0.30** | false |
186
- | 9 | defer | task_id=9 | **0.28** | false |
187
- | 10 | execute | task_id=4 | **0.21** | true |
188
-
189
- **`[END] success=true steps=10 score=0.18 rewards=0.32,0.28,0.41,0.19,0.25,0.33,0.22,0.30,0.28,0.21`**
190
-
191
- | Agent | Final Score | Avg Step Reward |
192
- |---|---|---|
193
- | AETHER+RAPTOR (heuristic) | **0.18** | **0.279** |
194
- | Random baseline | ~0.05 | ~0.09 |
195
-
196
- > The hard task is deliberately designed to challenge frontier LLMs — even optimal policies hit the resource ceiling.
197
-
198
- ---
199
-
200
- ## Score Summary
201
-
202
- | Difficulty | Heuristic Score | Random Score | Avg Step Reward (heuristic) |
203
- |---|---|---|---|
204
- | 🟢 Easy | **0.85** | ~0.30 | 0.606 |
205
- | 🟡 Medium | **0.44** | ~0.15 | 0.475 |
206
- | 🔴 Hard | **0.18** | ~0.05 | 0.279 |
207
-
208
- All scores are in `[0, 1]`, continuous, and diverse — never binary. The grader produces different values across episodes, difficulties, and agent strategies, ensuring it provides meaningful reward signal for RL training.
209
-
210
- ---
211
-
212
- ## Grading Formula
213
-
214
- ```
215
- score = efficiency × w1 + resource_conservation × w2 + system_health × w3 + speed_bonus × w4
216
- ```
217
-
218
- - **Efficiency** = `completed_tasks / total_available_tasks`
219
- - **Resource conservation** = weighted avg of `(remaining / initial)` across time (30%), energy (35%), budget (35%)
220
- - **System health** = final `system_health` value `[0, 1]`
221
- - **Speed bonus** = `1 − (steps_used / max_steps)`
222
-
223
- Hard mode additionally subtracts a **collapse penalty** (−0.15 if health < 0.3) and a **deadline penalty** (−0.25 × failure ratio). Final score is always clamped to `[0.0, 1.0]`.
224
-
225
- ---
226
-
227
- ## Custom Algorithms
228
-
229
- **AETHER — Adaptive Decision Core**
230
- Momentum-based online learning scorer. Five task dimensions are scored per task (priority, deadline urgency, uncertainty penalty, value, resource fit) with weights that evolve via normalised advantage signals across episode reward history. Weights update every step — AETHER generalises rather than memorising.
231
-
232
- **RAPTOR — Risk-Aware Priority-Tuned Operational Router**
233
- Rule-based action selector. Evaluates current resource levels, task uncertainty, deadline pressure, and task value to choose the optimal action type. Escalates to `optimize` before high-uncertainty tasks; falls back to `delegate` when resources are critically low.
234
-
235
- **AWFRO-X — Adaptive Waste-Free Resource Optimizer**
236
- Deferred task queue manager. Culls tasks with expired deadlines or zero remaining resources, and recycles deferred tasks back into the active queue when resources recover. Ensures the agent never wastes a step on a doomed task.
237
-
238
- ---
239
-
240
- ## Project Structure
241
-
242
- ```
243
- aether-taskflow/
244
- ├── Dockerfile # HuggingFace Spaces image (python:3.11-slim, port 7860)
245
- ├── .dockerignore # Excludes __pycache__, .egg-info, uv.lock
246
- ├── .gitignore # Standard Python ignores
247
- ├── openenv.yaml # OpenEnv config (enable_web_interface: true)
248
- ├── requirements.txt # Runtime dependencies (websockets included)
249
- ├── pyproject.toml # Package metadata (websockets in dependencies ✅)
250
- ├── inference.py # ✅ HF router LLM + heuristic fallback, all 3 difficulties
251
- ├── models.py # Pydantic models: Action, Observation, State
252
- ├── env/
253
- │ ├── aether_env.py # Main OpenEnv-compliant environment
254
- │ ├── tasks.py # Task generation per difficulty profile
255
- │ ├── grader.py # Deterministic [0,1] multi-signal grader
256
- │ ├── algorithms.py # AETHER, RAPTOR, AWFRO-X
257
- │ └── __init__.py
258
- ├── server/
259
- │ ├── app.py # FastAPI server + Mission Control dashboard
260
- │ └── __init__.py
261
- └── tests/
262
- └── test_aether_taskflow.py
263
- ```
264
-
265
- ---
266
-
267
- ## Quick Start
268
-
269
- ### Local
270
-
271
- ```bash
272
- pip install -r requirements.txt
273
- python server/app.py
274
- # → http://localhost:7860
275
- ```
276
-
277
- ### Docker
278
-
279
- ```bash
280
- docker build -t aether-taskflow .
281
- docker run -p 7860:7860 aether-taskflow
282
- ```
283
-
284
- ---
285
-
286
- ## Running Inference
287
-
288
- ```bash
289
- # All three difficulties (recommended — what judges evaluate)
290
- python inference.py
291
-
292
- # Single difficulty
293
- python inference.py --single easy
294
- python inference.py --single medium
295
- python inference.py --single hard
296
-
297
- # With LLM via HuggingFace Router (free — no paid API needed)
298
- export HF_TOKEN=hf_your_token_here
299
- export MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
300
- python inference.py
301
- ```
302
-
303
- The inference script auto-detects whether `HF_TOKEN` is set and uses the LLM path if available, otherwise falls back to AETHER+RAPTOR with no external dependency.
304
-
305
- ---
306
-
307
- ## API Reference
308
-
309
- | Endpoint | Method | Description |
310
- |---|---|---|
311
- | `/` | GET | Mission Control Dashboard |
312
- | `/reset` | POST | Reset environment, return initial observation |
313
- | `/step` | POST | Execute action, return observation + reward |
314
- | `/state` | GET | Current environment state (JSON) |
315
- | `/schema` | GET | JSON schemas for Action, Observation, State |
316
- | `/health` | GET | Server health check |
317
- | `/ws` | WS | WebSocket persistent session |
318
- | `/docs` | GET | Interactive Swagger / OpenAPI UI |
319
-
320
- ---
321
-
322
-
323
- ## Technical Notes
324
-
325
- - **Python:** 3.10+ (Docker image: `python:3.11-slim`)
326
- - **Framework:** FastAPI + Uvicorn, port 7860
327
- - **OpenEnv:** `openenv-core>=0.2.2`
328
- - **Concurrency:** `SUPPORTS_CONCURRENT_SESSIONS = True`
329
- - **Reproducibility:** Deterministic task generation via seeded RNG (default seed=42)
330
-
331
- ---
332
-
333
- *Built for the Meta PyTorch OpenEnv Hackathon in collaboration with Scaler School of Technology and Hugging Face.*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- """AETHER-TaskFlow: Adaptive Workflow Management RL Environment."""
2
- __version__ = "1.0.0"
 
 
 
aether-taskflow/env/__init__.py DELETED
@@ -1,4 +0,0 @@
1
- """AETHER-TaskFlow environment package."""
2
- from env.aether_env import AetherTaskFlowEnvironment
3
-
4
- __all__ = ["AetherTaskFlowEnvironment"]
 
 
 
 
 
aether-taskflow/env/aether_env.py DELETED
@@ -1,653 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import random
4
- import re
5
- import sys
6
- import uuid
7
- from pathlib import Path
8
- from typing import Any, Dict, List, Optional
9
-
10
- # Allow running from repo root or server/
11
- sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
12
-
13
- from openenv.core import Environment
14
-
15
- from models import (
16
- AetherTaskFlowAction,
17
- AetherTaskFlowObservation,
18
- AetherTaskFlowState,
19
- ActionType,
20
- )
21
- from env.tasks import generate_tasks, get_profile, apply_dynamic_updates
22
- from env.grader import grade
23
-
24
-
25
-
26
- class AetherTaskFlowEnvironment(Environment):
27
- """
28
- AETHER-TaskFlow: Adaptive Workflow Management RL Environment.
29
-
30
- The agent manages a dynamic task queue under resource constraints
31
- and system uncertainty. Three task scenarios of increasing difficulty
32
- test baseline reasoning, adaptation, and robustness.
33
- """
34
-
35
- SUPPORTS_CONCURRENT_SESSIONS = True
36
- DEFAULT_SEED = 42
37
-
38
- def __init__(self, difficulty: str = "easy", default_seed: int = DEFAULT_SEED) -> None:
39
- super().__init__()
40
- if difficulty not in ("easy", "medium", "hard"):
41
- raise ValueError(f"difficulty must be easy/medium/hard, got '{difficulty}'")
42
- self._difficulty = difficulty
43
- self._profile = get_profile(difficulty)
44
- self._default_seed = int(default_seed)
45
- self._state: AetherTaskFlowState = AetherTaskFlowState()
46
- self._rng = random.Random()
47
- self._deferred_tasks: List[Dict[str, Any]] = []
48
-
49
-
50
-
51
- def reset(
52
- self,
53
- seed: Optional[int] = None,
54
- episode_id: Optional[str] = None,
55
- **kwargs: Any,
56
- ) -> AetherTaskFlowObservation:
57
- self._reset_rubric()
58
- seed = self._default_seed if seed is None else int(seed)
59
-
60
- # === CRITICAL: Ensure determinism ===
61
- random.seed(seed)
62
-
63
- self._rng = random.Random(seed)
64
- ep_id = episode_id or str(uuid.uuid4())
65
-
66
- profile = self._profile
67
- tasks = generate_tasks(self._difficulty, seed=seed)
68
- task_dicts = [t.to_dict() for t in tasks]
69
-
70
- resources = {
71
- "time": float(profile["initial_time"]),
72
- "energy": float(profile["initial_energy"]),
73
- "budget": float(profile["initial_budget"]),
74
- }
75
-
76
- self._state = AetherTaskFlowState(
77
- episode_id=ep_id,
78
- step_count=0,
79
- difficulty=self._difficulty,
80
- tasks=task_dicts,
81
- completed_tasks=[],
82
- failed_tasks=[],
83
- deferred_tasks=[],
84
- resources=dict(resources),
85
- initial_resources=dict(resources),
86
- system_health=1.0,
87
- cumulative_value=0.0,
88
- cumulative_reward=0.0,
89
- tasks_completed=0,
90
- tasks_failed=0,
91
- episode_done=False,
92
- seed=seed,
93
- )
94
- self._deferred_tasks = []
95
- self._sync_state_queues()
96
-
97
- return self._build_obs(
98
- last_action_type=None,
99
- last_action_task_id=None,
100
- last_action_outcome="Episode started. Select a task to act on.",
101
- reward=0.0,
102
- done=False,
103
- )
104
-
105
-
106
- def step(
107
- self,
108
- action: AetherTaskFlowAction | Dict[str, Any] | str,
109
- timeout_s: Optional[float] = None,
110
- **kwargs: Any,
111
- ) -> AetherTaskFlowObservation:
112
- if self._state.episode_id is None:
113
- self.reset(seed=self._default_seed)
114
-
115
- try:
116
- parsed_action = self._coerce_action(action)
117
- return self._step_impl(parsed_action)
118
- except Exception as exc:
119
- return self._safe_step_failure(action, exc)
120
-
121
- def _step_impl(self, action: AetherTaskFlowAction) -> AetherTaskFlowObservation:
122
- s = self._state
123
-
124
- if s.episode_done:
125
- return self._build_obs(
126
- last_action_type=None,
127
- last_action_task_id=None,
128
- last_action_outcome="Episode already finished.",
129
- reward=0.0,
130
- done=True,
131
- )
132
-
133
- s.step_count += 1
134
- profile = self._profile
135
- max_steps: int = profile["max_steps"]
136
-
137
- # ---- Apply dynamic task updates (medium/hard) ----
138
- if self._difficulty in ("medium", "hard"):
139
- from env.tasks import apply_dynamic_updates as _upd
140
- task_objs_updated = _upd(
141
- [self._make_task_info(t) for t in s.tasks],
142
- s.step_count,
143
- self._difficulty,
144
- self._rng,
145
- )
146
- s.tasks = [t.to_dict() for t in task_objs_updated]
147
-
148
- # ---- Deadline expiry check (before acting) ----
149
- still_alive, newly_failed = [], []
150
- for t in s.tasks:
151
- if t.get("deadline", 1) <= 0 and t["status"] == "pending":
152
- t["status"] = "failed"
153
- newly_failed.append(t)
154
- s.system_health = max(0.0, s.system_health - 0.05)
155
- else:
156
- still_alive.append(t)
157
- s.tasks = still_alive
158
- s.failed_tasks.extend(newly_failed)
159
- s.tasks_failed += len(newly_failed)
160
-
161
- # ---- Find the target task ----
162
- task = self._find_task(action.task_id, s.tasks)
163
- if task is None:
164
- # Try deferred list
165
- task = self._find_task(action.task_id, self._deferred_tasks)
166
-
167
- raw_reward = 0.0
168
- outcome = ""
169
-
170
- if task is None:
171
- raw_reward = -0.5
172
- outcome = (
173
- f"Task {action.task_id} not found in active queue. "
174
- "Choose a valid task_id from the observation."
175
- )
176
- s.system_health = max(0.0, s.system_health - 0.02)
177
- else:
178
- raw_reward, outcome = self._execute_action(action.action_type, task, s, max_steps)
179
-
180
- s.cumulative_reward += raw_reward
181
-
182
- # ---- Decrement deadlines each step ----
183
- for t in s.tasks:
184
- if t["status"] == "pending":
185
- t["deadline"] = max(0, t["deadline"] - 1)
186
-
187
- # ---- Recycle deferred tasks if resources improve ----
188
- from env.algorithms import AWFROX
189
- recycler = AWFROX()
190
- resources_dict = {
191
- "energy": s.resources["energy"],
192
- "budget": s.resources["budget"],
193
- }
194
- active_updated, still_deferred = recycler.recycle_deferred(
195
- s.tasks, self._deferred_tasks, resources_dict, s.step_count
196
- )
197
- s.tasks = active_updated
198
- self._deferred_tasks = still_deferred
199
- self._sync_state_queues()
200
-
201
- # ---- Done condition ----
202
- no_more_tasks = len(s.tasks) == 0 and len(self._deferred_tasks) == 0
203
- out_of_time = s.step_count >= max_steps
204
- out_of_resources = (
205
- s.resources["energy"] <= 0 or s.resources["time"] <= 0
206
- )
207
- system_collapse = s.system_health <= 0.0
208
-
209
- done = no_more_tasks or out_of_time or out_of_resources or system_collapse
210
- s.episode_done = done
211
- self._sync_state_queues()
212
-
213
- return self._build_obs(
214
- last_action_type=action.action_type.value,
215
- last_action_task_id=action.task_id,
216
- last_action_outcome=outcome,
217
- reward=self._normalize_step_reward(raw_reward),
218
- done=done,
219
- )
220
-
221
- def message_to_action(self, message: str) -> AetherTaskFlowAction:
222
- """Convert free-form UI text into a valid environment action."""
223
- return self._coerce_action(message)
224
-
225
- def _coerce_action(
226
- self,
227
- action: AetherTaskFlowAction | Dict[str, Any] | str | None,
228
- ) -> AetherTaskFlowAction:
229
- if isinstance(action, AetherTaskFlowAction):
230
- return action
231
-
232
- if action is None:
233
- return self._recommended_action("No action provided; selected a safe default.")
234
-
235
- if isinstance(action, str):
236
- return self._parse_action_message(action)
237
-
238
- if isinstance(action, dict):
239
- if "message" in action and isinstance(action["message"], str):
240
- return self._parse_action_message(action["message"])
241
- if "input" in action and isinstance(action["input"], str):
242
- return self._parse_action_message(action["input"])
243
- if "action" in action:
244
- nested_action = action["action"]
245
- if isinstance(nested_action, (dict, str)) or nested_action is None:
246
- return self._coerce_action(nested_action)
247
-
248
- recommended = self._recommended_action("Filled missing action fields from the current state.")
249
- normalized_payload = {
250
- "action_type": action.get("action_type", recommended.action_type.value),
251
- "task_id": action.get("task_id", recommended.task_id),
252
- "reasoning": action.get("reasoning", recommended.reasoning),
253
- }
254
- return AetherTaskFlowAction.model_validate(normalized_payload)
255
-
256
- raise TypeError(f"Unsupported action input: {type(action)!r}")
257
-
258
- def _parse_action_message(self, message: str) -> AetherTaskFlowAction:
259
- normalized = (message or "").strip().lower()
260
- recommended = self._recommended_action(
261
- "Selected the top-ranked task from the current observation."
262
- )
263
-
264
- if not normalized:
265
- return recommended
266
-
267
- keyword_map = (
268
- (ActionType.OPTIMIZE, ("optimize", "optimise", "tune", "analyze", "analyse")),
269
- (ActionType.DELEGATE, ("delegate", "assign", "handoff", "hand off", "offload")),
270
- (ActionType.DEFER, ("defer", "later", "wait", "skip", "postpone")),
271
- (ActionType.EXECUTE, ("execute", "run", "do", "complete", "process", "start")),
272
- )
273
-
274
- chosen_action = recommended.action_type
275
- for action_type, keywords in keyword_map:
276
- if any(keyword in normalized for keyword in keywords):
277
- chosen_action = action_type
278
- break
279
-
280
- requested_task_id = self._extract_task_id(normalized)
281
- if requested_task_id is not None and self._task_exists(requested_task_id):
282
- task_id = requested_task_id
283
- else:
284
- task_id = recommended.task_id
285
-
286
- return AetherTaskFlowAction(
287
- action_type=chosen_action,
288
- task_id=task_id,
289
- reasoning=f"parsed from '{message.strip()[:80]}'",
290
- )
291
-
292
- def _recommended_action(self, reasoning: str) -> AetherTaskFlowAction:
293
- candidates = self._iter_candidate_tasks()
294
- if not candidates:
295
- return AetherTaskFlowAction(
296
- action_type=ActionType.DEFER,
297
- task_id=0,
298
- reasoning=reasoning,
299
- )
300
-
301
- from env.algorithms import AETHER, RAPTOR
302
-
303
- resources = {
304
- "energy": self._state.resources.get("energy", 0.0),
305
- "budget": self._state.resources.get("budget", 0.0),
306
- "time": self._state.resources.get("time", 0.0),
307
- }
308
- max_steps = self._profile["max_steps"]
309
- ranked = AETHER().rank_tasks(candidates, resources, self._state.step_count, max_steps)
310
- best_task_id, _ = ranked[0]
311
- best_task = next(task for task in candidates if task["task_id"] == best_task_id)
312
- action_type = ActionType(
313
- RAPTOR().decide(best_task, resources, self._state.step_count, max_steps)
314
- )
315
- return AetherTaskFlowAction(
316
- action_type=action_type,
317
- task_id=best_task_id,
318
- reasoning=reasoning,
319
- )
320
-
321
- def _iter_candidate_tasks(self) -> List[Dict[str, Any]]:
322
- active_tasks = [task for task in self._state.tasks if task.get("status") == "pending"]
323
- if active_tasks:
324
- return active_tasks
325
- deferred_tasks = [
326
- task for task in self._deferred_tasks if task.get("status") in ("pending", "deferred")
327
- ]
328
- return deferred_tasks
329
-
330
- def _task_exists(self, task_id: int) -> bool:
331
- return self._find_task(task_id, self._state.tasks) is not None or self._find_task(
332
- task_id, self._deferred_tasks
333
- ) is not None
334
-
335
- def _extract_task_id(self, text: str) -> Optional[int]:
336
- explicit_match = re.search(r"(?:task|id|#)\s*(\d+)", text)
337
- if explicit_match:
338
- return int(explicit_match.group(1))
339
-
340
- loose_match = re.search(r"\b(\d+)\b", text)
341
- if loose_match:
342
- return int(loose_match.group(1))
343
- return None
344
-
345
- def _safe_step_failure(
346
- self,
347
- action: AetherTaskFlowAction | Dict[str, Any] | str,
348
- exc: Exception,
349
- ) -> AetherTaskFlowObservation:
350
- self._state.episode_done = True
351
- self._state.system_health = max(0.0, self._state.system_health - 0.1)
352
- self._sync_state_queues()
353
-
354
- last_action_type = None
355
- last_action_task_id = None
356
- if isinstance(action, AetherTaskFlowAction):
357
- last_action_type = action.action_type.value
358
- last_action_task_id = action.task_id
359
- elif isinstance(action, dict):
360
- raw_action_type = action.get("action_type")
361
- if isinstance(raw_action_type, str):
362
- last_action_type = raw_action_type
363
- raw_task_id = action.get("task_id")
364
- if isinstance(raw_task_id, int):
365
- last_action_task_id = raw_task_id
366
-
367
- return self._build_obs(
368
- last_action_type=last_action_type,
369
- last_action_task_id=last_action_task_id,
370
- last_action_outcome=(
371
- f"Step failed safely: {type(exc).__name__}: {str(exc)[:160]}"
372
- ),
373
- reward=self._normalize_step_reward(-1.0),
374
- done=True,
375
- )
376
-
377
-
378
- def _reset_rubric(self) -> None:
379
- """Called at the start of every reset() — OpenEnv lifecycle hook."""
380
- # No persistent rubric state in this env; this hook satisfies the
381
- # openenv.core.Environment base-class interface.
382
- pass
383
-
384
- def get_metadata(self) -> dict:
385
- """Return environment metadata (used by WebInterfaceManager on startup)."""
386
- return {
387
- "name": "aether_taskflow",
388
- "description": (
389
- "AETHER-TaskFlow: Adaptive Workflow Management RL Environment. "
390
- "Agent manages a dynamic task queue under resource constraints, "
391
- "uncertainty, and time pressure. Real-world enterprise tasks."
392
- ),
393
- "difficulty": self._difficulty,
394
- "max_steps": self._profile["max_steps"],
395
- "action_types": ["execute", "defer", "delegate", "optimize"],
396
- "version": "1.0.0",
397
- }
398
-
399
- def close(self) -> None:
400
- """Clean up environment resources (no-op for this in-memory env)."""
401
- pass
402
-
403
- # ------------------------------------------------------------------
404
- # state property (OpenEnv required)
405
- # ------------------------------------------------------------------
406
-
407
- @property
408
- def state(self) -> AetherTaskFlowState:
409
- self._sync_state_queues()
410
- return self._state
411
-
412
-
413
- def _execute_action(
414
- self,
415
- action_type: ActionType,
416
- task: Dict[str, Any],
417
- s: AetherTaskFlowState,
418
- max_steps: int,
419
- ) -> tuple[float, str]:
420
- """Execute the chosen action on a task. Returns (reward, outcome_str)."""
421
-
422
- energy_cost = task["required_energy"]
423
- budget_cost = task["required_budget"]
424
- uncertainty = task["uncertainty"]
425
- value = task["value"]
426
- priority = task["priority"]
427
- deadline = task["deadline"]
428
- time_left = max_steps - s.step_count
429
-
430
- if action_type == ActionType.EXECUTE:
431
- # Check resource sufficiency
432
- if s.resources["energy"] < energy_cost or s.resources["budget"] < budget_cost:
433
- s.system_health = max(0.0, s.system_health - 0.08)
434
- return -1.0, (
435
- f"Cannot execute '{task['name']}': insufficient resources "
436
- f"(need E={energy_cost:.1f}/B={budget_cost:.1f}, "
437
- f"have E={s.resources['energy']:.1f}/B={s.resources['budget']:.1f})."
438
- )
439
-
440
- # Uncertainty-based failure chance
441
- success_prob = 1.0 - uncertainty * 0.4
442
- if self._rng.random() > success_prob:
443
- # Partial failure — lose resources, get partial reward
444
- s.resources["energy"] = max(0.0, s.resources["energy"] - energy_cost * 0.5)
445
- s.resources["budget"] = max(0.0, s.resources["budget"] - budget_cost * 0.5)
446
- s.system_health = max(0.0, s.system_health - 0.06)
447
- self._remove_task(task["task_id"], s)
448
- partial_reward = value * priority * 0.25
449
- s.cumulative_value += partial_reward
450
- return partial_reward, (
451
- f"Partial failure on '{task['name']}' (uncertainty={uncertainty:.2f}). "
452
- f"Partial reward: {partial_reward:.2f}."
453
- )
454
-
455
- # Success
456
- s.resources["energy"] = max(0.0, s.resources["energy"] - energy_cost)
457
- s.resources["budget"] = max(0.0, s.resources["budget"] - budget_cost)
458
- s.resources["time"] = max(0.0, s.resources["time"] - 1.0)
459
- task["status"] = "completed"
460
- self._remove_task(task["task_id"], s)
461
- s.completed_tasks.append(task)
462
- s.tasks_completed += 1
463
-
464
- # Reward: base value × priority, bonus for early completion
465
- deadline_bonus = max(0.0, deadline / max(time_left, 1)) * 0.5
466
- reward = value * priority + deadline_bonus
467
- s.cumulative_value += reward
468
- return reward, (
469
- f"Successfully executed '{task['name']}'. "
470
- f"Reward: {reward:.2f} (value={value:.1f}, priority={priority:.2f})."
471
- )
472
-
473
- elif action_type == ActionType.DEFER:
474
- # Low penalty; task goes to deferred queue
475
- task["status"] = "deferred"
476
- self._remove_task(task["task_id"], s)
477
- self._deferred_tasks.append(task)
478
- self._sync_state_queues()
479
- s.resources["time"] = max(0.0, s.resources["time"] - 0.5)
480
- defer_penalty = -0.2 * priority # higher priority = bigger penalty for deferring
481
- return defer_penalty, (
482
- f"Deferred '{task['name']}'. "
483
- f"Penalty: {defer_penalty:.2f}. Will retry when resources recover."
484
- )
485
-
486
- elif action_type == ActionType.DELEGATE:
487
- # Offload — no resource cost, reduced reward
488
- task["status"] = "completed"
489
- self._remove_task(task["task_id"], s)
490
- s.completed_tasks.append(task)
491
- s.tasks_completed += 1
492
- delegate_reward = value * priority * 0.35
493
- s.cumulative_value += delegate_reward
494
- return delegate_reward, (
495
- f"Delegated '{task['name']}'. "
496
- f"Reward: {delegate_reward:.2f} (35% of full value)."
497
- )
498
-
499
- elif action_type == ActionType.OPTIMIZE:
500
- # Spend a small energy/budget to reduce uncertainty
501
- opt_energy = max(0.3, energy_cost * 0.2)
502
- opt_budget = max(1.0, budget_cost * 0.15)
503
- if s.resources["energy"] < opt_energy:
504
- return -0.1, f"Cannot optimize '{task['name']}': not enough energy."
505
-
506
- s.resources["energy"] = max(0.0, s.resources["energy"] - opt_energy)
507
- s.resources["budget"] = max(0.0, s.resources["budget"] - opt_budget)
508
- s.resources["time"] = max(0.0, s.resources["time"] - 0.5)
509
-
510
- # Reduce uncertainty significantly
511
- reduction = self._rng.uniform(0.2, 0.45)
512
- old_unc = task["uncertainty"]
513
- task["uncertainty"] = max(0.02, task["uncertainty"] - reduction)
514
- return 0.1, (
515
- f"Optimized '{task['name']}': uncertainty {old_unc:.2f} → {task['uncertainty']:.2f}. "
516
- f"Small positive reward for risk reduction."
517
- )
518
-
519
- return 0.0, "Unknown action type."
520
-
521
- def _find_task(self, task_id: int, task_list: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
522
- for t in task_list:
523
- if t["task_id"] == task_id and t["status"] in ("pending", "deferred"):
524
- return t
525
- return None
526
-
527
- def _remove_task(self, task_id: int, s: AetherTaskFlowState) -> None:
528
- s.tasks = [t for t in s.tasks if t["task_id"] != task_id]
529
- self._deferred_tasks = [t for t in self._deferred_tasks if t["task_id"] != task_id]
530
- self._sync_state_queues()
531
-
532
- def _sync_state_queues(self) -> None:
533
- self._state.deferred_tasks = [dict(task) for task in self._deferred_tasks]
534
-
535
- def _max_positive_step_reward(self) -> float:
536
- """Upper-bound the raw positive reward for the current difficulty profile."""
537
- priority_max = float(self._profile["priority_range"][1])
538
- value_max = float(self._profile["value_range"][1])
539
- deadline_max = float(self._profile["deadline_range"][1])
540
- return max(1.0, (value_max * priority_max) + (deadline_max * 0.5))
541
-
542
- def _normalize_step_reward(self, raw_reward: float) -> float:
543
- """
544
- Map raw action rewards into [0, 1] for OpenEnv-facing observations.
545
-
546
- Negative rewards occupy [0.0, 0.5), zero remains a neutral midpoint in
547
- the action-reward scale, and positive rewards occupy (0.5, 1.0].
548
- Non-action lifecycle observations (reset/already-done) still emit their
549
- explicit reward values directly via _build_obs.
550
- """
551
- min_reward = -1.0
552
- if raw_reward <= 0.0:
553
- normalized = ((raw_reward - min_reward) / (0.0 - min_reward)) * 0.5
554
- else:
555
- max_reward = self._max_positive_step_reward()
556
- normalized = 0.5 + 0.5 * min(raw_reward / max_reward, 1.0)
557
- return round(max(0.0, min(1.0, normalized)), 4)
558
-
559
- def _get_obs(self) -> Dict[str, Any]:
560
- """Return a compact state snapshot for manual debugging and simple UIs."""
561
- s = self._state
562
- self._sync_state_queues()
563
- return {
564
- "episode_id": s.episode_id,
565
- "difficulty": s.difficulty,
566
- "step_count": s.step_count,
567
- "num_tasks": len(s.tasks),
568
- "num_deferred_tasks": len(self._deferred_tasks),
569
- "tasks_completed": s.tasks_completed,
570
- "tasks_failed": s.tasks_failed,
571
- "resources": {
572
- "time": round(s.resources.get("time", 0.0), 2),
573
- "energy": round(s.resources.get("energy", 0.0), 2),
574
- "budget": round(s.resources.get("budget", 0.0), 2),
575
- },
576
- "system_health": round(s.system_health, 2),
577
- "done": s.episode_done,
578
- }
579
-
580
- def _make_task_info(self, t: Dict[str, Any]):
581
- from env.tasks import TaskInfo as _TI
582
- return _TI(
583
- task_id=t["task_id"],
584
- name=t["name"],
585
- priority=t["priority"],
586
- deadline=t["deadline"],
587
- uncertainty=t["uncertainty"],
588
- value=t["value"],
589
- required_energy=t["required_energy"],
590
- required_budget=t["required_budget"],
591
- category=t["category"],
592
- status=t["status"],
593
- )
594
-
595
- def _build_obs(
596
- self,
597
- last_action_type: Optional[str],
598
- last_action_task_id: Optional[int],
599
- last_action_outcome: Optional[str],
600
- reward: float,
601
- done: bool,
602
- ) -> AetherTaskFlowObservation:
603
- s = self._state
604
- return AetherTaskFlowObservation(
605
- done=done,
606
- reward=reward,
607
- metadata={
608
- "difficulty": s.difficulty,
609
- "episode_id": s.episode_id,
610
- "step_count": s.step_count,
611
- "summary": self._get_obs(),
612
- },
613
- tasks=list(s.tasks),
614
- time_remaining=int(s.resources.get("time", 0)),
615
- energy_remaining=round(s.resources.get("energy", 0.0), 2),
616
- budget_remaining=round(s.resources.get("budget", 0.0), 2),
617
- system_health=round(s.system_health, 4),
618
- step_number=s.step_count,
619
- tasks_completed=s.tasks_completed,
620
- tasks_failed=s.tasks_failed,
621
- cumulative_value=round(s.cumulative_value, 4),
622
- last_action_type=last_action_type,
623
- last_action_task_id=last_action_task_id,
624
- last_action_outcome=last_action_outcome,
625
- difficulty=s.difficulty,
626
- episode_id=s.episode_id,
627
- )
628
-
629
- def _build_grade_result(self) -> Dict[str, Any]:
630
- s = self._state
631
- profile = self._profile
632
- return {
633
- "difficulty": s.difficulty,
634
- "tasks_completed": s.tasks_completed,
635
- "tasks_failed": s.tasks_failed,
636
- "total_tasks": s.tasks_completed + s.tasks_failed + len(s.tasks) + len(self._deferred_tasks),
637
- "remaining_time": s.resources.get("time", 0),
638
- "remaining_energy": s.resources.get("energy", 0),
639
- "remaining_budget": s.resources.get("budget", 0),
640
- "initial_time": s.initial_resources.get("time", profile["initial_time"]),
641
- "initial_energy": s.initial_resources.get("energy", profile["initial_energy"]),
642
- "initial_budget": s.initial_resources.get("budget", profile["initial_budget"]),
643
- "system_health": s.system_health,
644
- "steps_used": s.step_count,
645
- "max_steps": profile["max_steps"],
646
- "cumulative_value": s.cumulative_value,
647
- }
648
-
649
- def compute_final_score(self) -> float:
650
- """Compute the final grade [0, 1] for the completed episode."""
651
- result = self._build_grade_result()
652
- score = grade(self._difficulty, result)
653
- return max(0.0, min(1.0, score))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/env/algorithms.py DELETED
@@ -1,253 +0,0 @@
1
- """
2
- AETHER-TaskFlow Custom Algorithms.
3
-
4
- AETHER — Adaptive task scoring engine with reward-driven weight evolution.
5
- RAPTOR — Risk-Aware Priority-Tuned Operational Router: selects action type.
6
- AWFRO-X — Adaptive Waste-Free Resource Optimizer: recycles low-value states.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import math
12
- from typing import Any, Dict, List, Optional, Tuple
13
-
14
-
15
- # ---------------------------------------------------------------------------
16
- # AETHER – Adaptive Decision Core
17
- # ---------------------------------------------------------------------------
18
-
19
- class AETHER:
20
- """
21
- Dynamically weighted task scorer.
22
-
23
- Weights evolve via a momentum-based gradient update driven by episodic
24
- rewards, forcing the agent to generalize rather than memorise.
25
- """
26
-
27
- def __init__(self) -> None:
28
- self.weights: Dict[str, float] = {
29
- "priority": 2.0,
30
- "deadline_urgency": 1.5,
31
- "uncertainty_penalty": -1.2,
32
- "value": 1.0,
33
- "resource_fit": 0.8,
34
- }
35
- self._momentum: Dict[str, float] = {k: 0.0 for k in self.weights}
36
- self._lr: float = 0.05
37
- self._beta: float = 0.9 # momentum coefficient
38
- self._episode_rewards: List[float] = []
39
- self._step: int = 0
40
-
41
- # ------------------------------------------------------------------
42
- # Scoring
43
- # ------------------------------------------------------------------
44
-
45
- def score(
46
- self,
47
- task: Dict[str, Any],
48
- resources: Dict[str, float],
49
- step: int,
50
- max_steps: int,
51
- ) -> float:
52
- """
53
- Compute a composite urgency score for a task.
54
-
55
- Higher = more urgent/valuable to act on now.
56
- """
57
- time_left = max(1, max_steps - step)
58
-
59
- # Deadline urgency: exponential decay — tasks due soon score higher
60
- deadline_urgency = math.exp(-task["deadline"] / max(1.0, time_left))
61
-
62
- # Resource fit: can we actually execute this task?
63
- can_execute = (
64
- resources.get("energy", 0) >= task["required_energy"]
65
- and resources.get("budget", 0) >= task["required_budget"]
66
- )
67
- resource_fit = 1.0 if can_execute else -0.5
68
-
69
- score = (
70
- self.weights["priority"] * task["priority"]
71
- + self.weights["deadline_urgency"] * deadline_urgency
72
- + self.weights["uncertainty_penalty"] * task["uncertainty"]
73
- + self.weights["value"] * (task["value"] / 30.0) # normalise
74
- + self.weights["resource_fit"] * resource_fit
75
- )
76
- return score
77
-
78
- def rank_tasks(
79
- self,
80
- tasks: List[Dict[str, Any]],
81
- resources: Dict[str, float],
82
- step: int,
83
- max_steps: int,
84
- ) -> List[Tuple[int, float]]:
85
- """Return list of (task_id, score) sorted descending."""
86
- scored = [
87
- (t["task_id"], self.score(t, resources, step, max_steps))
88
- for t in tasks
89
- ]
90
- scored.sort(key=lambda x: x[1], reverse=True)
91
- return scored
92
-
93
- # ------------------------------------------------------------------
94
- # Online weight update
95
- # ------------------------------------------------------------------
96
-
97
- def update(self, reward: float) -> None:
98
- """Momentum-based weight update after each step."""
99
- self._episode_rewards.append(reward)
100
- self._step += 1
101
-
102
- # Compute a normalised advantage signal
103
- if len(self._episode_rewards) > 1:
104
- mean_r = sum(self._episode_rewards) / len(self._episode_rewards)
105
- std_r = (
106
- sum((r - mean_r) ** 2 for r in self._episode_rewards)
107
- / len(self._episode_rewards)
108
- ) ** 0.5
109
- advantage = (reward - mean_r) / max(std_r, 1e-6)
110
- else:
111
- advantage = reward
112
-
113
- # Update each weight with momentum
114
- for key in self.weights:
115
- grad = advantage * self._lr
116
- self._momentum[key] = (
117
- self._beta * self._momentum[key] + (1 - self._beta) * grad
118
- )
119
- self.weights[key] += self._momentum[key]
120
-
121
- # Clamp weights to sensible ranges
122
- self.weights["priority"] = max(0.5, min(4.0, self.weights["priority"]))
123
- self.weights["deadline_urgency"] = max(0.3, min(3.0, self.weights["deadline_urgency"]))
124
- self.weights["uncertainty_penalty"] = max(-3.0, min(-0.1, self.weights["uncertainty_penalty"]))
125
- self.weights["value"] = max(0.2, min(2.0, self.weights["value"]))
126
- self.weights["resource_fit"] = max(0.1, min(2.0, self.weights["resource_fit"]))
127
-
128
- def reset(self) -> None:
129
- self._episode_rewards = []
130
- self._step = 0
131
-
132
-
133
- # ---------------------------------------------------------------------------
134
- # RAPTOR – Execution Strategy Engine
135
- # ---------------------------------------------------------------------------
136
-
137
- class RAPTOR:
138
- """
139
- Risk-Aware Priority-Tuned Operational Router.
140
-
141
- Decides *how* to act on the highest-scored task based on
142
- current resource levels, task uncertainty, and deadline pressure.
143
- """
144
-
145
- def decide(
146
- self,
147
- task: Dict[str, Any],
148
- resources: Dict[str, float],
149
- step: int,
150
- max_steps: int,
151
- ) -> str:
152
- """
153
- Return the optimal action type for the given task + resource state.
154
-
155
- Decision logic (priority order):
156
- 1. If resources are critically low → defer
157
- 2. If uncertainty is very high → optimize first
158
- 3. If deadline is imminent and resources sufficient → execute
159
- 4. If task can be delegated cheaply → delegate
160
- 5. Default → execute
161
- """
162
- time_left = max_steps - step
163
- energy = resources.get("energy", 0.0)
164
- budget = resources.get("budget", 0.0)
165
- uncertainty = task.get("uncertainty", 0.0)
166
- deadline = task.get("deadline", 5)
167
- req_energy = task.get("required_energy", 1.0)
168
- req_budget = task.get("required_budget", 5.0)
169
-
170
- # Critical resource shortage
171
- if energy < req_energy * 0.5 or budget < req_budget * 0.5:
172
- if time_left > 2:
173
- return "defer"
174
- else:
175
- return "delegate"
176
-
177
- # Very high uncertainty – optimize first to reduce risk
178
- if uncertainty > 0.75 and time_left > 1:
179
- return "optimize"
180
-
181
- # Imminent deadline – must act now
182
- if deadline <= 1 and energy >= req_energy and budget >= req_budget:
183
- return "execute"
184
-
185
- # Low value + sufficient time → delegate to save resources
186
- if task.get("value", 10) < 8.0 and time_left > 3:
187
- return "delegate"
188
-
189
- # Sufficient resources – execute
190
- if energy >= req_energy and budget >= req_budget:
191
- return "execute"
192
-
193
- # Fallback
194
- return "defer"
195
-
196
-
197
- # ---------------------------------------------------------------------------
198
- # AWFRO-X – Adaptive Waste-Free Resource Optimizer
199
- # ---------------------------------------------------------------------------
200
-
201
- class AWFROX:
202
- """
203
- Converts low-value deferred states into usable outcomes.
204
-
205
- Filters the task queue to remove tasks that are guaranteed to fail
206
- (e.g. deadline passed, insufficient resources with no recovery path)
207
- and recycles deferred tasks back into the active queue if conditions improve.
208
- """
209
-
210
- def filter_viable(
211
- self,
212
- tasks: List[Dict[str, Any]],
213
- resources: Dict[str, float],
214
- step: int,
215
- max_steps: int,
216
- ) -> List[Dict[str, Any]]:
217
- """Remove tasks that cannot possibly be completed."""
218
- time_left = max_steps - step
219
- viable = []
220
- for task in tasks:
221
- # Deadline already passed
222
- if task.get("deadline", 0) < 0:
223
- continue
224
- # No time left
225
- if time_left <= 0:
226
- continue
227
- viable.append(task)
228
- return viable
229
-
230
- def recycle_deferred(
231
- self,
232
- active: List[Dict[str, Any]],
233
- deferred: List[Dict[str, Any]],
234
- resources: Dict[str, float],
235
- step: int,
236
- ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
237
- """
238
- Requeue deferred tasks when resources recover.
239
- Returns (updated_active, updated_deferred).
240
- """
241
- still_deferred = []
242
- for task in deferred:
243
- can_execute = (
244
- resources.get("energy", 0) >= task.get("required_energy", 1.0) * 0.8
245
- and resources.get("budget", 0) >= task.get("required_budget", 1.0) * 0.8
246
- and task.get("deadline", 0) > 0
247
- )
248
- if can_execute:
249
- task["status"] = "pending"
250
- active.append(task)
251
- else:
252
- still_deferred.append(task)
253
- return active, still_deferred
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/env/grader.py DELETED
@@ -1,79 +0,0 @@
1
- """
2
- AETHER-TaskFlow Grader — Explicit per-difficulty scoring
3
- Ensures scores are always in [0.0, 1.0] and deterministic.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from typing import Any, Dict
9
-
10
-
11
- def grade_easy(result: Dict[str, Any]) -> float:
12
- """Easy: Strong reward for completion and resource conservation."""
13
- return _compute_score(
14
- result,
15
- {"efficiency": 0.50, "resource": 0.25, "health": 0.15, "speed": 0.10},
16
- )
17
-
18
-
19
- def grade_medium(result: Dict[str, Any]) -> float:
20
- """Medium: Heavier penalty on missed deadlines."""
21
- base = _compute_score(
22
- result,
23
- {"efficiency": 0.45, "resource": 0.20, "health": 0.25, "speed": 0.10},
24
- )
25
- missed_ratio = result.get("tasks_failed", 0) / max(result.get("total_tasks", 1), 1)
26
- return max(0.0, base - missed_ratio * 0.20)
27
-
28
-
29
- def grade_hard(result: Dict[str, Any]) -> float:
30
- """Hard: Strong penalties for system collapse and failures."""
31
- base = _compute_score(
32
- result,
33
- {"efficiency": 0.40, "resource": 0.15, "health": 0.35, "speed": 0.10},
34
- )
35
- collapse_penalty = 0.15 if result.get("system_health", 1.0) < 0.3 else 0.0
36
- missed_ratio = result.get("tasks_failed", 0) / max(result.get("total_tasks", 1), 1)
37
- return max(0.0, base - collapse_penalty - missed_ratio * 0.25)
38
-
39
-
40
- def _compute_score(result: Dict[str, Any], weights: Dict[str, float]) -> float:
41
- total = max(result.get("total_tasks", 1), 1)
42
- efficiency = result.get("tasks_completed", 0) / total
43
-
44
- rem_time = result.get("remaining_time", 0)
45
- rem_energy = result.get("remaining_energy", 0)
46
- rem_budget = result.get("remaining_budget", 0)
47
- init_time = max(result.get("initial_time", 10), 1)
48
- init_energy = max(result.get("initial_energy", 12), 1)
49
- init_budget = max(result.get("initial_budget", 60), 1)
50
-
51
- resource_score = (
52
- (rem_time / init_time) * 0.3
53
- + (rem_energy / init_energy) * 0.35
54
- + (rem_budget / init_budget) * 0.35
55
- )
56
-
57
- health = max(0.0, min(1.0, result.get("system_health", 1.0)))
58
- steps_used = max(result.get("steps_used", 10), 1)
59
- max_steps = max(result.get("max_steps", 10), 1)
60
- speed = 1.0 - (steps_used / max_steps)
61
-
62
- raw = (
63
- weights.get("efficiency", 0.4) * efficiency
64
- + weights.get("resource", 0.2) * resource_score
65
- + weights.get("health", 0.2) * health
66
- + weights.get("speed", 0.1) * speed
67
- )
68
-
69
- return round(max(0.0, min(1.0, raw)), 4)
70
-
71
-
72
- def grade(difficulty: str, result: Dict[str, Any]) -> float:
73
- graders = {
74
- "easy": grade_easy,
75
- "medium": grade_medium,
76
- "hard": grade_hard,
77
- }
78
- fn = graders.get(difficulty, grade_easy)
79
- return fn(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/env/tasks.py DELETED
@@ -1,192 +0,0 @@
1
- """
2
- Task generation for AETHER-TaskFlow.
3
-
4
- Generates realistic workflow tasks across three difficulty levels:
5
- easy – stable, predictable, moderate resources
6
- medium – dynamic priorities, tighter deadlines
7
- hard – scarce resources, high uncertainty, adversarial failures
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import random
13
- from typing import List, Optional, Tuple
14
-
15
- from models import TaskInfo
16
-
17
-
18
- # ---------------------------------------------------------------------------
19
- # Task templates – drawn from realistic enterprise/ops domains
20
- # ---------------------------------------------------------------------------
21
-
22
- _TASK_TEMPLATES: List[Tuple[str, str]] = [
23
- # (name_template, category)
24
- ("Email triage: {volume} messages", "communication"),
25
- ("Code review: PR #{pr_id}", "engineering"),
26
- ("Data pipeline: {dataset} ETL", "data"),
27
- ("Customer support ticket #{tid}", "support"),
28
- ("Security audit: {module} module", "security"),
29
- ("Performance optimization: {service}", "engineering"),
30
- ("Database backup: {db_name}", "infrastructure"),
31
- ("Report generation: {report_type}", "analytics"),
32
- ("Incident response: {severity} alert", "operations"),
33
- ("Content moderation: batch #{bid}", "moderation"),
34
- ("API rate-limit review: {api_name}", "infrastructure"),
35
- ("ML model retraining: {model_name}", "ml"),
36
- ("Budget reconciliation: {quarter}", "finance"),
37
- ("Compliance check: {regulation}", "legal"),
38
- ("System health scan: {region}", "operations"),
39
- ]
40
-
41
- _FILL_VALUES: dict = {
42
- "volume": ["50", "120", "300", "500"],
43
- "pr_id": ["1042", "2381", "9001", "4417"],
44
- "dataset": ["sales_Q3", "user_events", "inventory", "logs_prod"],
45
- "tid": ["55123", "10984", "30021", "77654"],
46
- "module": ["auth", "payments", "admin", "reporting"],
47
- "service": ["checkout", "search", "recommendations", "notifications"],
48
- "db_name": ["prod_main", "analytics_dw", "user_db", "logs_archive"],
49
- "report_type": ["weekly_KPI", "SLA_breach", "revenue_forecast", "churn"],
50
- "severity": ["P1", "P2", "P3"],
51
- "bid": ["4401", "8812", "1123"],
52
- "api_name": ["stripe", "twilio", "sendgrid", "maps"],
53
- "model_name": ["churn_v3", "fraud_detector", "recommender_v2"],
54
- "quarter": ["Q3-2025", "Q4-2025", "Q1-2026"],
55
- "regulation": ["GDPR", "SOC2", "HIPAA", "PCI-DSS"],
56
- "region": ["us-east-1", "eu-west-2", "ap-southeast-1"],
57
- }
58
-
59
-
60
- def _fill_template(template: str, rng: random.Random) -> str:
61
- result = template
62
- for key, choices in _FILL_VALUES.items():
63
- placeholder = "{" + key + "}"
64
- if placeholder in result:
65
- result = result.replace(placeholder, rng.choice(choices))
66
- return result
67
-
68
-
69
- # ---------------------------------------------------------------------------
70
- # Difficulty profiles
71
- # ---------------------------------------------------------------------------
72
-
73
- _PROFILES: dict = {
74
- "easy": {
75
- "n_tasks": 5,
76
- "priority_range": (0.4, 0.9),
77
- "deadline_range": (4, 8),
78
- "uncertainty_range": (0.05, 0.35),
79
- "value_range": (8.0, 20.0),
80
- "energy_cost_range": (0.5, 1.5),
81
- "budget_cost_range": (1.0, 5.0),
82
- "initial_time": 10,
83
- "initial_energy": 12.0,
84
- "initial_budget": 60.0,
85
- "max_steps": 10,
86
- },
87
- "medium": {
88
- "n_tasks": 8,
89
- "priority_range": (0.3, 1.0),
90
- "deadline_range": (2, 6),
91
- "uncertainty_range": (0.15, 0.65),
92
- "value_range": (5.0, 25.0),
93
- "energy_cost_range": (0.8, 2.5),
94
- "budget_cost_range": (2.0, 10.0),
95
- "initial_time": 10,
96
- "initial_energy": 10.0,
97
- "initial_budget": 50.0,
98
- "max_steps": 10,
99
- },
100
- "hard": {
101
- "n_tasks": 12,
102
- "priority_range": (0.2, 1.0),
103
- "deadline_range": (1, 4),
104
- "uncertainty_range": (0.35, 0.95),
105
- "value_range": (3.0, 30.0),
106
- "energy_cost_range": (1.2, 4.0),
107
- "budget_cost_range": (5.0, 20.0),
108
- "initial_time": 10,
109
- "initial_energy": 8.0,
110
- "initial_budget": 40.0,
111
- "max_steps": 10,
112
- },
113
- }
114
-
115
-
116
- def get_profile(difficulty: str) -> dict:
117
- return _PROFILES[difficulty]
118
-
119
-
120
- def generate_tasks(difficulty: str, seed: Optional[int] = None) -> List[TaskInfo]:
121
- """Generate a task queue for the given difficulty level."""
122
- rng = random.Random(seed)
123
- profile = _PROFILES[difficulty]
124
-
125
- tasks: List[TaskInfo] = []
126
- used_templates = rng.choices(range(len(_TASK_TEMPLATES)), k=profile["n_tasks"])
127
-
128
- for i, t_idx in enumerate(used_templates):
129
- name_template, category = _TASK_TEMPLATES[t_idx]
130
- name = _fill_template(name_template, rng)
131
-
132
- priority = rng.uniform(*profile["priority_range"])
133
- deadline = rng.randint(*profile["deadline_range"])
134
- uncertainty = rng.uniform(*profile["uncertainty_range"])
135
- value = rng.uniform(*profile["value_range"])
136
- energy_cost = rng.uniform(*profile["energy_cost_range"])
137
- budget_cost = rng.uniform(*profile["budget_cost_range"])
138
-
139
- tasks.append(
140
- TaskInfo(
141
- task_id=i,
142
- name=name,
143
- priority=priority,
144
- deadline=deadline,
145
- uncertainty=uncertainty,
146
- value=value,
147
- required_energy=energy_cost,
148
- required_budget=budget_cost,
149
- category=category,
150
- )
151
- )
152
-
153
- # Sort by priority descending so agent sees most urgent first
154
- tasks.sort(key=lambda t: t.priority, reverse=True)
155
- # Re-index after sort
156
- for idx, t in enumerate(tasks):
157
- t.task_id = idx
158
-
159
- return tasks
160
-
161
-
162
- def apply_dynamic_updates(
163
- tasks: List[TaskInfo],
164
- step: int,
165
- difficulty: str,
166
- rng: random.Random,
167
- ) -> List[TaskInfo]:
168
- """
169
- Apply stochastic dynamic updates to the task queue (medium/hard only).
170
- - Priority drift
171
- - Deadline tightening
172
- - Uncertainty spikes
173
- """
174
- if difficulty == "easy":
175
- return tasks
176
-
177
- for task in tasks:
178
- if task.status != "pending":
179
- continue
180
-
181
- # NOTE: deadline countdown is handled centrally in aether_env.py
182
- # _step_impl() to avoid double-decrement on hard mode.
183
-
184
- # Priority drift ±0.1
185
- drift = rng.uniform(-0.08, 0.12)
186
- task.priority = min(1.0, max(0.1, task.priority + drift))
187
-
188
- # Uncertainty spike (hard mode)
189
- if difficulty == "hard" and rng.random() < 0.15:
190
- task.uncertainty = min(0.95, task.uncertainty + rng.uniform(0.1, 0.25))
191
-
192
- return tasks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/inference.py DELETED
@@ -1,229 +0,0 @@
1
- import argparse
2
- import json
3
- import os
4
- import sys
5
- from pathlib import Path
6
- from typing import Any, Dict, List, Optional
7
-
8
- from openai import OpenAI
9
-
10
- _REPO_ROOT = Path(__file__).resolve().parent
11
- sys.path.insert(0, str(_REPO_ROOT))
12
-
13
- from env.aether_env import AetherTaskFlowEnvironment
14
-
15
-
16
- API_KEY = os.getenv("API_KEY", os.getenv("OPENAI_API_KEY", os.getenv("HF_TOKEN", "")))
17
- API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
18
- MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
19
- TEMPERATURE = float(os.getenv("TEMPERATURE", "0.1"))
20
- MAX_LLM_TOKENS = int(os.getenv("MAX_LLM_TOKENS", "128"))
21
- USE_LLM = bool(API_KEY)
22
-
23
- SYSTEM_PROMPT = """You are an expert workflow orchestration agent inside the AETHER-TaskFlow RL environment.
24
-
25
- Each step you receive an observation and must output a single JSON action.
26
-
27
- RULES:
28
- - Output ONLY valid JSON with keys: action_type, task_id, reasoning
29
- - action_type must be one of: execute, defer, delegate, optimize
30
- - task_id must be an integer matching a pending task id
31
- - reasoning should be brief and may be empty
32
- - No explanation, no markdown, no extra text - raw JSON only
33
-
34
- STRATEGY:
35
- - execute: high-priority, low-uncertainty tasks with sufficient resources
36
- - optimize: before executing high-uncertainty tasks (reduces failure risk)
37
- - delegate: when resources are too low (free action, 35% reward)
38
- - defer: tasks you can handle later when resources recover
39
-
40
- Example: {"action_type": "execute", "task_id": 2, "reasoning": "highest value low risk"}"""
41
-
42
-
43
- def log_start(task: str, env: str, model: str) -> None:
44
- print(f"[START] task={task} env={env} model={model}", flush=True)
45
-
46
-
47
- def log_step(
48
- step: int,
49
- action: str,
50
- reward: float,
51
- done: bool,
52
- error: Optional[str] = None,
53
- ) -> None:
54
- error_val = error if error else "null"
55
- done_val = str(done).lower()
56
- action_safe = action.replace("\n", " ").replace("\r", "")[:120]
57
- print(
58
- f"[STEP] step={step} action={action_safe} reward={reward:.2f} "
59
- f"done={done_val} error={error_val}",
60
- flush=True,
61
- )
62
-
63
-
64
- def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
65
- rewards_str = ",".join(f"{r:.2f}" for r in rewards)
66
- print(
67
- f"[END] success={str(success).lower()} steps={steps} "
68
- f"score={score:.2f} rewards={rewards_str}",
69
- flush=True,
70
- )
71
-
72
-
73
- def _obs_to_prompt(obs_dict: Dict[str, Any]) -> str:
74
- tasks = obs_dict.get("tasks", [])
75
- task_lines = []
76
- for t in tasks:
77
- task_lines.append(
78
- f" id={t['task_id']} name='{t['name']}' priority={t['priority']:.2f} "
79
- f"deadline={t['deadline']} uncertainty={t['uncertainty']:.2f} "
80
- f"value={t['value']:.1f} energy_cost={t['required_energy']:.1f} "
81
- f"budget_cost={t['required_budget']:.1f} status={t.get('status', 'pending')}"
82
- )
83
- return (
84
- f"OBSERVATION:\n"
85
- f" time_remaining={obs_dict.get('time_remaining')} "
86
- f"energy={obs_dict.get('energy_remaining', 0):.1f} "
87
- f"budget={obs_dict.get('budget_remaining', 0):.1f} "
88
- f"system_health={obs_dict.get('system_health', 1):.2f}\n"
89
- f"PENDING TASKS:\n"
90
- + "\n".join(task_lines)
91
- + "\n\nOutput your action JSON:"
92
- )
93
-
94
-
95
- def _call_llm(client: OpenAI, obs_text: str, history: List[dict]) -> Optional[Dict[str, Any]]:
96
- """
97
- Call the LLM using proper OpenAI client (required by judges).
98
- Falls back to heuristic if it fails.
99
- """
100
- messages = [{"role": "system", "content": SYSTEM_PROMPT}]
101
- messages.extend(history[-4:])
102
- messages.append({"role": "user", "content": obs_text})
103
-
104
- try:
105
- completion = client.chat.completions.create(
106
- model=MODEL_NAME,
107
- messages=messages,
108
- temperature=TEMPERATURE,
109
- max_tokens=MAX_LLM_TOKENS,
110
- )
111
- raw = (completion.choices[0].message.content or "").strip()
112
- if raw.startswith("```"):
113
- raw = raw.split("```")[1]
114
- if raw.startswith("json"):
115
- raw = raw[4:].strip()
116
- parsed = json.loads(raw)
117
- return {
118
- "task_id": int(parsed["task_id"]),
119
- "action_type": str(parsed["action_type"]),
120
- "reasoning": str(parsed.get("reasoning", "")),
121
- }
122
- except Exception as exc:
123
- print(f"[DEBUG] LLM call/parse failed: {exc}", flush=True)
124
- return None
125
-
126
-
127
- def get_llm_action(
128
- client: OpenAI,
129
- obs_dict: Dict[str, Any],
130
- history: List[dict],
131
- ) -> Optional[Dict[str, Any]]:
132
- """Call the configured OpenAI-compatible endpoint for an LLM action."""
133
- obs_text = _obs_to_prompt(obs_dict)
134
- result = _call_llm(client, obs_text, history)
135
- if result is not None:
136
- history.append({"role": "user", "content": obs_text})
137
- history.append({"role": "assistant", "content": json.dumps(result)})
138
- return result
139
-
140
-
141
- def get_heuristic_action(env: AetherTaskFlowEnvironment) -> Dict[str, Any]:
142
- """Built-in AETHER + RAPTOR heuristic - no API required."""
143
- action = env.message_to_action("")
144
- if hasattr(action, "model_dump"):
145
- return action.model_dump(exclude={"reasoning"})
146
- return {
147
- "action_type": getattr(action, "action_type", "execute").value
148
- if hasattr(action, "action_type")
149
- else "execute",
150
- "task_id": getattr(action, "task_id", 0),
151
- }
152
-
153
-
154
- def get_action(
155
- env: AetherTaskFlowEnvironment,
156
- client: OpenAI,
157
- obs_dict: Dict[str, Any],
158
- history: List[dict],
159
- ) -> Dict[str, Any]:
160
- """Return LLM action if an API key is set, otherwise heuristic."""
161
- if USE_LLM:
162
- result = get_llm_action(client, obs_dict, history)
163
- if result is not None:
164
- return result
165
-
166
- heuristic = get_heuristic_action(env)
167
- obs_text = _obs_to_prompt(obs_dict)
168
- history.append({"role": "user", "content": obs_text})
169
- history.append({"role": "assistant", "content": json.dumps(heuristic)})
170
- return heuristic
171
-
172
-
173
- def run_episode(difficulty: str, client: OpenAI) -> None:
174
- os.environ["AETHER_DIFFICULTY"] = difficulty
175
- env = AetherTaskFlowEnvironment(difficulty=difficulty)
176
-
177
- model_label = MODEL_NAME if USE_LLM else "HEURISTIC-AETHER-RAPTOR"
178
- log_start(task=difficulty, env="aether_taskflow", model=model_label)
179
-
180
- obs = env.reset()
181
- obs_dict = obs.model_dump() if hasattr(obs, "model_dump") else obs
182
-
183
- rewards: List[float] = []
184
- history: List[dict] = []
185
- step = 0
186
-
187
- while True:
188
- step += 1
189
- action_dict = get_action(env, client, obs_dict, history)
190
- next_obs = env.step(action_dict)
191
-
192
- next_obs_dict = next_obs.model_dump() if hasattr(next_obs, "model_dump") else next_obs
193
- reward = next_obs_dict.get("reward", 0.0)
194
- done = next_obs_dict.get("done", False)
195
- rewards.append(reward)
196
-
197
- action_str = (
198
- f"{action_dict.get('action_type', 'execute')}"
199
- f"(task_id={action_dict.get('task_id', 0)})"
200
- )
201
- log_step(step, action_str, reward, done)
202
-
203
- obs_dict = next_obs_dict
204
- if done:
205
- break
206
-
207
- score = env.compute_final_score()
208
- log_end(success=True, steps=step, score=score, rewards=rewards)
209
-
210
-
211
- def main() -> None:
212
- client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
213
-
214
- parser = argparse.ArgumentParser(description="AETHER-TaskFlow Inference")
215
- parser.add_argument(
216
- "--single",
217
- choices=["easy", "medium", "hard"],
218
- default=None,
219
- help="Run a single difficulty (default: all three)",
220
- )
221
- args = parser.parse_args()
222
-
223
- difficulties = [args.single] if args.single else ["easy", "medium", "hard"]
224
- for diff in difficulties:
225
- run_episode(diff, client)
226
-
227
-
228
- if __name__ == "__main__":
229
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/models.py DELETED
@@ -1,298 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from enum import Enum
5
- from typing import Any, Dict, List, Optional
6
-
7
- from openenv.core.env_server.types import Action, Observation, State
8
- from pydantic import Field, model_validator
9
-
10
-
11
-
12
- class ActionType(str, Enum):
13
- EXECUTE = "execute"
14
- DEFER = "defer"
15
- DELEGATE = "delegate"
16
- OPTIMIZE = "optimize"
17
-
18
-
19
- class TaskStatus(str, Enum):
20
- PENDING = "pending"
21
- IN_PROGRESS = "in_progress"
22
- COMPLETED = "completed"
23
- DEFERRED = "deferred"
24
- FAILED = "failed"
25
-
26
-
27
- class DifficultyLevel(str, Enum):
28
- EASY = "easy"
29
- MEDIUM = "medium"
30
- HARD = "hard"
31
-
32
-
33
- def _coerce_action_type_from_text(text: str) -> ActionType:
34
- """
35
- Convert loose text from the web UI into the closest valid action type.
36
-
37
- The default web interface validates form fields against the Action model
38
- before our environment can run `message_to_action()`, so we accept
39
- friendly free-form input here and normalize it.
40
- """
41
- normalized = (text or "").strip().lower()
42
- if not normalized:
43
- return ActionType.EXECUTE
44
-
45
- keyword_map = (
46
- (ActionType.OPTIMIZE, ("optimize", "optimise", "tune", "analyze", "analyse")),
47
- (ActionType.DELEGATE, ("delegate", "assign", "handoff", "hand off", "offload")),
48
- (ActionType.DEFER, ("defer", "later", "wait", "skip", "postpone")),
49
- (ActionType.EXECUTE, ("execute", "run", "do", "complete", "process", "start")),
50
- )
51
- for action_type, keywords in keyword_map:
52
- if any(keyword in normalized for keyword in keywords):
53
- return action_type
54
-
55
- return ActionType.EXECUTE
56
-
57
-
58
- def _extract_task_id_from_text(text: str) -> Optional[int]:
59
- if not text:
60
- return None
61
-
62
- explicit_match = re.search(r"(?:task|id|#)\s*(\d+)", text, flags=re.IGNORECASE)
63
- if explicit_match:
64
- return int(explicit_match.group(1))
65
-
66
- loose_match = re.search(r"\b(\d+)\b", text)
67
- if loose_match:
68
- return int(loose_match.group(1))
69
-
70
- return None
71
-
72
-
73
- class TaskInfo(object):
74
- """Lightweight task descriptor (not a BaseModel to avoid nesting issues)."""
75
-
76
- def __init__(
77
- self,
78
- task_id: int,
79
- name: str,
80
- priority: float,
81
- deadline: int,
82
- uncertainty: float,
83
- value: float,
84
- required_energy: float,
85
- required_budget: float,
86
- category: str,
87
- status: str = "pending",
88
- ):
89
- self.task_id = task_id
90
- self.name = name
91
- self.priority = priority
92
- self.deadline = deadline
93
- self.uncertainty = uncertainty
94
- self.value = value
95
- self.required_energy = required_energy
96
- self.required_budget = required_budget
97
- self.category = category
98
- self.status = status
99
-
100
- def to_dict(self) -> Dict[str, Any]:
101
- return {
102
- "task_id": self.task_id,
103
- "name": self.name,
104
- "priority": round(self.priority, 3),
105
- "deadline": self.deadline,
106
- "uncertainty": round(self.uncertainty, 3),
107
- "value": round(self.value, 3),
108
- "required_energy": round(self.required_energy, 3),
109
- "required_budget": round(self.required_budget, 3),
110
- "category": self.category,
111
- "status": self.status,
112
- }
113
-
114
-
115
- class AetherTaskFlowAction(Action):
116
- """
117
- Action for the AETHER-TaskFlow environment.
118
-
119
- The agent selects a task by ID and decides how to act on it.
120
- """
121
-
122
- @model_validator(mode="before")
123
- @classmethod
124
- def normalize_web_input(cls, data: Any) -> Any:
125
- """
126
- Make the default web form resilient to casual text input.
127
-
128
- Examples that should validate cleanly:
129
- {"action_type": "hi"}
130
- {"action_type": "execute task 3"}
131
- {"message": "delegate 2"}
132
- """
133
- if isinstance(data, str):
134
- data = {"action_type": data}
135
-
136
- if not isinstance(data, dict):
137
- return data
138
-
139
- payload = dict(data)
140
- raw_message = payload.get("message")
141
- raw_action_type = payload.get("action_type")
142
-
143
- # When the UI sends a free-form message, treat it as action text.
144
- if isinstance(raw_message, str) and not raw_action_type:
145
- raw_action_type = raw_message
146
- payload.pop("message", None)
147
-
148
- if isinstance(raw_action_type, str):
149
- parsed_task_id = _extract_task_id_from_text(raw_action_type)
150
- payload["action_type"] = _coerce_action_type_from_text(raw_action_type).value
151
-
152
- if payload.get("task_id") in (None, "") and parsed_task_id is not None:
153
- payload["task_id"] = parsed_task_id
154
-
155
- if payload.get("reasoning") in (None, "") and raw_action_type.strip():
156
- payload["reasoning"] = f"parsed from '{raw_action_type.strip()[:80]}'"
157
-
158
- if payload.get("task_id") in (None, ""):
159
- payload["task_id"] = 0
160
-
161
- return payload
162
-
163
- action_type: ActionType = Field(
164
- ...,
165
- description=(
166
- "How to act on the selected task. "
167
- "'execute': consume resources and complete the task; "
168
- "'defer': postpone to a later step (low penalty); "
169
- "'delegate': offload at reduced reward but no resource cost; "
170
- "'optimize': reduce task uncertainty before execution."
171
- ),
172
- )
173
- task_id: int = Field(
174
- ...,
175
- ge=0,
176
- description="ID of the task to act on (from the current task list).",
177
- )
178
- reasoning: Optional[str] = Field(
179
- default=None,
180
- max_length=500,
181
- description="Optional agent reasoning for this action (logged but not scored).",
182
- )
183
-
184
-
185
- class AetherTaskFlowObservation(Observation):
186
- """
187
- Observation returned after each step in the AETHER-TaskFlow environment.
188
-
189
- Contains all information the agent needs to make the next decision.
190
- """
191
-
192
- # Task queue
193
- tasks: List[Dict[str, Any]] = Field(
194
- default_factory=list,
195
- description="Current list of pending/deferred tasks as dicts.",
196
- )
197
-
198
- # Resource pool
199
- time_remaining: int = Field(
200
- default=10,
201
- ge=0,
202
- description="Time steps remaining in this episode.",
203
- )
204
- energy_remaining: float = Field(
205
- default=10.0,
206
- ge=0.0,
207
- description="Energy units remaining.",
208
- )
209
- budget_remaining: float = Field(
210
- default=50.0,
211
- ge=0.0,
212
- description="Budget units remaining.",
213
- )
214
-
215
- # System health
216
- system_health: float = Field(
217
- default=1.0,
218
- ge=0.0,
219
- le=1.0,
220
- description="Overall system health [0,1]. Drops on overload or missed deadlines.",
221
- )
222
-
223
- # Episode progress
224
- step_number: int = Field(
225
- default=0,
226
- ge=0,
227
- description="Current step number in this episode.",
228
- )
229
- tasks_completed: int = Field(
230
- default=0,
231
- ge=0,
232
- description="Total tasks completed so far.",
233
- )
234
- tasks_failed: int = Field(
235
- default=0,
236
- ge=0,
237
- description="Total tasks that missed their deadline.",
238
- )
239
- cumulative_value: float = Field(
240
- default=0.0,
241
- description="Total value accumulated so far.",
242
- )
243
-
244
- # Last action feedback
245
- last_action_type: Optional[str] = Field(
246
- default=None,
247
- description="Action type taken in the previous step.",
248
- )
249
- last_action_task_id: Optional[int] = Field(
250
- default=None,
251
- description="Task ID acted on in the previous step.",
252
- )
253
- last_action_outcome: Optional[str] = Field(
254
- default=None,
255
- description="Human-readable outcome of the last action.",
256
- )
257
-
258
- # Episode info
259
- difficulty: str = Field(
260
- default="easy",
261
- description="Current task difficulty level.",
262
- )
263
- episode_id: Optional[str] = Field(
264
- default=None,
265
- description="Unique identifier for this episode.",
266
- )
267
-
268
-
269
-
270
- class AetherTaskFlowState(State):
271
- """
272
- Internal state of the AETHER-TaskFlow environment.
273
-
274
- This is the ground truth state used by the grader.
275
- """
276
-
277
- difficulty: str = Field(default="easy")
278
- tasks: List[Dict[str, Any]] = Field(default_factory=list)
279
- completed_tasks: List[Dict[str, Any]] = Field(default_factory=list)
280
- failed_tasks: List[Dict[str, Any]] = Field(default_factory=list)
281
- deferred_tasks: List[Dict[str, Any]] = Field(default_factory=list)
282
-
283
- resources: Dict[str, float] = Field(
284
- default_factory=lambda: {"time": 10.0, "energy": 10.0, "budget": 50.0}
285
- )
286
- initial_resources: Dict[str, float] = Field(
287
- default_factory=lambda: {"time": 10.0, "energy": 10.0, "budget": 50.0}
288
- )
289
-
290
- system_health: float = Field(default=1.0)
291
- cumulative_value: float = Field(default=0.0)
292
- cumulative_reward: float = Field(default=0.0)
293
-
294
- tasks_completed: int = Field(default=0)
295
- tasks_failed: int = Field(default=0)
296
-
297
- episode_done: bool = Field(default=False)
298
- seed: Optional[int] = Field(default=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/openenv.yaml DELETED
@@ -1,27 +0,0 @@
1
- spec_version: 1
2
- name: aether_taskflow
3
- description: Adaptive Workflow Management RL Environment — dynamic task scheduling under resource constraints, uncertainty, and shifting priorities. Real-world enterprise operations simulation.
4
-
5
- api:
6
- reset: /reset
7
- step: /step
8
- state: /state
9
- schema: /schema
10
-
11
- tasks:
12
- - id: easy
13
- description: Stable environment with 5 tasks and moderate resources
14
- - id: medium
15
- description: Dynamic priorities and tighter deadlines with 8 tasks
16
- - id: hard
17
- description: Scarce resources, high uncertainty spikes, and 12 tasks
18
-
19
- models:
20
- action: AetherTaskFlowAction
21
- observation: AetherTaskFlowObservation
22
- state: AetherTaskFlowState
23
-
24
- runtime: fastapi
25
- app: server.app:app
26
- port: 7860
27
- enable_web_interface: true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/pyproject.toml DELETED
@@ -1,32 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=45", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "aether-taskflow"
7
- version = "1.0.0"
8
- description = "AETHER-TaskFlow: Adaptive Workflow Management RL Environment for OpenEnv"
9
- requires-python = ">=3.10"
10
- dependencies = [
11
- "openenv-core[core]>=0.2.2",
12
- "openai>=1.0.0",
13
- "fastapi>=0.100.0",
14
- "uvicorn[standard]>=0.22.0",
15
- "pydantic>=2.0.0",
16
- "httpx>=0.24.0",
17
- "websockets>=11.0",
18
- ]
19
-
20
- [project.optional-dependencies]
21
- dev = [
22
- "pytest>=8.0.0",
23
- "pytest-asyncio>=0.23.0",
24
- ]
25
-
26
- [project.scripts]
27
- server = "aether_taskflow.server.app:main"
28
-
29
- [tool.setuptools]
30
- include-package-data = true
31
- packages = ["aether_taskflow", "aether_taskflow.server"]
32
- package-dir = { "aether_taskflow" = ".", "aether_taskflow.server" = "server" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/pytest.ini DELETED
@@ -1,4 +0,0 @@
1
- [pytest]
2
- testpaths = tests
3
- norecursedirs = pytest-cache-files-* __pycache__
4
- addopts = -p no:cacheprovider
 
 
 
 
 
aether-taskflow/requirements.txt DELETED
@@ -1,8 +0,0 @@
1
- # AETHER-TaskFlow requirements
2
- openenv-core>=0.2.2
3
- openai>=1.0.0
4
- fastapi>=0.100.0
5
- uvicorn[standard]>=0.22.0
6
- pydantic>=2.0.0
7
- httpx>=0.24.0
8
- websockets>=11.0
 
 
 
 
 
 
 
 
 
aether-taskflow/server/__init__.py DELETED
@@ -1 +0,0 @@
1
- """AETHER-TaskFlow server package."""
 
 
aether-taskflow/server/__pycache__/__init__.cpython-312.pyc DELETED
Binary file (196 Bytes)
 
aether-taskflow/server/__pycache__/app.cpython-312.pyc DELETED
Binary file (42.5 kB)
 
aether-taskflow/server/app.py DELETED
@@ -1,1068 +0,0 @@
1
- """
2
- FastAPI application for the AETHER-TaskFlow Environment.
3
-
4
- Endpoints:
5
- GET / - Interactive Mission Control Dashboard
6
- POST /reset - Reset the environment, return initial observation
7
- POST /step - Execute an action, return next observation
8
- GET /state - Return current internal state
9
- GET /schema - Return action/observation/state JSON schemas
10
- GET /health - Health check
11
- WS /ws - WebSocket for persistent sessions
12
- GET /docs - Swagger UI
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import os
18
- import sys
19
- from pathlib import Path
20
- from typing import Any, Dict
21
-
22
- from fastapi import Body, HTTPException, status
23
- from fastapi.responses import HTMLResponse
24
- from openenv.core.env_server.http_server import create_app
25
- from openenv.core.env_server.types import ResetRequest, ResetResponse, SchemaResponse, StepResponse
26
- from openenv.core.env_server.web_interface import WebInterfaceManager
27
- from pydantic import ValidationError
28
-
29
- _REPO_ROOT = Path(__file__).resolve().parents[1]
30
- sys.path.insert(0, str(_REPO_ROOT))
31
-
32
- try:
33
- from models import AetherTaskFlowAction, AetherTaskFlowObservation, AetherTaskFlowState
34
- from env.aether_env import AetherTaskFlowEnvironment
35
- except ModuleNotFoundError:
36
- sys.path.insert(0, str(_REPO_ROOT))
37
- from models import AetherTaskFlowAction, AetherTaskFlowObservation, AetherTaskFlowState
38
- from env.aether_env import AetherTaskFlowEnvironment
39
-
40
- _DIFFICULTY = os.getenv("AETHER_DIFFICULTY", "easy")
41
-
42
-
43
- def _make_env() -> AetherTaskFlowEnvironment:
44
- difficulty = os.getenv("AETHER_DIFFICULTY", _DIFFICULTY)
45
- return AetherTaskFlowEnvironment(difficulty=difficulty)
46
-
47
-
48
- def _create_persistent_manager() -> WebInterfaceManager:
49
- temp_env = _make_env()
50
- metadata: dict = {}
51
- try:
52
- if hasattr(temp_env, "get_metadata"):
53
- metadata = temp_env.get_metadata()
54
- except Exception:
55
- pass
56
- finally:
57
- if hasattr(temp_env, "close"):
58
- try:
59
- temp_env.close()
60
- except Exception:
61
- pass
62
- return WebInterfaceManager(_make_env, AetherTaskFlowAction, AetherTaskFlowObservation, metadata=metadata)
63
-
64
-
65
- app = create_app(
66
- _make_env, AetherTaskFlowAction, AetherTaskFlowObservation,
67
- env_name="aether_taskflow", max_concurrent_envs=4,
68
- )
69
-
70
- _persistent_manager = _create_persistent_manager()
71
-
72
-
73
- def _remove_route(path: str, method: str) -> None:
74
- app.router.routes = [
75
- route for route in app.router.routes
76
- if not (getattr(route, "path", None) == path and method in (getattr(route, "methods", set()) or set()))
77
- ]
78
-
79
-
80
- for _path, _method in (("/reset", "POST"), ("/step", "POST"), ("/state", "GET"), ("/schema", "GET")):
81
- _remove_route(_path, _method)
82
-
83
-
84
- def _action_to_payload(action: Any) -> Dict[str, Any]:
85
- if hasattr(action, "model_dump"):
86
- return action.model_dump(exclude={"metadata"})
87
- if isinstance(action, dict):
88
- return action
89
- raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid action")
90
-
91
-
92
- def _extract_action_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
93
- env = _persistent_manager.env
94
- if "message" in payload and isinstance(payload["message"], str):
95
- if hasattr(env, "message_to_action"):
96
- return _action_to_payload(env.message_to_action(payload["message"]))
97
- return {"message": payload["message"]}
98
- action_payload = payload.get("action", payload)
99
- if isinstance(action_payload, str):
100
- if hasattr(env, "message_to_action"):
101
- return _action_to_payload(env.message_to_action(action_payload))
102
- return {"message": action_payload}
103
- if isinstance(action_payload, dict):
104
- if "message" in action_payload and isinstance(action_payload["message"], str):
105
- if hasattr(env, "message_to_action"):
106
- return _action_to_payload(env.message_to_action(action_payload["message"]))
107
- return {"message": action_payload["message"]}
108
- return action_payload
109
- raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid payload")
110
-
111
-
112
-
113
- @app.get("/", response_class=HTMLResponse, tags=["UI"], summary="Mission Control Dashboard")
114
- async def root() -> HTMLResponse:
115
- difficulty = os.getenv("AETHER_DIFFICULTY", "easy")
116
- html = f"""<!DOCTYPE html>
117
- <html lang="en">
118
- <head>
119
- <meta charset="UTF-8"/>
120
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
121
- <title>AETHER-TaskFlow · Mission Control</title>
122
- <link rel="preconnect" href="https://fonts.googleapis.com"/>
123
- <link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet"/>
124
- <style>
125
- :root {{
126
- --bg: #050810;
127
- --panel: #0b0f1e;
128
- --border: #1a2040;
129
- --border2: #252d50;
130
- --accent: #3d7eff;
131
- --accent2: #6b3dff;
132
- --cyan: #00d4ff;
133
- --green: #00e676;
134
- --amber: #ffab00;
135
- --red: #ff3d3d;
136
- --text: #cdd6f4;
137
- --muted: #6c7a9c;
138
- --glow: rgba(61,126,255,0.18);
139
- }}
140
- *, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
141
-
142
- body {{
143
- font-family: 'Syne', sans-serif;
144
- background: var(--bg);
145
- color: var(--text);
146
- min-height: 100vh;
147
- overflow-x: hidden;
148
- }}
149
-
150
- /* Grid scanline overlay */
151
- body::before {{
152
- content: '';
153
- position: fixed; inset: 0;
154
- background:
155
- repeating-linear-gradient(0deg, transparent, transparent 39px, rgba(61,126,255,0.03) 40px),
156
- repeating-linear-gradient(90deg, transparent, transparent 39px, rgba(61,126,255,0.03) 40px);
157
- pointer-events: none; z-index: 0;
158
- }}
159
-
160
- /* ── TOP BAR ── */
161
- header {{
162
- position: relative; z-index: 10;
163
- display: flex; align-items: center; justify-content: space-between;
164
- padding: 0 28px;
165
- height: 60px;
166
- background: rgba(11,15,30,0.95);
167
- border-bottom: 1px solid var(--border2);
168
- backdrop-filter: blur(12px);
169
- }}
170
- .logo {{
171
- display: flex; align-items: center; gap: 12px;
172
- }}
173
- .logo-icon {{
174
- width: 32px; height: 32px;
175
- background: linear-gradient(135deg, var(--accent), var(--accent2));
176
- border-radius: 8px;
177
- display: flex; align-items: center; justify-content: center;
178
- font-size: 16px;
179
- box-shadow: 0 0 20px rgba(61,126,255,0.4);
180
- }}
181
- .logo-text {{ font-size: 1rem; font-weight: 800; letter-spacing: -0.02em; }}
182
- .logo-sub {{ font-size: 0.7rem; color: var(--muted); font-family: 'Space Mono', monospace; margin-top: 1px; }}
183
- .header-links {{ display: flex; gap: 6px; }}
184
- .hlink {{
185
- font-family: 'Space Mono', monospace;
186
- font-size: 0.68rem; color: var(--muted);
187
- text-decoration: none; padding: 5px 10px;
188
- border: 1px solid var(--border2); border-radius: 4px;
189
- transition: all .15s;
190
- }}
191
- .hlink:hover {{ color: var(--cyan); border-color: var(--cyan); }}
192
- .live-indicator {{
193
- display: flex; align-items: center; gap: 6px;
194
- font-family: 'Space Mono', monospace; font-size: 0.68rem; color: var(--green);
195
- }}
196
- .live-dot {{
197
- width: 6px; height: 6px; border-radius: 50%;
198
- background: var(--green);
199
- box-shadow: 0 0 6px var(--green);
200
- animation: pulse-dot 1.8s ease-in-out infinite;
201
- }}
202
- @keyframes pulse-dot {{
203
- 0%,100% {{ opacity: 1; transform: scale(1); }}
204
- 50% {{ opacity: 0.5; transform: scale(0.7); }}
205
- }}
206
-
207
- /* ── LAYOUT ── */
208
- .workspace {{
209
- position: relative; z-index: 1;
210
- display: grid;
211
- grid-template-columns: 300px 1fr 260px;
212
- grid-template-rows: auto 1fr auto;
213
- gap: 0;
214
- height: calc(100vh - 60px);
215
- overflow: hidden;
216
- }}
217
-
218
- /* ── COMMAND BAR ── */
219
- .cmd-bar {{
220
- grid-column: 1 / -1;
221
- display: flex; align-items: center; gap: 10px;
222
- padding: 10px 20px;
223
- background: rgba(11,15,30,0.8);
224
- border-bottom: 1px solid var(--border);
225
- }}
226
- .cmd-label {{
227
- font-family: 'Space Mono', monospace;
228
- font-size: 0.65rem; color: var(--muted);
229
- text-transform: uppercase; letter-spacing: .1em;
230
- white-space: nowrap;
231
- }}
232
- select.diff-select {{
233
- background: var(--panel); border: 1px solid var(--border2);
234
- color: var(--text); padding: 7px 14px; border-radius: 6px;
235
- font-family: 'Space Mono', monospace; font-size: 0.75rem;
236
- cursor: pointer; outline: none;
237
- transition: border-color .15s;
238
- }}
239
- select.diff-select:focus {{ border-color: var(--accent); }}
240
- .cmd-btn {{
241
- padding: 7px 18px; border: none; border-radius: 6px;
242
- font-family: 'Syne', sans-serif; font-size: 0.78rem; font-weight: 700;
243
- cursor: pointer; transition: all .15s; white-space: nowrap;
244
- }}
245
- .cmd-btn:hover {{ transform: translateY(-1px); filter: brightness(1.1); }}
246
- .cmd-btn:active {{ transform: translateY(0); }}
247
- .cmd-btn:disabled {{ opacity: 0.35; cursor: not-allowed; transform: none; }}
248
- .btn-reset {{ background: linear-gradient(135deg, var(--accent), var(--accent2)); color: white; }}
249
- .btn-step {{ background: var(--green); color: #000; }}
250
- .btn-auto {{ background: var(--amber); color: #000; }}
251
- .btn-stop {{ background: var(--red); color: white; }}
252
- .status-pill {{
253
- margin-left: auto;
254
- font-family: 'Space Mono', monospace; font-size: 0.68rem;
255
- padding: 4px 12px; border-radius: 20px;
256
- border: 1px solid var(--border2); color: var(--muted);
257
- transition: all .3s;
258
- }}
259
- .status-pill.ready {{ border-color: var(--green); color: var(--green); }}
260
- .status-pill.done {{ border-color: var(--amber); color: var(--amber); }}
261
-
262
- /* ── LEFT PANEL: Task Queue ── */
263
- .panel-left {{
264
- grid-row: 2;
265
- background: var(--panel);
266
- border-right: 1px solid var(--border);
267
- display: flex; flex-direction: column;
268
- overflow: hidden;
269
- }}
270
- .panel-title {{
271
- padding: 12px 16px 8px;
272
- font-size: 0.65rem; font-weight: 700;
273
- text-transform: uppercase; letter-spacing: .12em;
274
- color: var(--muted);
275
- border-bottom: 1px solid var(--border);
276
- display: flex; align-items: center; justify-content: space-between;
277
- }}
278
- .task-scroll {{
279
- flex: 1; overflow-y: auto; padding: 10px;
280
- scrollbar-width: thin; scrollbar-color: var(--border2) transparent;
281
- }}
282
- .task-card {{
283
- border: 1px solid var(--border2);
284
- border-radius: 8px; padding: 10px 12px;
285
- margin-bottom: 8px; cursor: pointer;
286
- transition: all .15s;
287
- position: relative; overflow: hidden;
288
- }}
289
- .task-card::before {{
290
- content: '';
291
- position: absolute; left: 0; top: 0; bottom: 0;
292
- width: 3px;
293
- background: var(--muted);
294
- transition: background .15s;
295
- }}
296
- .task-card:hover {{ border-color: var(--accent); background: rgba(61,126,255,0.05); }}
297
- .task-card:hover::before {{ background: var(--accent); }}
298
- .task-card.selected {{ border-color: var(--accent); background: rgba(61,126,255,0.08); box-shadow: 0 0 0 1px rgba(61,126,255,0.2); }}
299
- .task-card.selected::before {{ background: var(--accent); box-shadow: 0 0 8px var(--accent); }}
300
- .task-card.urgent {{ border-color: rgba(255,61,61,0.4); }}
301
- .task-card.urgent::before {{ background: var(--red); }}
302
- .task-name {{ font-size: 0.78rem; font-weight: 600; margin-bottom: 6px; line-height: 1.3; padding-left: 6px; }}
303
- .task-chips {{ display: flex; flex-wrap: wrap; gap: 4px; padding-left: 6px; }}
304
- .chip {{
305
- font-family: 'Space Mono', monospace;
306
- font-size: 0.6rem; padding: 2px 5px;
307
- border-radius: 3px; border: 1px solid var(--border2);
308
- color: var(--muted);
309
- }}
310
- .chip.hi {{ color: var(--green); border-color: rgba(0,230,118,0.3); }}
311
- .chip.mid {{ color: var(--amber); border-color: rgba(255,171,0,0.3); }}
312
- .chip.lo {{ color: var(--red); border-color: rgba(255,61,61,0.3); }}
313
- .chip.val {{ color: var(--cyan); border-color: rgba(0,212,255,0.3); }}
314
- .task-empty {{
315
- text-align: center; padding: 40px 16px;
316
- color: var(--muted); font-size: 0.8rem; line-height: 1.6;
317
- }}
318
-
319
- /* ── CENTRE: Visualisation ── */
320
- .panel-centre {{
321
- grid-row: 2;
322
- display: flex; flex-direction: column;
323
- overflow: hidden;
324
- border-right: 1px solid var(--border);
325
- }}
326
-
327
- /* Resource meters */
328
- .resource-row {{
329
- display: grid; grid-template-columns: repeat(4, 1fr);
330
- gap: 10px; padding: 14px 16px;
331
- border-bottom: 1px solid var(--border);
332
- background: rgba(11,15,30,0.5);
333
- }}
334
- .res-cell {{
335
- background: var(--panel);
336
- border: 1px solid var(--border2);
337
- border-radius: 8px; padding: 10px 14px;
338
- position: relative; overflow: hidden;
339
- }}
340
- .res-cell::after {{
341
- content: '';
342
- position: absolute; bottom: 0; left: 0;
343
- height: 2px; width: var(--fill, 0%);
344
- background: var(--fill-color, var(--accent));
345
- transition: width .5s ease, background .5s ease;
346
- }}
347
- .res-label {{
348
- font-family: 'Space Mono', monospace;
349
- font-size: 0.6rem; color: var(--muted);
350
- text-transform: uppercase; letter-spacing: .08em;
351
- }}
352
- .res-value {{
353
- font-family: 'Space Mono', monospace;
354
- font-size: 1.25rem; font-weight: 700;
355
- color: var(--text); margin: 4px 0 0;
356
- }}
357
-
358
- /* Action picker */
359
- .action-panel {{
360
- padding: 12px 16px;
361
- border-bottom: 1px solid var(--border);
362
- background: rgba(11,15,30,0.3);
363
- }}
364
- .action-grid {{ display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; margin-top: 8px; }}
365
- .act-btn {{
366
- border: 1px solid var(--border2);
367
- border-radius: 8px; padding: 10px 6px;
368
- cursor: pointer; text-align: center;
369
- transition: all .15s; background: var(--panel);
370
- color: var(--muted); font-family: 'Syne', sans-serif;
371
- }}
372
- .act-btn:hover {{ border-color: var(--accent); color: var(--text); background: rgba(61,126,255,0.07); }}
373
- .act-btn.active {{
374
- border-color: var(--accent); color: var(--accent);
375
- background: rgba(61,126,255,0.12);
376
- box-shadow: 0 0 12px rgba(61,126,255,0.15);
377
- }}
378
- .act-btn .act-icon {{ font-size: 1.2rem; display: block; margin-bottom: 3px; }}
379
- .act-btn .act-name {{ font-size: 0.72rem; font-weight: 700; }}
380
- .act-btn .act-sub {{ font-size: 0.6rem; color: var(--muted); margin-top: 1px; font-family: 'Space Mono', monospace; }}
381
-
382
- /* Outcome text */
383
- .outcome-bar {{
384
- padding: 10px 16px;
385
- border-bottom: 1px solid var(--border);
386
- font-family: 'Space Mono', monospace;
387
- font-size: 0.72rem; color: var(--muted);
388
- background: rgba(5,8,16,0.5);
389
- min-height: 38px;
390
- }}
391
- .outcome-bar span {{ color: var(--cyan); }}
392
-
393
- /* Reward chart area */
394
- .chart-area {{
395
- flex: 1; padding: 14px 16px;
396
- display: flex; flex-direction: column; gap: 10px;
397
- }}
398
- .chart-title {{
399
- font-size: 0.62rem; font-weight: 700;
400
- text-transform: uppercase; letter-spacing: .1em;
401
- color: var(--muted);
402
- }}
403
- .reward-chart {{
404
- flex: 1;
405
- display: flex; align-items: flex-end;
406
- gap: 4px; border-bottom: 1px solid var(--border2);
407
- padding-bottom: 4px; min-height: 80px;
408
- }}
409
- .reward-bar {{
410
- flex: 1; min-width: 8px; border-radius: 3px 3px 0 0;
411
- transition: height .4s ease, background .4s;
412
- position: relative;
413
- }}
414
- .reward-bar::after {{
415
- content: attr(data-val);
416
- position: absolute; bottom: calc(100% + 2px); left: 50%;
417
- transform: translateX(-50%);
418
- font-family: 'Space Mono', monospace; font-size: 0.5rem;
419
- color: var(--muted); white-space: nowrap;
420
- opacity: 0; transition: opacity .2s;
421
- }}
422
- .reward-bar:hover::after {{ opacity: 1; }}
423
-
424
- /* ── RIGHT PANEL: Telemetry ── */
425
- .panel-right {{
426
- grid-row: 2;
427
- background: var(--panel);
428
- display: flex; flex-direction: column;
429
- overflow-y: auto;
430
- scrollbar-width: thin; scrollbar-color: var(--border2) transparent;
431
- }}
432
- .score-hero {{
433
- padding: 20px 16px 14px;
434
- text-align: center;
435
- border-bottom: 1px solid var(--border);
436
- position: relative;
437
- }}
438
- .score-ring {{
439
- width: 110px; height: 110px;
440
- margin: 0 auto 10px;
441
- position: relative;
442
- }}
443
- .score-ring svg {{ transform: rotate(-90deg); }}
444
- .score-ring .ring-bg {{ fill: none; stroke: var(--border2); stroke-width: 8; }}
445
- .score-ring .ring-fill {{
446
- fill: none; stroke-width: 8;
447
- stroke-linecap: round;
448
- stroke-dasharray: 283;
449
- stroke-dashoffset: 283;
450
- transition: stroke-dashoffset .8s cubic-bezier(.4,0,.2,1), stroke .4s;
451
- stroke: var(--accent);
452
- }}
453
- .score-num {{
454
- position: absolute; top: 50%; left: 50%;
455
- transform: translate(-50%,-50%);
456
- font-family: 'Space Mono', monospace;
457
- font-size: 1.5rem; font-weight: 700;
458
- }}
459
- .score-label {{ font-size: 0.62rem; color: var(--muted); text-transform: uppercase; letter-spacing: .1em; }}
460
-
461
- .stat-grid {{
462
- display: grid; grid-template-columns: 1fr 1fr;
463
- gap: 8px; padding: 12px;
464
- border-bottom: 1px solid var(--border);
465
- }}
466
- .stat-box {{
467
- background: rgba(5,8,16,0.6);
468
- border: 1px solid var(--border2);
469
- border-radius: 8px; padding: 10px;
470
- text-align: center;
471
- }}
472
- .stat-box .sn {{
473
- font-family: 'Space Mono', monospace;
474
- font-size: 1.3rem; font-weight: 700;
475
- }}
476
- .stat-box .sl {{ font-size: 0.6rem; color: var(--muted); margin-top: 2px; text-transform: uppercase; letter-spacing: .07em; }}
477
- .stat-box.green .sn {{ color: var(--green); }}
478
- .stat-box.red .sn {{ color: var(--red); }}
479
- .stat-box.cyan .sn {{ color: var(--cyan); }}
480
- .stat-box.amber .sn {{ color: var(--amber); }}
481
-
482
- /* Health bar */
483
- .health-section {{ padding: 12px; border-bottom: 1px solid var(--border); }}
484
- .health-label {{ font-size: 0.62rem; color: var(--muted); text-transform: uppercase; letter-spacing: .1em; margin-bottom: 8px; display: flex; justify-content: space-between; }}
485
- .health-track {{
486
- height: 8px; background: var(--border2); border-radius: 4px; overflow: hidden;
487
- }}
488
- .health-fill {{
489
- height: 100%; border-radius: 4px;
490
- transition: width .5s ease, background .5s ease;
491
- background: var(--green);
492
- }}
493
-
494
- /* Log */
495
- .log-panel {{ flex: 1; padding: 10px; }}
496
- .log-title {{ font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); margin-bottom: 8px; }}
497
- .log-scroll {{
498
- height: 200px; overflow-y: auto;
499
- scrollbar-width: thin; scrollbar-color: var(--border2) transparent;
500
- }}
501
- .log-entry {{
502
- font-family: 'Space Mono', monospace;
503
- font-size: 0.62rem; line-height: 1.7;
504
- padding: 1px 0; border-bottom: 1px solid rgba(26,32,64,0.4);
505
- color: var(--muted);
506
- }}
507
- .log-entry.pos {{ color: var(--green); }}
508
- .log-entry.neg {{ color: var(--red); }}
509
- .log-entry.inf {{ color: var(--cyan); }}
510
- .log-entry.wrn {{ color: var(--amber); }}
511
-
512
- /* ── STATUS BAR ── */
513
- .status-bar {{
514
- grid-column: 1 / -1;
515
- display: flex; align-items: center; gap: 16px;
516
- padding: 6px 20px;
517
- background: rgba(11,15,30,0.95);
518
- border-top: 1px solid var(--border);
519
- font-family: 'Space Mono', monospace;
520
- font-size: 0.62rem; color: var(--muted);
521
- }}
522
- .sb-item {{ display: flex; align-items: center; gap: 6px; }}
523
- .sb-dot {{ width: 5px; height: 5px; border-radius: 50%; background: var(--muted); }}
524
- .sb-dot.ok {{ background: var(--green); box-shadow: 0 0 5px var(--green); }}
525
- .sb-dot.bad {{ background: var(--red); }}
526
-
527
- /* Spinner */
528
- .spinner {{
529
- display: inline-block; width: 12px; height: 12px;
530
- border: 2px solid rgba(255,255,255,0.2);
531
- border-top-color: white; border-radius: 50%;
532
- animation: spin .6s linear infinite;
533
- vertical-align: middle; margin-right: 5px;
534
- }}
535
- @keyframes spin {{ to {{ transform: rotate(360deg); }} }}
536
-
537
- /* DONE FLASH */
538
- @keyframes flash-border {{
539
- 0%,100% {{ border-color: var(--border2); }}
540
- 50% {{ border-color: var(--amber); box-shadow: 0 0 20px rgba(255,171,0,0.3); }}
541
- }}
542
- .episode-done {{ animation: flash-border 1.5s ease 2; }}
543
- </style>
544
- </head>
545
- <body>
546
-
547
- <!-- ── TOP BAR ── -->
548
- <header>
549
- <div class="logo">
550
- <div class="logo-icon">⚡</div>
551
- <div>
552
- <div class="logo-text">AETHER-TaskFlow</div>
553
- <div class="logo-sub">OPENENV · RL ENVIRONMENT · v1.0.0</div>
554
- </div>
555
- </div>
556
- <div class="live-indicator"><div class="live-dot"></div>LIVE</div>
557
- <div class="header-links">
558
- <a class="hlink" href="/docs">Swagger</a>
559
- <a class="hlink" href="/schema">Schema</a>
560
- <a class="hlink" href="/state">State</a>
561
- <a class="hlink" href="/health">Health</a>
562
- </div>
563
- </header>
564
-
565
- <!-- ── WORKSPACE ── -->
566
- <div class="workspace">
567
-
568
- <!-- COMMAND BAR -->
569
- <div class="cmd-bar">
570
- <span class="cmd-label">Difficulty</span>
571
- <select class="diff-select" id="difficulty">
572
- <option value="easy" {"selected" if difficulty=="easy" else ""}>🟢 EASY — 5 tasks, stable</option>
573
- <option value="medium" {"selected" if difficulty=="medium" else ""}>🟡 MEDIUM — 8 tasks, dynamic</option>
574
- <option value="hard" {"selected" if difficulty=="hard" else ""}>🔴 HARD — 12 tasks, scarce</option>
575
- </select>
576
- <button class="cmd-btn btn-reset" id="reset-btn" onclick="resetEnv()">
577
- <span id="reset-spinner" style="display:none" class="spinner"></span>⟳ RESET
578
- </button>
579
- <button class="cmd-btn btn-step" id="step-btn" onclick="stepEnv()" disabled>▶ STEP</button>
580
- <button class="cmd-btn btn-auto" id="auto-btn" onclick="toggleAuto()" disabled>⏩ AUTO</button>
581
- <span class="status-pill" id="status-pill">STANDBY</span>
582
- </div>
583
-
584
- <!-- LEFT: Task Queue -->
585
- <div class="panel-left">
586
- <div class="panel-title">
587
- <span>📋 TASK QUEUE</span>
588
- <span id="task-badge" style="font-family:'Space Mono',monospace;font-size:0.65rem;color:var(--accent);">0 tasks</span>
589
- </div>
590
- <div class="task-scroll" id="task-list">
591
- <div class="task-empty">
592
- Reset the environment<br/>to load tasks
593
- </div>
594
- </div>
595
- </div>
596
-
597
- <!-- CENTRE: Main visualisation -->
598
- <div class="panel-centre">
599
-
600
- <!-- Resource meters -->
601
- <div class="resource-row">
602
- <div class="res-cell" id="rc-time">
603
- <div class="res-label">⏱ Time Remaining</div>
604
- <div class="res-value" id="res-time">—</div>
605
- </div>
606
- <div class="res-cell" id="rc-energy">
607
- <div class="res-label">⚡ Energy</div>
608
- <div class="res-value" id="res-energy">—</div>
609
- </div>
610
- <div class="res-cell" id="rc-budget">
611
- <div class="res-label">💰 Budget</div>
612
- <div class="res-value" id="res-budget">—</div>
613
- </div>
614
- <div class="res-cell" id="rc-value">
615
- <div class="res-label">🏆 Cumulative Value</div>
616
- <div class="res-value" id="res-value">—</div>
617
- </div>
618
- </div>
619
-
620
- <!-- Action picker -->
621
- <div class="action-panel">
622
- <div class="chart-title">SELECT ACTION</div>
623
- <div class="action-grid">
624
- <div class="act-btn active" onclick="selectAction('execute')" id="act-execute">
625
- <span class="act-icon">⚡</span>
626
- <div class="act-name">EXECUTE</div>
627
- <div class="act-sub">Full reward</div>
628
- </div>
629
- <div class="act-btn" onclick="selectAction('optimize')" id="act-optimize">
630
- <span class="act-icon">🔧</span>
631
- <div class="act-name">OPTIMIZE</div>
632
- <div class="act-sub">Reduce risk</div>
633
- </div>
634
- <div class="act-btn" onclick="selectAction('delegate')" id="act-delegate">
635
- <span class="act-icon">📤</span>
636
- <div class="act-name">DELEGATE</div>
637
- <div class="act-sub">35% reward</div>
638
- </div>
639
- <div class="act-btn" onclick="selectAction('defer')" id="act-defer">
640
- <span class="act-icon">⏳</span>
641
- <div class="act-name">DEFER</div>
642
- <div class="act-sub">Small penalty</div>
643
- </div>
644
- </div>
645
- </div>
646
-
647
- <!-- Outcome readout -->
648
- <div class="outcome-bar" id="outcome-bar">
649
- <span>›</span> Waiting for first action...
650
- </div>
651
-
652
- <!-- Reward history chart -->
653
- <div class="chart-area">
654
- <div class="chart-title">REWARD HISTORY — per step</div>
655
- <div class="reward-chart" id="reward-chart">
656
- <div style="color:var(--muted);font-size:0.72rem;width:100%;text-align:center;padding-bottom:10px;">No data yet</div>
657
- </div>
658
- </div>
659
- </div>
660
-
661
- <!-- RIGHT: Telemetry -->
662
- <div class="panel-right">
663
-
664
- <!-- Score ring -->
665
- <div class="score-hero" id="score-hero">
666
- <div class="score-ring">
667
- <svg viewBox="0 0 100 100" width="110" height="110">
668
- <circle class="ring-bg" cx="50" cy="50" r="45"/>
669
- <circle class="ring-fill" id="ring-fill" cx="50" cy="50" r="45"/>
670
- </svg>
671
- <div class="score-num" id="score-num">—</div>
672
- </div>
673
- <div class="score-label">Episode Score</div>
674
- </div>
675
-
676
- <!-- Stats -->
677
- <div class="stat-grid">
678
- <div class="stat-box cyan">
679
- <div class="sn" id="stat-step">0</div>
680
- <div class="sl">Steps</div>
681
- </div>
682
- <div class="stat-box amber">
683
- <div class="sn" id="stat-reward">0.00</div>
684
- <div class="sl">Last Reward</div>
685
- </div>
686
- <div class="stat-box green">
687
- <div class="sn" id="stat-done">0</div>
688
- <div class="sl">Completed</div>
689
- </div>
690
- <div class="stat-box red">
691
- <div class="sn" id="stat-fail">0</div>
692
- <div class="sl">Failed</div>
693
- </div>
694
- </div>
695
-
696
- <!-- Health -->
697
- <div class="health-section">
698
- <div class="health-label">
699
- <span>SYSTEM HEALTH</span>
700
- <span id="health-pct" style="color:var(--green);font-family:'Space Mono',monospace;">—</span>
701
- </div>
702
- <div class="health-track">
703
- <div class="health-fill" id="health-fill" style="width:0%"></div>
704
- </div>
705
- </div>
706
-
707
- <!-- Log -->
708
- <div class="log-panel">
709
- <div class="log-title">ACTION LOG</div>
710
- <div class="log-scroll" id="log">
711
- <div class="log-entry inf">› System online. Select difficulty and press RESET.</div>
712
- </div>
713
- </div>
714
- </div>
715
-
716
- <!-- STATUS BAR -->
717
- <div class="status-bar">
718
- <div class="sb-item"><div class="sb-dot ok" id="sb-api"></div>API ONLINE</div>
719
- <div class="sb-item"><div class="sb-dot" id="sb-ep"></div><span id="sb-ep-text">NO EPISODE</span></div>
720
- <div class="sb-item" style="margin-left:auto;">
721
- <span>Meta PyTorch OpenEnv Hackathon 2025</span>
722
- </div>
723
- <div class="sb-item">
724
- <a href="/docs" style="color:var(--muted);text-decoration:none;">OpenAPI ↗</a>
725
- </div>
726
- </div>
727
- </div>
728
-
729
- <script>
730
- // ── STATE ──
731
- let selectedTask = null;
732
- let selectedAction = 'execute';
733
- let episodeDone = false;
734
- let autoTimer = null;
735
- let rewardHistory = [];
736
- let stepCount = 0;
737
-
738
- // ── HELPERS ──
739
- function log(msg, cls='') {{
740
- const container = document.getElementById('log');
741
- const el = document.createElement('div');
742
- el.className = 'log-entry ' + cls;
743
- el.textContent = new Date().toLocaleTimeString('en-GB', {{hour:'2-digit',minute:'2-digit',second:'2-digit'}}) + ' › ' + msg;
744
- container.appendChild(el);
745
- container.scrollTop = container.scrollHeight;
746
- }}
747
-
748
- function setStatusPill(text, cls='') {{
749
- const p = document.getElementById('status-pill');
750
- p.textContent = text;
751
- p.className = 'status-pill ' + cls;
752
- }}
753
-
754
- function selectAction(a) {{
755
- selectedAction = a;
756
- ['execute','optimize','delegate','defer'].forEach(x => {{
757
- document.getElementById('act-' + x).classList.toggle('active', x === a);
758
- }});
759
- }}
760
-
761
- function selectTask(id) {{
762
- selectedTask = id;
763
- document.querySelectorAll('.task-card').forEach(c => {{
764
- c.classList.toggle('selected', parseInt(c.dataset.id) === id);
765
- }});
766
- }}
767
-
768
- // ── RESOURCES ──
769
- function updateResources(obs) {{
770
- const time = obs.time_remaining ?? 0;
771
- const energy = obs.energy_remaining ?? 0;
772
- const budget = obs.budget_remaining ?? 0;
773
- const value = obs.cumulative_value ?? 0;
774
- const health = obs.system_health ?? 1;
775
-
776
- document.getElementById('res-time').textContent = time;
777
- document.getElementById('res-energy').textContent = energy.toFixed(1);
778
- document.getElementById('res-budget').textContent = budget.toFixed(1);
779
- document.getElementById('res-value').textContent = value.toFixed(1);
780
-
781
- // fill bars — approximate max from difficulty
782
- const maxEnergy = 12; const maxBudget = 60;
783
- setResFill('rc-time', Math.min(time/10,1) * 100, time < 3 ? 'var(--red)' : time < 6 ? 'var(--amber)' : 'var(--green)');
784
- setResFill('rc-energy', Math.min(energy/maxEnergy,1) * 100, energy < 2 ? 'var(--red)' : energy < 5 ? 'var(--amber)' : 'var(--cyan)');
785
- setResFill('rc-budget', Math.min(budget/maxBudget,1) * 100, budget < 5 ? 'var(--red)' : budget < 15 ? 'var(--amber)' : 'var(--accent2)');
786
- setResFill('rc-value', Math.min(value/50,1) * 100, 'var(--amber)');
787
-
788
- // health bar
789
- const h = Math.max(0, Math.min(1, health));
790
- const hc = h > 0.6 ? 'var(--green)' : h > 0.3 ? 'var(--amber)' : 'var(--red)';
791
- document.getElementById('health-fill').style.width = (h * 100) + '%';
792
- document.getElementById('health-fill').style.background = hc;
793
- document.getElementById('health-pct').textContent = (h * 100).toFixed(0) + '%';
794
- document.getElementById('health-pct').style.color = hc;
795
-
796
- // stats
797
- document.getElementById('stat-step').textContent = obs.step_number ?? stepCount;
798
- document.getElementById('stat-done').textContent = obs.tasks_completed ?? 0;
799
- document.getElementById('stat-fail').textContent = obs.tasks_failed ?? 0;
800
-
801
- // outcome
802
- if (obs.last_action_outcome) {{
803
- document.getElementById('outcome-bar').innerHTML =
804
- '<span>›</span> ' + obs.last_action_outcome;
805
- }}
806
- }}
807
-
808
- function setResFill(id, pct, color) {{
809
- const el = document.getElementById(id);
810
- el.style.setProperty('--fill', pct + '%');
811
- el.style.setProperty('--fill-color', color);
812
- }}
813
-
814
- // ── SCORE RING ──
815
- function updateScore(score) {{
816
- const n = typeof score === 'number' ? score : parseFloat(score);
817
- if (isNaN(n)) return;
818
- const circumference = 283;
819
- const offset = circumference * (1 - Math.max(0, Math.min(1, n)));
820
- document.getElementById('ring-fill').style.strokeDashoffset = offset;
821
- const color = n >= 0.7 ? 'var(--green)' : n >= 0.4 ? 'var(--amber)' : 'var(--red)';
822
- document.getElementById('ring-fill').style.stroke = color;
823
- document.getElementById('score-num').textContent = n.toFixed(2);
824
- document.getElementById('score-num').style.color = color;
825
- }}
826
-
827
- // ── REWARD CHART ──
828
- function pushReward(reward) {{
829
- rewardHistory.push(reward);
830
- if (rewardHistory.length > 30) rewardHistory.shift();
831
- renderChart();
832
- }}
833
-
834
- function renderChart() {{
835
- const chart = document.getElementById('reward-chart');
836
- if (rewardHistory.length === 0) {{
837
- chart.innerHTML = '<div style="color:var(--muted);font-size:0.72rem;width:100%;text-align:center;padding-bottom:10px;">No data yet</div>';
838
- return;
839
- }}
840
- const max = Math.max(...rewardHistory.map(Math.abs), 0.01);
841
- chart.innerHTML = '';
842
- rewardHistory.forEach((r, i) => {{
843
- const bar = document.createElement('div');
844
- bar.className = 'reward-bar';
845
- bar.dataset.val = r.toFixed(2);
846
- const hPct = (Math.abs(r) / max) * 90 + 5;
847
- bar.style.height = hPct + '%';
848
- bar.style.background = r >= 0.5 ? 'var(--green)' : r >= 0.3 ? 'var(--cyan)' : r >= 0 ? 'var(--amber)' : 'var(--red)';
849
- bar.style.opacity = 0.5 + (i / rewardHistory.length) * 0.5;
850
- chart.appendChild(bar);
851
- }});
852
- }}
853
-
854
- // ── TASK CARDS ──
855
- function renderTasks(tasks) {{
856
- const list = document.getElementById('task-list');
857
- document.getElementById('task-badge').textContent = (tasks?.length ?? 0) + ' tasks';
858
-
859
- if (!tasks || tasks.length === 0) {{
860
- list.innerHTML = '<div class="task-empty">All tasks resolved</div>';
861
- return;
862
- }}
863
-
864
- list.innerHTML = '';
865
- tasks.forEach(t => {{
866
- const urgency = t.deadline <= 2;
867
- const medUrgent = t.deadline <= 4;
868
- const card = document.createElement('div');
869
- card.className = 'task-card' + (urgency ? ' urgent' : '') + (selectedTask === t.task_id ? ' selected' : '');
870
- card.dataset.id = t.task_id;
871
- card.onclick = () => selectTask(t.task_id);
872
-
873
- const priCls = t.priority > 0.7 ? 'hi' : t.priority > 0.4 ? 'mid' : 'lo';
874
- const dlCls = urgency ? 'lo' : medUrgent ? 'mid' : 'hi';
875
- const riskCls = t.uncertainty > 0.6 ? 'lo' : t.uncertainty > 0.3 ? 'mid' : 'hi';
876
-
877
- card.innerHTML = `
878
- <div class="task-name">[#${{t.task_id}}] ${{t.name}}</div>
879
- <div class="task-chips">
880
- <span class="chip ${{priCls}}">PRI ${{(t.priority*100).toFixed(0)}}%</span>
881
- <span class="chip ${{dlCls}}">⏰ ${{t.deadline}}s</span>
882
- <span class="chip ${{riskCls}}">RISK ${{(t.uncertainty*100).toFixed(0)}}%</span>
883
- <span class="chip val">${{t.value.toFixed(1)}}v</span>
884
- <span class="chip">E${{t.required_energy.toFixed(1)}}</span>
885
- <span class="chip">B${{t.required_budget.toFixed(1)}}</span>
886
- </div>`;
887
- list.appendChild(card);
888
- }});
889
-
890
- if (selectedTask === null && tasks.length > 0) selectTask(tasks[0].task_id);
891
- }}
892
-
893
- // ── RESET ──
894
- async function resetEnv() {{
895
- stopAuto();
896
- episodeDone = false;
897
- rewardHistory = [];
898
- stepCount = 0;
899
- selectedTask = null;
900
- renderChart();
901
-
902
- document.getElementById('reset-spinner').style.display = 'inline-block';
903
- document.getElementById('score-num').textContent = '—';
904
- document.getElementById('score-num').style.color = 'var(--text)';
905
- document.getElementById('ring-fill').style.strokeDashoffset = 283;
906
- document.getElementById('ring-fill').style.stroke = 'var(--accent)';
907
- document.getElementById('stat-reward').textContent = '0.00';
908
-
909
- setStatusPill('RESETTING…');
910
- const diff = document.getElementById('difficulty').value;
911
-
912
- try {{
913
- const r = await fetch('/reset', {{
914
- method: 'POST',
915
- headers: {{'Content-Type': 'application/json'}},
916
- body: JSON.stringify({{difficulty: diff}})
917
- }});
918
- const data = await r.json();
919
- const obs = data.observation || data;
920
-
921
- renderTasks(obs.tasks || []);
922
- updateResources(obs);
923
-
924
- document.getElementById('step-btn').disabled = false;
925
- document.getElementById('auto-btn').disabled = false;
926
- document.getElementById('sb-ep').classList.add('ok');
927
- document.getElementById('sb-ep-text').textContent = 'EPISODE ACTIVE · ' + diff.toUpperCase();
928
- setStatusPill('READY — ' + diff.toUpperCase(), 'ready');
929
- log('Episode reset · ' + diff + ' · ' + (obs.tasks||[]).length + ' tasks loaded', 'inf');
930
- }} catch(e) {{
931
- log('Reset failed: ' + e.message, 'wrn');
932
- setStatusPill('ERROR');
933
- }}
934
- document.getElementById('reset-spinner').style.display = 'none';
935
- }}
936
-
937
- // ── STEP ──
938
- async function stepEnv() {{
939
- if (episodeDone) return;
940
- const tid = selectedTask ?? 0;
941
-
942
- try {{
943
- const r = await fetch('/step', {{
944
- method: 'POST',
945
- headers: {{'Content-Type': 'application/json'}},
946
- body: JSON.stringify({{action_type: selectedAction, task_id: tid, reasoning: 'UI step'}})
947
- }});
948
- const data = await r.json();
949
- const obs = data.observation || data;
950
- const reward = data.reward ?? obs.reward ?? 0;
951
- const done = data.done ?? obs.done ?? false;
952
-
953
- stepCount++;
954
- pushReward(reward);
955
- renderTasks(obs.tasks || []);
956
- updateResources(obs);
957
-
958
- document.getElementById('stat-reward').textContent = reward.toFixed(3);
959
- document.getElementById('stat-reward').style.color = reward >= 0.5 ? 'var(--green)' : reward >= 0 ? 'var(--amber)' : 'var(--red)';
960
-
961
- const cls = reward >= 0.5 ? 'pos' : reward >= 0 ? 'wrn' : 'neg';
962
- log(selectedAction + '(task=' + tid + ') → reward=' + reward.toFixed(3) + (done ? ' [DONE]' : ''), cls);
963
-
964
- if (done) {{
965
- episodeDone = true;
966
- stopAuto();
967
- document.getElementById('step-btn').disabled = true;
968
- document.getElementById('auto-btn').disabled = true;
969
- document.getElementById('sb-ep-text').textContent = 'EPISODE COMPLETE';
970
- document.getElementById('sb-ep').classList.remove('ok');
971
- setStatusPill('EPISODE COMPLETE', 'done');
972
- document.getElementById('score-hero').classList.add('episode-done');
973
- await fetchFinalScore();
974
- }}
975
- }} catch(e) {{
976
- log('Step error: ' + e.message, 'wrn');
977
- }}
978
- }}
979
-
980
- // ── FETCH SCORE ──
981
- async function fetchFinalScore() {{
982
- try {{
983
- const r = await fetch('/state');
984
- const state = await r.json();
985
- const completed = state.tasks_completed ?? 0;
986
- const failed = state.tasks_failed ?? 0;
987
- const total = completed + failed + (state.tasks?.length ?? 0);
988
- const eff = total > 0 ? completed / total : 0;
989
- updateScore(eff);
990
- log('Final — completed ' + completed + '/' + total + ' · health ' + ((state.system_health??1)*100).toFixed(0) + '%', 'inf');
991
- }} catch(e) {{}}
992
- }}
993
-
994
- // ── AUTO PLAY ──
995
- function toggleAuto() {{
996
- if (autoTimer) {{ stopAuto(); return; }}
997
- const btn = document.getElementById('auto-btn');
998
- btn.textContent = '⏹ STOP';
999
- btn.className = 'cmd-btn btn-stop';
1000
- autoTimer = setInterval(async () => {{
1001
- if (episodeDone) {{ stopAuto(); return; }}
1002
- await stepEnv();
1003
- }}, 700);
1004
- }}
1005
-
1006
- function stopAuto() {{
1007
- clearInterval(autoTimer);
1008
- autoTimer = null;
1009
- const btn = document.getElementById('auto-btn');
1010
- btn.textContent = '⏩ AUTO';
1011
- btn.className = 'cmd-btn btn-auto';
1012
- }}
1013
- </script>
1014
- </body>
1015
- </html>"""
1016
- return HTMLResponse(content=html)
1017
-
1018
-
1019
- # ---------------------------------------------------------------------------
1020
- # API Routes
1021
- # ---------------------------------------------------------------------------
1022
-
1023
- @app.post("/reset", response_model=ResetResponse, tags=["Environment Control"], summary="Reset the environment")
1024
- async def reset(request: ResetRequest = Body(default_factory=ResetRequest)) -> ResetResponse:
1025
- response = await _persistent_manager.reset_environment(request.model_dump(exclude_unset=True))
1026
- return ResetResponse(**response)
1027
-
1028
-
1029
- @app.post("/step", response_model=StepResponse, tags=["Environment Control"], summary="Execute an action")
1030
- async def step(payload: Dict[str, Any] = Body(default_factory=dict)) -> StepResponse:
1031
- try:
1032
- action_payload = _extract_action_payload(payload)
1033
- response = await _persistent_manager.step_environment(action_payload)
1034
- except ValidationError as exc:
1035
- raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=exc.errors()) from exc
1036
- return StepResponse(**response)
1037
-
1038
-
1039
- @app.get("/state", response_model=AetherTaskFlowState, tags=["State Management"], summary="Get current state")
1040
- async def get_state() -> AetherTaskFlowState:
1041
- return _persistent_manager.env.state
1042
-
1043
-
1044
- @app.get("/schema", response_model=SchemaResponse, tags=["Schema"], summary="Get JSON schemas")
1045
- async def get_schemas() -> SchemaResponse:
1046
- return SchemaResponse(
1047
- action=AetherTaskFlowAction.model_json_schema(),
1048
- observation=AetherTaskFlowObservation.model_json_schema(),
1049
- state=AetherTaskFlowState.model_json_schema(),
1050
- )
1051
-
1052
-
1053
-
1054
- def main(host: str = "0.0.0.0", port: int = 7860) -> None:
1055
- import uvicorn
1056
- uvicorn.run(app, host=host, port=port)
1057
-
1058
-
1059
- if __name__ == "__main__":
1060
- import argparse
1061
- parser = argparse.ArgumentParser(description="AETHER-TaskFlow server")
1062
- parser.add_argument("--host", default="0.0.0.0")
1063
- parser.add_argument("--port", type=int, default=7860)
1064
- parser.add_argument("--difficulty", choices=["easy", "medium", "hard"], default=_DIFFICULTY)
1065
- args = parser.parse_args()
1066
- if args.difficulty != _DIFFICULTY:
1067
- os.environ["AETHER_DIFFICULTY"] = args.difficulty
1068
- main(host=args.host, port=args.port)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/tests/__init__.py DELETED
File without changes
aether-taskflow/tests/__pycache__/__init__.cpython-312.pyc DELETED
Binary file (149 Bytes)
 
aether-taskflow/tests/__pycache__/test_aether_taskflow.cpython-312-pytest-9.0.3.pyc DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:f1098ee062416b1c3a5ed3de5631bd01436302c4a2885a9e4236996097f6f5d9
3
- size 107270
 
 
 
 
aether-taskflow/tests/test_aether_taskflow.py DELETED
@@ -1,587 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import sys
4
- from pathlib import Path
5
-
6
- sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
7
-
8
- import pytest
9
- from fastapi.testclient import TestClient
10
-
11
- from models import AetherTaskFlowAction, AetherTaskFlowObservation, AetherTaskFlowState, ActionType
12
- from env.tasks import generate_tasks, get_profile
13
- from env.algorithms import AETHER, RAPTOR, AWFROX
14
- from env.grader import grade, grade_easy, grade_medium, grade_hard
15
- from env.aether_env import AetherTaskFlowEnvironment
16
- from server.app import app
17
-
18
-
19
- @pytest.fixture
20
- def easy_env():
21
- return AetherTaskFlowEnvironment(difficulty="easy")
22
-
23
-
24
- @pytest.fixture
25
- def medium_env():
26
- return AetherTaskFlowEnvironment(difficulty="medium")
27
-
28
-
29
- @pytest.fixture
30
- def hard_env():
31
- return AetherTaskFlowEnvironment(difficulty="hard")
32
-
33
-
34
- @pytest.fixture
35
- def resources():
36
- return {"energy": 10.0, "budget": 50.0, "time": 10.0}
37
-
38
-
39
- @pytest.fixture
40
- def api_client():
41
- return TestClient(app)
42
-
43
-
44
-
45
- class TestTaskGeneration:
46
- def test_easy_task_count(self):
47
- tasks = generate_tasks("easy", seed=42)
48
- assert len(tasks) == 5
49
-
50
- def test_medium_task_count(self):
51
- tasks = generate_tasks("medium", seed=42)
52
- assert len(tasks) == 8
53
-
54
- def test_hard_task_count(self):
55
- tasks = generate_tasks("hard", seed=42)
56
- assert len(tasks) == 12
57
-
58
- def test_tasks_have_required_fields(self):
59
- tasks = generate_tasks("easy", seed=1)
60
- for t in tasks:
61
- d = t.to_dict()
62
- assert "task_id" in d
63
- assert "priority" in d
64
- assert "deadline" in d
65
- assert "uncertainty" in d
66
- assert "value" in d
67
- assert "required_energy" in d
68
- assert "required_budget" in d
69
- assert "category" in d
70
- assert "status" in d
71
-
72
- def test_priority_range(self):
73
- tasks = generate_tasks("easy", seed=7)
74
- for t in tasks:
75
- assert 0.0 <= t.priority <= 1.0
76
-
77
- def test_uncertainty_range(self):
78
- tasks = generate_tasks("hard", seed=7)
79
- for t in tasks:
80
- assert 0.0 <= t.uncertainty <= 1.0
81
-
82
- def test_reproducibility(self):
83
- a = generate_tasks("medium", seed=99)
84
- b = generate_tasks("medium", seed=99)
85
- assert [t.task_id for t in a] == [t.task_id for t in b]
86
- assert [round(t.priority, 5) for t in a] == [round(t.priority, 5) for t in b]
87
-
88
- def test_different_seeds_differ(self):
89
- a = generate_tasks("easy", seed=1)
90
- b = generate_tasks("easy", seed=2)
91
- # At least one task should differ
92
- priorities_a = [t.priority for t in a]
93
- priorities_b = [t.priority for t in b]
94
- assert priorities_a != priorities_b
95
-
96
-
97
-
98
- class TestAETHER:
99
- def test_score_returns_float(self, resources):
100
- aether = AETHER()
101
- task = {
102
- "task_id": 0, "priority": 0.8, "deadline": 3,
103
- "uncertainty": 0.2, "value": 15.0,
104
- "required_energy": 1.0, "required_budget": 5.0,
105
- }
106
- score = aether.score(task, resources, step=0, max_steps=10)
107
- assert isinstance(score, float)
108
-
109
- def test_higher_priority_scores_higher(self, resources):
110
- aether = AETHER()
111
- low = {"task_id": 0, "priority": 0.2, "deadline": 5, "uncertainty": 0.1, "value": 10.0, "required_energy": 1.0, "required_budget": 5.0}
112
- high = {"task_id": 1, "priority": 0.9, "deadline": 5, "uncertainty": 0.1, "value": 10.0, "required_energy": 1.0, "required_budget": 5.0}
113
- assert aether.score(high, resources, 0, 10) > aether.score(low, resources, 0, 10)
114
-
115
- def test_high_uncertainty_penalised(self, resources):
116
- aether = AETHER()
117
- base = {"task_id": 0, "priority": 0.7, "deadline": 4, "value": 10.0, "required_energy": 1.0, "required_budget": 5.0}
118
- low_unc = {**base, "uncertainty": 0.1}
119
- high_unc = {**base, "uncertainty": 0.9}
120
- assert aether.score(low_unc, resources, 0, 10) > aether.score(high_unc, resources, 0, 10)
121
-
122
- def test_rank_tasks_sorted_descending(self, resources):
123
- aether = AETHER()
124
- tasks = [
125
- {"task_id": i, "priority": 0.3 + i * 0.2, "deadline": 5,
126
- "uncertainty": 0.1, "value": 10.0, "required_energy": 1.0, "required_budget": 5.0}
127
- for i in range(4)
128
- ]
129
- ranked = aether.rank_tasks(tasks, resources, 0, 10)
130
- scores = [s for _, s in ranked]
131
- assert scores == sorted(scores, reverse=True)
132
-
133
- def test_update_modifies_weights(self):
134
- aether = AETHER()
135
- weights_before = dict(aether.weights)
136
- aether.update(5.0)
137
- aether.update(5.0)
138
- assert aether.weights != weights_before
139
-
140
- def test_weights_stay_in_range(self):
141
- aether = AETHER()
142
- for _ in range(50):
143
- aether.update(10.0)
144
- assert aether.weights["priority"] >= 0.5
145
- assert aether.weights["uncertainty_penalty"] <= -0.1
146
-
147
-
148
- class TestRAPTOR:
149
- def test_defers_on_low_energy(self):
150
- raptor = RAPTOR()
151
- task = {"priority": 0.8, "deadline": 3, "uncertainty": 0.2, "value": 15.0,
152
- "required_energy": 5.0, "required_budget": 5.0}
153
- resources = {"energy": 1.0, "budget": 50.0} # energy too low
154
- action = raptor.decide(task, resources, step=0, max_steps=10)
155
- assert action in ("defer", "delegate")
156
-
157
- def test_optimizes_high_uncertainty(self):
158
- raptor = RAPTOR()
159
- task = {"priority": 0.8, "deadline": 4, "uncertainty": 0.9, "value": 15.0,
160
- "required_energy": 1.0, "required_budget": 5.0}
161
- resources = {"energy": 10.0, "budget": 50.0}
162
- action = raptor.decide(task, resources, step=0, max_steps=10)
163
- assert action == "optimize"
164
-
165
- def test_executes_with_good_resources(self):
166
- raptor = RAPTOR()
167
- task = {"priority": 0.8, "deadline": 3, "uncertainty": 0.1, "value": 15.0,
168
- "required_energy": 1.0, "required_budget": 5.0}
169
- resources = {"energy": 10.0, "budget": 50.0}
170
- action = raptor.decide(task, resources, step=0, max_steps=10)
171
- assert action == "execute"
172
-
173
- def test_all_return_valid_action(self):
174
- raptor = RAPTOR()
175
- valid = {"execute", "defer", "delegate", "optimize"}
176
- for seed in range(20):
177
- import random
178
- rng = random.Random(seed)
179
- task = {"priority": rng.random(), "deadline": rng.randint(0, 8),
180
- "uncertainty": rng.random(), "value": rng.uniform(3, 30),
181
- "required_energy": rng.uniform(0.5, 4), "required_budget": rng.uniform(1, 20)}
182
- resources = {"energy": rng.uniform(0, 12), "budget": rng.uniform(0, 60)}
183
- action = raptor.decide(task, resources, rng.randint(0, 9), 10)
184
- assert action in valid
185
-
186
-
187
- class TestAWFROX:
188
- def test_removes_expired_tasks(self):
189
- recycler = AWFROX()
190
- tasks = [
191
- {"task_id": 0, "deadline": -1, "status": "pending"},
192
- {"task_id": 1, "deadline": 3, "status": "pending"},
193
- ]
194
- viable = recycler.filter_viable(tasks, {}, step=0, max_steps=10)
195
- assert len(viable) == 1
196
- assert viable[0]["task_id"] == 1
197
-
198
- def test_recycles_deferred_when_resources_available(self):
199
- recycler = AWFROX()
200
- active = []
201
- deferred = [{"task_id": 5, "status": "deferred", "deadline": 3,
202
- "required_energy": 1.0, "required_budget": 5.0}]
203
- resources = {"energy": 10.0, "budget": 50.0}
204
- new_active, new_deferred = recycler.recycle_deferred(active, deferred, resources, step=2)
205
- assert len(new_active) == 1
206
- assert new_active[0]["status"] == "pending"
207
- assert len(new_deferred) == 0
208
-
209
- def test_keeps_deferred_when_resources_insufficient(self):
210
- recycler = AWFROX()
211
- active = []
212
- deferred = [{"task_id": 5, "status": "deferred", "deadline": 3,
213
- "required_energy": 10.0, "required_budget": 50.0}]
214
- resources = {"energy": 0.5, "budget": 1.0} # insufficient
215
- new_active, new_deferred = recycler.recycle_deferred(active, deferred, resources, step=2)
216
- assert len(new_active) == 0
217
- assert len(new_deferred) == 1
218
-
219
-
220
-
221
- class TestGrader:
222
- def _result(self, **kwargs):
223
- base = {
224
- "tasks_completed": 4, "tasks_failed": 1, "total_tasks": 5,
225
- "remaining_time": 3.0, "remaining_energy": 5.0, "remaining_budget": 30.0,
226
- "initial_time": 10.0, "initial_energy": 12.0, "initial_budget": 60.0,
227
- "system_health": 0.9, "steps_used": 7, "max_steps": 10,
228
- }
229
- base.update(kwargs)
230
- return base
231
-
232
- def test_score_in_range(self):
233
- for diff in ["easy", "medium", "hard"]:
234
- s = grade(diff, self._result())
235
- assert 0.0 <= s <= 1.0, f"{diff}: {s}"
236
-
237
- def test_perfect_score_near_one(self):
238
- perfect = {
239
- "tasks_completed": 10, "tasks_failed": 0, "total_tasks": 10,
240
- "remaining_time": 5.0, "remaining_energy": 8.0, "remaining_budget": 40.0,
241
- "initial_time": 10.0, "initial_energy": 12.0, "initial_budget": 60.0,
242
- "system_health": 1.0, "steps_used": 5, "max_steps": 10,
243
- }
244
- for diff in ["easy", "medium", "hard"]:
245
- s = grade(diff, perfect)
246
- assert s >= 0.6, f"{diff}: {s}"
247
-
248
- def test_zero_score_on_all_failed(self):
249
- worst = {
250
- "tasks_completed": 0, "tasks_failed": 10, "total_tasks": 10,
251
- "remaining_time": 0.0, "remaining_energy": 0.0, "remaining_budget": 0.0,
252
- "initial_time": 10.0, "initial_energy": 12.0, "initial_budget": 60.0,
253
- "system_health": 0.0, "steps_used": 10, "max_steps": 10,
254
- }
255
- for diff in ["easy", "medium", "hard"]:
256
- s = grade(diff, worst)
257
- assert s == 0.0, f"{diff}: {s}"
258
-
259
- def test_deterministic(self):
260
- result = self._result()
261
- s1 = grade("medium", result)
262
- s2 = grade("medium", result)
263
- assert s1 == s2
264
-
265
- def test_hard_collapse_penalty(self):
266
- collapsed = self._result(system_health=0.1)
267
- normal = self._result(system_health=0.8)
268
- assert grade("hard", collapsed) < grade("hard", normal)
269
-
270
-
271
-
272
- class TestEnvironmentReset:
273
- def test_reset_returns_observation(self, easy_env):
274
- obs = easy_env.reset(seed=42)
275
- assert isinstance(obs, AetherTaskFlowObservation)
276
-
277
- def test_reset_provides_tasks(self, easy_env):
278
- obs = easy_env.reset(seed=42)
279
- assert len(obs.tasks) > 0
280
-
281
- def test_reset_has_full_resources(self, easy_env):
282
- obs = easy_env.reset(seed=42)
283
- assert obs.time_remaining == 10
284
- assert obs.energy_remaining > 0
285
- assert obs.budget_remaining > 0
286
-
287
- def test_reset_health_is_one(self, easy_env):
288
- obs = easy_env.reset(seed=42)
289
- assert obs.system_health == 1.0
290
-
291
- def test_reset_not_done(self, easy_env):
292
- obs = easy_env.reset(seed=42)
293
- assert obs.done is False
294
-
295
- def test_reset_is_reproducible(self, easy_env):
296
- obs1 = easy_env.reset(seed=7)
297
- obs2 = easy_env.reset(seed=7)
298
- assert len(obs1.tasks) == len(obs2.tasks)
299
- assert obs1.tasks[0]["task_id"] == obs2.tasks[0]["task_id"]
300
-
301
- def test_reset_names_are_reproducible(self, easy_env):
302
- obs1 = easy_env.reset(seed=42)
303
- obs2 = easy_env.reset(seed=42)
304
- assert [task["name"] for task in obs1.tasks] == [task["name"] for task in obs2.tasks]
305
-
306
- def test_reset_episode_id_provided(self, easy_env):
307
- obs = easy_env.reset(seed=1, episode_id="test-ep-001")
308
- assert obs.episode_id == "test-ep-001"
309
-
310
- def test_reset_generates_episode_id_if_missing(self, easy_env):
311
- obs = easy_env.reset(seed=1)
312
- assert obs.episode_id is not None
313
- assert len(obs.episode_id) > 0
314
-
315
-
316
- class TestEnvironmentStep:
317
- def test_execute_reduces_resources(self, easy_env):
318
- obs = easy_env.reset(seed=42)
319
- energy_before = obs.energy_remaining
320
- task = obs.tasks[0]
321
- action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=task["task_id"])
322
- obs2 = easy_env.step(action)
323
- assert obs2.energy_remaining <= energy_before
324
-
325
- def test_execute_valid_task_earns_positive_reward(self, easy_env):
326
- obs = easy_env.reset(seed=42)
327
- task = obs.tasks[0]
328
- action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=task["task_id"])
329
- obs2 = easy_env.step(action)
330
- # Positive action rewards are normalized into the upper half of [0, 1].
331
- assert obs2.reward is not None
332
- assert 0.5 < obs2.reward <= 1.0
333
-
334
- def test_delegate_earns_positive_reward(self, easy_env):
335
- obs = easy_env.reset(seed=42)
336
- task = obs.tasks[0]
337
- action = AetherTaskFlowAction(action_type=ActionType.DELEGATE, task_id=task["task_id"])
338
- obs2 = easy_env.step(action)
339
- assert obs2.reward is not None
340
- assert 0.5 < obs2.reward <= 1.0
341
-
342
- def test_defer_earns_negative_reward(self, easy_env):
343
- obs = easy_env.reset(seed=42)
344
- task = obs.tasks[0]
345
- action = AetherTaskFlowAction(action_type=ActionType.DEFER, task_id=task["task_id"])
346
- obs2 = easy_env.step(action)
347
- assert obs2.reward is not None
348
- assert 0.0 <= obs2.reward < 0.5
349
-
350
- def test_optimize_returns_small_positive(self, easy_env):
351
- obs = easy_env.reset(seed=42)
352
- task = obs.tasks[0]
353
- action = AetherTaskFlowAction(action_type=ActionType.OPTIMIZE, task_id=task["task_id"])
354
- obs2 = easy_env.step(action)
355
- assert 0.5 < obs2.reward < 0.55
356
-
357
- def test_invalid_task_id_penalised(self, easy_env):
358
- obs = easy_env.reset(seed=42)
359
- action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=9999)
360
- obs2 = easy_env.step(action)
361
- assert obs2.reward is not None
362
- assert 0.0 <= obs2.reward < 0.5
363
-
364
- def test_step_reward_is_normalized(self, easy_env):
365
- obs = easy_env.reset(seed=42)
366
- task = obs.tasks[0]
367
- action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=task["task_id"])
368
- obs2 = easy_env.step(action)
369
- assert 0.0 <= obs2.reward <= 1.0
370
-
371
- def test_step_accepts_string_action(self, easy_env):
372
- easy_env.reset(seed=42)
373
- obs = easy_env.step("execute")
374
- assert isinstance(obs, AetherTaskFlowObservation)
375
- assert obs.last_action_type in {"execute", "defer", "delegate", "optimize"}
376
-
377
- def test_step_safe_failure_returns_terminal_observation(self, easy_env):
378
- easy_env.reset(seed=42)
379
- obs = easy_env.step({"task_id": "not-an-int"})
380
- assert obs.done is True
381
- assert obs.reward == 0.0
382
- assert "failed safely" in (obs.last_action_outcome or "").lower()
383
-
384
- def test_last_action_feedback_populated(self, easy_env):
385
- obs = easy_env.reset(seed=42)
386
- task = obs.tasks[0]
387
- action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=task["task_id"])
388
- obs2 = easy_env.step(action)
389
- assert obs2.last_action_type == "execute"
390
- assert obs2.last_action_task_id == task["task_id"]
391
- assert obs2.last_action_outcome is not None
392
-
393
- def test_step_after_done_returns_done(self, easy_env):
394
- obs = easy_env.reset(seed=42)
395
- # Exhaust all tasks
396
- for _ in range(15):
397
- if obs.done:
398
- break
399
- tasks = obs.tasks
400
- if not tasks:
401
- break
402
- action = AetherTaskFlowAction(
403
- action_type=ActionType.EXECUTE,
404
- task_id=tasks[0]["task_id"]
405
- )
406
- obs = easy_env.step(action)
407
- # Extra step after done should return done
408
- if obs.done:
409
- action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=0)
410
- obs2 = easy_env.step(action)
411
- assert obs2.done is True
412
-
413
- def test_defer_is_visible_in_state(self, easy_env):
414
- obs = easy_env.reset(seed=42)
415
- task = obs.tasks[0]
416
- easy_env.state.resources["energy"] = 0.0
417
- easy_env.state.resources["budget"] = 0.0
418
- easy_env.step(
419
- AetherTaskFlowAction(action_type=ActionType.DEFER, task_id=task["task_id"])
420
- )
421
- assert len(easy_env.state.deferred_tasks) == 1
422
-
423
-
424
- class TestFullEpisode:
425
- def _run_episode(self, difficulty: str, seed: int = 42) -> dict:
426
- env = AetherTaskFlowEnvironment(difficulty=difficulty)
427
- obs = env.reset(seed=seed)
428
- rewards = []
429
- steps = 0
430
- while not obs.done and steps < 15:
431
- tasks = obs.tasks
432
- if not tasks:
433
- break
434
- task = tasks[0]
435
- action = AetherTaskFlowAction(
436
- action_type=ActionType.EXECUTE,
437
- task_id=task["task_id"],
438
- )
439
- obs = env.step(action)
440
- rewards.append(obs.reward or 0)
441
- steps += 1
442
- score = env.compute_final_score()
443
- return {"score": score, "steps": steps, "rewards": rewards}
444
-
445
- def test_easy_episode_completes(self):
446
- result = self._run_episode("easy")
447
- assert result["score"] >= 0.0
448
- assert result["steps"] > 0
449
-
450
- def test_medium_episode_completes(self):
451
- result = self._run_episode("medium")
452
- assert result["score"] >= 0.0
453
-
454
- def test_hard_episode_completes(self):
455
- result = self._run_episode("hard")
456
- assert result["score"] >= 0.0
457
-
458
- def test_score_in_range_all_difficulties(self):
459
- for diff in ["easy", "medium", "hard"]:
460
- result = self._run_episode(diff)
461
- assert 0.0 <= result["score"] <= 1.0, f"{diff}: {result['score']}"
462
-
463
- def test_rewards_in_range_all_difficulties(self):
464
- for diff in ["easy", "medium", "hard"]:
465
- result = self._run_episode(diff)
466
- assert all(0.0 <= reward <= 1.0 for reward in result["rewards"]), (
467
- f"{diff}: {result['rewards']}"
468
- )
469
-
470
- def test_easy_score_higher_than_hard(self):
471
- easy = self._run_episode("easy")
472
- hard = self._run_episode("hard")
473
- # Easy should generally score higher than hard with naive agent
474
- assert easy["score"] >= hard["score"]
475
-
476
-
477
- class TestStateProperty:
478
- def test_state_is_aether_state(self, easy_env):
479
- easy_env.reset(seed=42)
480
- state = easy_env.state
481
- assert isinstance(state, AetherTaskFlowState)
482
-
483
- def test_state_tracks_steps(self, easy_env):
484
- obs = easy_env.reset(seed=42)
485
- assert easy_env.state.step_count == 0
486
- task = obs.tasks[0]
487
- action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=task["task_id"])
488
- easy_env.step(action)
489
- assert easy_env.state.step_count == 1
490
-
491
- def test_state_tracks_completions(self, easy_env):
492
- obs = easy_env.reset(seed=42)
493
- assert easy_env.state.tasks_completed == 0
494
- task = obs.tasks[0]
495
- action = AetherTaskFlowAction(action_type=ActionType.DELEGATE, task_id=task["task_id"])
496
- easy_env.step(action)
497
- assert easy_env.state.tasks_completed == 1
498
-
499
- def test_state_difficulty_matches_env(self):
500
- for diff in ["easy", "medium", "hard"]:
501
- env = AetherTaskFlowEnvironment(difficulty=diff)
502
- env.reset(seed=1)
503
- assert env.state.difficulty == diff
504
-
505
- def test_debug_snapshot_is_readable(self, easy_env):
506
- easy_env.reset(seed=42)
507
- snapshot = easy_env._get_obs()
508
- assert snapshot["num_tasks"] > 0
509
- assert "resources" in snapshot
510
- assert "system_health" in snapshot
511
-
512
-
513
- class TestOpenEnvCompliance:
514
- def test_observation_is_pydantic_model(self, easy_env):
515
- obs = easy_env.reset(seed=1)
516
- assert hasattr(obs, "model_dump")
517
- d = obs.model_dump()
518
- assert isinstance(d, dict)
519
-
520
- def test_observation_has_done_field(self, easy_env):
521
- obs = easy_env.reset(seed=1)
522
- assert hasattr(obs, "done")
523
- assert isinstance(obs.done, bool)
524
-
525
- def test_observation_has_reward_field(self, easy_env):
526
- obs = easy_env.reset(seed=1)
527
- assert hasattr(obs, "reward")
528
-
529
- def test_state_has_episode_id(self, easy_env):
530
- easy_env.reset(seed=1, episode_id="abc-123")
531
- assert easy_env.state.episode_id == "abc-123"
532
-
533
- def test_state_has_step_count(self, easy_env):
534
- easy_env.reset(seed=1)
535
- assert hasattr(easy_env.state, "step_count")
536
-
537
- def test_invalid_difficulty_raises(self):
538
- with pytest.raises(ValueError):
539
- AetherTaskFlowEnvironment(difficulty="impossible")
540
-
541
- def test_action_coerces_freeform_action_type(self):
542
- action = AetherTaskFlowAction(action_type="hi", task_id=0)
543
- assert action.action_type == ActionType.EXECUTE
544
- assert action.task_id == 0
545
-
546
- def test_action_extracts_task_id_from_freeform_text(self):
547
- action = AetherTaskFlowAction(action_type="delegate task 3")
548
- assert action.action_type == ActionType.DELEGATE
549
- assert action.task_id == 3
550
-
551
- def test_action_accepts_message_payload_shape(self):
552
- action = AetherTaskFlowAction.model_validate({"message": "optimize 2"})
553
- assert action.action_type == ActionType.OPTIMIZE
554
- assert action.task_id == 2
555
-
556
-
557
- class TestPersistentServerRoutes:
558
- def test_reset_step_state_share_same_session(self, api_client):
559
- reset_response = api_client.post("/reset", json={"seed": 42})
560
- assert reset_response.status_code == 200
561
- reset_payload = reset_response.json()
562
- first_task_id = reset_payload["observation"]["tasks"][0]["task_id"]
563
- episode_id = reset_payload["observation"]["episode_id"]
564
-
565
- step_response = api_client.post(
566
- "/step",
567
- json={"action": {"action_type": "execute", "task_id": first_task_id}},
568
- )
569
- assert step_response.status_code == 200
570
-
571
- state_response = api_client.get("/state")
572
- assert state_response.status_code == 200
573
- state_payload = state_response.json()
574
- assert state_payload["episode_id"] == episode_id
575
- assert state_payload["step_count"] == 1
576
-
577
- def test_step_accepts_message_payload(self, api_client):
578
- api_client.post("/reset", json={"seed": 42})
579
- response = api_client.post("/step", json={"message": "execute"})
580
- assert response.status_code == 200
581
- payload = response.json()
582
- assert payload["observation"]["last_action_type"] in {
583
- "execute",
584
- "defer",
585
- "delegate",
586
- "optimize",
587
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aether-taskflow/uv.lock DELETED
The diff for this file is too large to render. See raw diff
 
aether-taskflow/validation-su DELETED
File without changes
aether-taskflow/validation-submission.sh DELETED
@@ -1,185 +0,0 @@
1
- #!/usr/bin/env bash
2
- #
3
- # validate-submission.sh — OpenEnv Submission Validator
4
- #
5
- # Checks that your HF Space is live, Docker image builds, and openenv validate passes.
6
- #
7
- # Prerequisites:
8
- # - Docker: https://docs.docker.com/get-docker/
9
- # - openenv-core: pip install openenv-core
10
- # - curl (usually pre-installed)
11
- #
12
- # Run:
13
- # curl -fsSL https://raw.githubusercontent.com/<owner>/<repo>/main/scripts/validate-submission.sh | bash -s -- <ping_url> [repo_dir]
14
- #
15
- # Or download and run locally:
16
- # chmod +x validate-submission.sh
17
- # ./validate-submission.sh <ping_url> [repo_dir]
18
- #
19
- # Arguments:
20
- # ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)
21
- # repo_dir Path to your repo (default: current directory)
22
- #
23
- # Examples:
24
- # ./validate-submission.sh https://my-team.hf.space
25
- # ./validate-submission.sh https://my-team.hf.space ./my-repo
26
- #
27
-
28
- set -uo pipefail
29
-
30
- DOCKER_BUILD_TIMEOUT=600
31
- if [ -t 1 ]; then
32
- RED='\033[0;31m'
33
- GREEN='\033[0;32m'
34
- YELLOW='\033[1;33m'
35
- BOLD='\033[1m'
36
- NC='\033[0m'
37
- else
38
- RED='' GREEN='' YELLOW='' BOLD='' NC=''
39
- fi
40
-
41
- run_with_timeout() {
42
- local secs="$1"; shift
43
- if command -v timeout &>/dev/null; then
44
- timeout "$secs" "$@"
45
- elif command -v gtimeout &>/dev/null; then
46
- gtimeout "$secs" "$@"
47
- else
48
- "$@" &
49
- local pid=$!
50
- ( sleep "$secs" && kill "$pid" 2>/dev/null ) &
51
- local watcher=$!
52
- wait "$pid" 2>/dev/null
53
- local rc=$?
54
- kill "$watcher" 2>/dev/null
55
- wait "$watcher" 2>/dev/null
56
- return $rc
57
- fi
58
- }
59
-
60
- portable_mktemp() {
61
- local prefix="${1:-validate}"
62
- mktemp "${TMPDIR:-/tmp}/${prefix}-XXXXXX" 2>/dev/null || mktemp
63
- }
64
-
65
- CLEANUP_FILES=()
66
- cleanup() { rm -f "${CLEANUP_FILES[@]+"${CLEANUP_FILES[@]}"}"; }
67
- trap cleanup EXIT
68
-
69
- PING_URL="${1:-}"
70
- REPO_DIR="${2:-.}"
71
-
72
- if [ -z "$PING_URL" ]; then
73
- printf "Usage: %s <ping_url> [repo_dir]\n" "$0"
74
- printf "\n"
75
- printf " ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)\n"
76
- printf " repo_dir Path to your repo (default: current directory)\n"
77
- exit 1
78
- fi
79
-
80
- if ! REPO_DIR="$(cd "$REPO_DIR" 2>/dev/null && pwd)"; then
81
- printf "Error: directory '%s' not found\n" "${2:-.}"
82
- exit 1
83
- fi
84
- PING_URL="${PING_URL%/}"
85
- export PING_URL
86
- PASS=0
87
-
88
- log() { printf "[%s] %b\n" "$(date -u +%H:%M:%S)" "$*"; }
89
- pass() { log "${GREEN}PASSED${NC} -- $1"; PASS=$((PASS + 1)); }
90
- fail() { log "${RED}FAILED${NC} -- $1"; }
91
- hint() { printf " ${YELLOW}Hint:${NC} %b\n" "$1"; }
92
- stop_at() {
93
- printf "\n"
94
- printf "${RED}${BOLD}Validation stopped at %s.${NC} Fix the above before continuing.\n" "$1"
95
- exit 1
96
- }
97
-
98
- printf "\n"
99
- printf "${BOLD}========================================${NC}\n"
100
- printf "${BOLD} OpenEnv Submission Validator${NC}\n"
101
- printf "${BOLD}========================================${NC}\n"
102
- log "Repo: $REPO_DIR"
103
- log "Ping URL: $PING_URL"
104
- printf "\n"
105
-
106
- log "${BOLD}Step 1/3: Pinging HF Space${NC} ($PING_URL/reset) ..."
107
-
108
- CURL_OUTPUT=$(portable_mktemp "validate-curl")
109
- CLEANUP_FILES+=("$CURL_OUTPUT")
110
- HTTP_CODE=$(curl -s -o "$CURL_OUTPUT" -w "%{http_code}" -X POST \
111
- -H "Content-Type: application/json" -d '{}' \
112
- "$PING_URL/reset" --max-time 30 2>"$CURL_OUTPUT" || printf "000")
113
-
114
- if [ "$HTTP_CODE" = "200" ]; then
115
- pass "HF Space is live and responds to /reset"
116
- elif [ "$HTTP_CODE" = "000" ]; then
117
- fail "HF Space not reachable (connection failed or timed out)"
118
- hint "Check your network connection and that the Space is running."
119
- hint "Try: curl -s -o /dev/null -w '%%{http_code}' -X POST $PING_URL/reset"
120
- stop_at "Step 1"
121
- else
122
- fail "HF Space /reset returned HTTP $HTTP_CODE (expected 200)"
123
- hint "Make sure your Space is running and the URL is correct."
124
- hint "Try opening $PING_URL in your browser first."
125
- stop_at "Step 1"
126
- fi
127
-
128
- log "${BOLD}Step 2/3: Running docker build${NC} ..."
129
-
130
- if ! command -v docker &>/dev/null; then
131
- fail "docker command not found"
132
- hint "Install Docker: https://docs.docker.com/get-docker/"
133
- stop_at "Step 2"
134
- fi
135
-
136
- if [ -f "$REPO_DIR/Dockerfile" ]; then
137
- DOCKER_CONTEXT="$REPO_DIR"
138
- elif [ -f "$REPO_DIR/server/Dockerfile" ]; then
139
- DOCKER_CONTEXT="$REPO_DIR/server"
140
- else
141
- fail "No Dockerfile found in repo root or server/ directory"
142
- stop_at "Step 2"
143
- fi
144
-
145
- log " Found Dockerfile in $DOCKER_CONTEXT"
146
-
147
- BUILD_OK=false
148
- BUILD_OUTPUT=$(run_with_timeout "$DOCKER_BUILD_TIMEOUT" docker build "$DOCKER_CONTEXT" 2>&1) && BUILD_OK=true
149
-
150
- if [ "$BUILD_OK" = true ]; then
151
- pass "Docker build succeeded"
152
- else
153
- fail "Docker build failed (timeout=${DOCKER_BUILD_TIMEOUT}s)"
154
- printf "%s\n" "$BUILD_OUTPUT" | tail -20
155
- stop_at "Step 2"
156
- fi
157
-
158
- log "${BOLD}Step 3/3: Running openenv validate${NC} ..."
159
-
160
- if ! command -v openenv &>/dev/null; then
161
- fail "openenv command not found"
162
- hint "Install it: pip install openenv-core"
163
- stop_at "Step 3"
164
- fi
165
-
166
- VALIDATE_OK=false
167
- VALIDATE_OUTPUT=$(cd "$REPO_DIR" && openenv validate 2>&1) && VALIDATE_OK=true
168
-
169
- if [ "$VALIDATE_OK" = true ]; then
170
- pass "openenv validate passed"
171
- [ -n "$VALIDATE_OUTPUT" ] && log " $VALIDATE_OUTPUT"
172
- else
173
- fail "openenv validate failed"
174
- printf "%s\n" "$VALIDATE_OUTPUT"
175
- stop_at "Step 3"
176
- fi
177
-
178
- printf "\n"
179
- printf "${BOLD}========================================${NC}\n"
180
- printf "${GREEN}${BOLD} All 3/3 checks passed!${NC}\n"
181
- printf "${GREEN}${BOLD} Your submission is ready to submit.${NC}\n"
182
- printf "${BOLD}========================================${NC}\n"
183
- printf "\n"
184
-
185
- exit 0