From 8784d05db4952ac1fd75cd8fbfe951626cf265cf Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 5 Mar 2026 15:10:53 -0500 Subject: [PATCH] WIP: PAIN3 --- project_history.toml | 2 +- src/ai_client.py | 6 +++- src/api_hook_client.py | 4 +-- src/app_controller.py | 43 +++++++++++++++------- src/project_manager.py | 4 +-- tests/test_conductor_engine.py | 51 +++++++++++++-------------- tests/test_gui_phase4.py | 2 +- tests/test_mma_models.py | 2 +- tests/test_project_manager_tracks.py | 4 +-- tests/test_spawn_interception.py | 2 +- tests/test_track_state_persistence.py | 2 +- tests/test_track_state_schema.py | 2 +- 12 files changed, 72 insertions(+), 52 deletions(-) diff --git a/project_history.toml b/project_history.toml index 9952a33..c55280c 100644 --- a/project_history.toml +++ b/project_history.toml @@ -8,5 +8,5 @@ active = "main" [discussions.main] git_commit = "" -last_updated = "2026-03-05T14:39:44" +last_updated = "2026-03-05T14:42:31" history = [] diff --git a/src/ai_client.py b/src/ai_client.py index e097762..547228b 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -290,7 +290,11 @@ def reset_session() -> None: _gemini_cache = None _gemini_cache_md_hash = None _gemini_cache_created_at = None - _gemini_cli_adapter = None + + # Preserve binary_path if adapter exists + old_path = _gemini_cli_adapter.binary_path if _gemini_cli_adapter else "gemini" + _gemini_cli_adapter = GeminiCliAdapter(binary_path=old_path) + _anthropic_client = None with _anthropic_history_lock: diff --git a/src/api_hook_client.py b/src/api_hook_client.py index 72686d9..139dfa8 100644 --- a/src/api_hook_client.py +++ b/src/api_hook_client.py @@ -137,8 +137,8 @@ class ApiHookClient: return {"performance": diag} def get_mma_status(self) -> dict[str, Any]: - """Convenience to get the current MMA engine status. Returns FULL state.""" - return self.get_gui_state() + """Retrieves the dedicated MMA engine status.""" + return self._make_request('GET', '/api/gui/mma_status') or {} def get_node_status(self, node_id: str) -> dict[str, Any]: """Retrieves status for a specific node in the MMA DAG.""" diff --git a/src/app_controller.py b/src/app_controller.py index b77ca5b..c73399c 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -298,6 +298,7 @@ class AppController: 'show_confirm_modal': 'show_confirm_modal', 'mma_epic_input': 'ui_epic_input', 'mma_status': 'mma_status', + 'ai_status': 'ai_status', 'mma_active_tier': 'active_tier', 'ui_new_track_name': 'ui_new_track_name', 'ui_new_track_desc': 'ui_new_track_desc', @@ -409,10 +410,10 @@ class AppController: "collapsed": False, "ts": project_manager.now_ts() }) - elif action == "mma_stream_append": - payload = task.get("payload", {}) - stream_id = payload.get("stream_id") - text = payload.get("text", "") + elif action in ("mma_stream", "mma_stream_append"): + # Some events might have these at top level, some in a 'payload' dict + stream_id = task.get("stream_id") or task.get("payload", {}).get("stream_id") + text = task.get("text") or task.get("payload", {}).get("text", "") if stream_id: if stream_id not in self.mma_streams: self.mma_streams[stream_id] = "" @@ -421,7 +422,11 @@ class AppController: self.proposed_tracks = task.get("payload", []) self._show_track_proposal_modal = True elif action == "mma_state_update": - payload = task.get("payload", {}) + # Handle both internal (nested) and hook-server (flattened) payloads + payload = task.get("payload") + if not isinstance(payload, dict): + payload = task # Fallback to task if payload missing or wrong type + 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) @@ -783,12 +788,11 @@ class AppController: break if event_name == "user_request": threading.Thread(target=self._handle_request_event, args=(payload,), daemon=True).start() - elif event_name == "response": + elif event_name == "gui_task": with self._pending_gui_tasks_lock: - self._pending_gui_tasks.append({ - "action": "handle_ai_response", - "payload": payload - }) + # Directly append the task from the hook server. + # It already contains 'action' and any necessary fields. + self._pending_gui_tasks.append(payload) elif event_name == "mma_state_update": with self._pending_gui_tasks_lock: self._pending_gui_tasks.append({ @@ -803,6 +807,7 @@ class AppController: }) elif event_name in ("mma_spawn_approval", "mma_step_approval"): with self._pending_gui_tasks_lock: + # These payloads already contain the 'action' field self._pending_gui_tasks.append(payload) def _handle_request_event(self, event: events.UserRequestEvent) -> None: @@ -1679,13 +1684,17 @@ class AppController: def _cb_accept_tracks(self) -> None: self._show_track_proposal_modal = False def _bg_task() -> None: + sys.stderr.write("[DEBUG] _cb_accept_tracks _bg_task started\n") # Generate skeletons once self._set_status("Phase 2: Generating skeletons for all tracks...") - parser = file_cache.ASTParser(language="python") + sys.stderr.write("[DEBUG] Creating ASTParser...\n") + parser = ASTParser(language="python") + generated_skeletons = "" try: # Use a local copy of files to avoid concurrent modification issues files_to_scan = list(self.files) + sys.stderr.write(f"[DEBUG] Scanning {len(files_to_scan)} files for skeletons...\n") for i, file_path in enumerate(files_to_scan): try: self._set_status(f"Phase 2: Scanning files ({i+1}/{len(files_to_scan)})...") @@ -1695,12 +1704,13 @@ class AppController: code = f.read() generated_skeletons += f"\nFile: {file_path}\n{parser.get_skeleton(code)}\n" except Exception as e: - print(f"Error parsing skeleton for {file_path}: {e}") + sys.stderr.write(f"[DEBUG] Error parsing skeleton for {file_path}: {e}\n") except Exception as e: + sys.stderr.write(f"[DEBUG] Error in scan loop: {e}\n") self._set_status(f"Error generating skeletons: {e}") - print(f"Error generating skeletons: {e}") return # Exit if skeleton generation fails + sys.stderr.write("[DEBUG] Skeleton generation complete. Starting tracks...\n") # Now loop through tracks and call _start_track_logic with generated skeletons total_tracks = len(self.proposed_tracks) for i, track_data in enumerate(self.proposed_tracks): @@ -1708,6 +1718,7 @@ class AppController: self._set_status(f"Processing track {i+1} of {total_tracks}: '{title}'...") self._start_track_logic(track_data, skeletons_str=generated_skeletons) # Pass skeletons + sys.stderr.write("[DEBUG] All tracks started. Refreshing...\n") with self._pending_gui_tasks_lock: self._pending_gui_tasks.append({'action': 'refresh_from_project'}) # Ensure UI refresh after tracks are started self._set_status(f"All {total_tracks} tracks accepted and execution started.") @@ -1797,6 +1808,12 @@ class AppController: 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) + + # Add to memory and notify UI + self.tracks.append({"id": track_id, "title": title, "status": "todo"}) + with self._pending_gui_tasks_lock: + self._pending_gui_tasks.append({'action': 'refresh_from_project'}) + # 4. Initialize ConductorEngine and run loop engine = multi_agent_conductor.ConductorEngine(track, self.event_queue, auto_queue=not self.mma_step_mode) # Use current full markdown context for the track execution diff --git a/src/project_manager.py b/src/project_manager.py index 0356741..4c2ab47 100644 --- a/src/project_manager.py +++ b/src/project_manager.py @@ -14,7 +14,7 @@ import json from typing import Any, Optional, TYPE_CHECKING, Union from pathlib import Path if TYPE_CHECKING: - from models import TrackState + from src.models import TrackState TS_FMT: str = "%Y-%m-%dT%H:%M:%S" def now_ts() -> str: @@ -248,7 +248,7 @@ def load_track_state(track_id: str, base_dir: Union[str, Path] = ".") -> Optiona """ Loads a TrackState object from conductor/tracks//state.toml. """ - from models import TrackState + from src.models import TrackState state_file = Path(base_dir) / "conductor" / "tracks" / track_id / "state.toml" if not state_file.exists(): return None diff --git a/tests/test_conductor_engine.py b/tests/test_conductor_engine.py index 66e3a64..9a9da1f 100644 --- a/tests/test_conductor_engine.py +++ b/tests/test_conductor_engine.py @@ -1,7 +1,7 @@ -import pytest +import pytest from unittest.mock import MagicMock, patch -from models import Ticket, Track, WorkerContext -import ai_client +from src.models import Ticket, Track, WorkerContext +from src import ai_client # These tests define the expected interface for multi_agent_conductor.py # which will be implemented in the next phase of TDD. @@ -11,7 +11,7 @@ def test_conductor_engine_initialization() -> None: Test that ConductorEngine can be initialized with a Track. """ track = Track(id="test_track", description="Test Track") - from multi_agent_conductor import ConductorEngine + from src.multi_agent_conductor import ConductorEngine engine = ConductorEngine(track=track, auto_queue=True) assert engine.track == track @@ -23,7 +23,7 @@ async def test_conductor_engine_run_executes_tickets_in_order(monkeypatch: pytes ticket1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1") ticket2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker2", depends_on=["T1"]) track = Track(id="track1", description="Track 1", tickets=[ticket1, ticket2]) - from multi_agent_conductor import ConductorEngine + from src.multi_agent_conductor import ConductorEngine engine = ConductorEngine(track=track, auto_queue=True) vlogger.log_state("Ticket Count", 0, 2) @@ -34,7 +34,7 @@ async def test_conductor_engine_run_executes_tickets_in_order(monkeypatch: pytes mock_send = MagicMock() monkeypatch.setattr(ai_client, 'send', mock_send) # We mock run_worker_lifecycle as it is expected to be in the same module - with patch("multi_agent_conductor.run_worker_lifecycle") as mock_lifecycle: + with patch("src.multi_agent_conductor.run_worker_lifecycle") as mock_lifecycle: # Mocking lifecycle to mark ticket as complete so dependencies can be resolved def side_effect(ticket, context, *args, **kwargs): @@ -64,7 +64,7 @@ async def test_run_worker_lifecycle_calls_ai_client_send(monkeypatch: pytest.Mon """ ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1") context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[]) - from multi_agent_conductor import run_worker_lifecycle + from src.multi_agent_conductor import run_worker_lifecycle # Mock ai_client.send using monkeypatch mock_send = MagicMock() monkeypatch.setattr(ai_client, 'send', mock_send) @@ -86,12 +86,12 @@ async def test_run_worker_lifecycle_context_injection(monkeypatch: pytest.Monkey ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1") context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[]) context_files = ["primary.py", "secondary.py"] - from multi_agent_conductor import run_worker_lifecycle + from src.multi_agent_conductor import run_worker_lifecycle # Mock ai_client.send using monkeypatch mock_send = MagicMock() monkeypatch.setattr(ai_client, 'send', mock_send) # We mock ASTParser which is expected to be imported in multi_agent_conductor - with patch("multi_agent_conductor.ASTParser") as mock_ast_parser_class, \ + with patch("src.multi_agent_conductor.ASTParser") as mock_ast_parser_class, \ patch("builtins.open", new_callable=MagicMock) as mock_open: # Setup open mock to return different content for different files file_contents = { @@ -131,7 +131,7 @@ async def test_run_worker_lifecycle_handles_blocked_response(monkeypatch: pytest """ ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1") context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[]) - from multi_agent_conductor import run_worker_lifecycle + from src.multi_agent_conductor import run_worker_lifecycle # Mock ai_client.send using monkeypatch mock_send = MagicMock() monkeypatch.setattr(ai_client, 'send', mock_send) @@ -150,14 +150,14 @@ async def test_run_worker_lifecycle_step_mode_confirmation(monkeypatch: pytest.M """ ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1", step_mode=True) context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[]) - from multi_agent_conductor import run_worker_lifecycle + from src.multi_agent_conductor import run_worker_lifecycle # Mock ai_client.send using monkeypatch mock_send = MagicMock() monkeypatch.setattr(ai_client, 'send', mock_send) # Important: confirm_spawn is called first if event_queue is present! - with patch("multi_agent_conductor.confirm_spawn") as mock_spawn, \ - patch("multi_agent_conductor.confirm_execution") as mock_confirm: + with patch("src.multi_agent_conductor.confirm_spawn") as mock_spawn, \ + patch("src.multi_agent_conductor.confirm_execution") as mock_confirm: mock_spawn.return_value = (True, "mock prompt", "mock context") mock_confirm.return_value = True @@ -186,12 +186,12 @@ async def test_run_worker_lifecycle_step_mode_rejection(monkeypatch: pytest.Monk """ ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1", step_mode=True) context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[]) - from multi_agent_conductor import run_worker_lifecycle + from src.multi_agent_conductor import run_worker_lifecycle # Mock ai_client.send using monkeypatch mock_send = MagicMock() monkeypatch.setattr(ai_client, 'send', mock_send) - with patch("multi_agent_conductor.confirm_spawn") as mock_spawn, \ - patch("multi_agent_conductor.confirm_execution") as mock_confirm: + with patch("src.multi_agent_conductor.confirm_spawn") as mock_spawn, \ + patch("src.multi_agent_conductor.confirm_execution") as mock_confirm: mock_spawn.return_value = (True, "mock prompt", "mock context") mock_confirm.return_value = False mock_send.return_value = "Task failed because tool execution was rejected." @@ -209,7 +209,7 @@ async def test_conductor_engine_dynamic_parsing_and_execution(monkeypatch: pytes Test that parse_json_tickets correctly populates the track and run executes them in dependency order. """ import json - from multi_agent_conductor import ConductorEngine + from src.multi_agent_conductor import ConductorEngine track = Track(id="dynamic_track", description="Dynamic Track") engine = ConductorEngine(track=track, auto_queue=True) tickets_json = json.dumps([ @@ -246,7 +246,7 @@ async def test_conductor_engine_dynamic_parsing_and_execution(monkeypatch: pytes mock_send = MagicMock() monkeypatch.setattr(ai_client, 'send', mock_send) # Mock run_worker_lifecycle to mark tickets as complete - with patch("multi_agent_conductor.run_worker_lifecycle") as mock_lifecycle: + with patch("src.multi_agent_conductor.run_worker_lifecycle") as mock_lifecycle: def side_effect(ticket, context, *args, **kwargs): ticket.mark_complete() return "Success" @@ -279,9 +279,9 @@ def test_run_worker_lifecycle_pushes_response_via_queue(monkeypatch: pytest.Monk mock_send = MagicMock(return_value="Task complete.") monkeypatch.setattr(ai_client, 'send', mock_send) monkeypatch.setattr(ai_client, 'reset_session', MagicMock()) - from multi_agent_conductor import run_worker_lifecycle - with patch("multi_agent_conductor.confirm_spawn") as mock_spawn, \ - patch("multi_agent_conductor._queue_put") as mock_queue_put: + from src.multi_agent_conductor import run_worker_lifecycle + with patch("src.multi_agent_conductor.confirm_spawn") as mock_spawn, \ + patch("src.multi_agent_conductor._queue_put") as mock_queue_put: mock_spawn.return_value = (True, "prompt", "context") run_worker_lifecycle(ticket, context, event_queue=mock_event_queue, loop=mock_loop) mock_queue_put.assert_called_once() @@ -309,14 +309,13 @@ def test_run_worker_lifecycle_token_usage_from_comms_log(monkeypatch: pytest.Mon [], # baseline call (before send) fake_comms, # after-send call ])) - from multi_agent_conductor import run_worker_lifecycle, ConductorEngine - from models import Track + from src.multi_agent_conductor import run_worker_lifecycle, ConductorEngine + from src.models import Track track = Track(id="test_track", description="Test") engine = ConductorEngine(track=track, auto_queue=True) - with patch("multi_agent_conductor.confirm_spawn") as mock_spawn, \ - patch("multi_agent_conductor._queue_put"): + with patch("src.multi_agent_conductor.confirm_spawn") as mock_spawn, \ + patch("src.multi_agent_conductor._queue_put"): mock_spawn.return_value = (True, "prompt", "ctx") run_worker_lifecycle(ticket, context, event_queue=MagicMock(), loop=MagicMock(), engine=engine) assert engine.tier_usage["Tier 3"]["input"] == 120 assert engine.tier_usage["Tier 3"]["output"] == 45 - diff --git a/tests/test_gui_phase4.py b/tests/test_gui_phase4.py index 1fb91eb..7d463c4 100644 --- a/tests/test_gui_phase4.py +++ b/tests/test_gui_phase4.py @@ -2,7 +2,7 @@ import pytest from unittest.mock import MagicMock, patch from gui_2 import App -from models import Track +from src.models import Track @pytest.fixture(autouse=True) def setup_mock_app(mock_app: App): diff --git a/tests/test_mma_models.py b/tests/test_mma_models.py index a2bcfc9..ba01ca7 100644 --- a/tests/test_mma_models.py +++ b/tests/test_mma_models.py @@ -1,4 +1,4 @@ -from models import Ticket, Track, WorkerContext +from src.models import Ticket, Track, WorkerContext def test_ticket_instantiation() -> None: """ diff --git a/tests/test_project_manager_tracks.py b/tests/test_project_manager_tracks.py index e52d282..2b91959 100644 --- a/tests/test_project_manager_tracks.py +++ b/tests/test_project_manager_tracks.py @@ -1,8 +1,8 @@ import pytest from typing import Any import json -from project_manager import get_all_tracks, save_track_state -from models import TrackState, Metadata, Ticket +from src.project_manager import get_all_tracks, save_track_state +from src.models import TrackState, Metadata, Ticket from datetime import datetime def test_get_all_tracks_empty(tmp_path: Any) -> None: diff --git a/tests/test_spawn_interception.py b/tests/test_spawn_interception.py index 0bd4963..ec48cdf 100644 --- a/tests/test_spawn_interception.py +++ b/tests/test_spawn_interception.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import MagicMock, patch import multi_agent_conductor -from models import Ticket, WorkerContext +from src.models import Ticket, WorkerContext import events import asyncio import concurrent.futures diff --git a/tests/test_track_state_persistence.py b/tests/test_track_state_persistence.py index 648c29e..0fb136e 100644 --- a/tests/test_track_state_persistence.py +++ b/tests/test_track_state_persistence.py @@ -1,7 +1,7 @@ from datetime import datetime # Import the real models -from models import TrackState, Metadata, Ticket +from src.models import TrackState, Metadata, Ticket # Import the persistence functions from project_manager from project_manager import save_track_state, load_track_state diff --git a/tests/test_track_state_schema.py b/tests/test_track_state_schema.py index bc66561..7b37439 100644 --- a/tests/test_track_state_schema.py +++ b/tests/test_track_state_schema.py @@ -1,7 +1,7 @@ from datetime import datetime, timezone, timedelta # Import necessary classes from models.py -from models import Metadata, TrackState, Ticket +from src.models import Metadata, TrackState, Ticket # --- Pytest Tests ---