feat(mma): Connect ExecutionEngine to ConductorEngine and Tech Lead
This commit is contained in:
@@ -7,11 +7,13 @@ import events
|
||||
from models import Ticket, Track, WorkerContext
|
||||
from file_cache import ASTParser
|
||||
|
||||
from 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):
|
||||
def __init__(self, track: Track, event_queue: Optional[events.AsyncEventQueue] = None, auto_queue: bool = False):
|
||||
self.track = track
|
||||
self.event_queue = event_queue
|
||||
self.tier_usage = {
|
||||
@@ -20,6 +22,8 @@ class ConductorEngine:
|
||||
"Tier 3": {"input": 0, "output": 0},
|
||||
"Tier 4": {"input": 0, "output": 0},
|
||||
}
|
||||
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):
|
||||
if not self.event_queue:
|
||||
@@ -59,58 +63,72 @@ class ConductorEngine:
|
||||
step_mode=ticket_data.get("step_mode", False)
|
||||
)
|
||||
self.track.tickets.append(ticket)
|
||||
|
||||
# Rebuild DAG and Engine after parsing new tickets
|
||||
self.dag = TrackDAG(self.track.tickets)
|
||||
self.engine = ExecutionEngine(self.dag, auto_queue=self.engine.auto_queue)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing JSON tickets: {e}")
|
||||
except KeyError as e:
|
||||
print(f"Missing required field in ticket definition: {e}")
|
||||
|
||||
async def run_linear(self):
|
||||
async def run(self):
|
||||
"""
|
||||
Executes tickets sequentially according to their dependencies.
|
||||
Iterates through the track's executable tickets until no more can be run.
|
||||
Supports dynamic execution as tickets added during runtime will be picked up
|
||||
in the next iteration of the main loop.
|
||||
Main execution loop using the DAG engine.
|
||||
"""
|
||||
await self._push_state(status="running", active_tier="Tier 2 (Tech Lead)")
|
||||
|
||||
while True:
|
||||
executable = self.track.get_executable_tickets()
|
||||
if not executable:
|
||||
# Check if we are finished or blocked
|
||||
# 1. Identify ready tasks
|
||||
ready_tasks = self.engine.tick()
|
||||
|
||||
# 2. Check for completion or blockage
|
||||
if not ready_tasks:
|
||||
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)
|
||||
else:
|
||||
# If we have no executable tickets but some are not completed, we might be blocked
|
||||
# or there are simply no more tickets to run at this moment.
|
||||
incomplete = [t for t in self.track.tickets if t.status != "completed"]
|
||||
if not incomplete:
|
||||
print("Track completed successfully.")
|
||||
await self._push_state(status="done", active_tier=None)
|
||||
else:
|
||||
print(f"No more executable tickets. {len(incomplete)} tickets remain incomplete.")
|
||||
await self._push_state(status="blocked", active_tier=None)
|
||||
# Check if any tasks are in-progress or could be ready
|
||||
if any(t.status == "running" for t in self.track.tickets):
|
||||
# Wait for async tasks to complete
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
print("No more executable tickets. Track is blocked or finished.")
|
||||
await self._push_state(status="blocked", active_tier=None)
|
||||
break
|
||||
|
||||
for ticket in executable:
|
||||
# We re-check status in case it was modified by a parallel/dynamic process
|
||||
# (though run_linear is currently single-threaded)
|
||||
if ticket.status != "todo":
|
||||
continue
|
||||
|
||||
print(f"Executing ticket {ticket.id}: {ticket.description}")
|
||||
ticket.status = "running"
|
||||
await self._push_state(active_tier=f"Tier 3 (Worker): {ticket.id}")
|
||||
# 3. Process ready tasks
|
||||
for ticket in ready_tasks:
|
||||
# If auto_queue is on and step_mode is off, engine.tick() already marked it 'running'
|
||||
# but we need to verify and handle the lifecycle.
|
||||
if ticket.status != "running" and not ticket.step_mode and self.engine.auto_queue:
|
||||
# This shouldn't happen with current ExecutionEngine.tick() but for safety:
|
||||
ticket.status = "running"
|
||||
|
||||
if ticket.status == "running":
|
||||
print(f"Executing ticket {ticket.id}: {ticket.description}")
|
||||
await self._push_state(active_tier=f"Tier 3 (Worker): {ticket.id}")
|
||||
|
||||
context = WorkerContext(
|
||||
ticket_id=ticket.id,
|
||||
model_name="gemini-2.5-flash-lite",
|
||||
messages=[]
|
||||
)
|
||||
# Note: In a fully async version, we would wrap this in a task
|
||||
run_worker_lifecycle(ticket, context, event_queue=self.event_queue, engine=self)
|
||||
await self._push_state(active_tier="Tier 2 (Tech Lead)")
|
||||
|
||||
# For now, we use a default model name or take it from config
|
||||
context = WorkerContext(
|
||||
ticket_id=ticket.id,
|
||||
model_name="gemini-2.5-flash-lite",
|
||||
messages=[]
|
||||
)
|
||||
run_worker_lifecycle(ticket, context, event_queue=self.event_queue, engine=self)
|
||||
await self._push_state(active_tier="Tier 2 (Tech Lead)")
|
||||
elif ticket.status == "todo" and (ticket.step_mode or not self.engine.auto_queue):
|
||||
# Task is ready but needs approval
|
||||
print(f"Ticket {ticket.id} is ready and awaiting approval.")
|
||||
await self._push_state(active_tier=f"Awaiting Approval: {ticket.id}")
|
||||
# In a real UI, this would wait for a user event.
|
||||
# For headless/linear run, we might need a signal.
|
||||
# For now, we'll treat it as a pause point if not auto-queued.
|
||||
pass
|
||||
|
||||
def confirm_execution(payload: str, event_queue: events.AsyncEventQueue, ticket_id: str) -> bool:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user