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.
Files changed (75) hide show
  1. ctrlcode/__init__.py +8 -0
  2. ctrlcode/agents/__init__.py +29 -0
  3. ctrlcode/agents/cleanup.py +388 -0
  4. ctrlcode/agents/communication.py +439 -0
  5. ctrlcode/agents/observability.py +421 -0
  6. ctrlcode/agents/react_loop.py +297 -0
  7. ctrlcode/agents/registry.py +211 -0
  8. ctrlcode/agents/result_parser.py +242 -0
  9. ctrlcode/agents/workflow.py +723 -0
  10. ctrlcode/analysis/__init__.py +28 -0
  11. ctrlcode/analysis/ast_diff.py +163 -0
  12. ctrlcode/analysis/bug_detector.py +149 -0
  13. ctrlcode/analysis/code_graphs.py +329 -0
  14. ctrlcode/analysis/semantic.py +205 -0
  15. ctrlcode/analysis/static.py +183 -0
  16. ctrlcode/analysis/synthesizer.py +281 -0
  17. ctrlcode/analysis/tests.py +189 -0
  18. ctrlcode/cleanup/__init__.py +16 -0
  19. ctrlcode/cleanup/auto_merge.py +350 -0
  20. ctrlcode/cleanup/doc_gardening.py +388 -0
  21. ctrlcode/cleanup/pr_automation.py +330 -0
  22. ctrlcode/cleanup/scheduler.py +356 -0
  23. ctrlcode/config.py +380 -0
  24. ctrlcode/embeddings/__init__.py +6 -0
  25. ctrlcode/embeddings/embedder.py +192 -0
  26. ctrlcode/embeddings/vector_store.py +213 -0
  27. ctrlcode/fuzzing/__init__.py +24 -0
  28. ctrlcode/fuzzing/analyzer.py +280 -0
  29. ctrlcode/fuzzing/budget.py +112 -0
  30. ctrlcode/fuzzing/context.py +665 -0
  31. ctrlcode/fuzzing/context_fuzzer.py +506 -0
  32. ctrlcode/fuzzing/derived_orchestrator.py +732 -0
  33. ctrlcode/fuzzing/oracle_adapter.py +135 -0
  34. ctrlcode/linters/__init__.py +11 -0
  35. ctrlcode/linters/hand_rolled_utils.py +221 -0
  36. ctrlcode/linters/yolo_parsing.py +217 -0
  37. ctrlcode/metrics/__init__.py +6 -0
  38. ctrlcode/metrics/dashboard.py +283 -0
  39. ctrlcode/metrics/tech_debt.py +663 -0
  40. ctrlcode/paths.py +68 -0
  41. ctrlcode/permissions.py +179 -0
  42. ctrlcode/providers/__init__.py +15 -0
  43. ctrlcode/providers/anthropic.py +138 -0
  44. ctrlcode/providers/base.py +77 -0
  45. ctrlcode/providers/openai.py +197 -0
  46. ctrlcode/providers/parallel.py +104 -0
  47. ctrlcode/server.py +871 -0
  48. ctrlcode/session/__init__.py +6 -0
  49. ctrlcode/session/baseline.py +57 -0
  50. ctrlcode/session/manager.py +967 -0
  51. ctrlcode/skills/__init__.py +10 -0
  52. ctrlcode/skills/builtin/commit.toml +29 -0
  53. ctrlcode/skills/builtin/docs.toml +25 -0
  54. ctrlcode/skills/builtin/refactor.toml +33 -0
  55. ctrlcode/skills/builtin/review.toml +28 -0
  56. ctrlcode/skills/builtin/test.toml +28 -0
  57. ctrlcode/skills/loader.py +111 -0
  58. ctrlcode/skills/registry.py +139 -0
  59. ctrlcode/storage/__init__.py +19 -0
  60. ctrlcode/storage/history_db.py +708 -0
  61. ctrlcode/tools/__init__.py +220 -0
  62. ctrlcode/tools/bash.py +112 -0
  63. ctrlcode/tools/browser.py +352 -0
  64. ctrlcode/tools/executor.py +153 -0
  65. ctrlcode/tools/explore.py +486 -0
  66. ctrlcode/tools/mcp.py +108 -0
  67. ctrlcode/tools/observability.py +561 -0
  68. ctrlcode/tools/registry.py +193 -0
  69. ctrlcode/tools/todo.py +291 -0
  70. ctrlcode/tools/update.py +266 -0
  71. ctrlcode/tools/webfetch.py +147 -0
  72. ctrlcode-0.1.0.dist-info/METADATA +93 -0
  73. ctrlcode-0.1.0.dist-info/RECORD +75 -0
  74. ctrlcode-0.1.0.dist-info/WHEEL +4 -0
  75. 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]