feat(mma): Connect ExecutionEngine to ConductorEngine and Tech Lead
This commit is contained in:
@@ -56,43 +56,29 @@ def generate_tickets(track_brief: str, module_skeletons: str) -> list[dict]:
|
|||||||
# Restore old system prompt
|
# Restore old system prompt
|
||||||
ai_client.set_custom_system_prompt(old_system_prompt)
|
ai_client.set_custom_system_prompt(old_system_prompt)
|
||||||
|
|
||||||
|
from dag_engine import TrackDAG
|
||||||
|
from models import Ticket
|
||||||
|
|
||||||
def topological_sort(tickets: list[dict]) -> list[dict]:
|
def topological_sort(tickets: list[dict]) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Sorts a list of tickets based on their 'depends_on' field.
|
Sorts a list of tickets based on their 'depends_on' field.
|
||||||
Raises ValueError if a circular dependency or missing internal dependency is detected.
|
Raises ValueError if a circular dependency or missing internal dependency is detected.
|
||||||
"""
|
"""
|
||||||
# 1. Map ID to ticket and build graph
|
# 1. Convert to Ticket objects for TrackDAG
|
||||||
|
ticket_objs = []
|
||||||
|
for t_data in tickets:
|
||||||
|
ticket_objs.append(Ticket.from_dict(t_data))
|
||||||
|
|
||||||
|
# 2. Use TrackDAG for validation and sorting
|
||||||
|
dag = TrackDAG(ticket_objs)
|
||||||
|
try:
|
||||||
|
sorted_ids = dag.topological_sort()
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(f"DAG Validation Error: {e}")
|
||||||
|
|
||||||
|
# 3. Return sorted dictionaries
|
||||||
ticket_map = {t['id']: t for t in tickets}
|
ticket_map = {t['id']: t for t in tickets}
|
||||||
adj = {t['id']: [] for t in tickets}
|
return [ticket_map[tid] for tid in sorted_ids]
|
||||||
in_degree = {t['id']: 0 for t in tickets}
|
|
||||||
|
|
||||||
for t in tickets:
|
|
||||||
for dep_id in t.get('depends_on', []):
|
|
||||||
if dep_id not in ticket_map:
|
|
||||||
raise ValueError(f"Missing dependency: Ticket '{t['id']}' depends on '{dep_id}', but '{dep_id}' is not in the ticket list.")
|
|
||||||
adj[dep_id].append(t['id'])
|
|
||||||
in_degree[t['id']] += 1
|
|
||||||
|
|
||||||
# 2. Find nodes with in-degree 0
|
|
||||||
queue = [t['id'] for t in tickets if in_degree[t['id']] == 0]
|
|
||||||
sorted_ids = []
|
|
||||||
|
|
||||||
# 3. Process queue
|
|
||||||
while queue:
|
|
||||||
u_id = queue.pop(0)
|
|
||||||
sorted_ids.append(u_id)
|
|
||||||
for v_id in adj[u_id]:
|
|
||||||
in_degree[v_id] -= 1
|
|
||||||
if in_degree[v_id] == 0:
|
|
||||||
queue.append(v_id)
|
|
||||||
|
|
||||||
# 4. Check for cycles
|
|
||||||
if len(sorted_ids) != len(tickets):
|
|
||||||
# Find which tickets are part of a cycle (or blocked by one)
|
|
||||||
remaining = [t_id for t_id in ticket_map if t_id not in sorted_ids]
|
|
||||||
raise ValueError(f"Circular dependency detected among tickets: {remaining}")
|
|
||||||
|
|
||||||
return [ticket_map[t_id] for t_id in sorted_ids]
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Quick test if run directly
|
# Quick test if run directly
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import events
|
|||||||
from models import Ticket, Track, WorkerContext
|
from models import Ticket, Track, WorkerContext
|
||||||
from file_cache import ASTParser
|
from file_cache import ASTParser
|
||||||
|
|
||||||
|
from dag_engine import TrackDAG, ExecutionEngine
|
||||||
|
|
||||||
class ConductorEngine:
|
class ConductorEngine:
|
||||||
"""
|
"""
|
||||||
Orchestrates the execution of tickets within a track.
|
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.track = track
|
||||||
self.event_queue = event_queue
|
self.event_queue = event_queue
|
||||||
self.tier_usage = {
|
self.tier_usage = {
|
||||||
@@ -20,6 +22,8 @@ class ConductorEngine:
|
|||||||
"Tier 3": {"input": 0, "output": 0},
|
"Tier 3": {"input": 0, "output": 0},
|
||||||
"Tier 4": {"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):
|
async def _push_state(self, status: str = "running", active_tier: str = None):
|
||||||
if not self.event_queue:
|
if not self.event_queue:
|
||||||
@@ -59,58 +63,72 @@ class ConductorEngine:
|
|||||||
step_mode=ticket_data.get("step_mode", False)
|
step_mode=ticket_data.get("step_mode", False)
|
||||||
)
|
)
|
||||||
self.track.tickets.append(ticket)
|
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:
|
except json.JSONDecodeError as e:
|
||||||
print(f"Error parsing JSON tickets: {e}")
|
print(f"Error parsing JSON tickets: {e}")
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
print(f"Missing required field in ticket definition: {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.
|
Main execution loop using the DAG engine.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
await self._push_state(status="running", active_tier="Tier 2 (Tech Lead)")
|
await self._push_state(status="running", active_tier="Tier 2 (Tech Lead)")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
executable = self.track.get_executable_tickets()
|
# 1. Identify ready tasks
|
||||||
if not executable:
|
ready_tasks = self.engine.tick()
|
||||||
# Check if we are finished or blocked
|
|
||||||
|
# 2. Check for completion or blockage
|
||||||
|
if not ready_tasks:
|
||||||
all_done = all(t.status == "completed" for t in self.track.tickets)
|
all_done = all(t.status == "completed" for t in self.track.tickets)
|
||||||
if all_done:
|
if all_done:
|
||||||
print("Track completed successfully.")
|
print("Track completed successfully.")
|
||||||
await self._push_state(status="done", active_tier=None)
|
await self._push_state(status="done", active_tier=None)
|
||||||
else:
|
else:
|
||||||
# If we have no executable tickets but some are not completed, we might be blocked
|
# Check if any tasks are in-progress or could be ready
|
||||||
# or there are simply no more tickets to run at this moment.
|
if any(t.status == "running" for t in self.track.tickets):
|
||||||
incomplete = [t for t in self.track.tickets if t.status != "completed"]
|
# Wait for async tasks to complete
|
||||||
if not incomplete:
|
await asyncio.sleep(1)
|
||||||
print("Track completed successfully.")
|
continue
|
||||||
await self._push_state(status="done", active_tier=None)
|
|
||||||
else:
|
print("No more executable tickets. Track is blocked or finished.")
|
||||||
print(f"No more executable tickets. {len(incomplete)} tickets remain incomplete.")
|
await self._push_state(status="blocked", active_tier=None)
|
||||||
await self._push_state(status="blocked", active_tier=None)
|
|
||||||
break
|
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}")
|
# 3. Process ready tasks
|
||||||
ticket.status = "running"
|
for ticket in ready_tasks:
|
||||||
await self._push_state(active_tier=f"Tier 3 (Worker): {ticket.id}")
|
# 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
|
elif ticket.status == "todo" and (ticket.step_mode or not self.engine.auto_queue):
|
||||||
context = WorkerContext(
|
# Task is ready but needs approval
|
||||||
ticket_id=ticket.id,
|
print(f"Ticket {ticket.id} is ready and awaiting approval.")
|
||||||
model_name="gemini-2.5-flash-lite",
|
await self._push_state(active_tier=f"Awaiting Approval: {ticket.id}")
|
||||||
messages=[]
|
# In a real UI, this would wait for a user event.
|
||||||
)
|
# For headless/linear run, we might need a signal.
|
||||||
run_worker_lifecycle(ticket, context, event_queue=self.event_queue, engine=self)
|
# For now, we'll treat it as a pause point if not auto-queued.
|
||||||
await self._push_state(active_tier="Tier 2 (Tech Lead)")
|
pass
|
||||||
|
|
||||||
def confirm_execution(payload: str, event_queue: events.AsyncEventQueue, ticket_id: str) -> bool:
|
def confirm_execution(payload: str, event_queue: events.AsyncEventQueue, ticket_id: str) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user