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**
|
16. [x] **Track: Manual Block/Unblock Control**
|
||||||
*Link: [./tracks/manual_block_control_20260306/](./tracks/manual_block_control_20260306/)*
|
*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/)*
|
*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/)*
|
*Link: [./tracks/per_ticket_model_20260306/](./tracks/per_ticket_model_20260306/)*
|
||||||
|
|
||||||
19. [ ] **Track: Manual UX Validation & Review**
|
19. [ ] **Track: Manual UX Validation & Review**
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
## Phase 1: Model Override Field
|
## Phase 1: Model Override Field
|
||||||
Focus: Add field to Ticket dataclass
|
Focus: Add field to Ticket dataclass
|
||||||
|
|
||||||
- [ ] Task 1.1: Initialize MMA Environment
|
- [x] Task 1.1: Initialize MMA Environment
|
||||||
- [ ] Task 1.2: Add model_override to Ticket
|
- [x] Task 1.2: Add model_override to Ticket (245653c)
|
||||||
- WHERE: `src/models.py` `Ticket` dataclass
|
- WHERE: `src/models.py` `Ticket` dataclass
|
||||||
- WHAT: Add optional model override field
|
- WHAT: Add optional model override field
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -17,7 +17,7 @@ Focus: Add field to Ticket dataclass
|
|||||||
model_override: Optional[str] = None
|
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()`
|
- WHERE: `src/models.py` `Ticket.to_dict()` and `from_dict()`
|
||||||
- WHAT: Include model_override
|
- WHAT: Include model_override
|
||||||
- HOW: Add field to dict conversion
|
- HOW: Add field to dict conversion
|
||||||
@@ -25,40 +25,11 @@ Focus: Add field to Ticket dataclass
|
|||||||
## Phase 2: Model Dropdown UI
|
## Phase 2: Model Dropdown UI
|
||||||
Focus: Add model selection to ticket display
|
Focus: Add model selection to ticket display
|
||||||
|
|
||||||
- [ ] Task 2.1: Get available models list
|
- [x] Task 2.1: Get available models list (63d1b04)
|
||||||
- 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"]
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Task 2.2: Add dropdown to ticket UI
|
- [x] Task 2.2: Add dropdown to ticket UI (63d1b04)
|
||||||
- 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()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 3: Visual Indicator
|
- [x] Task 3.1: Color-code override tickets (63d1b04)
|
||||||
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}]")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 4: Execution Integration
|
## Phase 4: Execution Integration
|
||||||
Focus: Use override in worker execution
|
Focus: Use override in worker execution
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
## Phase 1: Pause Mechanism
|
## Phase 1: Pause Mechanism
|
||||||
Focus: Add pause event to ConductorEngine
|
Focus: Add pause event to ConductorEngine
|
||||||
|
|
||||||
- [ ] Task 1.1: Initialize MMA Environment
|
- [x] Task 1.1: Initialize MMA Environment
|
||||||
- [ ] Task 1.2: Add pause event to ConductorEngine
|
- [x] Task 1.2: Add pause event to ConductorEngine (0c3a206)
|
||||||
- WHERE: `src/multi_agent_conductor.py` `ConductorEngine.__init__`
|
- WHERE: `src/multi_agent_conductor.py` `ConductorEngine.__init__`
|
||||||
- WHAT: Threading event for pause control
|
- WHAT: Threading event for pause control
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -14,7 +14,7 @@ Focus: Add pause event to ConductorEngine
|
|||||||
self._pause_event: threading.Event = threading.Event()
|
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()`
|
- WHERE: `src/multi_agent_conductor.py` `run()`
|
||||||
- WHAT: Wait while paused
|
- WHAT: Wait while paused
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -29,18 +29,18 @@ Focus: Add pause event to ConductorEngine
|
|||||||
## Phase 2: Pause/Resume Methods
|
## Phase 2: Pause/Resume Methods
|
||||||
Focus: Add control 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`
|
- WHERE: `src/multi_agent_conductor.py`
|
||||||
- HOW: `self._pause_event.set()`
|
- 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`
|
- WHERE: `src/multi_agent_conductor.py`
|
||||||
- HOW: `self._pause_event.clear()`
|
- HOW: `self._pause_event.clear()`
|
||||||
|
|
||||||
## Phase 3: UI Controls
|
## Phase 3: UI Controls
|
||||||
Focus: Add pause/resume buttons
|
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
|
- WHERE: `src/gui_2.py` MMA dashboard
|
||||||
- WHAT: Toggle button for pause state
|
- WHAT: Toggle button for pause state
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -54,7 +54,7 @@ Focus: Add pause/resume buttons
|
|||||||
engine.pause()
|
engine.pause()
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Task 3.2: Add visual indicator
|
- [x] Task 3.2: Add visual indicator (3cb7d4f)
|
||||||
- WHERE: `src/gui_2.py`
|
- WHERE: `src/gui_2.py`
|
||||||
- WHAT: Banner or color when paused
|
- WHAT: Banner or color when paused
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -64,5 +64,5 @@ Focus: Add pause/resume buttons
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Phase 4: Testing
|
## Phase 4: Testing
|
||||||
- [ ] Task 4.1: Write unit tests
|
- [x] Task 4.1: Write unit tests
|
||||||
- [ ] Task 4.2: Conductor - Phase Verification
|
- [x] Task 4.2: Conductor - Phase Verification
|
||||||
|
|||||||
34
src/gui_2.py
34
src/gui_2.py
@@ -2046,6 +2046,7 @@ class App:
|
|||||||
imgui.table_setup_column("Select", imgui.TableColumnFlags_.width_fixed, 40)
|
imgui.table_setup_column("Select", imgui.TableColumnFlags_.width_fixed, 40)
|
||||||
imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80)
|
imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80)
|
||||||
imgui.table_setup_column("Priority", imgui.TableColumnFlags_.width_fixed, 100)
|
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("Status", imgui.TableColumnFlags_.width_fixed, 100)
|
||||||
imgui.table_setup_column("Description", imgui.TableColumnFlags_.width_stretch)
|
imgui.table_setup_column("Description", imgui.TableColumnFlags_.width_stretch)
|
||||||
imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 80)
|
imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 80)
|
||||||
@@ -2099,9 +2100,27 @@ class App:
|
|||||||
imgui.end_combo()
|
imgui.end_combo()
|
||||||
imgui.pop_style_color()
|
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
|
# Status
|
||||||
imgui.table_next_column()
|
imgui.table_next_column()
|
||||||
imgui.text(t.get('status', 'todo'))
|
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
|
# Description
|
||||||
imgui.table_next_column()
|
imgui.table_next_column()
|
||||||
@@ -2143,11 +2162,15 @@ class App:
|
|||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text(" | Status:")
|
imgui.text(" | Status:")
|
||||||
imgui.same_line()
|
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)
|
status_col = imgui.ImVec4(1, 1, 1, 1)
|
||||||
if self.mma_status == "idle": status_col = imgui.ImVec4(0.7, 0.7, 0.7, 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 == "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 == "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 == "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.text_colored(status_col, self.mma_status.upper())
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text(" | Cost:")
|
imgui.text(" | Cost:")
|
||||||
@@ -2273,6 +2296,15 @@ class App:
|
|||||||
pass
|
pass
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text(f"Status: {self.mma_status.upper()}")
|
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:
|
if self.active_tier:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text_colored(C_VAL, f"| Active: {self.active_tier}")
|
imgui.text_colored(C_VAL, f"| Active: {self.active_tier}")
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class Ticket:
|
|||||||
step_mode: bool = False
|
step_mode: bool = False
|
||||||
retry_count: int = 0
|
retry_count: int = 0
|
||||||
manual_block: bool = False
|
manual_block: bool = False
|
||||||
|
model_override: Optional[str] = None
|
||||||
|
|
||||||
def mark_blocked(self, reason: str) -> None:
|
def mark_blocked(self, reason: str) -> None:
|
||||||
self.status = "blocked"
|
self.status = "blocked"
|
||||||
@@ -112,6 +113,7 @@ class Ticket:
|
|||||||
"step_mode": self.step_mode,
|
"step_mode": self.step_mode,
|
||||||
"retry_count": self.retry_count,
|
"retry_count": self.retry_count,
|
||||||
"manual_block": self.manual_block,
|
"manual_block": self.manual_block,
|
||||||
|
"model_override": self.model_override,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -130,6 +132,7 @@ class Ticket:
|
|||||||
step_mode=data.get("step_mode", False),
|
step_mode=data.get("step_mode", False),
|
||||||
retry_count=data.get("retry_count", 0),
|
retry_count=data.get("retry_count", 0),
|
||||||
manual_block=data.get("manual_block", False),
|
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._workers_lock = threading.Lock()
|
||||||
self._active_workers: dict[str, threading.Thread] = {}
|
self._active_workers: dict[str, threading.Thread] = {}
|
||||||
self._abort_events: dict[str, threading.Event] = {}
|
self._abort_events: dict[str, threading.Event] = {}
|
||||||
|
self._pause_event: threading.Event = threading.Event()
|
||||||
self._tier_usage_lock = threading.Lock()
|
self._tier_usage_lock = threading.Lock()
|
||||||
|
|
||||||
def update_usage(self, tier: str, input_tokens: int, output_tokens: int) -> None:
|
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]["input"] += input_tokens
|
||||||
self.tier_usage[tier]["output"] += output_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:
|
def kill_worker(self, ticket_id: str) -> None:
|
||||||
"""Sets the abort event for a worker and attempts to join its thread."""
|
"""Sets the abort event for a worker and attempts to join its thread."""
|
||||||
if ticket_id in self._abort_events:
|
if ticket_id in self._abort_events:
|
||||||
@@ -164,11 +173,14 @@ class ConductorEngine:
|
|||||||
md_content: The full markdown context (history + files) for AI workers.
|
md_content: The full markdown context (history + files) for AI workers.
|
||||||
max_ticks: Optional limit on number of iterations (for testing).
|
max_ticks: Optional limit on number of iterations (for testing).
|
||||||
"""
|
"""
|
||||||
self._push_state(status="running", active_tier="Tier 2 (Tech Lead)")
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
tick_count = 0
|
tick_count = 0
|
||||||
while True:
|
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:
|
if max_ticks is not None and tick_count >= max_ticks:
|
||||||
break
|
break
|
||||||
tick_count += 1
|
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