feat(mma): Finalize Orchestrator Integration and fix all regressions

This commit is contained in:
2026-02-27 18:31:14 -05:00
parent 8438f69197
commit 3b2d82ed0d
27 changed files with 706 additions and 297 deletions

201
gui_2.py
View File

@@ -251,6 +251,13 @@ class App:
self.proposed_tracks: list[dict] = []
self._show_track_proposal_modal = False
self.mma_tier_usage = {
"Tier 1": {"input": 0, "output": 0},
"Tier 2": {"input": 0, "output": 0},
"Tier 3": {"input": 0, "output": 0},
"Tier 4": {"input": 0, "output": 0},
}
self._tool_log: list[tuple[str, str]] = []
self._comms_log: list[dict] = []
@@ -403,7 +410,10 @@ class App:
'token_budget_pct': '_token_budget_pct',
'token_budget_current': '_token_budget_current',
'token_budget_label': '_token_budget_label',
'show_confirm_modal': 'show_confirm_modal'
'show_confirm_modal': 'show_confirm_modal',
'mma_epic_input': 'ui_epic_input',
'mma_status': 'mma_status',
'mma_active_tier': 'active_tier'
}
self._clickable_actions = {
@@ -414,6 +424,9 @@ class App:
'btn_reject_script': self._handle_reject_script,
'btn_project_save': self._cb_project_save,
'btn_disc_create': self._cb_disc_create,
'btn_mma_plan_epic': self._cb_plan_epic,
'btn_mma_accept_tracks': self._cb_accept_tracks,
'btn_mma_start_track': self._cb_start_track,
}
self._predefined_callbacks = {
'_test_callback_func_write_to_file': self._test_callback_func_write_to_file
@@ -899,6 +912,7 @@ class App:
payload = task.get("payload", {})
self.mma_status = payload.get("status", "idle")
self.active_tier = payload.get("active_tier")
self.mma_tier_usage = payload.get("tier_usage", self.mma_tier_usage)
self.active_track = payload.get("track")
self.active_tickets = payload.get("tickets", [])
@@ -921,7 +935,17 @@ class App:
if item == "btn_project_new_automated":
self._cb_new_project_automated(user_data)
elif item in self._clickable_actions:
self._clickable_actions[item]()
# Check if it's a method that accepts user_data
import inspect
func = self._clickable_actions[item]
try:
sig = inspect.signature(func)
if 'user_data' in sig.parameters:
func(user_data=user_data)
else:
func()
except Exception:
func()
elif action == "select_list_item":
item = task.get("listbox", task.get("item"))
@@ -1098,6 +1122,20 @@ class App:
self._loop.create_task(self._process_event_queue())
self._loop.run_forever()
def shutdown(self):
"""Cleanly shuts down the app's background tasks."""
if self._loop.is_running():
self._loop.call_soon_threadsafe(self._loop.stop)
if self._loop_thread.is_alive():
self._loop_thread.join(timeout=2.0)
# Join other threads if they exist
if self.send_thread and self.send_thread.is_alive():
self.send_thread.join(timeout=1.0)
if self.models_thread and self.models_thread.is_alive():
self.models_thread.join(timeout=1.0)
async def _process_event_queue(self):
"""Listens for and processes events from the AsyncEventQueue."""
while True:
@@ -1971,68 +2009,83 @@ class App:
def _cb_accept_tracks(self):
def _bg_task():
try:
self.ai_status = "Generating tickets (Tier 2)..."
# 1. Get skeletons for context
parser = ASTParser(language="python")
skeletons = ""
for file_path in self.files:
try:
abs_path = Path(self.ui_files_base_dir) / file_path
if abs_path.exists() and abs_path.suffix == ".py":
with open(abs_path, "r", encoding="utf-8") as f:
code = f.read()
skeletons += f"\nFile: {file_path}\n{parser.get_skeleton(code)}\n"
except Exception as e:
print(f"Error parsing skeleton for {file_path}: {e}")
# 2. For each proposed track, generate and sort tickets
for track_data in self.proposed_tracks:
goal = track_data.get("goal", "")
title = track_data.get("title", "Untitled Track")
raw_tickets = conductor_tech_lead.generate_tickets(goal, skeletons)
if not raw_tickets:
print(f"Warning: No tickets generated for track: {title}")
continue
try:
sorted_tickets_data = conductor_tech_lead.topological_sort(raw_tickets)
except ValueError as e:
print(f"Dependency error in track '{title}': {e}")
# Fallback to unsorted if sort fails? Or skip?
sorted_tickets_data = raw_tickets
# 3. Create Track and Ticket objects
tickets = []
for t_data in sorted_tickets_data:
ticket = Ticket(
id=t_data["id"],
description=t_data["description"],
status=t_data.get("status", "todo"),
assigned_to=t_data.get("assigned_to", "unassigned"),
depends_on=t_data.get("depends_on", []),
step_mode=t_data.get("step_mode", False)
)
tickets.append(ticket)
track_id = f"track_{uuid.uuid4().hex[:8]}"
track = Track(id=track_id, description=title, tickets=tickets)
# 4. Initialize ConductorEngine and run_linear loop
engine = multi_agent_conductor.ConductorEngine(track, self.event_queue)
# Schedule the coroutine on the internal event loop
asyncio.run_coroutine_threadsafe(engine.run_linear(), self._loop)
self.ai_status = "Tracks accepted and execution started."
except Exception as e:
self.ai_status = f"Track acceptance error: {e}"
print(f"ERROR in _cb_accept_tracks background task: {e}")
for track_data in self.proposed_tracks:
self._start_track_logic(track_data)
self.ai_status = "Tracks accepted and execution started."
threading.Thread(target=_bg_task, daemon=True).start()
def _cb_start_track(self, user_data=None):
idx = 0
if isinstance(user_data, int):
idx = user_data
elif isinstance(user_data, dict):
idx = user_data.get("index", 0)
if 0 <= idx < len(self.proposed_tracks):
track_data = self.proposed_tracks[idx]
title = track_data.get("title") or track_data.get("goal", "Untitled Track")
threading.Thread(target=lambda: self._start_track_logic(track_data), daemon=True).start()
self.ai_status = f"Track '{title}' started."
def _start_track_logic(self, track_data):
try:
goal = track_data.get("goal", "")
title = track_data.get("title") or track_data.get("goal", "Untitled Track")
self.ai_status = f"Phase 2: Generating tickets for {title}..."
# 1. Get skeletons for context
parser = ASTParser(language="python")
skeletons = ""
for i, file_path in enumerate(self.files):
try:
self.ai_status = f"Phase 2: Scanning files ({i+1}/{len(self.files)})..."
abs_path = Path(self.ui_files_base_dir) / file_path
if abs_path.exists() and abs_path.suffix == ".py":
with open(abs_path, "r", encoding="utf-8") as f:
code = f.read()
skeletons += f"\nFile: {file_path}\n{parser.get_skeleton(code)}\n"
except Exception as e:
print(f"Error parsing skeleton for {file_path}: {e}")
self.ai_status = "Phase 2: Calling Tech Lead..."
raw_tickets = conductor_tech_lead.generate_tickets(goal, skeletons)
if not raw_tickets:
self.ai_status = f"Error: No tickets generated for track: {title}"
print(f"Warning: No tickets generated for track: {title}")
return
self.ai_status = "Phase 2: Sorting tickets..."
try:
sorted_tickets_data = conductor_tech_lead.topological_sort(raw_tickets)
except ValueError as e:
print(f"Dependency error in track '{title}': {e}")
sorted_tickets_data = raw_tickets
# 3. Create Track and Ticket objects
tickets = []
for t_data in sorted_tickets_data:
ticket = Ticket(
id=t_data["id"],
description=t_data.get("description") or t_data.get("goal", "No description"),
status=t_data.get("status", "todo"),
assigned_to=t_data.get("assigned_to", "unassigned"),
depends_on=t_data.get("depends_on", []),
step_mode=t_data.get("step_mode", False)
)
tickets.append(ticket)
track_id = f"track_{uuid.uuid4().hex[:8]}"
track = Track(id=track_id, description=title, tickets=tickets)
# 4. Initialize ConductorEngine and run_linear loop
engine = multi_agent_conductor.ConductorEngine(track, self.event_queue)
# Schedule the coroutine on the internal event loop
asyncio.run_coroutine_threadsafe(engine.run_linear(), self._loop)
except Exception as e:
self.ai_status = f"Track start error: {e}"
print(f"ERROR in _start_track_logic: {e}")
def _render_track_proposal_modal(self):
if self._show_track_proposal_modal:
imgui.open_popup("Track Proposal")
@@ -2047,6 +2100,8 @@ class App:
for idx, track in enumerate(self.proposed_tracks):
imgui.text_colored(C_LBL, f"Track {idx+1}: {track.get('title', 'Untitled')}")
imgui.text_wrapped(f"Goal: {track.get('goal', 'N/A')}")
if imgui.button(f"Start This Track##{idx}"):
self._cb_start_track(idx)
imgui.separator()
if imgui.button("Accept", imgui.ImVec2(120, 0)):
@@ -2592,9 +2647,29 @@ class App:
else:
imgui.text_disabled("No active MMA track.")
# 3. Token Usage Table
imgui.separator()
imgui.text("Tier Usage (Tokens)")
if imgui.begin_table("mma_usage", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg):
imgui.table_setup_column("Tier")
imgui.table_setup_column("Input")
imgui.table_setup_column("Output")
imgui.table_headers_row()
usage = self.mma_tier_usage
for tier, stats in usage.items():
imgui.table_next_row()
imgui.table_next_column()
imgui.text(tier)
imgui.table_next_column()
imgui.text(f"{stats.get('input', 0):,}")
imgui.table_next_column()
imgui.text(f"{stats.get('output', 0):,}")
imgui.end_table()
imgui.separator()
# 3. Ticket Queue
# 4. Ticket Queue
imgui.text("Ticket Queue")
if imgui.begin_table("mma_tickets", 4, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.resizable):
imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80)