WIP: I HATE PYTHON

This commit is contained in:
2026-03-05 13:55:40 -05:00
parent 107608cd76
commit 5e69617f88
43 changed files with 1854 additions and 1671 deletions

View File

@@ -1,9 +1,9 @@
import asyncio
import queue
import threading
import time
import sys
import os
from typing import Any, List, Dict, Optional, Tuple, Callable
from typing import Any, List, Dict, Optional, Tuple, Callable, Union, cast
from pathlib import Path
import json
import uuid
@@ -20,24 +20,24 @@ from pydantic import BaseModel
from src import events
from src import session_logger
from src import project_manager
from src.performance_monitor import PerformanceMonitor
from src.models import Track, Ticket, load_config, parse_history_entries, DISC_ROLES, AGENT_TOOL_NAMES, CONFIG_PATH
from src import performance_monitor
from src import models
from src.log_registry import LogRegistry
from src.log_pruner import LogPruner
from src.file_cache import ASTParser
import ai_client
import shell_runner
import mcp_client
import aggregate
import orchestrator_pm
import conductor_tech_lead
import cost_tracker
import multi_agent_conductor
from src import ai_client
from src import shell_runner
from src import mcp_client
from src import aggregate
from src import orchestrator_pm
from src import conductor_tech_lead
from src import cost_tracker
from src import multi_agent_conductor
from src import theme
from ai_client import ProviderError
from src.ai_client import ProviderError
def save_config(config: dict[str, Any]) -> None:
with open(CONFIG_PATH, "wb") as f:
with open(models.CONFIG_PATH, "wb") as f:
tomli_w.dump(config, f)
def hide_tk_root() -> Tk:
@@ -141,12 +141,11 @@ class AppController:
self.files: List[str] = []
self.screenshots: List[str] = []
self.event_queue: events.AsyncEventQueue = events.AsyncEventQueue()
self._loop: Optional[asyncio.AbstractEventLoop] = None
self.event_queue: events.SyncEventQueue = events.SyncEventQueue()
self._loop_thread: Optional[threading.Thread] = None
self.tracks: List[Dict[str, Any]] = []
self.active_track: Optional[Track] = None
self.active_track: Optional[models.Track] = None
self.active_tickets: List[Dict[str, Any]] = []
self.mma_streams: Dict[str, str] = {}
self.mma_status: str = "idle"
@@ -169,7 +168,7 @@ class AppController:
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
}
self.perf_monitor: PerformanceMonitor = PerformanceMonitor()
self.perf_monitor: performance_monitor.PerformanceMonitor = performance_monitor.PerformanceMonitor()
self._pending_gui_tasks: List[Dict[str, Any]] = []
self._api_event_queue: List[Dict[str, Any]] = []
@@ -278,6 +277,40 @@ class AppController:
self.prior_session_entries: List[Dict[str, Any]] = []
self.test_hooks_enabled: bool = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1")
self.ui_manual_approve: bool = False
self._settable_fields: Dict[str, str] = {
'ai_input': 'ui_ai_input',
'project_git_dir': 'ui_project_git_dir',
'auto_add_history': 'ui_auto_add_history',
'disc_new_name_input': 'ui_disc_new_name_input',
'project_main_context': 'ui_project_main_context',
'gcli_path': 'ui_gemini_cli_path',
'output_dir': 'ui_output_dir',
'files_base_dir': 'ui_files_base_dir',
'ai_status': 'ai_status',
'ai_response': 'ai_response',
'active_discussion': 'active_discussion',
'current_provider': 'current_provider',
'current_model': 'current_model',
'token_budget_pct': '_token_budget_pct',
'token_budget_current': '_token_budget_current',
'token_budget_label': '_token_budget_label',
'show_confirm_modal': 'show_confirm_modal',
'mma_epic_input': 'ui_epic_input',
'mma_status': 'mma_status',
'mma_active_tier': 'active_tier',
'ui_new_track_name': 'ui_new_track_name',
'ui_new_track_desc': 'ui_new_track_desc',
'manual_approve': 'ui_manual_approve'
}
self._gettable_fields = dict(self._settable_fields)
self._gettable_fields.update({
'ui_focus_agent': 'ui_focus_agent',
'active_discussion': 'active_discussion',
'_track_discussion_active': '_track_discussion_active'
})
self._init_actions()
def _init_actions(self) -> None:
@@ -302,6 +335,14 @@ class AppController:
'_test_callback_func_write_to_file': self._test_callback_func_write_to_file
}
def _update_gcli_adapter(self, path: str) -> None:
sys.stderr.write(f"[DEBUG] _update_gcli_adapter called with: {path}\n")
sys.stderr.flush()
if not ai_client._gemini_cli_adapter:
ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=str(path))
else:
ai_client._gemini_cli_adapter.binary_path = str(path)
def _process_pending_gui_tasks(self) -> None:
if not self._pending_gui_tasks:
return
@@ -336,6 +377,8 @@ class AppController:
else:
self.ai_response = text
self.ai_status = payload.get("status", "done")
sys.stderr.write(f"[DEBUG] Updated ai_status to: {self.ai_status}\n")
sys.stderr.flush()
self._trigger_blink = True
if not stream_id:
self._token_stats_dirty = True
@@ -370,8 +413,8 @@ class AppController:
if track_data:
tickets = []
for t_data in self.active_tickets:
tickets.append(Ticket(**t_data))
self.active_track = Track(
tickets.append(models.Ticket(**t_data))
self.active_track = models.Track(
id=track_data.get("id"),
description=track_data.get("title", ""),
tickets=tickets
@@ -379,17 +422,20 @@ class AppController:
elif action == "set_value":
item = task.get("item")
value = task.get("value")
sys.stderr.write(f"[DEBUG] Processing set_value: {item}={value}\n")
sys.stderr.flush()
if item in self._settable_fields:
attr_name = self._settable_fields[item]
setattr(self, attr_name, value)
sys.stderr.write(f"[DEBUG] Set {attr_name} to {value}\n")
sys.stderr.flush()
if item == "gcli_path":
if not ai_client._gemini_cli_adapter:
ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=str(value))
else:
ai_client._gemini_cli_adapter.binary_path = str(value)
self._update_gcli_adapter(str(value))
elif action == "click":
item = task.get("item")
user_data = task.get("user_data")
sys.stderr.write(f"[DEBUG] Processing click: {item} (user_data={user_data})\n")
sys.stderr.flush()
if item == "btn_project_new_automated":
self._cb_new_project_automated(user_data)
elif item == "btn_mma_load_track":
@@ -449,6 +495,9 @@ class AppController:
if "dialog_container" in task:
task["dialog_container"][0] = spawn_dlg
except Exception as e:
import traceback
sys.stderr.write(f"[DEBUG] Error executing GUI task: {e}\n{traceback.format_exc()}\n")
sys.stderr.flush()
print(f"Error executing GUI task: {e}")
def _process_pending_history_adds(self) -> None:
@@ -505,7 +554,7 @@ class AppController:
def init_state(self):
"""Initializes the application state from configurations."""
self.config = load_config()
self.config = models.load_config()
ai_cfg = self.config.get("ai", {})
self._current_provider = ai_cfg.get("provider", "gemini")
self._current_model = ai_cfg.get("model", "gemini-2.5-flash-lite")
@@ -523,11 +572,11 @@ class AppController:
self.screenshots = list(self.project.get("screenshots", {}).get("paths", []))
disc_sec = self.project.get("discussion", {})
self.disc_roles = list(disc_sec.get("roles", list(DISC_ROLES)))
self.disc_roles = list(disc_sec.get("roles", list(models.DISC_ROLES)))
self.active_discussion = disc_sec.get("active", "main")
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
with self._disc_entries_lock:
self.disc_entries = parse_history_entries(disc_data.get("history", []), self.disc_roles)
self.disc_entries = models.parse_history_entries(disc_data.get("history", []), self.disc_roles)
# UI state
self.ui_output_dir = self.project.get("output", {}).get("output_dir", "./md_gen")
@@ -538,6 +587,7 @@ class AppController:
self.ui_project_main_context = proj_meta.get("main_context", "")
self.ui_project_system_prompt = proj_meta.get("system_prompt", "")
self.ui_gemini_cli_path = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
self._update_gcli_adapter(self.ui_gemini_cli_path)
self.ui_word_wrap = proj_meta.get("word_wrap", True)
self.ui_summary_only = proj_meta.get("summary_only", False)
self.ui_auto_add_history = disc_sec.get("auto_add", False)
@@ -562,7 +612,7 @@ class AppController:
self.show_windows = {k: saved.get(k, v) for k, v in _default_windows.items()}
agent_tools_cfg = self.project.get("agent", {}).get("tools", {})
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES}
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in models.AGENT_TOOL_NAMES}
label = self.project.get("project", {}).get("name", "")
session_logger.open_session(label=label)
@@ -622,8 +672,10 @@ class AppController:
"""Asynchronously prunes old insignificant logs on startup."""
def run_prune() -> None:
try:
registry = LogRegistry("logs/log_registry.toml")
pruner = LogPruner(registry, "logs")
from src import log_registry
from src import log_pruner
registry = log_registry.LogRegistry("logs/log_registry.toml")
pruner = log_pruner.LogPruner(registry, "logs")
pruner.prune()
except Exception as e:
print(f"Error during log pruning: {e}")
@@ -646,26 +698,28 @@ class AppController:
self.models_thread.start()
def start_services(self, app: Any = None):
"""Starts background threads and async event loop."""
"""Starts background threads."""
sys.stderr.write("[DEBUG] AppController.start_services called\n")
sys.stderr.flush()
self._prune_old_logs()
self._init_ai_and_hooks(app)
self._loop = asyncio.new_event_loop()
self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
self._loop_thread.start()
sys.stderr.write(f"[DEBUG] _loop_thread started: {self._loop_thread.ident}\n")
sys.stderr.flush()
def shutdown(self) -> None:
"""Stops background threads and cleans up resources."""
import ai_client
from src import ai_client
ai_client.cleanup()
if hasattr(self, 'hook_server') and self.hook_server:
self.hook_server.stop()
if self._loop and self._loop.is_running():
self._loop.call_soon_threadsafe(self._loop.stop)
self.event_queue.put("shutdown", None)
if self._loop_thread and self._loop_thread.is_alive():
self._loop_thread.join(timeout=2.0)
def _init_ai_and_hooks(self, app: Any = None) -> None:
import api_hooks
from src import api_hooks
ai_client.set_provider(self._current_provider, self._current_model)
if self._current_provider == "gemini_cli":
if not ai_client._gemini_cli_adapter:
@@ -681,77 +735,37 @@ class AppController:
ai_client.events.on("response_received", lambda **kw: self._on_api_event("response_received", **kw))
ai_client.events.on("tool_execution", lambda **kw: self._on_api_event("tool_execution", **kw))
self._settable_fields: Dict[str, str] = {
'ai_input': 'ui_ai_input',
'project_git_dir': 'ui_project_git_dir',
'auto_add_history': 'ui_auto_add_history',
'disc_new_name_input': 'ui_disc_new_name_input',
'project_main_context': 'ui_project_main_context',
'gcli_path': 'ui_gemini_cli_path',
'output_dir': 'ui_output_dir',
'files_base_dir': 'ui_files_base_dir',
'ai_status': 'ai_status',
'ai_response': 'ai_response',
'active_discussion': 'active_discussion',
'current_provider': 'current_provider',
'current_model': 'current_model',
'token_budget_pct': '_token_budget_pct',
'token_budget_current': '_token_budget_current',
'token_budget_label': '_token_budget_label',
'show_confirm_modal': 'show_confirm_modal',
'mma_epic_input': 'ui_epic_input',
'mma_status': 'mma_status',
'mma_active_tier': 'active_tier',
'ui_new_track_name': 'ui_new_track_name',
'ui_new_track_desc': 'ui_new_track_desc',
'manual_approve': 'ui_manual_approve'
}
self._gettable_fields = dict(self._settable_fields)
self._gettable_fields.update({
'ui_focus_agent': 'ui_focus_agent',
'active_discussion': 'active_discussion',
'_track_discussion_active': '_track_discussion_active'
})
self.hook_server = api_hooks.HookServer(app if app else self)
self.hook_server.start()
def _run_event_loop(self):
"""Internal loop runner."""
asyncio.set_event_loop(self._loop)
self._loop.create_task(self._process_event_queue())
# Fallback: process queues even if GUI thread is idling/stuck (or in headless mode)
async def queue_fallback() -> None:
def queue_fallback() -> None:
while True:
try:
# These methods are normally called by the GUI thread,
# but we call them here as a fallback for headless/background operations.
# The methods themselves are expected to be thread-safe or handle locks.
# Since they are on 'self' (the controller), and App delegates to them,
# we need to make sure we don't double-process if App is also calling them.
# However, _pending_gui_tasks uses a lock, so it's safe.
if hasattr(self, '_process_pending_gui_tasks'):
self._process_pending_gui_tasks()
if hasattr(self, '_process_pending_history_adds'):
self._process_pending_history_adds()
except: pass
await asyncio.sleep(0.1)
time.sleep(0.1)
fallback_thread = threading.Thread(target=queue_fallback, daemon=True)
fallback_thread.start()
self._process_event_queue()
self._loop.create_task(queue_fallback())
self._loop.run_forever()
async def _process_event_queue(self) -> None:
"""Listens for and processes events from the AsyncEventQueue."""
sys.stderr.write("[DEBUG] _process_event_queue started\n")
def _process_event_queue(self) -> None:
"""Listens for and processes events from the SyncEventQueue."""
sys.stderr.write("[DEBUG] _process_event_queue entered\n")
sys.stderr.flush()
while True:
event_name, payload = await self.event_queue.get()
event_name, payload = self.event_queue.get()
sys.stderr.write(f"[DEBUG] _process_event_queue got event: {event_name}\n")
sys.stderr.flush()
if event_name == "shutdown":
break
if event_name == "user_request":
self._loop.run_in_executor(None, self._handle_request_event, payload)
threading.Thread(target=self._handle_request_event, args=(payload,), daemon=True).start()
elif event_name == "response":
with self._pending_gui_tasks_lock:
self._pending_gui_tasks.append({
@@ -792,6 +806,10 @@ class AppController:
ai_client.set_custom_system_prompt("\n\n".join(csp))
ai_client.set_model_params(self.temperature, self.max_tokens, self.history_trunc_limit)
ai_client.set_agent_tools(self.ui_agent_tools)
# Force update adapter path right before send to bypass potential duplication issues
self._update_gcli_adapter(self.ui_gemini_cli_path)
sys.stderr.write(f"[DEBUG] Calling ai_client.send with provider={ai_client.get_provider()}, model={self.current_model}, gcli_path={self.ui_gemini_cli_path}\n")
sys.stderr.flush()
try:
resp = ai_client.send(
event.stable_md,
@@ -804,27 +822,20 @@ class AppController:
pre_tool_callback=self._confirm_and_run,
qa_callback=ai_client.run_tier4_analysis
)
asyncio.run_coroutine_threadsafe(
self.event_queue.put("response", {"text": resp, "status": "done", "role": "AI"}),
self._loop
)
except ProviderError as e:
asyncio.run_coroutine_threadsafe(
self.event_queue.put("response", {"text": e.ui_message(), "status": "error", "role": "Vendor API"}),
self._loop
)
self.event_queue.put("response", {"text": resp, "status": "done", "role": "AI"})
except ai_client.ProviderError as e:
sys.stderr.write(f"[DEBUG] _handle_request_event ai_client.ProviderError: {e.ui_message()}\n")
sys.stderr.flush()
self.event_queue.put("response", {"text": e.ui_message(), "status": "error", "role": "Vendor API"})
except Exception as e:
asyncio.run_coroutine_threadsafe(
self.event_queue.put("response", {"text": f"ERROR: {e}", "status": "error", "role": "System"}),
self._loop
)
import traceback
sys.stderr.write(f"[DEBUG] _handle_request_event ERROR: {e}\n{traceback.format_exc()}\n")
sys.stderr.flush()
self.event_queue.put("response", {"text": f"ERROR: {e}", "status": "error", "role": "System"})
def _on_ai_stream(self, text: str) -> None:
"""Handles streaming text from the AI."""
asyncio.run_coroutine_threadsafe(
self.event_queue.put("response", {"text": text, "status": "streaming...", "role": "AI"}),
self._loop
)
self.event_queue.put("response", {"text": text, "status": "streaming...", "role": "AI"})
def _on_comms_entry(self, entry: Dict[str, Any]) -> None:
session_logger.log_comms(entry)
@@ -866,7 +877,7 @@ class AppController:
with self._pending_tool_calls_lock:
self._pending_tool_calls.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier})
def _on_api_event(self, event_name: str, **kwargs: Any) -> None:
def _on_api_event(self, event_name: str = "generic_event", **kwargs: Any) -> None:
payload = kwargs.get("payload", {})
with self._pending_gui_tasks_lock:
self._pending_gui_tasks.append({"action": "refresh_api_metrics", "payload": payload})
@@ -1083,7 +1094,7 @@ class AppController:
},
"usage": self.session_usage
}
except ProviderError as e:
except ai_client.ProviderError as e:
raise HTTPException(status_code=502, detail=f"AI Provider Error: {e.ui_message()}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"In-flight AI request failure: {e}")
@@ -1207,11 +1218,11 @@ class AppController:
self.files = list(self.project.get("files", {}).get("paths", []))
self.screenshots = list(self.project.get("screenshots", {}).get("paths", []))
disc_sec = self.project.get("discussion", {})
self.disc_roles = list(disc_sec.get("roles", list(DISC_ROLES)))
self.disc_roles = list(disc_sec.get("roles", list(models.DISC_ROLES)))
self.active_discussion = disc_sec.get("active", "main")
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
with self._disc_entries_lock:
self.disc_entries = parse_history_entries(disc_data.get("history", []), self.disc_roles)
self.disc_entries = models.parse_history_entries(disc_data.get("history", []), self.disc_roles)
proj = self.project
self.ui_output_dir = proj.get("output", {}).get("output_dir", "./md_gen")
self.ui_files_base_dir = proj.get("files", {}).get("base_dir", ".")
@@ -1226,7 +1237,7 @@ class AppController:
self.ui_word_wrap = proj.get("project", {}).get("word_wrap", True)
self.ui_summary_only = proj.get("project", {}).get("summary_only", False)
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}
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in models.AGENT_TOOL_NAMES}
# MMA Tracks
self.tracks = project_manager.get_all_tracks(self.ui_files_base_dir)
# Restore MMA state
@@ -1237,8 +1248,8 @@ class AppController:
try:
tickets = []
for t_data in at_data.get("tickets", []):
tickets.append(Ticket(**t_data))
self.active_track = Track(
tickets.append(models.Ticket(**t_data))
self.active_track = models.Track(
id=at_data.get("id"),
description=at_data.get("description"),
tickets=tickets
@@ -1255,7 +1266,7 @@ class AppController:
track_history = project_manager.load_track_history(self.active_track.id, self.ui_files_base_dir)
if track_history:
with self._disc_entries_lock:
self.disc_entries = parse_history_entries(track_history, self.disc_roles)
self.disc_entries = models.parse_history_entries(track_history, self.disc_roles)
def _cb_load_track(self, track_id: str) -> None:
state = project_manager.load_track_state(track_id, self.ui_files_base_dir)
@@ -1265,21 +1276,21 @@ class AppController:
tickets = []
for t in state.tasks:
if isinstance(t, dict):
tickets.append(Ticket(**t))
tickets.append(models.Ticket(**t))
else:
tickets.append(t)
self.active_track = Track(
self.active_track = models.Track(
id=state.metadata.id,
description=state.metadata.name,
tickets=tickets
)
# Keep dicts for UI table (or convert Ticket objects back to dicts if needed)
# Keep dicts for UI table (or convert models.Ticket objects back to dicts if needed)
self.active_tickets = [asdict(t) if not isinstance(t, dict) else t for t in tickets]
# Load track-scoped history
history = project_manager.load_track_history(track_id, self.ui_files_base_dir)
with self._disc_entries_lock:
if history:
self.disc_entries = parse_history_entries(history, self.disc_roles)
self.disc_entries = models.parse_history_entries(history, self.disc_roles)
else:
self.disc_entries = []
self._recalculate_session_usage()
@@ -1312,7 +1323,7 @@ class AppController:
disc_sec["active"] = name
disc_data = discussions[name]
with self._disc_entries_lock:
self.disc_entries = parse_history_entries(disc_data.get("history", []), self.disc_roles)
self.disc_entries = models.parse_history_entries(disc_data.get("history", []), self.disc_roles)
self.ai_status = f"discussion: {name}"
def _flush_disc_entries_to_project(self) -> None:
@@ -1491,10 +1502,7 @@ class AppController:
base_dir=base_dir
)
# Push to async queue
asyncio.run_coroutine_threadsafe(
self.event_queue.put("user_request", event_payload),
self._loop
)
self.event_queue.put("user_request", event_payload)
sys.stderr.write("[DEBUG] Enqueued user_request event\n")
sys.stderr.flush()
except Exception as e:
@@ -1549,7 +1557,7 @@ class AppController:
proj["project"]["auto_scroll_tool_calls"] = self.ui_auto_scroll_tool_calls
proj.setdefault("gemini_cli", {})["binary_path"] = self.ui_gemini_cli_path
proj.setdefault("agent", {}).setdefault("tools", {})
for t_name in AGENT_TOOL_NAMES:
for t_name in models.AGENT_TOOL_NAMES:
proj["agent"]["tools"][t_name] = self.ui_agent_tools.get(t_name, True)
self._flush_disc_entries_to_project()
disc_sec = proj.setdefault("discussion", {})
@@ -1598,9 +1606,13 @@ class AppController:
def _cb_plan_epic(self) -> None:
def _bg_task() -> None:
sys.stderr.write("[DEBUG] _cb_plan_epic _bg_task started\n")
sys.stderr.flush()
try:
self.ai_status = "Planning Epic (Tier 1)..."
history = orchestrator_pm.get_track_history_summary()
sys.stderr.write(f"[DEBUG] History summary length: {len(history)}\n")
sys.stderr.flush()
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", []))
@@ -1641,7 +1653,7 @@ class AppController:
def _bg_task() -> None:
# Generate skeletons once
self.ai_status = "Phase 2: Generating skeletons for all tracks..."
parser = ASTParser(language="python")
parser = file_cache.ASTParser(language="python")
generated_skeletons = ""
try:
for i, file_path in enumerate(self.files):
@@ -1685,7 +1697,7 @@ class AppController:
engine = multi_agent_conductor.ConductorEngine(self.active_track, self.event_queue, auto_queue=not self.mma_step_mode)
flat = project_manager.flat_config(self.project, self.active_discussion, track_id=self.active_track.id)
full_md, _, _ = aggregate.run(flat)
asyncio.run_coroutine_threadsafe(engine.run(md_content=full_md), self._loop)
threading.Thread(target=engine.run, kwargs={"md_content": full_md}, daemon=True).start()
self.ai_status = f"Track '{self.active_track.description}' started."
return
@@ -1709,7 +1721,7 @@ class AppController:
skeletons = "" # Initialize skeletons variable
if skeletons_str is None: # Only generate if not provided
# 1. Get skeletons for context
parser = ASTParser(language="python")
parser = file_cache.ASTParser(language="python")
for i, file_path in enumerate(self.files):
try:
self.ai_status = f"Phase 2: Scanning files ({i+1}/{len(self.files)})..."
@@ -1745,7 +1757,7 @@ class AppController:
# 3. Create Track and Ticket objects
tickets = []
for t_data in sorted_tickets_data:
ticket = Ticket(
ticket = models.Ticket(
id=t_data["id"],
description=t_data.get("description") or t_data.get("goal", "No description"),
status=t_data.get("status", "todo"),
@@ -1755,11 +1767,10 @@ class AppController:
)
tickets.append(ticket)
track_id = f"track_{uuid.uuid5(uuid.NAMESPACE_DNS, f'{self.active_project_path}_{title}').hex[:12]}"
track = Track(id=track_id, description=title, tickets=tickets)
track = models.Track(id=track_id, description=title, tickets=tickets)
# Initialize track state in the filesystem
from src.models import TrackState, Metadata
meta = Metadata(id=track_id, name=title, status="todo", created_at=datetime.now(), updated_at=datetime.now())
state = TrackState(metadata=meta, discussion=[], tasks=tickets)
meta = models.Metadata(id=track_id, name=title, status="todo", created_at=datetime.now(), updated_at=datetime.now())
state = models.TrackState(metadata=meta, discussion=[], tasks=tickets)
project_manager.save_track_state(track_id, state, self.ui_files_base_dir)
# 4. Initialize ConductorEngine and run loop
engine = multi_agent_conductor.ConductorEngine(track, self.event_queue, auto_queue=not self.mma_step_mode)
@@ -1767,8 +1778,8 @@ class AppController:
track_id_param = track.id
flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id_param)
full_md, _, _ = aggregate.run(flat)
# Schedule the coroutine on the internal event loop
asyncio.run_coroutine_threadsafe(engine.run(md_content=full_md), self._loop)
# Start the engine in a separate thread
threading.Thread(target=engine.run, kwargs={"md_content": full_md}, daemon=True).start()
except Exception as e:
self.ai_status = f"Track start error: {e}"
print(f"ERROR in _start_track_logic: {e}")
@@ -1778,20 +1789,14 @@ class AppController:
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
)
self.event_queue.put("mma_retry", {"ticket_id": ticket_id})
def _cb_ticket_skip(self, ticket_id: str) -> None:
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
)
self.event_queue.put("mma_skip", {"ticket_id": ticket_id})
def _cb_run_conductor_setup(self) -> None:
base = Path("conductor")
@@ -1848,23 +1853,21 @@ class AppController:
def _push_mma_state_update(self) -> None:
if not self.active_track:
return
# Sync active_tickets (list of dicts) back to active_track.tickets (list of Ticket objects)
self.active_track.tickets = [Ticket.from_dict(t) for t in self.active_tickets]
# Sync active_tickets (list of dicts) back to active_track.tickets (list of models.Ticket objects)
self.active_track.tickets = [models.Ticket.from_dict(t) for t in self.active_tickets]
# Save the state to disk
from src.project_manager import save_track_state, load_track_state
from src.models import TrackState, Metadata
existing = load_track_state(self.active_track.id, self.ui_files_base_dir)
meta = Metadata(
existing = project_manager.load_track_state(self.active_track.id, self.ui_files_base_dir)
meta = models.Metadata(
id=self.active_track.id,
name=self.active_track.description,
status=self.mma_status,
created_at=existing.metadata.created_at if existing else datetime.now(),
updated_at=datetime.now()
)
state = TrackState(
state = models.TrackState(
metadata=meta,
discussion=existing.discussion if existing else [],
tasks=self.active_track.tickets
)
save_track_state(self.active_track.id, state, self.ui_files_base_dir)
project_manager.save_track_state(self.active_track.id, state, self.ui_files_base_dir)