ctrlcode 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ctrlcode/__init__.py +8 -0
- ctrlcode/agents/__init__.py +29 -0
- ctrlcode/agents/cleanup.py +388 -0
- ctrlcode/agents/communication.py +439 -0
- ctrlcode/agents/observability.py +421 -0
- ctrlcode/agents/react_loop.py +297 -0
- ctrlcode/agents/registry.py +211 -0
- ctrlcode/agents/result_parser.py +242 -0
- ctrlcode/agents/workflow.py +723 -0
- ctrlcode/analysis/__init__.py +28 -0
- ctrlcode/analysis/ast_diff.py +163 -0
- ctrlcode/analysis/bug_detector.py +149 -0
- ctrlcode/analysis/code_graphs.py +329 -0
- ctrlcode/analysis/semantic.py +205 -0
- ctrlcode/analysis/static.py +183 -0
- ctrlcode/analysis/synthesizer.py +281 -0
- ctrlcode/analysis/tests.py +189 -0
- ctrlcode/cleanup/__init__.py +16 -0
- ctrlcode/cleanup/auto_merge.py +350 -0
- ctrlcode/cleanup/doc_gardening.py +388 -0
- ctrlcode/cleanup/pr_automation.py +330 -0
- ctrlcode/cleanup/scheduler.py +356 -0
- ctrlcode/config.py +380 -0
- ctrlcode/embeddings/__init__.py +6 -0
- ctrlcode/embeddings/embedder.py +192 -0
- ctrlcode/embeddings/vector_store.py +213 -0
- ctrlcode/fuzzing/__init__.py +24 -0
- ctrlcode/fuzzing/analyzer.py +280 -0
- ctrlcode/fuzzing/budget.py +112 -0
- ctrlcode/fuzzing/context.py +665 -0
- ctrlcode/fuzzing/context_fuzzer.py +506 -0
- ctrlcode/fuzzing/derived_orchestrator.py +732 -0
- ctrlcode/fuzzing/oracle_adapter.py +135 -0
- ctrlcode/linters/__init__.py +11 -0
- ctrlcode/linters/hand_rolled_utils.py +221 -0
- ctrlcode/linters/yolo_parsing.py +217 -0
- ctrlcode/metrics/__init__.py +6 -0
- ctrlcode/metrics/dashboard.py +283 -0
- ctrlcode/metrics/tech_debt.py +663 -0
- ctrlcode/paths.py +68 -0
- ctrlcode/permissions.py +179 -0
- ctrlcode/providers/__init__.py +15 -0
- ctrlcode/providers/anthropic.py +138 -0
- ctrlcode/providers/base.py +77 -0
- ctrlcode/providers/openai.py +197 -0
- ctrlcode/providers/parallel.py +104 -0
- ctrlcode/server.py +871 -0
- ctrlcode/session/__init__.py +6 -0
- ctrlcode/session/baseline.py +57 -0
- ctrlcode/session/manager.py +967 -0
- ctrlcode/skills/__init__.py +10 -0
- ctrlcode/skills/builtin/commit.toml +29 -0
- ctrlcode/skills/builtin/docs.toml +25 -0
- ctrlcode/skills/builtin/refactor.toml +33 -0
- ctrlcode/skills/builtin/review.toml +28 -0
- ctrlcode/skills/builtin/test.toml +28 -0
- ctrlcode/skills/loader.py +111 -0
- ctrlcode/skills/registry.py +139 -0
- ctrlcode/storage/__init__.py +19 -0
- ctrlcode/storage/history_db.py +708 -0
- ctrlcode/tools/__init__.py +220 -0
- ctrlcode/tools/bash.py +112 -0
- ctrlcode/tools/browser.py +352 -0
- ctrlcode/tools/executor.py +153 -0
- ctrlcode/tools/explore.py +486 -0
- ctrlcode/tools/mcp.py +108 -0
- ctrlcode/tools/observability.py +561 -0
- ctrlcode/tools/registry.py +193 -0
- ctrlcode/tools/todo.py +291 -0
- ctrlcode/tools/update.py +266 -0
- ctrlcode/tools/webfetch.py +147 -0
- ctrlcode-0.1.0.dist-info/METADATA +93 -0
- ctrlcode-0.1.0.dist-info/RECORD +75 -0
- ctrlcode-0.1.0.dist-info/WHEEL +4 -0
- ctrlcode-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""Agent communication protocol for message passing between agents."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable, Awaitable
|
|
5
|
+
from collections import deque
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from enum import Enum
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from harnessutils import ConversationManager, Message, TextPart
|
|
12
|
+
from harnessutils.storage import FilesystemStorage, MemoryStorage
|
|
13
|
+
from harnessutils.config import HarnessConfig, StorageConfig
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AgentVerbosity(Enum):
|
|
19
|
+
"""Control agent event streaming verbosity."""
|
|
20
|
+
|
|
21
|
+
SILENT = 0 # Only workflow events
|
|
22
|
+
WORKFLOW = 1 # + agent summaries (default)
|
|
23
|
+
DETAILED = 2 # Stream all internal events
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class AgentMessage:
|
|
28
|
+
"""Message passed between agents."""
|
|
29
|
+
|
|
30
|
+
from_agent: str
|
|
31
|
+
to_agent: str
|
|
32
|
+
message_type: str # "task", "result", "feedback", "question"
|
|
33
|
+
payload: dict[str, Any]
|
|
34
|
+
context: dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
message_id: str | None = None
|
|
36
|
+
|
|
37
|
+
def __post_init__(self):
|
|
38
|
+
"""Generate message ID if not provided."""
|
|
39
|
+
if self.message_id is None:
|
|
40
|
+
import uuid
|
|
41
|
+
self.message_id = str(uuid.uuid4())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AgentBus:
|
|
45
|
+
"""Event bus for inter-agent communication."""
|
|
46
|
+
|
|
47
|
+
def __init__(self):
|
|
48
|
+
"""Initialize agent bus."""
|
|
49
|
+
self.message_queue: deque[AgentMessage] = deque()
|
|
50
|
+
self.handlers: dict[str, Callable[[AgentMessage], Awaitable[Any]]] = {}
|
|
51
|
+
self.message_log: list[AgentMessage] = []
|
|
52
|
+
|
|
53
|
+
async def send(self, message: AgentMessage) -> Any:
|
|
54
|
+
"""
|
|
55
|
+
Send message to target agent.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
message: AgentMessage to send
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Handler result if handler exists, None otherwise
|
|
62
|
+
"""
|
|
63
|
+
# Log message
|
|
64
|
+
self.message_log.append(message)
|
|
65
|
+
|
|
66
|
+
# Add to queue
|
|
67
|
+
self.message_queue.append(message)
|
|
68
|
+
|
|
69
|
+
# Find handler for target agent
|
|
70
|
+
if message.to_agent in self.handlers:
|
|
71
|
+
handler = self.handlers[message.to_agent]
|
|
72
|
+
return await handler(message)
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def register_handler(
|
|
77
|
+
self,
|
|
78
|
+
agent_name: str,
|
|
79
|
+
handler: Callable[[AgentMessage], Awaitable[Any]]
|
|
80
|
+
):
|
|
81
|
+
"""
|
|
82
|
+
Register message handler for agent.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
agent_name: Agent identifier
|
|
86
|
+
handler: Async function to handle messages
|
|
87
|
+
"""
|
|
88
|
+
self.handlers[agent_name] = handler
|
|
89
|
+
|
|
90
|
+
def unregister_handler(self, agent_name: str):
|
|
91
|
+
"""
|
|
92
|
+
Remove message handler for agent.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
agent_name: Agent identifier
|
|
96
|
+
"""
|
|
97
|
+
if agent_name in self.handlers:
|
|
98
|
+
del self.handlers[agent_name]
|
|
99
|
+
|
|
100
|
+
def get_messages_for_agent(self, agent_name: str) -> list[AgentMessage]:
|
|
101
|
+
"""
|
|
102
|
+
Get all messages sent to specific agent.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
agent_name: Agent identifier
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List of messages
|
|
109
|
+
"""
|
|
110
|
+
return [msg for msg in self.message_log if msg.to_agent == agent_name]
|
|
111
|
+
|
|
112
|
+
def get_conversation(
|
|
113
|
+
self,
|
|
114
|
+
agent1: str,
|
|
115
|
+
agent2: str
|
|
116
|
+
) -> list[AgentMessage]:
|
|
117
|
+
"""
|
|
118
|
+
Get all messages between two agents.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
agent1: First agent identifier
|
|
122
|
+
agent2: Second agent identifier
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of messages in chronological order
|
|
126
|
+
"""
|
|
127
|
+
return [
|
|
128
|
+
msg for msg in self.message_log
|
|
129
|
+
if (msg.from_agent == agent1 and msg.to_agent == agent2) or
|
|
130
|
+
(msg.from_agent == agent2 and msg.to_agent == agent1)
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
def clear(self):
|
|
134
|
+
"""Clear all messages and queue."""
|
|
135
|
+
self.message_queue.clear()
|
|
136
|
+
self.message_log.clear()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class AgentCoordinator:
|
|
140
|
+
"""Coordinates agent spawning and communication."""
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
agent_registry: Any,
|
|
145
|
+
storage_path: Path | str,
|
|
146
|
+
provider: Any,
|
|
147
|
+
tool_registry: Any | None = None,
|
|
148
|
+
):
|
|
149
|
+
"""
|
|
150
|
+
Initialize coordinator.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
agent_registry: AgentRegistry instance
|
|
154
|
+
storage_path: Base path for agent conversation storage
|
|
155
|
+
provider: LLM provider instance
|
|
156
|
+
tool_registry: Optional ToolRegistry for tool access
|
|
157
|
+
"""
|
|
158
|
+
self.registry = agent_registry
|
|
159
|
+
self.bus = AgentBus()
|
|
160
|
+
self.active_agents: dict[str, Any] = {}
|
|
161
|
+
self.storage_path = Path(storage_path)
|
|
162
|
+
self.provider = provider
|
|
163
|
+
self.tool_registry = tool_registry
|
|
164
|
+
self.conversation_managers: dict[str, ConversationManager] = {}
|
|
165
|
+
|
|
166
|
+
def _emit_agent_event(
|
|
167
|
+
self,
|
|
168
|
+
event: Any,
|
|
169
|
+
agent_id: str,
|
|
170
|
+
verbosity: AgentVerbosity,
|
|
171
|
+
event_callback: Callable | None = None
|
|
172
|
+
):
|
|
173
|
+
"""
|
|
174
|
+
Filter and emit agent events based on verbosity level.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
event: StreamEvent to potentially emit
|
|
178
|
+
agent_id: Agent identifier
|
|
179
|
+
verbosity: Verbosity level
|
|
180
|
+
event_callback: Optional callback for events
|
|
181
|
+
"""
|
|
182
|
+
if not event_callback:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# SILENT: no internal events
|
|
186
|
+
if verbosity == AgentVerbosity.SILENT:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# WORKFLOW: only summaries and major events
|
|
190
|
+
if verbosity == AgentVerbosity.WORKFLOW:
|
|
191
|
+
if event.type in ["continuation_complete", "tool_result", "usage"]:
|
|
192
|
+
event_callback(event)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
# DETAILED: all events
|
|
196
|
+
if verbosity == AgentVerbosity.DETAILED:
|
|
197
|
+
event_callback(event)
|
|
198
|
+
|
|
199
|
+
async def spawn_agent(
|
|
200
|
+
self,
|
|
201
|
+
agent_type: str,
|
|
202
|
+
task: dict[str, Any],
|
|
203
|
+
agent_id: str | None = None,
|
|
204
|
+
verbosity: AgentVerbosity = AgentVerbosity.WORKFLOW,
|
|
205
|
+
event_callback: Callable | None = None
|
|
206
|
+
) -> dict[str, Any]:
|
|
207
|
+
"""
|
|
208
|
+
Spawn specialized agent to execute task.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
agent_type: Type of agent (planner, coder, reviewer, executor)
|
|
212
|
+
task: Task data for agent
|
|
213
|
+
agent_id: Optional agent identifier
|
|
214
|
+
verbosity: Event streaming verbosity level
|
|
215
|
+
event_callback: Optional callback for streaming events
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Agent execution result
|
|
219
|
+
"""
|
|
220
|
+
# Get agent configuration
|
|
221
|
+
agent_config = self.registry.get_agent_configs()[agent_type]
|
|
222
|
+
|
|
223
|
+
# Generate agent ID if not provided
|
|
224
|
+
if agent_id is None:
|
|
225
|
+
import uuid
|
|
226
|
+
agent_id = f"{agent_type}-{uuid.uuid4().hex[:8]}"
|
|
227
|
+
|
|
228
|
+
# Create harness-utils config from agent config
|
|
229
|
+
harness_config = HarnessConfig()
|
|
230
|
+
harness_config.pruning.prune_protect = agent_config.prune_protect
|
|
231
|
+
harness_config.pruning.prune_minimum = agent_config.prune_minimum
|
|
232
|
+
harness_config.truncation.max_lines = agent_config.max_lines
|
|
233
|
+
harness_config.compaction.use_predictive = agent_config.use_predictive
|
|
234
|
+
|
|
235
|
+
# Create storage (filesystem for persistent agents, memory for ephemeral)
|
|
236
|
+
if agent_type in ["planner", "coder", "reviewer"]:
|
|
237
|
+
# Persistent storage
|
|
238
|
+
storage_config = StorageConfig(
|
|
239
|
+
base_path=self.storage_path / "agents" / agent_type
|
|
240
|
+
)
|
|
241
|
+
storage = FilesystemStorage(storage_config)
|
|
242
|
+
else:
|
|
243
|
+
# Ephemeral storage for executor, orchestrator
|
|
244
|
+
storage = MemoryStorage()
|
|
245
|
+
|
|
246
|
+
# Create ConversationManager for this agent
|
|
247
|
+
conv_manager = ConversationManager(
|
|
248
|
+
storage=storage,
|
|
249
|
+
config=harness_config
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Create conversation
|
|
253
|
+
conv = conv_manager.create_conversation(
|
|
254
|
+
project_id=f"agent-{agent_type}"
|
|
255
|
+
)
|
|
256
|
+
conv_id = conv.id
|
|
257
|
+
|
|
258
|
+
# Store conversation manager
|
|
259
|
+
self.conversation_managers[agent_id] = conv_manager
|
|
260
|
+
|
|
261
|
+
# Load system prompt
|
|
262
|
+
system_prompt = self.registry.load_system_prompt(agent_type)
|
|
263
|
+
|
|
264
|
+
# Add system prompt as first message
|
|
265
|
+
system_msg = Message(id="system", role="system")
|
|
266
|
+
system_msg.add_part(TextPart(text=system_prompt))
|
|
267
|
+
conv_manager.add_message(conv_id, system_msg)
|
|
268
|
+
|
|
269
|
+
# Add task as user message
|
|
270
|
+
task_description = task.get("description", str(task))
|
|
271
|
+
task_message = Message(id="task", role="user")
|
|
272
|
+
task_message.add_part(TextPart(text=task_description))
|
|
273
|
+
conv_manager.add_message(conv_id, task_message)
|
|
274
|
+
|
|
275
|
+
# Get filtered tool definitions
|
|
276
|
+
if self.tool_registry:
|
|
277
|
+
tools = self.tool_registry.get_tool_definitions_filtered(agent_config.tools)
|
|
278
|
+
else:
|
|
279
|
+
tools = []
|
|
280
|
+
logger.warning(f"No tool_registry provided for agent {agent_type}")
|
|
281
|
+
|
|
282
|
+
# Execute ReAct loop
|
|
283
|
+
try:
|
|
284
|
+
from ..tools.executor import ToolExecutor
|
|
285
|
+
from .react_loop import AgentReActLoop
|
|
286
|
+
from .result_parser import AgentResultParser
|
|
287
|
+
|
|
288
|
+
# Create tool executor
|
|
289
|
+
tool_executor = ToolExecutor(self.tool_registry) if self.tool_registry else None
|
|
290
|
+
|
|
291
|
+
# Create event callback with filtering
|
|
292
|
+
def filtered_callback(event):
|
|
293
|
+
self._emit_agent_event(event, agent_id, verbosity, event_callback)
|
|
294
|
+
|
|
295
|
+
# Execute ReAct loop
|
|
296
|
+
loop = AgentReActLoop()
|
|
297
|
+
execution_result = await loop.execute(
|
|
298
|
+
provider=self.provider,
|
|
299
|
+
conv_manager=conv_manager,
|
|
300
|
+
conv_id=conv_id,
|
|
301
|
+
tool_executor=tool_executor,
|
|
302
|
+
tools=tools,
|
|
303
|
+
allowed_tools=agent_config.tools,
|
|
304
|
+
max_continuations=50,
|
|
305
|
+
event_callback=filtered_callback if event_callback else None
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Parse result based on agent type
|
|
309
|
+
if agent_type == "planner":
|
|
310
|
+
parsed = AgentResultParser.parse_planner_result(
|
|
311
|
+
execution_result.assistant_text,
|
|
312
|
+
execution_result.tool_calls
|
|
313
|
+
)
|
|
314
|
+
result = {
|
|
315
|
+
"agent_id": agent_id,
|
|
316
|
+
"agent_type": agent_type,
|
|
317
|
+
"status": "completed",
|
|
318
|
+
"task_graph": parsed,
|
|
319
|
+
"conv_id": conv_id,
|
|
320
|
+
"usage_tokens": execution_result.usage_tokens
|
|
321
|
+
}
|
|
322
|
+
elif agent_type == "coder":
|
|
323
|
+
parsed = AgentResultParser.parse_coder_result(
|
|
324
|
+
execution_result.assistant_text,
|
|
325
|
+
execution_result.tool_calls
|
|
326
|
+
)
|
|
327
|
+
result = {
|
|
328
|
+
"agent_id": agent_id,
|
|
329
|
+
"agent_type": agent_type,
|
|
330
|
+
"status": "completed",
|
|
331
|
+
"files_changed": parsed,
|
|
332
|
+
"conv_id": conv_id,
|
|
333
|
+
"usage_tokens": execution_result.usage_tokens
|
|
334
|
+
}
|
|
335
|
+
elif agent_type == "reviewer":
|
|
336
|
+
parsed = AgentResultParser.parse_reviewer_result(
|
|
337
|
+
execution_result.assistant_text,
|
|
338
|
+
execution_result.tool_calls
|
|
339
|
+
)
|
|
340
|
+
result = {
|
|
341
|
+
"agent_id": agent_id,
|
|
342
|
+
"agent_type": agent_type,
|
|
343
|
+
"status": "completed",
|
|
344
|
+
"review": parsed,
|
|
345
|
+
"conv_id": conv_id,
|
|
346
|
+
"usage_tokens": execution_result.usage_tokens
|
|
347
|
+
}
|
|
348
|
+
elif agent_type == "executor":
|
|
349
|
+
parsed = AgentResultParser.parse_executor_result(
|
|
350
|
+
execution_result.assistant_text,
|
|
351
|
+
execution_result.tool_calls
|
|
352
|
+
)
|
|
353
|
+
result = {
|
|
354
|
+
"agent_id": agent_id,
|
|
355
|
+
"agent_type": agent_type,
|
|
356
|
+
"status": "completed",
|
|
357
|
+
"validation": parsed,
|
|
358
|
+
"conv_id": conv_id,
|
|
359
|
+
"usage_tokens": execution_result.usage_tokens
|
|
360
|
+
}
|
|
361
|
+
else:
|
|
362
|
+
# Generic result
|
|
363
|
+
result = {
|
|
364
|
+
"agent_id": agent_id,
|
|
365
|
+
"agent_type": agent_type,
|
|
366
|
+
"status": "completed",
|
|
367
|
+
"output": execution_result.assistant_text,
|
|
368
|
+
"tool_calls": execution_result.tool_calls,
|
|
369
|
+
"conv_id": conv_id,
|
|
370
|
+
"usage_tokens": execution_result.usage_tokens
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.error(f"Agent execution failed: {e}", exc_info=True)
|
|
375
|
+
result = {
|
|
376
|
+
"agent_id": agent_id,
|
|
377
|
+
"agent_type": agent_type,
|
|
378
|
+
"status": "error",
|
|
379
|
+
"error": str(e),
|
|
380
|
+
"task": task,
|
|
381
|
+
"conv_id": conv_id
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
# Track active agent
|
|
385
|
+
self.active_agents[agent_id] = {
|
|
386
|
+
"type": agent_type,
|
|
387
|
+
"config": agent_config,
|
|
388
|
+
"result": result,
|
|
389
|
+
"conv_manager": conv_manager,
|
|
390
|
+
"conv_id": conv_id,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return result
|
|
394
|
+
|
|
395
|
+
async def spawn_agents_parallel(
|
|
396
|
+
self,
|
|
397
|
+
agents_tasks: list[dict[str, Any]]
|
|
398
|
+
) -> list[dict[str, Any]]:
|
|
399
|
+
"""
|
|
400
|
+
Spawn multiple agents in parallel.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
agents_tasks: List of dicts with 'type' and 'task' keys
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
List of agent execution results
|
|
407
|
+
"""
|
|
408
|
+
tasks = [
|
|
409
|
+
self.spawn_agent(
|
|
410
|
+
agent_type=agent_task["type"],
|
|
411
|
+
task=agent_task["task"]
|
|
412
|
+
)
|
|
413
|
+
for agent_task in agents_tasks
|
|
414
|
+
]
|
|
415
|
+
|
|
416
|
+
results = await asyncio.gather(*tasks)
|
|
417
|
+
return list(results)
|
|
418
|
+
|
|
419
|
+
def get_agent_status(self, agent_id: str) -> dict[str, Any] | None:
|
|
420
|
+
"""
|
|
421
|
+
Get status of agent.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
agent_id: Agent identifier
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Agent status dict or None if not found
|
|
428
|
+
"""
|
|
429
|
+
return self.active_agents.get(agent_id)
|
|
430
|
+
|
|
431
|
+
def cleanup_agent(self, agent_id: str):
|
|
432
|
+
"""
|
|
433
|
+
Remove agent from active tracking.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
agent_id: Agent identifier
|
|
437
|
+
"""
|
|
438
|
+
if agent_id in self.active_agents:
|
|
439
|
+
del self.active_agents[agent_id]
|