perf(core): Optimize DAG engine, orchestrator loop, and simulations
This commit is contained in:
@@ -14,10 +14,10 @@
|
|||||||
- [x] Task: Conductor - User Manual Verification 'Phase 2: Audit and Profiling (`src/` and `simulation/`)' (Protocol in workflow.md) (7a72987)
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: Audit and Profiling (`src/` and `simulation/`)' (Protocol in workflow.md) (7a72987)
|
||||||
|
|
||||||
## Phase 3: Targeted Optimization and Refactoring
|
## Phase 3: Targeted Optimization and Refactoring
|
||||||
- [ ] Task: Write/update tests for the first identified bottleneck to establish a performance or structural baseline (Red Phase).
|
- [x] Task: Write/update tests for the first identified bottleneck to establish a performance or structural baseline (Red Phase). (2e68f1e)
|
||||||
- [ ] Task: Refactor the first identified bottleneck to align with data-oriented guidelines (Green Phase).
|
- [x] Task: Refactor the first identified bottleneck to align with data-oriented guidelines (Green Phase). (2e68f1e)
|
||||||
- [ ] Task: Write/update tests for remaining identified bottlenecks.
|
- [x] Task: Write/update tests for remaining identified bottlenecks. (56e9627)
|
||||||
- [ ] Task: Refactor remaining identified bottlenecks.
|
- [x] Task: Refactor remaining identified bottlenecks. (d0aff71)
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Targeted Optimization and Refactoring' (Protocol in workflow.md)
|
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Targeted Optimization and Refactoring' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 4: Final Evaluation and Documentation
|
## Phase 4: Final Evaluation and Documentation
|
||||||
|
|||||||
+18
-13
@@ -4,10 +4,11 @@ from typing import Any, Callable
|
|||||||
from src import ai_client
|
from src import ai_client
|
||||||
|
|
||||||
class UserSimAgent:
|
class UserSimAgent:
|
||||||
def __init__(self, hook_client: Any, model: str = "gemini-2.5-flash-lite", enable_delays: bool = True) -> None:
|
def __init__(self, hook_client: Any, model: str = "gemini-2.5-flash-lite", enable_delays: bool = True, batch_typing: bool = False) -> None:
|
||||||
self.hook_client = hook_client
|
self.hook_client = hook_client
|
||||||
self.model = model
|
self.model = model
|
||||||
self.enable_delays = enable_delays
|
self.enable_delays = enable_delays
|
||||||
|
self.batch_typing = batch_typing
|
||||||
self.system_prompt = (
|
self.system_prompt = (
|
||||||
"You are a software engineer testing an AI coding assistant called 'Manual Slop'. "
|
"You are a software engineer testing an AI coding assistant called 'Manual Slop'. "
|
||||||
"You want to build a small Python project and verify the assistant's capabilities. "
|
"You want to build a small Python project and verify the assistant's capabilities. "
|
||||||
@@ -30,18 +31,22 @@ class UserSimAgent:
|
|||||||
delay = random.uniform(min_delay, max_delay)
|
delay = random.uniform(min_delay, max_delay)
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
|
||||||
def simulate_typing(self, text: str, jitter_range: tuple[float, float] = (0.01, 0.05)) -> None:
|
def simulate_typing(self, text: str, jitter_range: tuple[float, float] = (0.01, 0.05), batch_typing: bool = False) -> None:
|
||||||
if self.enable_delays:
|
if not self.enable_delays:
|
||||||
# Simulate typing by sleeping after chunks or characters to balance speed and realism
|
return
|
||||||
if len(text) > 200:
|
if batch_typing or self.batch_typing:
|
||||||
for i in range(0, len(text), 10):
|
time.sleep(0.01)
|
||||||
time.sleep(random.uniform(jitter_range[0] * 3, jitter_range[1] * 3))
|
return
|
||||||
elif len(text) > 50:
|
# Simulate typing by sleeping after chunks or characters to balance speed and realism
|
||||||
for i in range(0, len(text), 3):
|
if len(text) > 200:
|
||||||
time.sleep(random.uniform(jitter_range[0] * 1.5, jitter_range[1] * 1.5))
|
for i in range(0, len(text), 10):
|
||||||
else:
|
time.sleep(random.uniform(jitter_range[0] * 3, jitter_range[1] * 3))
|
||||||
for char in text:
|
elif len(text) > 50:
|
||||||
time.sleep(random.uniform(jitter_range[0], jitter_range[1]))
|
for i in range(0, len(text), 3):
|
||||||
|
time.sleep(random.uniform(jitter_range[0] * 1.5, jitter_range[1] * 1.5))
|
||||||
|
else:
|
||||||
|
for char in text:
|
||||||
|
time.sleep(random.uniform(jitter_range[0], jitter_range[1]))
|
||||||
|
|
||||||
def generate_response(self, conversation_history: list[dict]) -> str:
|
def generate_response(self, conversation_history: list[dict]) -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
+66
-50
@@ -48,19 +48,29 @@ class TrackDAG:
|
|||||||
def cascade_blocks(self) -> None:
|
def cascade_blocks(self) -> None:
|
||||||
"""
|
"""
|
||||||
Transitively marks `todo` tickets as `blocked` if any dependency is `blocked`.
|
Transitively marks `todo` tickets as `blocked` if any dependency is `blocked`.
|
||||||
Runs until stable (handles multi-hop chains: A→B→C where A blocked cascades to B then C).
|
Propagates 'blocked' status from initially blocked nodes to their dependents.
|
||||||
"""
|
"""
|
||||||
changed = True
|
with get_monitor().scope("dag_cascade_blocks"):
|
||||||
while changed:
|
# Build adjacency list of dependents using object references to avoid lookups
|
||||||
changed = False
|
dependents = {t.id: [] for t in self.tickets}
|
||||||
for ticket in self.tickets:
|
for t in self.tickets:
|
||||||
if ticket.status == 'todo':
|
for dep_id in t.depends_on:
|
||||||
for dep_id in ticket.depends_on:
|
if dep_id in dependents:
|
||||||
dep = self.ticket_map.get(dep_id)
|
dependents[dep_id].append(t)
|
||||||
if dep and dep.status == 'blocked':
|
|
||||||
ticket.status = 'blocked'
|
# Use a queue-based propagation (BFS) from all currently blocked tickets
|
||||||
changed = True
|
queue = [t for t in self.tickets if t.status == 'blocked']
|
||||||
break
|
idx = 0
|
||||||
|
while idx < len(queue):
|
||||||
|
curr = queue[idx]
|
||||||
|
idx += 1
|
||||||
|
for dep_ticket in dependents.get(curr.id, []):
|
||||||
|
if dep_ticket.status == 'todo':
|
||||||
|
dep_ticket.status = 'blocked'
|
||||||
|
# Optional: preserve the reason for blocking
|
||||||
|
if not dep_ticket.blocked_reason:
|
||||||
|
dep_ticket.blocked_reason = f"Dependency {curr.id} is blocked."
|
||||||
|
queue.append(dep_ticket)
|
||||||
|
|
||||||
def is_ticket_ready(self, ticket: Ticket) -> bool:
|
def is_ticket_ready(self, ticket: Ticket) -> bool:
|
||||||
"""Returns True if all dependencies of the ticket are completed."""
|
"""Returns True if all dependencies of the ticket are completed."""
|
||||||
@@ -84,62 +94,68 @@ class TrackDAG:
|
|||||||
|
|
||||||
def has_cycle(self) -> bool:
|
def has_cycle(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Performs a Depth-First Search to detect cycles in the dependency graph.
|
Performs an iterative Depth-First Search to detect cycles in the dependency graph.
|
||||||
Returns:
|
Returns:
|
||||||
True if a cycle is detected, False otherwise.
|
True if a cycle is detected, False otherwise.
|
||||||
"""
|
"""
|
||||||
with get_monitor().scope("dag_has_cycle"):
|
with get_monitor().scope("dag_has_cycle"):
|
||||||
visited = set()
|
visited = set()
|
||||||
rec_stack = set()
|
for start_ticket in self.tickets:
|
||||||
|
if start_ticket.id in visited:
|
||||||
def is_cyclic(ticket_id: str) -> bool:
|
continue
|
||||||
"""Internal recursive helper for cycle detection."""
|
stack = [(start_ticket.id, False)] # (id, is_backtracking)
|
||||||
if ticket_id in rec_stack:
|
path = set()
|
||||||
return True
|
while stack:
|
||||||
if ticket_id in visited:
|
node_id, is_backtracking = stack.pop()
|
||||||
return False
|
if is_backtracking:
|
||||||
visited.add(ticket_id)
|
path.remove(node_id)
|
||||||
rec_stack.add(ticket_id)
|
continue
|
||||||
ticket = self.ticket_map.get(ticket_id)
|
if node_id in path:
|
||||||
if ticket:
|
|
||||||
for neighbor in ticket.depends_on:
|
|
||||||
if is_cyclic(neighbor):
|
|
||||||
return True
|
|
||||||
rec_stack.remove(ticket_id)
|
|
||||||
return False
|
|
||||||
for ticket in self.tickets:
|
|
||||||
if ticket.id not in visited:
|
|
||||||
if is_cyclic(ticket.id):
|
|
||||||
return True
|
return True
|
||||||
|
if node_id in visited:
|
||||||
|
continue
|
||||||
|
visited.add(node_id)
|
||||||
|
path.add(node_id)
|
||||||
|
stack.append((node_id, True))
|
||||||
|
ticket = self.ticket_map.get(node_id)
|
||||||
|
if ticket:
|
||||||
|
for neighbor_id in ticket.depends_on:
|
||||||
|
stack.append((neighbor_id, False))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def topological_sort(self) -> List[str]:
|
def topological_sort(self) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Returns a list of ticket IDs in topological order (dependencies before dependents).
|
Returns a list of ticket IDs in topological order (dependencies before dependents).
|
||||||
|
Uses Kahn's algorithm for efficient O(V+E) sorting and cycle detection.
|
||||||
Returns:
|
Returns:
|
||||||
A list of ticket ID strings.
|
A list of ticket ID strings.
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If a dependency cycle is detected.
|
ValueError: If a dependency cycle is detected.
|
||||||
"""
|
"""
|
||||||
with get_monitor().scope("dag_topological_sort"):
|
with get_monitor().scope("dag_topological_sort"):
|
||||||
if self.has_cycle():
|
in_degree = {t.id: len(t.depends_on) for t in self.tickets}
|
||||||
raise ValueError("Dependency cycle detected")
|
dependents = {t.id: [] for t in self.tickets}
|
||||||
visited = set()
|
for t in self.tickets:
|
||||||
stack = []
|
for dep_id in t.depends_on:
|
||||||
|
if dep_id in dependents:
|
||||||
|
dependents[dep_id].append(t.id)
|
||||||
|
|
||||||
def visit(ticket_id: str) -> None:
|
# Queue starts with nodes having no dependencies
|
||||||
"""Internal recursive helper for topological sorting."""
|
queue = [t.id for t in self.tickets if in_degree[t.id] == 0]
|
||||||
if ticket_id in visited:
|
result = []
|
||||||
return
|
idx = 0
|
||||||
visited.add(ticket_id)
|
while idx < len(queue):
|
||||||
ticket = self.ticket_map.get(ticket_id)
|
u = queue[idx]
|
||||||
if ticket:
|
idx += 1
|
||||||
for dep_id in ticket.depends_on:
|
result.append(u)
|
||||||
visit(dep_id)
|
for v_id in dependents.get(u, []):
|
||||||
stack.append(ticket_id)
|
in_degree[v_id] -= 1
|
||||||
for ticket in self.tickets:
|
if in_degree[v_id] == 0:
|
||||||
visit(ticket.id)
|
queue.append(v_id)
|
||||||
return stack
|
|
||||||
|
if len(result) < len(self.tickets):
|
||||||
|
raise ValueError("Dependency cycle detected")
|
||||||
|
return result
|
||||||
|
|
||||||
class ExecutionEngine:
|
class ExecutionEngine:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ from src import models
|
|||||||
from src.models import Ticket, Track, WorkerContext
|
from src.models import Ticket, Track, WorkerContext
|
||||||
from src.file_cache import ASTParser
|
from src.file_cache import ASTParser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from src.personas import PersonaManager
|
||||||
|
from src import paths
|
||||||
|
|
||||||
from src.dag_engine import TrackDAG, ExecutionEngine
|
from src.dag_engine import TrackDAG, ExecutionEngine
|
||||||
|
|
||||||
@@ -122,6 +124,7 @@ class ConductorEngine:
|
|||||||
self._abort_events: dict[str, threading.Event] = {}
|
self._abort_events: dict[str, threading.Event] = {}
|
||||||
self._pause_event: threading.Event = threading.Event()
|
self._pause_event: threading.Event = threading.Event()
|
||||||
self._tier_usage_lock = threading.Lock()
|
self._tier_usage_lock = threading.Lock()
|
||||||
|
self._dirty: bool = True
|
||||||
|
|
||||||
def update_usage(self, tier: str, input_tokens: int, output_tokens: int) -> None:
|
def update_usage(self, tier: str, input_tokens: int, output_tokens: int) -> None:
|
||||||
"""Updates token usage for a specific tier."""
|
"""Updates token usage for a specific tier."""
|
||||||
@@ -138,6 +141,16 @@ class ConductorEngine:
|
|||||||
"""Resumes the pipeline execution."""
|
"""Resumes the pipeline execution."""
|
||||||
self._pause_event.clear()
|
self._pause_event.clear()
|
||||||
|
|
||||||
|
def approve_task(self, task_id: str) -> None:
|
||||||
|
"""Manually transition todo to in_progress and mark engine dirty."""
|
||||||
|
self.engine.approve_task(task_id)
|
||||||
|
self._dirty = True
|
||||||
|
|
||||||
|
def update_task_status(self, task_id: str, status: str) -> None:
|
||||||
|
"""Force-update ticket status and mark engine dirty."""
|
||||||
|
self.engine.update_task_status(task_id, status)
|
||||||
|
self._dirty = True
|
||||||
|
|
||||||
def kill_worker(self, ticket_id: str) -> None:
|
def kill_worker(self, ticket_id: str) -> None:
|
||||||
"""Sets the abort event for a worker and attempts to join its thread."""
|
"""Sets the abort event for a worker and attempts to join its thread."""
|
||||||
if ticket_id in self._abort_events:
|
if ticket_id in self._abort_events:
|
||||||
@@ -216,10 +229,14 @@ class ConductorEngine:
|
|||||||
if max_ticks is not None and tick_count >= max_ticks:
|
if max_ticks is not None and tick_count >= max_ticks:
|
||||||
break
|
break
|
||||||
tick_count += 1
|
tick_count += 1
|
||||||
# 1. Identify ready tasks
|
# 1. Identify ready tasks
|
||||||
ready_tasks = self.engine.tick()
|
if self._dirty:
|
||||||
|
self._ready_tasks = self.engine.tick()
|
||||||
|
self._dirty = False
|
||||||
|
ready_tasks = self._ready_tasks
|
||||||
|
|
||||||
# 2. Check for completion or blockage
|
# 2. Check for completion or blockage
|
||||||
|
|
||||||
if not ready_tasks:
|
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:
|
||||||
@@ -404,8 +421,6 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
|
|||||||
persona_tool_preset = None
|
persona_tool_preset = None
|
||||||
persona = None
|
persona = None
|
||||||
if context.persona_id:
|
if context.persona_id:
|
||||||
from src.personas import PersonaManager
|
|
||||||
from src import paths
|
|
||||||
pm = PersonaManager(Path(paths.get_project_personas_path(Path.cwd())) if paths.get_project_personas_path(Path.cwd()).exists() else None)
|
pm = PersonaManager(Path(paths.get_project_personas_path(Path.cwd())) if paths.get_project_personas_path(Path.cwd()).exists() else None)
|
||||||
try:
|
try:
|
||||||
personas = pm.load_all()
|
personas = pm.load_all()
|
||||||
@@ -587,6 +602,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
|
|||||||
_in_tokens = sum(e.get("payload", {}).get("usage", {}).get("input_tokens", 0) for e in _resp_entries)
|
_in_tokens = sum(e.get("payload", {}).get("usage", {}).get("input_tokens", 0) for e in _resp_entries)
|
||||||
_out_tokens = sum(e.get("payload", {}).get("usage", {}).get("output_tokens", 0) for e in _resp_entries)
|
_out_tokens = sum(e.get("payload", {}).get("usage", {}).get("output_tokens", 0) for e in _resp_entries)
|
||||||
engine.update_usage("Tier 3", _in_tokens, _out_tokens)
|
engine.update_usage("Tier 3", _in_tokens, _out_tokens)
|
||||||
|
engine._dirty = True
|
||||||
if "BLOCKED" in response.upper():
|
if "BLOCKED" in response.upper():
|
||||||
ticket.mark_blocked(response)
|
ticket.mark_blocked(response)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user