Compare commits

...

11 Commits

8 changed files with 113 additions and 49 deletions

View File

@@ -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**

View File

@@ -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

View File

@@ -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

View File

@@ -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}")

View File

@@ -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"),
)

View File

@@ -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

View 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"

View 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"