Compare commits
11 Commits
ce99c18cbd
...
4d32d41cd1
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d32d41cd1 | |||
| 63d1b04479 | |||
| 3c9d8da292 | |||
| 245653ce62 | |||
| 3d89d0e026 | |||
| 86973e2401 | |||
| 925a7a9fcf | |||
| 203fcd5b5c | |||
| 3cb7d4fd6d | |||
| 570527a955 | |||
| 0c3a2061e7 |
@@ -75,10 +75,10 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
16. [x] **Track: Manual Block/Unblock Control**
|
||||
*Link: [./tracks/manual_block_control_20260306/](./tracks/manual_block_control_20260306/)*
|
||||
|
||||
17. [~] **Track: Pipeline Pause/Resume**
|
||||
17. [x] **Track: Pipeline Pause/Resume**
|
||||
*Link: [./tracks/pipeline_pause_resume_20260306/](./tracks/pipeline_pause_resume_20260306/)*
|
||||
|
||||
18. [ ] **Track: Per-Ticket Model Override**
|
||||
18. [~] **Track: Per-Ticket Model Override**
|
||||
*Link: [./tracks/per_ticket_model_20260306/](./tracks/per_ticket_model_20260306/)*
|
||||
|
||||
19. [ ] **Track: Manual UX Validation & Review**
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
## Phase 1: Model Override Field
|
||||
Focus: Add field to Ticket dataclass
|
||||
|
||||
- [ ] Task 1.1: Initialize MMA Environment
|
||||
- [ ] Task 1.2: Add model_override to Ticket
|
||||
- [x] Task 1.1: Initialize MMA Environment
|
||||
- [x] Task 1.2: Add model_override to Ticket (245653c)
|
||||
- WHERE: `src/models.py` `Ticket` dataclass
|
||||
- WHAT: Add optional model override field
|
||||
- HOW:
|
||||
@@ -17,7 +17,7 @@ Focus: Add field to Ticket dataclass
|
||||
model_override: Optional[str] = None
|
||||
```
|
||||
|
||||
- [ ] Task 1.3: Update serialization
|
||||
- [x] Task 1.3: Update serialization (245653c)
|
||||
- WHERE: `src/models.py` `Ticket.to_dict()` and `from_dict()`
|
||||
- WHAT: Include model_override
|
||||
- HOW: Add field to dict conversion
|
||||
@@ -25,40 +25,11 @@ Focus: Add field to Ticket dataclass
|
||||
## Phase 2: Model Dropdown UI
|
||||
Focus: Add model selection to ticket display
|
||||
|
||||
- [ ] Task 2.1: Get available models list
|
||||
- WHERE: `src/gui_2.py` or from cost_tracker
|
||||
- WHAT: List of available models
|
||||
- HOW:
|
||||
```python
|
||||
AVAILABLE_MODELS = ["gemini-2.5-flash-lite", "gemini-2.5-flash", "gemini-3.1-pro-preview", "claude-3-5-sonnet", "deepseek-v3"]
|
||||
```
|
||||
- [x] Task 2.1: Get available models list (63d1b04)
|
||||
|
||||
- [ ] Task 2.2: Add dropdown to ticket UI
|
||||
- WHERE: `src/gui_2.py` ticket rendering
|
||||
- WHAT: Combo for model selection
|
||||
- HOW:
|
||||
```python
|
||||
current_model = ticket.model_override or "Default"
|
||||
if imgui.begin_combo("Model", current_model):
|
||||
if imgui.selectable("Default", ticket.model_override is None):
|
||||
ticket.model_override = None
|
||||
for model in AVAILABLE_MODELS:
|
||||
if imgui.selectable(model, ticket.model_override == model):
|
||||
ticket.model_override = model
|
||||
imgui.end_combo()
|
||||
```
|
||||
- [x] Task 2.2: Add dropdown to ticket UI (63d1b04)
|
||||
|
||||
## Phase 3: Visual Indicator
|
||||
Focus: Show when override is active
|
||||
|
||||
- [ ] Task 3.1: Color-code override tickets
|
||||
- WHERE: `src/gui_2.py` ticket rendering
|
||||
- WHAT: Visual distinction for override
|
||||
- HOW:
|
||||
```python
|
||||
if ticket.model_override:
|
||||
imgui.text_colored(vec4(255, 200, 100, 255), f"[{ticket.model_override}]")
|
||||
```
|
||||
- [x] Task 3.1: Color-code override tickets (63d1b04)
|
||||
|
||||
## Phase 4: Execution Integration
|
||||
Focus: Use override in worker execution
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
## Phase 1: Pause Mechanism
|
||||
Focus: Add pause event to ConductorEngine
|
||||
|
||||
- [ ] Task 1.1: Initialize MMA Environment
|
||||
- [ ] Task 1.2: Add pause event to ConductorEngine
|
||||
- [x] Task 1.1: Initialize MMA Environment
|
||||
- [x] Task 1.2: Add pause event to ConductorEngine (0c3a206)
|
||||
- WHERE: `src/multi_agent_conductor.py` `ConductorEngine.__init__`
|
||||
- WHAT: Threading event for pause control
|
||||
- HOW:
|
||||
@@ -14,7 +14,7 @@ Focus: Add pause event to ConductorEngine
|
||||
self._pause_event: threading.Event = threading.Event()
|
||||
```
|
||||
|
||||
- [ ] Task 1.3: Check pause in run loop
|
||||
- [x] Task 1.3: Check pause in run loop (0c3a206)
|
||||
- WHERE: `src/multi_agent_conductor.py` `run()`
|
||||
- WHAT: Wait while paused
|
||||
- HOW:
|
||||
@@ -29,18 +29,18 @@ Focus: Add pause event to ConductorEngine
|
||||
## Phase 2: Pause/Resume Methods
|
||||
Focus: Add control methods
|
||||
|
||||
- [ ] Task 2.1: Add pause method
|
||||
- [x] Task 2.1: Add pause method (0c3a206)
|
||||
- WHERE: `src/multi_agent_conductor.py`
|
||||
- HOW: `self._pause_event.set()`
|
||||
|
||||
- [ ] Task 2.2: Add resume method
|
||||
- [x] Task 2.2: Add resume method (0c3a206)
|
||||
- WHERE: `src/multi_agent_conductor.py`
|
||||
- HOW: `self._pause_event.clear()`
|
||||
|
||||
## Phase 3: UI Controls
|
||||
Focus: Add pause/resume buttons
|
||||
|
||||
- [ ] Task 3.1: Add pause/resume button
|
||||
- [x] Task 3.1: Add pause/resume button (3cb7d4f)
|
||||
- WHERE: `src/gui_2.py` MMA dashboard
|
||||
- WHAT: Toggle button for pause state
|
||||
- HOW:
|
||||
@@ -54,7 +54,7 @@ Focus: Add pause/resume buttons
|
||||
engine.pause()
|
||||
```
|
||||
|
||||
- [ ] Task 3.2: Add visual indicator
|
||||
- [x] Task 3.2: Add visual indicator (3cb7d4f)
|
||||
- WHERE: `src/gui_2.py`
|
||||
- WHAT: Banner or color when paused
|
||||
- HOW:
|
||||
@@ -64,5 +64,5 @@ Focus: Add pause/resume buttons
|
||||
```
|
||||
|
||||
## Phase 4: Testing
|
||||
- [ ] Task 4.1: Write unit tests
|
||||
- [ ] Task 4.2: Conductor - Phase Verification
|
||||
- [x] Task 4.1: Write unit tests
|
||||
- [x] Task 4.2: Conductor - Phase Verification
|
||||
|
||||
32
src/gui_2.py
32
src/gui_2.py
@@ -2046,6 +2046,7 @@ class App:
|
||||
imgui.table_setup_column("Select", imgui.TableColumnFlags_.width_fixed, 40)
|
||||
imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80)
|
||||
imgui.table_setup_column("Priority", imgui.TableColumnFlags_.width_fixed, 100)
|
||||
imgui.table_setup_column("Model", imgui.TableColumnFlags_.width_fixed, 150)
|
||||
imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 100)
|
||||
imgui.table_setup_column("Description", imgui.TableColumnFlags_.width_stretch)
|
||||
imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 80)
|
||||
@@ -2099,8 +2100,26 @@ class App:
|
||||
imgui.end_combo()
|
||||
imgui.pop_style_color()
|
||||
|
||||
# Model
|
||||
imgui.table_next_column()
|
||||
model_override = t.get('model_override')
|
||||
current_model = model_override if model_override else "Default"
|
||||
if imgui.begin_combo(f"##model_{tid}", current_model, imgui.ComboFlags_.height_small):
|
||||
if imgui.selectable("Default", model_override is None)[0]:
|
||||
t['model_override'] = None
|
||||
self._push_mma_state_update()
|
||||
for model in ["gemini-2.5-flash-lite", "gemini-2.5-flash", "gemini-3-flash-preview", "gemini-3.1-pro-preview", "deepseek-v3"]:
|
||||
if imgui.selectable(model, model_override == model)[0]:
|
||||
t['model_override'] = model
|
||||
self._push_mma_state_update()
|
||||
imgui.end_combo()
|
||||
|
||||
# Status
|
||||
imgui.table_next_column()
|
||||
status = t.get('status', 'todo')
|
||||
if t.get('model_override'):
|
||||
imgui.text_colored(imgui.ImVec4(1, 0.5, 0, 1), f"{status} [{t.get('model_override')}]")
|
||||
else:
|
||||
imgui.text(t.get('status', 'todo'))
|
||||
|
||||
# Description
|
||||
@@ -2143,11 +2162,15 @@ class App:
|
||||
imgui.same_line()
|
||||
imgui.text(" | Status:")
|
||||
imgui.same_line()
|
||||
if self.mma_status == "paused":
|
||||
imgui.text_colored(imgui.ImVec4(1, 0.5, 0, 1), "PIPELINE PAUSED")
|
||||
imgui.same_line()
|
||||
status_col = imgui.ImVec4(1, 1, 1, 1)
|
||||
if self.mma_status == "idle": status_col = imgui.ImVec4(0.7, 0.7, 0.7, 1)
|
||||
elif self.mma_status == "running": status_col = imgui.ImVec4(1, 1, 0, 1)
|
||||
elif self.mma_status == "done": status_col = imgui.ImVec4(0, 1, 0, 1)
|
||||
elif self.mma_status == "error": status_col = imgui.ImVec4(1, 0, 0, 1)
|
||||
elif self.mma_status == "paused": status_col = imgui.ImVec4(1, 0.5, 0, 1)
|
||||
imgui.text_colored(status_col, self.mma_status.upper())
|
||||
imgui.same_line()
|
||||
imgui.text(" | Cost:")
|
||||
@@ -2273,6 +2296,15 @@ class App:
|
||||
pass
|
||||
imgui.same_line()
|
||||
imgui.text(f"Status: {self.mma_status.upper()}")
|
||||
if self.controller and self.controller.engine and hasattr(self.controller.engine, '_pause_event'):
|
||||
imgui.same_line()
|
||||
is_paused = self.controller.engine._pause_event.is_set()
|
||||
label = "Resume" if is_paused else "Pause"
|
||||
if imgui.button(label):
|
||||
if is_paused:
|
||||
self.controller.engine.resume()
|
||||
else:
|
||||
self.controller.engine.pause()
|
||||
if self.active_tier:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(C_VAL, f"| Active: {self.active_tier}")
|
||||
|
||||
@@ -75,6 +75,7 @@ class Ticket:
|
||||
step_mode: bool = False
|
||||
retry_count: int = 0
|
||||
manual_block: bool = False
|
||||
model_override: Optional[str] = None
|
||||
|
||||
def mark_blocked(self, reason: str) -> None:
|
||||
self.status = "blocked"
|
||||
@@ -112,6 +113,7 @@ class Ticket:
|
||||
"step_mode": self.step_mode,
|
||||
"retry_count": self.retry_count,
|
||||
"manual_block": self.manual_block,
|
||||
"model_override": self.model_override,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -130,6 +132,7 @@ class Ticket:
|
||||
step_mode=data.get("step_mode", False),
|
||||
retry_count=data.get("retry_count", 0),
|
||||
manual_block=data.get("manual_block", False),
|
||||
model_override=data.get("model_override"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ class ConductorEngine:
|
||||
self._workers_lock = threading.Lock()
|
||||
self._active_workers: dict[str, threading.Thread] = {}
|
||||
self._abort_events: dict[str, threading.Event] = {}
|
||||
self._pause_event: threading.Event = threading.Event()
|
||||
self._tier_usage_lock = threading.Lock()
|
||||
|
||||
def update_usage(self, tier: str, input_tokens: int, output_tokens: int) -> None:
|
||||
@@ -98,6 +99,14 @@ class ConductorEngine:
|
||||
self.tier_usage[tier]["input"] += input_tokens
|
||||
self.tier_usage[tier]["output"] += output_tokens
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pauses the pipeline execution."""
|
||||
self._pause_event.set()
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Resumes the pipeline execution."""
|
||||
self._pause_event.clear()
|
||||
|
||||
def kill_worker(self, ticket_id: str) -> None:
|
||||
"""Sets the abort event for a worker and attempts to join its thread."""
|
||||
if ticket_id in self._abort_events:
|
||||
@@ -164,11 +173,14 @@ class ConductorEngine:
|
||||
md_content: The full markdown context (history + files) for AI workers.
|
||||
max_ticks: Optional limit on number of iterations (for testing).
|
||||
"""
|
||||
self._push_state(status="running", active_tier="Tier 2 (Tech Lead)")
|
||||
|
||||
import sys
|
||||
tick_count = 0
|
||||
while True:
|
||||
if self._pause_event.is_set():
|
||||
self._push_state(status="paused", active_tier="Paused")
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
self._push_state(status="running", active_tier="Tier 2 (Tech Lead)")
|
||||
if max_ticks is not None and tick_count >= max_ticks:
|
||||
break
|
||||
tick_count += 1
|
||||
|
||||
22
tests/test_per_ticket_model.py
Normal file
22
tests/test_per_ticket_model.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
from src.models import Ticket
|
||||
|
||||
def test_ticket_has_model_override_field():
|
||||
t = Ticket(id="T-001", description="Test")
|
||||
assert hasattr(t, 'model_override'), "Ticket must have model_override field"
|
||||
assert t.model_override is None, "model_override should default to None"
|
||||
|
||||
def test_model_override_serialization():
|
||||
t = Ticket(id="T-001", description="Test", model_override="gemini-3.1-pro-preview")
|
||||
d = t.to_dict()
|
||||
assert d.get('model_override') == "gemini-3.1-pro-preview", "to_dict should include model_override"
|
||||
|
||||
def test_model_override_deserialization():
|
||||
d = {"id": "T-001", "description": "Test", "model_override": "gemini-2.5-flash"}
|
||||
t = Ticket.from_dict(d)
|
||||
assert t.model_override == "gemini-2.5-flash", "from_dict should restore model_override"
|
||||
|
||||
def test_model_override_default_on_deserialize():
|
||||
d = {"id": "T-001", "description": "Test"}
|
||||
t = Ticket.from_dict(d)
|
||||
assert t.model_override is None, "Missing model_override should default to None"
|
||||
24
tests/test_pipeline_pause.py
Normal file
24
tests/test_pipeline_pause.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from src.models import Ticket, Track
|
||||
from src.multi_agent_conductor import ConductorEngine
|
||||
|
||||
def test_conductor_engine_has_pause_event():
|
||||
track = Track(id="test", description="Test", tickets=[])
|
||||
engine = ConductorEngine(track)
|
||||
assert hasattr(engine, '_pause_event'), "ConductorEngine must have _pause_event"
|
||||
assert engine._pause_event.is_set() == False, "Pause event should start unset (not paused)"
|
||||
|
||||
def test_pause_method():
|
||||
track = Track(id="test", description="Test", tickets=[])
|
||||
engine = ConductorEngine(track)
|
||||
engine.pause()
|
||||
assert engine._pause_event.is_set() == True, "Pause should set the event"
|
||||
|
||||
def test_resume_method():
|
||||
track = Track(id="test", description="Test", tickets=[])
|
||||
engine = ConductorEngine(track)
|
||||
engine.pause()
|
||||
assert engine._pause_event.is_set() == True
|
||||
engine.resume()
|
||||
assert engine._pause_event.is_set() == False, "Resume should clear the event"
|
||||
Reference in New Issue
Block a user