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

@@ -17,9 +17,9 @@ import re
import glob
from pathlib import Path, PureWindowsPath
from typing import Any, cast
import summarize
import project_manager
from file_cache import ASTParser
from src import summarize
from src import project_manager
from src.file_cache import ASTParser
def find_next_increment(output_dir: Path, namespace: str) -> int:
pattern = re.compile(rf"^{re.escape(namespace)}_(\d+)\.md$")

View File

@@ -23,14 +23,14 @@ import threading
import requests # type: ignore[import-untyped]
from typing import Optional, Callable, Any, List, Union, cast, Iterable
import os
import project_manager
import file_cache
import mcp_client
from src import project_manager
from src import file_cache
from src import mcp_client
import anthropic
from gemini_cli_adapter import GeminiCliAdapter as GeminiCliAdapter
from src.gemini_cli_adapter import GeminiCliAdapter as GeminiCliAdapter
from google import genai
from google.genai import types
from events import EventEmitter
from src.events import EventEmitter
_provider: str = "gemini"
_model: str = "gemini-2.5-flash-lite"
@@ -779,6 +779,9 @@ def _send_gemini_cli(md_content: str, user_message: str, base_dir: str,
qa_callback: Optional[Callable[[str], str]] = None,
stream_callback: Optional[Callable[[str], None]] = None) -> str:
global _gemini_cli_adapter
import sys
sys.stderr.write(f"[DEBUG] _send_gemini_cli running in module {__name__}, adapter is {_gemini_cli_adapter}\n")
sys.stderr.flush()
try:
if _gemini_cli_adapter is None:
_gemini_cli_adapter = GeminiCliAdapter(binary_path="gemini")

View File

@@ -5,7 +5,7 @@ import uuid
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from typing import Any
import logging
import session_logger
from src import session_logger
def _get_app_attr(app: Any, name: str, default: Any = None) -> Any:
if hasattr(app, name):
@@ -44,7 +44,7 @@ class HookHandler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(json.dumps({"status": "ok"}).encode("utf-8"))
elif self.path == "/api/project":
import project_manager
from src import project_manager
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()

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)

View File

@@ -1,6 +1,6 @@
import json
import ai_client
import mma_prompts
from src import ai_client
from src import mma_prompts
import re
from typing import Any
@@ -49,8 +49,8 @@ def generate_tickets(track_brief: str, module_skeletons: str) -> list[dict[str,
ai_client.set_custom_system_prompt(old_system_prompt or "")
ai_client.current_tier = None
from dag_engine import TrackDAG
from models import Ticket
from src.dag_engine import TrackDAG
from src.models import Ticket
def topological_sort(tickets: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""

View File

@@ -1,5 +1,5 @@
from typing import List
from models import Ticket
from src.models import Ticket
class TrackDAG:
"""

View File

@@ -1,13 +1,13 @@
"""
Decoupled event emission system for cross-module communication.
"""
import asyncio
import queue
from typing import Callable, Any, Dict, List, Tuple
class EventEmitter:
"""
Simple event emitter for decoupled communication between modules.
"""
Simple event emitter for decoupled communication between modules.
"""
def __init__(self) -> None:
"""Initializes the EventEmitter with an empty listener map."""
@@ -15,25 +15,25 @@ class EventEmitter:
def on(self, event_name: str, callback: Callable[..., Any]) -> None:
"""
Registers a callback for a specific event.
Args:
event_name: The name of the event to listen for.
callback: The function to call when the event is emitted.
"""
Registers a callback for a specific event.
Args:
event_name: The name of the event to listen for.
callback: The function to call when the event is emitted.
"""
if event_name not in self._listeners:
self._listeners[event_name] = []
self._listeners[event_name].append(callback)
def emit(self, event_name: str, *args: Any, **kwargs: Any) -> None:
"""
Emits an event, calling all registered callbacks.
Args:
event_name: The name of the event to emit.
*args: Positional arguments to pass to callbacks.
**kwargs: Keyword arguments to pass to callbacks.
"""
Emits an event, calling all registered callbacks.
Args:
event_name: The name of the event to emit.
*args: Positional arguments to pass to callbacks.
**kwargs: Keyword arguments to pass to callbacks.
"""
if event_name in self._listeners:
for callback in self._listeners[event_name]:
callback(*args, **kwargs)
@@ -42,46 +42,46 @@ class EventEmitter:
"""Clears all registered listeners."""
self._listeners.clear()
class AsyncEventQueue:
class SyncEventQueue:
"""
Synchronous event queue for decoupled communication using queue.Queue.
"""
Asynchronous event queue for decoupled communication using asyncio.Queue.
"""
def __init__(self) -> None:
"""Initializes the AsyncEventQueue with an internal asyncio.Queue."""
self._queue: asyncio.Queue[Tuple[str, Any]] = asyncio.Queue()
"""Initializes the SyncEventQueue with an internal queue.Queue."""
self._queue: queue.Queue[Tuple[str, Any]] = queue.Queue()
async def put(self, event_name: str, payload: Any = None) -> None:
def put(self, event_name: str, payload: Any = None) -> None:
"""
Puts an event into the queue.
Args:
event_name: The name of the event.
payload: Optional data associated with the event.
"""
await self._queue.put((event_name, payload))
Puts an event into the queue.
Args:
event_name: The name of the event.
payload: Optional data associated with the event.
"""
self._queue.put((event_name, payload))
async def get(self) -> Tuple[str, Any]:
def get(self) -> Tuple[str, Any]:
"""
Gets an event from the queue.
Returns:
A tuple containing (event_name, payload).
"""
return await self._queue.get()
Gets an event from the queue.
Returns:
A tuple containing (event_name, payload).
"""
return self._queue.get()
def task_done(self) -> None:
"""Signals that a formerly enqueued task is complete."""
self._queue.task_done()
async def join(self) -> None:
def join(self) -> None:
"""Blocks until all items in the queue have been gotten and processed."""
await self._queue.join()
self._queue.join()
class UserRequestEvent:
"""
Payload for a user request event.
"""
Payload for a user request event.
"""
def __init__(self, prompt: str, stable_md: str, file_items: List[Any], disc_text: str, base_dir: str) -> None:
self.prompt = prompt

View File

@@ -2,7 +2,8 @@ import subprocess
import json
import os
import time
import session_logger
import sys
from src import session_logger
from typing import Optional, Callable, Any
class GeminiCliAdapter:
@@ -61,6 +62,8 @@ class GeminiCliAdapter:
# Filter out empty strings and strip quotes (Popen doesn't want them in cmd_list elements)
cmd_list = [c.strip('"') for c in cmd_list if c]
sys.stderr.write(f"[DEBUG] GeminiCliAdapter cmd_list: {cmd_list}\n")
sys.stderr.flush()
process = subprocess.Popen(
cmd_list,

View File

@@ -13,28 +13,27 @@ import requests # type: ignore[import-untyped]
from pathlib import Path
from tkinter import filedialog, Tk
from typing import Optional, Callable, Any
import aggregate
import ai_client
import cost_tracker
from ai_client import ProviderError
import shell_runner
import session_logger
import project_manager
import theme_2 as theme
from src import aggregate
from src import ai_client
from src import cost_tracker
from src import shell_runner
from src import session_logger
from src import project_manager
from src import theme_2 as theme
import tomllib
import events
from src 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, DISC_ROLES, AGENT_TOOL_NAMES, CONFIG_PATH, load_config, parse_history_entries
from app_controller import AppController, ConfirmDialog, MMAApprovalDialog, MMASpawnApprovalDialog
from file_cache import ASTParser
from src import api_hooks
from src import mcp_client
from src import orchestrator_pm
from src import performance_monitor
from src import log_registry
from src import log_pruner
from src import conductor_tech_lead
from src import multi_agent_conductor
from src import models
from src import app_controller
from src import file_cache
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security.api_key import APIKeyHeader
@@ -45,7 +44,7 @@ PROVIDERS: list[str] = ["gemini", "anthropic", "gemini_cli", "deepseek"]
COMMS_CLAMP_CHARS: int = 300
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:
@@ -102,7 +101,7 @@ class App:
def __init__(self) -> None:
# Initialize controller and delegate state
self.controller = AppController()
self.controller = app_controller.AppController()
# Restore legacy PROVIDERS to controller if needed (it already has it via delegation if set on class level, but let's be explicit)
if not hasattr(self.controller, 'PROVIDERS'):
self.controller.PROVIDERS = PROVIDERS
@@ -739,7 +738,7 @@ class App:
ch, self.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", self.ui_auto_scroll_comms)
ch, self.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", self.ui_auto_scroll_tool_calls)
if imgui.collapsing_header("Agent Tools"):
for t_name in AGENT_TOOL_NAMES:
for t_name in models.AGENT_TOOL_NAMES:
val = self.ui_agent_tools.get(t_name, True)
ch, val = imgui.checkbox(f"Enable {t_name}", val)
if ch:
@@ -800,7 +799,7 @@ class App:
if not exp:
imgui.end()
return
registry = LogRegistry("logs/log_registry.toml")
registry = log_registry.LogRegistry("logs/log_registry.toml")
sessions = registry.data
if imgui.begin_table("sessions_table", 7, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable):
imgui.table_setup_column("Session ID")
@@ -976,7 +975,7 @@ class App:
self._flush_disc_entries_to_project()
history_strings = project_manager.load_track_history(self.active_track.id, self.ui_files_base_dir)
with self._disc_entries_lock:
self.disc_entries = parse_history_entries(history_strings, self.disc_roles)
self.disc_entries = models.parse_history_entries(history_strings, self.disc_roles)
self.ai_status = f"track discussion: {self.active_track.id}"
else:
self._flush_disc_entries_to_project()

View File

@@ -1,7 +1,7 @@
import os
import shutil
from datetime import datetime, timedelta
from log_registry import LogRegistry
from src.log_registry import LogRegistry
class LogPruner:
"""

View File

@@ -35,8 +35,8 @@ from typing import Optional, Callable, Any, cast
import os
import ast
import subprocess
import summarize
import outline_tool
from src import summarize
from src import outline_tool
import urllib.request
import urllib.parse
from html.parser import HTMLParser
@@ -254,7 +254,7 @@ def py_get_skeleton(path: str) -> str:
if not p.is_file() or p.suffix != ".py":
return f"ERROR: not a python file: {path}"
try:
from file_cache import ASTParser
from src.file_cache import ASTParser
code = p.read_text(encoding="utf-8")
parser = ASTParser("python")
return parser.get_skeleton(code)

View File

@@ -1,23 +1,23 @@
import ai_client
from src import ai_client
import json
import asyncio
import threading
import time
import traceback
from typing import List, Optional, Tuple
from dataclasses import asdict
import events
from models import Ticket, Track, WorkerContext
from file_cache import ASTParser
from src import events
from src.models import Ticket, Track, WorkerContext
from src.file_cache import ASTParser
from pathlib import Path
from dag_engine import TrackDAG, ExecutionEngine
from src.dag_engine import TrackDAG, ExecutionEngine
class ConductorEngine:
"""
Orchestrates the execution of tickets within a track.
"""
def __init__(self, track: Track, event_queue: Optional[events.AsyncEventQueue] = None, auto_queue: bool = False) -> None:
def __init__(self, track: Track, event_queue: Optional[events.SyncEventQueue] = None, auto_queue: bool = False) -> None:
self.track = track
self.event_queue = event_queue
self.tier_usage = {
@@ -29,7 +29,7 @@ class ConductorEngine:
self.dag = TrackDAG(self.track.tickets)
self.engine = ExecutionEngine(self.dag, auto_queue=auto_queue)
async def _push_state(self, status: str = "running", active_tier: str = None) -> None:
def _push_state(self, status: str = "running", active_tier: str = None) -> None:
if not self.event_queue:
return
payload = {
@@ -42,7 +42,7 @@ class ConductorEngine:
},
"tickets": [asdict(t) for t in self.track.tickets]
}
await self.event_queue.put("mma_state_update", payload)
self.event_queue.put("mma_state_update", payload)
def parse_json_tickets(self, json_str: str) -> None:
"""
@@ -73,14 +73,14 @@ class ConductorEngine:
except KeyError as e:
print(f"Missing required field in ticket definition: {e}")
async def run(self, md_content: str = "") -> None:
def run(self, md_content: str = "") -> None:
"""
Main execution loop using the DAG engine.
Args:
md_content: The full markdown context (history + files) for AI workers.
"""
await self._push_state(status="running", active_tier="Tier 2 (Tech Lead)")
loop = asyncio.get_event_loop()
self._push_state(status="running", active_tier="Tier 2 (Tech Lead)")
while True:
# 1. Identify ready tasks
ready_tasks = self.engine.tick()
@@ -89,15 +89,15 @@ class ConductorEngine:
all_done = all(t.status == "completed" for t in self.track.tickets)
if all_done:
print("Track completed successfully.")
await self._push_state(status="done", active_tier=None)
self._push_state(status="done", active_tier=None)
else:
# Check if any tasks are in-progress or could be ready
if any(t.status == "in_progress" for t in self.track.tickets):
# Wait for async tasks to complete
await asyncio.sleep(1)
# Wait for tasks to complete
time.sleep(1)
continue
print("No more executable tickets. Track is blocked or finished.")
await self._push_state(status="blocked", active_tier=None)
self._push_state(status="blocked", active_tier=None)
break
# 3. Process ready tasks
to_run = [t for t in ready_tasks if t.status == "in_progress" or (not t.step_mode and self.engine.auto_queue)]
@@ -106,15 +106,15 @@ class ConductorEngine:
for ticket in ready_tasks:
if ticket not in to_run and ticket.status == "todo":
print(f"Ticket {ticket.id} is ready and awaiting approval.")
await self._push_state(active_tier=f"Awaiting Approval: {ticket.id}")
await asyncio.sleep(1)
self._push_state(active_tier=f"Awaiting Approval: {ticket.id}")
time.sleep(1)
if to_run:
tasks = []
threads = []
for ticket in to_run:
ticket.status = "in_progress"
print(f"Executing ticket {ticket.id}: {ticket.description}")
await self._push_state(active_tier=f"Tier 3 (Worker): {ticket.id}")
self._push_state(active_tier=f"Tier 3 (Worker): {ticket.id}")
# Escalation logic based on retry_count
models = ["gemini-2.5-flash-lite", "gemini-2.5-flash", "gemini-3.1-pro-preview"]
@@ -127,19 +127,17 @@ class ConductorEngine:
messages=[]
)
context_files = ticket.context_requirements if ticket.context_requirements else None
tasks.append(loop.run_in_executor(
None,
run_worker_lifecycle,
ticket,
context,
context_files,
self.event_queue,
self,
md_content,
loop
))
t = threading.Thread(
target=run_worker_lifecycle,
args=(ticket, context, context_files, self.event_queue, self, md_content),
daemon=True
)
threads.append(t)
t.start()
await asyncio.gather(*tasks)
for t in threads:
t.join()
# 4. Retry and escalation logic
for ticket in to_run:
@@ -149,13 +147,13 @@ class ConductorEngine:
ticket.status = 'todo'
print(f"Ticket {ticket.id} BLOCKED. Escalating to {models[min(ticket.retry_count, len(models)-1)]} and retrying...")
await self._push_state(active_tier="Tier 2 (Tech Lead)")
self._push_state(active_tier="Tier 2 (Tech Lead)")
def _queue_put(event_queue: events.AsyncEventQueue, loop: asyncio.AbstractEventLoop, event_name: str, payload) -> None:
"""Thread-safe helper to push an event to the AsyncEventQueue from a worker thread."""
asyncio.run_coroutine_threadsafe(event_queue.put(event_name, payload), loop)
def _queue_put(event_queue: events.SyncEventQueue, event_name: str, payload) -> None:
"""Thread-safe helper to push an event to the SyncEventQueue from a worker thread."""
event_queue.put(event_name, payload)
def confirm_execution(payload: str, event_queue: events.AsyncEventQueue, ticket_id: str, loop: asyncio.AbstractEventLoop = None) -> bool:
def confirm_execution(payload: str, event_queue: events.SyncEventQueue, ticket_id: str) -> bool:
"""
Pushes an approval request to the GUI and waits for response.
"""
@@ -166,11 +164,9 @@ def confirm_execution(payload: str, event_queue: events.AsyncEventQueue, ticket_
"payload": payload,
"dialog_container": dialog_container
}
if loop:
_queue_put(event_queue, loop, "mma_step_approval", task)
else:
raise RuntimeError("loop is required for thread-safe event queue access")
# Wait for the GUI to create the dialog and for the user to respond
_queue_put(event_queue, "mma_step_approval", task)
# Wait for the GUI to create the dialog and for the user to respond
start = time.time()
while dialog_container[0] is None and time.time() - start < 60:
time.sleep(0.1)
@@ -179,7 +175,7 @@ def confirm_execution(payload: str, event_queue: events.AsyncEventQueue, ticket_
return approved
return False
def confirm_spawn(role: str, prompt: str, context_md: str, event_queue: events.AsyncEventQueue, ticket_id: str, loop: asyncio.AbstractEventLoop = None) -> Tuple[bool, str, str]:
def confirm_spawn(role: str, prompt: str, context_md: str, event_queue: events.SyncEventQueue, ticket_id: str) -> Tuple[bool, str, str]:
"""
Pushes a spawn approval request to the GUI and waits for response.
Returns (approved, modified_prompt, modified_context)
@@ -193,11 +189,9 @@ def confirm_spawn(role: str, prompt: str, context_md: str, event_queue: events.A
"context_md": context_md,
"dialog_container": dialog_container
}
if loop:
_queue_put(event_queue, loop, "mma_spawn_approval", task)
else:
raise RuntimeError("loop is required for thread-safe event queue access")
# Wait for the GUI to create the dialog and for the user to respond
_queue_put(event_queue, "mma_spawn_approval", task)
# Wait for the GUI to create the dialog and for the user to respond
start = time.time()
while dialog_container[0] is None and time.time() - start < 60:
time.sleep(0.1)
@@ -220,7 +214,7 @@ def confirm_spawn(role: str, prompt: str, context_md: str, event_queue: events.A
return approved, modified_prompt, modified_context
return False, prompt, context_md
def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files: List[str] | None = None, event_queue: events.AsyncEventQueue | None = None, engine: Optional['ConductorEngine'] = None, md_content: str = "", loop: asyncio.AbstractEventLoop = None) -> None:
def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files: List[str] | None = None, event_queue: events.SyncEventQueue | None = None, engine: Optional['ConductorEngine'] = None, md_content: str = "") -> None:
"""
Simulates the lifecycle of a single agent working on a ticket.
Calls the AI client and updates the ticket status based on the response.
@@ -231,7 +225,6 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
event_queue: Queue for pushing state updates and receiving approvals.
engine: The conductor engine.
md_content: The markdown context (history + files) for AI workers.
loop: The main asyncio event loop (required for thread-safe queue access).
"""
# Enforce Context Amnesia: each ticket starts with a clean slate.
ai_client.reset_session()
@@ -270,8 +263,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
prompt=user_message,
context_md=md_content,
event_queue=event_queue,
ticket_id=ticket.id,
loop=loop
ticket_id=ticket.id
)
if not approved:
ticket.mark_blocked("Spawn rejected by user.")
@@ -283,15 +275,15 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
def clutch_callback(payload: str) -> bool:
if not event_queue:
return True
return confirm_execution(payload, event_queue, ticket.id, loop=loop)
return confirm_execution(payload, event_queue, ticket.id)
def stream_callback(chunk: str) -> None:
if event_queue and loop:
_queue_put(event_queue, loop, 'mma_stream', {'stream_id': f'Tier 3 (Worker): {ticket.id}', 'text': chunk})
if event_queue:
_queue_put(event_queue, 'mma_stream', {'stream_id': f'Tier 3 (Worker): {ticket.id}', 'text': chunk})
old_comms_cb = ai_client.comms_log_callback
def worker_comms_callback(entry: dict) -> None:
if event_queue and loop:
if event_queue:
kind = entry.get("kind")
payload = entry.get("payload", {})
chunk = ""
@@ -303,7 +295,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
chunk = f"\n[TOOL RESULT]\n{res}\n"
if chunk:
_queue_put(event_queue, loop, "response", {"text": chunk, "stream_id": f"Tier 3 (Worker): {ticket.id}", "status": "streaming..."})
_queue_put(event_queue, "response", {"text": chunk, "stream_id": f"Tier 3 (Worker): {ticket.id}", "status": "streaming..."})
if old_comms_cb:
old_comms_cb(entry)
@@ -331,11 +323,8 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
"stream_id": f"Tier 3 (Worker): {ticket.id}",
"status": "done"
}
print(f"[MMA] Pushing Tier 3 response for {ticket.id}, loop={'present' if loop else 'NONE'}, stream_id={response_payload['stream_id']}")
if loop:
_queue_put(event_queue, loop, "response", response_payload)
else:
raise RuntimeError("loop is required for thread-safe event queue access")
print(f"[MMA] Pushing Tier 3 response for {ticket.id}, stream_id={response_payload['stream_id']}")
_queue_put(event_queue, "response", response_payload)
except Exception as e:
print(f"[MMA] ERROR pushing response to UI: {e}\n{traceback.format_exc()}")

View File

@@ -1,9 +1,9 @@
import json
import ai_client
import mma_prompts
import aggregate
import summarize
from src import ai_client
from src import mma_prompts
from src import aggregate
from src import summarize
from pathlib import Path
from typing import Any, Optional

View File

@@ -71,7 +71,8 @@ def open_session(label: Optional[str] = None) -> None:
_cli_fh.flush()
try:
from log_registry import LogRegistry
from src.log_registry import LogRegistry
registry = LogRegistry(str(_LOG_DIR / "log_registry.toml"))
registry.register_session(_session_id, str(_session_dir), datetime.datetime.now())
except Exception as e:
@@ -99,7 +100,8 @@ def close_session() -> None:
_cli_fh = None
try:
from log_registry import LogRegistry
from src.log_registry import LogRegistry
registry = LogRegistry(str(_LOG_DIR / "log_registry.toml"))
registry.update_auto_whitelist_status(_session_id)
except Exception as e: