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,297 @@
|
|
|
1
|
+
"""Reusable ReAct loop execution for agents and sessions."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from harnessutils import ConversationManager, Message, TextPart
|
|
9
|
+
|
|
10
|
+
from ..providers.base import Provider, StreamEvent
|
|
11
|
+
from ..tools.executor import ToolExecutor
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ReActResult:
|
|
18
|
+
"""Result from executing a ReAct loop."""
|
|
19
|
+
|
|
20
|
+
assistant_text: str
|
|
21
|
+
tool_calls: list[dict[str, Any]]
|
|
22
|
+
usage_tokens: int
|
|
23
|
+
continuation_count: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentReActLoop:
|
|
27
|
+
"""Execute ReAct (Reason + Act) loops with tool calling."""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
"""Initialize ReAct loop executor."""
|
|
31
|
+
self.msg_id_counter = 0
|
|
32
|
+
|
|
33
|
+
def _generate_msg_id(self) -> str:
|
|
34
|
+
"""Generate unique message ID."""
|
|
35
|
+
self.msg_id_counter += 1
|
|
36
|
+
return f"msg_{self.msg_id_counter}"
|
|
37
|
+
|
|
38
|
+
async def execute(
|
|
39
|
+
self,
|
|
40
|
+
provider: Provider,
|
|
41
|
+
conv_manager: ConversationManager,
|
|
42
|
+
conv_id: str,
|
|
43
|
+
tool_executor: ToolExecutor,
|
|
44
|
+
tools: list[dict[str, Any]],
|
|
45
|
+
allowed_tools: list[str] | None = None,
|
|
46
|
+
max_continuations: int = 50,
|
|
47
|
+
event_callback: Callable[[StreamEvent], None] | None = None,
|
|
48
|
+
) -> ReActResult:
|
|
49
|
+
"""
|
|
50
|
+
Execute ReAct loop with tool calling.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
provider: LLM provider to stream from
|
|
54
|
+
conv_manager: Conversation manager
|
|
55
|
+
conv_id: Conversation ID
|
|
56
|
+
tool_executor: Tool executor for running tools
|
|
57
|
+
tools: Tool definitions to pass to LLM
|
|
58
|
+
allowed_tools: Optional list of allowed tool names (for filtering)
|
|
59
|
+
max_continuations: Maximum continuation iterations
|
|
60
|
+
event_callback: Optional callback for streaming events
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
ReActResult with aggregated output
|
|
64
|
+
"""
|
|
65
|
+
usage_tokens = 0
|
|
66
|
+
accumulated_text: list[str] = []
|
|
67
|
+
all_tool_calls: list[dict] = []
|
|
68
|
+
|
|
69
|
+
# Get current messages
|
|
70
|
+
messages = conv_manager.to_model_format(conv_id)
|
|
71
|
+
|
|
72
|
+
# First pass: stream and collect tool calls
|
|
73
|
+
assistant_text = []
|
|
74
|
+
tool_calls: list[dict] = []
|
|
75
|
+
current_tool_call: dict | None = None
|
|
76
|
+
|
|
77
|
+
async for event in provider.stream(messages, tools=tools):
|
|
78
|
+
if event_callback:
|
|
79
|
+
event_callback(event)
|
|
80
|
+
|
|
81
|
+
if event.type == "text":
|
|
82
|
+
text_chunk = event.data["text"]
|
|
83
|
+
assistant_text.append(text_chunk)
|
|
84
|
+
accumulated_text.append(text_chunk)
|
|
85
|
+
|
|
86
|
+
elif event.type == "usage":
|
|
87
|
+
usage = event.data.get("usage", {})
|
|
88
|
+
usage_tokens += usage.get("completion_tokens", 0)
|
|
89
|
+
|
|
90
|
+
elif event.type == "tool_call_start":
|
|
91
|
+
current_tool_call = {
|
|
92
|
+
"tool": event.data["tool"],
|
|
93
|
+
"call_id": event.data["call_id"],
|
|
94
|
+
"input": ""
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
elif event.type == "tool_call_delta":
|
|
98
|
+
if current_tool_call:
|
|
99
|
+
current_tool_call["input"] += event.data.get("delta", "")
|
|
100
|
+
|
|
101
|
+
elif event.type == "content_block_stop":
|
|
102
|
+
if current_tool_call:
|
|
103
|
+
try:
|
|
104
|
+
current_tool_call["input"] = json.loads(current_tool_call["input"])
|
|
105
|
+
except json.JSONDecodeError:
|
|
106
|
+
current_tool_call["input"] = {}
|
|
107
|
+
tool_calls.append(current_tool_call)
|
|
108
|
+
all_tool_calls.append(current_tool_call)
|
|
109
|
+
current_tool_call = None
|
|
110
|
+
|
|
111
|
+
# Execute tools if any were called
|
|
112
|
+
if tool_calls and tool_executor:
|
|
113
|
+
# Add assistant message with tool use
|
|
114
|
+
assistant_msg = Message(id=self._generate_msg_id(), role="assistant")
|
|
115
|
+
if assistant_text:
|
|
116
|
+
assistant_msg.add_part(TextPart(text="".join(assistant_text)))
|
|
117
|
+
conv_manager.add_message(conv_id, assistant_msg)
|
|
118
|
+
|
|
119
|
+
# Execute all tools
|
|
120
|
+
for tool_call in tool_calls:
|
|
121
|
+
# Filter by allowed_tools if specified
|
|
122
|
+
if allowed_tools and tool_call["tool"] not in allowed_tools:
|
|
123
|
+
logger.warning(f"Tool {tool_call['tool']} not in allowed list, skipping")
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
result = await tool_executor.execute(
|
|
127
|
+
tool_name=tool_call["tool"],
|
|
128
|
+
arguments=tool_call["input"],
|
|
129
|
+
call_id=tool_call["call_id"]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Emit tool result event
|
|
133
|
+
if event_callback:
|
|
134
|
+
event_callback(StreamEvent(
|
|
135
|
+
type="tool_result",
|
|
136
|
+
data={
|
|
137
|
+
"tool": tool_call["tool"],
|
|
138
|
+
"success": result.success,
|
|
139
|
+
"result": result.result if result.success else result.error
|
|
140
|
+
}
|
|
141
|
+
))
|
|
142
|
+
|
|
143
|
+
# Add tool result message
|
|
144
|
+
tool_result_msg = Message(id=self._generate_msg_id(), role="user")
|
|
145
|
+
result_text = f"[Tool: {tool_call['tool']}]\n"
|
|
146
|
+
if result.success:
|
|
147
|
+
result_text += f"Result: {result.result}"
|
|
148
|
+
else:
|
|
149
|
+
result_text += f"Error: {result.error}"
|
|
150
|
+
tool_result_msg.add_part(TextPart(text=result_text))
|
|
151
|
+
conv_manager.add_message(conv_id, tool_result_msg)
|
|
152
|
+
|
|
153
|
+
# Continuation loop: keep calling tools until LLM stops
|
|
154
|
+
continuation_count = 0
|
|
155
|
+
|
|
156
|
+
while tool_calls and continuation_count < max_continuations:
|
|
157
|
+
continuation_count += 1
|
|
158
|
+
logger.info(f"ReAct continuation {continuation_count}/{max_continuations}")
|
|
159
|
+
|
|
160
|
+
# Add continuation prompt
|
|
161
|
+
reminder_msg = Message(id=self._generate_msg_id(), role="user")
|
|
162
|
+
reminder_msg.add_part(TextPart(
|
|
163
|
+
text="Continue using tools to complete the task. Call more tools if needed."
|
|
164
|
+
))
|
|
165
|
+
conv_manager.add_message(conv_id, reminder_msg)
|
|
166
|
+
|
|
167
|
+
# Get updated messages
|
|
168
|
+
messages = conv_manager.to_model_format(conv_id)
|
|
169
|
+
|
|
170
|
+
# Stream continuation
|
|
171
|
+
continuation_text = []
|
|
172
|
+
continuation_tool_calls = []
|
|
173
|
+
current_continuation_tool = None
|
|
174
|
+
|
|
175
|
+
async for event in provider.stream(messages, tools=tools):
|
|
176
|
+
if event_callback:
|
|
177
|
+
event_callback(event)
|
|
178
|
+
|
|
179
|
+
if event.type == "text":
|
|
180
|
+
continuation_text.append(event.data["text"])
|
|
181
|
+
accumulated_text.append(event.data["text"])
|
|
182
|
+
|
|
183
|
+
elif event.type == "usage":
|
|
184
|
+
usage = event.data.get("usage", {})
|
|
185
|
+
usage_tokens += usage.get("completion_tokens", 0)
|
|
186
|
+
|
|
187
|
+
elif event.type == "tool_call_start":
|
|
188
|
+
current_continuation_tool = {
|
|
189
|
+
"tool": event.data["tool"],
|
|
190
|
+
"call_id": event.data["call_id"],
|
|
191
|
+
"input": ""
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
elif event.type == "tool_call_delta":
|
|
195
|
+
if current_continuation_tool:
|
|
196
|
+
current_continuation_tool["input"] += event.data.get("delta", "")
|
|
197
|
+
|
|
198
|
+
elif event.type == "content_block_stop":
|
|
199
|
+
if current_continuation_tool:
|
|
200
|
+
try:
|
|
201
|
+
current_continuation_tool["input"] = json.loads(
|
|
202
|
+
current_continuation_tool["input"]
|
|
203
|
+
)
|
|
204
|
+
except json.JSONDecodeError:
|
|
205
|
+
current_continuation_tool["input"] = {}
|
|
206
|
+
continuation_tool_calls.append(current_continuation_tool)
|
|
207
|
+
all_tool_calls.append(current_continuation_tool)
|
|
208
|
+
current_continuation_tool = None
|
|
209
|
+
|
|
210
|
+
# Add continuation assistant message
|
|
211
|
+
cont_assistant_msg = Message(id=self._generate_msg_id(), role="assistant")
|
|
212
|
+
if continuation_text:
|
|
213
|
+
cont_assistant_msg.add_part(TextPart(text="".join(continuation_text)))
|
|
214
|
+
conv_manager.add_message(conv_id, cont_assistant_msg)
|
|
215
|
+
|
|
216
|
+
# Execute continuation tools
|
|
217
|
+
if continuation_tool_calls:
|
|
218
|
+
for tool_call in continuation_tool_calls:
|
|
219
|
+
# Filter by allowed_tools
|
|
220
|
+
if allowed_tools and tool_call["tool"] not in allowed_tools:
|
|
221
|
+
logger.warning(f"Tool {tool_call['tool']} not allowed, skipping")
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
result = await tool_executor.execute(
|
|
225
|
+
tool_name=tool_call["tool"],
|
|
226
|
+
arguments=tool_call["input"],
|
|
227
|
+
call_id=tool_call["call_id"]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Emit tool result
|
|
231
|
+
if event_callback:
|
|
232
|
+
event_callback(StreamEvent(
|
|
233
|
+
type="tool_result",
|
|
234
|
+
data={
|
|
235
|
+
"tool": tool_call["tool"],
|
|
236
|
+
"success": result.success,
|
|
237
|
+
"result": result.result if result.success else result.error,
|
|
238
|
+
"continuation": True,
|
|
239
|
+
"continuation_index": continuation_count
|
|
240
|
+
}
|
|
241
|
+
))
|
|
242
|
+
|
|
243
|
+
# Add tool result message
|
|
244
|
+
tool_result_msg = Message(id=self._generate_msg_id(), role="user")
|
|
245
|
+
result_text = f"[Tool: {tool_call['tool']}]\n"
|
|
246
|
+
if result.success:
|
|
247
|
+
result_text += f"Result: {result.result}"
|
|
248
|
+
else:
|
|
249
|
+
result_text += f"Error: {result.error}"
|
|
250
|
+
tool_result_msg.add_part(TextPart(text=result_text))
|
|
251
|
+
conv_manager.add_message(conv_id, tool_result_msg)
|
|
252
|
+
|
|
253
|
+
# Update tool_calls for next iteration
|
|
254
|
+
tool_calls = continuation_tool_calls
|
|
255
|
+
else:
|
|
256
|
+
# No more tools called, exit loop
|
|
257
|
+
logger.info(f"ReAct loop ending after {continuation_count} continuations")
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
# Emit continuation summary
|
|
261
|
+
if event_callback:
|
|
262
|
+
event_callback(StreamEvent(
|
|
263
|
+
type="continuation_complete",
|
|
264
|
+
data={
|
|
265
|
+
"iterations": continuation_count,
|
|
266
|
+
"tool_count": len(all_tool_calls)
|
|
267
|
+
}
|
|
268
|
+
))
|
|
269
|
+
|
|
270
|
+
# Final summary call (no tools)
|
|
271
|
+
messages = conv_manager.to_model_format(conv_id)
|
|
272
|
+
final_text = []
|
|
273
|
+
|
|
274
|
+
async for event in provider.stream(messages, tools=tools):
|
|
275
|
+
if event_callback:
|
|
276
|
+
event_callback(event)
|
|
277
|
+
|
|
278
|
+
if event.type == "text":
|
|
279
|
+
final_text.append(event.data["text"])
|
|
280
|
+
accumulated_text.append(event.data["text"])
|
|
281
|
+
|
|
282
|
+
elif event.type == "usage":
|
|
283
|
+
usage = event.data.get("usage", {})
|
|
284
|
+
usage_tokens += usage.get("completion_tokens", 0)
|
|
285
|
+
|
|
286
|
+
# Add final assistant message
|
|
287
|
+
if final_text:
|
|
288
|
+
final_msg = Message(id=self._generate_msg_id(), role="assistant")
|
|
289
|
+
final_msg.add_part(TextPart(text="".join(final_text)))
|
|
290
|
+
conv_manager.add_message(conv_id, final_msg)
|
|
291
|
+
|
|
292
|
+
return ReActResult(
|
|
293
|
+
assistant_text="".join(accumulated_text),
|
|
294
|
+
tool_calls=all_tool_calls,
|
|
295
|
+
usage_tokens=usage_tokens,
|
|
296
|
+
continuation_count=continuation_count if tool_calls else 0
|
|
297
|
+
)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Agent registry for creating specialized agents."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
# Note: harness-utils integration will be added when we integrate
|
|
7
|
+
# For now, this is the structure/interface
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentConfig:
|
|
11
|
+
"""Configuration for a specialized agent."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
name: str,
|
|
16
|
+
system_prompt_path: str,
|
|
17
|
+
tools: list[str],
|
|
18
|
+
prune_protect: int = 50_000,
|
|
19
|
+
prune_minimum: int = 25_000,
|
|
20
|
+
max_lines: int = 3000,
|
|
21
|
+
use_predictive: bool = True,
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Initialize agent configuration.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
name: Agent identifier (e.g., "planner", "coder")
|
|
28
|
+
system_prompt_path: Path to agent-specific prompt file
|
|
29
|
+
tools: List of allowed tool names
|
|
30
|
+
prune_protect: Context size before pruning starts (tokens)
|
|
31
|
+
prune_minimum: Minimum context after pruning (tokens)
|
|
32
|
+
max_lines: Maximum lines in tool output
|
|
33
|
+
use_predictive: Enable predictive overflow detection
|
|
34
|
+
"""
|
|
35
|
+
self.name = name
|
|
36
|
+
self.system_prompt_path = system_prompt_path
|
|
37
|
+
self.tools = tools
|
|
38
|
+
self.prune_protect = prune_protect
|
|
39
|
+
self.prune_minimum = prune_minimum
|
|
40
|
+
self.max_lines = max_lines
|
|
41
|
+
self.use_predictive = use_predictive
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AgentRegistry:
|
|
45
|
+
"""Registry of specialized agent configurations."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, workspace_root: Path, tool_registry: Any):
|
|
48
|
+
"""
|
|
49
|
+
Initialize agent registry.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
workspace_root: Root directory of workspace
|
|
53
|
+
tool_registry: ToolRegistry instance
|
|
54
|
+
"""
|
|
55
|
+
self.workspace = workspace_root
|
|
56
|
+
self.tools = tool_registry
|
|
57
|
+
self.prompts_dir = workspace_root / "prompts" / "agents"
|
|
58
|
+
|
|
59
|
+
def get_planner_config(self) -> AgentConfig:
|
|
60
|
+
"""Get configuration for Planner agent."""
|
|
61
|
+
return AgentConfig(
|
|
62
|
+
name="planner",
|
|
63
|
+
system_prompt_path=str(self.prompts_dir / "planner.md"),
|
|
64
|
+
tools=[
|
|
65
|
+
"search_files",
|
|
66
|
+
"search_code",
|
|
67
|
+
"read_file",
|
|
68
|
+
"list_directory",
|
|
69
|
+
"task_write",
|
|
70
|
+
],
|
|
71
|
+
prune_protect=50_000, # Moderate - needs context for planning
|
|
72
|
+
prune_minimum=25_000,
|
|
73
|
+
max_lines=3000,
|
|
74
|
+
use_predictive=True,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def get_coder_config(self, task_id: str | None = None) -> AgentConfig:
|
|
78
|
+
"""
|
|
79
|
+
Get configuration for Coder agent.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
task_id: Optional task identifier for naming
|
|
83
|
+
"""
|
|
84
|
+
name = f"coder-{task_id}" if task_id else "coder"
|
|
85
|
+
|
|
86
|
+
return AgentConfig(
|
|
87
|
+
name=name,
|
|
88
|
+
system_prompt_path=str(self.prompts_dir / "coder.md"),
|
|
89
|
+
tools=[
|
|
90
|
+
"read_file",
|
|
91
|
+
"write_file",
|
|
92
|
+
"update_file",
|
|
93
|
+
"run_command",
|
|
94
|
+
"search_code",
|
|
95
|
+
],
|
|
96
|
+
prune_protect=100_000, # Large - needs full context for coding
|
|
97
|
+
prune_minimum=50_000,
|
|
98
|
+
max_lines=5000,
|
|
99
|
+
use_predictive=True,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def get_reviewer_config(self) -> AgentConfig:
|
|
103
|
+
"""Get configuration for Reviewer agent."""
|
|
104
|
+
return AgentConfig(
|
|
105
|
+
name="reviewer",
|
|
106
|
+
system_prompt_path=str(self.prompts_dir / "reviewer.md"),
|
|
107
|
+
tools=[
|
|
108
|
+
"read_file",
|
|
109
|
+
"run_command",
|
|
110
|
+
"search_code",
|
|
111
|
+
"task_update",
|
|
112
|
+
],
|
|
113
|
+
prune_protect=75_000, # Moderate-large - needs context for review
|
|
114
|
+
prune_minimum=40_000,
|
|
115
|
+
max_lines=4000,
|
|
116
|
+
use_predictive=True,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def get_executor_config(self) -> AgentConfig:
|
|
120
|
+
"""Get configuration for Executor agent."""
|
|
121
|
+
return AgentConfig(
|
|
122
|
+
name="executor",
|
|
123
|
+
system_prompt_path=str(self.prompts_dir / "executor.md"),
|
|
124
|
+
tools=[
|
|
125
|
+
"run_command",
|
|
126
|
+
"fetch",
|
|
127
|
+
"query_logs",
|
|
128
|
+
"query_metrics",
|
|
129
|
+
"screenshot",
|
|
130
|
+
"dom_snapshot",
|
|
131
|
+
],
|
|
132
|
+
prune_protect=50_000, # Moderate - runtime validation
|
|
133
|
+
prune_minimum=25_000,
|
|
134
|
+
max_lines=3000,
|
|
135
|
+
use_predictive=True,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def get_orchestrator_config(self) -> AgentConfig:
|
|
139
|
+
"""Get configuration for Orchestrator agent."""
|
|
140
|
+
return AgentConfig(
|
|
141
|
+
name="orchestrator",
|
|
142
|
+
system_prompt_path=str(self.prompts_dir / "orchestrator.md"),
|
|
143
|
+
tools=[], # Orchestrator delegates, doesn't use tools directly
|
|
144
|
+
prune_protect=30_000, # Small - just coordinates
|
|
145
|
+
prune_minimum=15_000,
|
|
146
|
+
max_lines=2000,
|
|
147
|
+
use_predictive=True,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def get_agent_configs(self) -> dict[str, AgentConfig]:
|
|
151
|
+
"""
|
|
152
|
+
Get all agent configurations.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Dict mapping agent type to AgentConfig
|
|
156
|
+
"""
|
|
157
|
+
return {
|
|
158
|
+
"planner": self.get_planner_config(),
|
|
159
|
+
"coder": self.get_coder_config(),
|
|
160
|
+
"reviewer": self.get_reviewer_config(),
|
|
161
|
+
"executor": self.get_executor_config(),
|
|
162
|
+
"orchestrator": self.get_orchestrator_config(),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
def load_system_prompt(self, agent_type: str) -> str:
|
|
166
|
+
"""
|
|
167
|
+
Load system prompt for agent type.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
agent_type: Agent type (planner, coder, reviewer, executor, orchestrator)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
System prompt content
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
FileNotFoundError: If prompt file doesn't exist
|
|
177
|
+
"""
|
|
178
|
+
config = self.get_agent_configs()[agent_type]
|
|
179
|
+
prompt_path = Path(config.system_prompt_path)
|
|
180
|
+
|
|
181
|
+
if not prompt_path.exists():
|
|
182
|
+
raise FileNotFoundError(f"Prompt file not found: {prompt_path}")
|
|
183
|
+
|
|
184
|
+
return prompt_path.read_text()
|
|
185
|
+
|
|
186
|
+
def get_allowed_tools(self, agent_type: str) -> list[str]:
|
|
187
|
+
"""
|
|
188
|
+
Get list of tools allowed for agent type.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
agent_type: Agent type
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
List of tool names
|
|
195
|
+
"""
|
|
196
|
+
config = self.get_agent_configs()[agent_type]
|
|
197
|
+
return config.tools
|
|
198
|
+
|
|
199
|
+
def validate_tool_access(self, agent_type: str, tool_name: str) -> bool:
|
|
200
|
+
"""
|
|
201
|
+
Check if agent is allowed to use tool.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
agent_type: Agent type
|
|
205
|
+
tool_name: Tool to check
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if allowed, False otherwise
|
|
209
|
+
"""
|
|
210
|
+
allowed_tools = self.get_allowed_tools(agent_type)
|
|
211
|
+
return tool_name in allowed_tools
|