Tracks active agents and their states for multi-agent mode. Both coding and testing agents are tracked using a composite key of (feature_id, agent_type) to allow simultaneous tracking of both agent types for the same feature.
| 82 | |
| 83 | |
| 84 | class AgentTracker: |
| 85 | """Tracks active agents and their states for multi-agent mode. |
| 86 | |
| 87 | Both coding and testing agents are tracked using a composite key of |
| 88 | (feature_id, agent_type) to allow simultaneous tracking of both agent |
| 89 | types for the same feature. |
| 90 | """ |
| 91 | |
| 92 | def __init__(self): |
| 93 | # (feature_id, agent_type) -> {name, state, last_thought, agent_index, agent_type} |
| 94 | self.active_agents: dict[tuple[int, str], dict] = {} |
| 95 | self._next_agent_index = 0 |
| 96 | self._lock = asyncio.Lock() |
| 97 | |
| 98 | async def process_line(self, line: str) -> dict | None: |
| 99 | """ |
| 100 | Process an output line and return an agent_update message if relevant. |
| 101 | |
| 102 | Returns None if no update should be emitted. |
| 103 | """ |
| 104 | # Check for orchestrator status messages first |
| 105 | # These don't have [Feature #X] prefix |
| 106 | |
| 107 | # Batch coding agent start: "Started coding agent for features #5, #8, #12" |
| 108 | batch_start_match = BATCH_CODING_AGENT_START_PATTERN.match(line) |
| 109 | if batch_start_match: |
| 110 | try: |
| 111 | feature_ids = [int(x.strip().lstrip('#')) for x in batch_start_match.group(1).split(',')] |
| 112 | if feature_ids: |
| 113 | return await self._handle_batch_agent_start(feature_ids, "coding") |
| 114 | except ValueError: |
| 115 | pass |
| 116 | |
| 117 | # Single coding agent start: "Started coding agent for feature #X" |
| 118 | if line.startswith("Started coding agent for feature #"): |
| 119 | m = re.search(r'#(\d+)', line) |
| 120 | if m: |
| 121 | try: |
| 122 | feature_id = int(m.group(1)) |
| 123 | return await self._handle_agent_start(feature_id, line, agent_type="coding") |
| 124 | except ValueError: |
| 125 | pass |
| 126 | |
| 127 | # Testing agent start: "Started testing agent for feature #X (PID xxx)" |
| 128 | testing_start_match = TESTING_AGENT_START_PATTERN.match(line) |
| 129 | if testing_start_match: |
| 130 | feature_id = int(testing_start_match.group(1)) |
| 131 | return await self._handle_agent_start(feature_id, line, agent_type="testing") |
| 132 | |
| 133 | # Testing agent complete: "Feature #X testing completed/failed" |
| 134 | testing_complete_match = TESTING_AGENT_COMPLETE_PATTERN.match(line) |
| 135 | if testing_complete_match: |
| 136 | feature_id = int(testing_complete_match.group(1)) |
| 137 | is_success = testing_complete_match.group(2) == "completed" |
| 138 | return await self._handle_agent_complete(feature_id, is_success, agent_type="testing") |
| 139 | |
| 140 | # Batch features complete: "Features #5, #8, #12 completed/failed" |
| 141 | batch_complete_match = BATCH_FEATURES_COMPLETE_PATTERN.match(line) |