checkpoint: mma_orchestrator track

This commit is contained in:
2026-02-26 22:59:26 -05:00
parent f05fa3d340
commit d087a20f7b
17 changed files with 930 additions and 73 deletions

205
gui_2.py
View File

@@ -24,9 +24,14 @@ import events
import numpy as np
import api_hooks
import mcp_client
import orchestrator_pm
from performance_monitor import PerformanceMonitor
from log_registry import LogRegistry
from log_pruner import LogPruner
import conductor_tech_lead
import multi_agent_conductor
from models import Track, Ticket
from file_cache import ASTParser
from fastapi import FastAPI, Depends, HTTPException, Security
from fastapi.security.api_key import APIKeyHeader
@@ -181,6 +186,9 @@ class App:
self.ui_ai_input = ""
self.ui_disc_new_name_input = ""
self.ui_disc_new_role_input = ""
self.ui_epic_input = ""
self.proposed_tracks = []
self._show_track_proposal_modal = False
# Last Script popup variables
self.ui_last_script_text = ""
@@ -238,6 +246,11 @@ class App:
self._mma_approval_edit_mode = False
self._mma_approval_payload = ""
# Orchestration State
self.ui_epic_input = ""
self.proposed_tracks: list[dict] = []
self._show_track_proposal_modal = False
self._tool_log: list[tuple[str, str]] = []
self._comms_log: list[dict] = []
@@ -706,6 +719,28 @@ class App:
agent_tools_cfg = proj.get("agent", {}).get("tools", {})
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES}
# Restore MMA state
mma_sec = proj.get("mma", {})
self.ui_epic_input = mma_sec.get("epic", "")
at_data = mma_sec.get("active_track")
if at_data:
try:
tickets = []
for t_data in at_data.get("tickets", []):
tickets.append(Ticket(**t_data))
self.active_track = Track(
id=at_data.get("id"),
description=at_data.get("description"),
tickets=tickets
)
self.active_tickets = at_data.get("tickets", []) # Keep dicts for UI table
except Exception as e:
print(f"Failed to deserialize active track: {e}")
self.active_track = None
else:
self.active_track = None
self.active_tickets = []
def _save_active_project(self):
if self.active_project_path:
try:
@@ -856,6 +891,10 @@ class App:
"ts": project_manager.now_ts()
})
elif action == "show_track_proposal":
self.proposed_tracks = task.get("payload", [])
self._show_track_proposal_modal = True
elif action == "mma_state_update":
payload = task.get("payload", {})
self.mma_status = payload.get("status", "idle")
@@ -1291,6 +1330,17 @@ class App:
disc_sec["active"] = self.active_discussion
disc_sec["auto_add"] = self.ui_auto_add_history
# Save MMA State
mma_sec = proj.setdefault("mma", {})
mma_sec["epic"] = self.ui_epic_input
if self.active_track:
# We only persist the basic metadata if full serialization is too complex
# For now, let's try full serialization via asdict
from dataclasses import asdict
mma_sec["active_track"] = asdict(self.active_track)
else:
mma_sec["active_track"] = None
def _flush_to_config(self):
self.config["ai"] = {
"provider": self.current_provider,
@@ -1408,6 +1458,8 @@ class App:
# Process GUI task queue
self._process_pending_gui_tasks()
self._render_track_proposal_modal()
# Auto-save (every 60s)
now = time.time()
if now - self._last_autosave >= self._autosave_interval:
@@ -1887,6 +1939,126 @@ class App:
if ch:
self.ui_agent_tools[t_name] = val
imgui.separator()
imgui.text_colored(C_LBL, 'MMA Orchestration')
_, self.ui_epic_input = imgui.input_text_multiline('##epic_input', self.ui_epic_input, imgui.ImVec2(-1, 80))
if imgui.button('Plan Epic (Tier 1)', imgui.ImVec2(-1, 0)):
self._cb_plan_epic()
def _cb_plan_epic(self):
def _bg_task():
try:
self.ai_status = "Planning Epic (Tier 1)..."
history = orchestrator_pm.get_track_history_summary()
proj = project_manager.load_project(self.active_project_path)
flat = project_manager.flat_config(proj)
file_items = aggregate.build_file_items(Path("."), flat.get("files", {}).get("paths", []))
tracks = orchestrator_pm.generate_tracks(self.ui_epic_input, flat, file_items, history_summary=history)
with self._pending_gui_tasks_lock:
self._pending_gui_tasks.append({
"action": "show_track_proposal",
"payload": tracks
})
self.ai_status = "Epic tracks generated."
except Exception as e:
self.ai_status = f"Epic plan error: {e}"
print(f"ERROR in _cb_plan_epic background task: {e}")
threading.Thread(target=_bg_task, daemon=True).start()
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}")
threading.Thread(target=_bg_task, daemon=True).start()
def _render_track_proposal_modal(self):
if self._show_track_proposal_modal:
imgui.open_popup("Track Proposal")
if imgui.begin_popup_modal("Track Proposal", True, imgui.WindowFlags_.always_auto_resize)[0]:
imgui.text_colored(C_IN, "Proposed Implementation Tracks")
imgui.separator()
if not self.proposed_tracks:
imgui.text("No tracks generated.")
else:
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')}")
imgui.separator()
if imgui.button("Accept", imgui.ImVec2(120, 0)):
self._cb_accept_tracks()
self._show_track_proposal_modal = False
imgui.close_current_popup()
imgui.same_line()
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
self._show_track_proposal_modal = False
imgui.close_current_popup()
imgui.end_popup()
def _render_log_management(self):
exp, self.show_windows["Log Management"] = imgui.begin("Log Management", self.show_windows["Log Management"])
if not exp:
@@ -2371,6 +2543,26 @@ class App:
if is_blinking:
imgui.pop_style_color(2)
def _cb_ticket_retry(self, ticket_id):
for t in self.active_tickets:
if t.get('id') == ticket_id:
t['status'] = 'todo'
break
asyncio.run_coroutine_threadsafe(
self.event_queue.put("mma_retry", {"ticket_id": ticket_id}),
self._loop
)
def _cb_ticket_skip(self, ticket_id):
for t in self.active_tickets:
if t.get('id') == ticket_id:
t['status'] = 'skipped'
break
asyncio.run_coroutine_threadsafe(
self.event_queue.put("mma_skip", {"ticket_id": ticket_id}),
self._loop
)
def _render_mma_dashboard(self):
# 1. Global Controls
changed, self.mma_step_mode = imgui.checkbox("Step Mode (HITL)", self.mma_step_mode)
@@ -2404,16 +2596,18 @@ class App:
# 3. Ticket Queue
imgui.text("Ticket Queue")
if imgui.begin_table("mma_tickets", 3, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.resizable):
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)
imgui.table_setup_column("Target", imgui.TableColumnFlags_.width_stretch)
imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 100)
imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 120)
imgui.table_headers_row()
for t in self.active_tickets:
tid = t.get('id', '??')
imgui.table_next_row()
imgui.table_next_column()
imgui.text(str(t.get('id', '??')))
imgui.text(str(tid))
imgui.table_next_column()
imgui.text(str(t.get('target_file', 'general')))
@@ -2435,6 +2629,13 @@ class App:
if status in ['RUNNING', 'COMPLETE', 'BLOCKED', 'ERROR', 'PAUSED']:
imgui.pop_style_color()
imgui.table_next_column()
if imgui.button(f"Retry##{tid}"):
self._cb_ticket_retry(tid)
imgui.same_line()
if imgui.button(f"Skip##{tid}"):
self._cb_ticket_skip(tid)
imgui.end_table()
def _render_tool_calls_panel(self):