kolega-code 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.
- kolega_code/__init__.py +151 -0
- kolega_code/agent/__init__.py +42 -0
- kolega_code/agent/baseagent.py +998 -0
- kolega_code/agent/browseragent.py +123 -0
- kolega_code/agent/coder.py +157 -0
- kolega_code/agent/common.py +41 -0
- kolega_code/agent/compression.py +81 -0
- kolega_code/agent/context.py +112 -0
- kolega_code/agent/conversation.py +408 -0
- kolega_code/agent/generalagent.py +146 -0
- kolega_code/agent/investigationagent.py +123 -0
- kolega_code/agent/planningagent.py +187 -0
- kolega_code/agent/prompt_provider.py +196 -0
- kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
- kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
- kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
- kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
- kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
- kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
- kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
- kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
- kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
- kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
- kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
- kolega_code/agent/prompts.py +192 -0
- kolega_code/agent/tests/__init__.py +0 -0
- kolega_code/agent/tests/llm/__init__.py +0 -0
- kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
- kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
- kolega_code/agent/tests/llm/test_client.py +773 -0
- kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
- kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
- kolega_code/agent/tests/llm/test_exceptions.py +249 -0
- kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
- kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
- kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
- kolega_code/agent/tests/llm/test_model_specs.py +17 -0
- kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
- kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
- kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
- kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
- kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
- kolega_code/agent/tests/services/__init__.py +1 -0
- kolega_code/agent/tests/services/test_browser.py +447 -0
- kolega_code/agent/tests/services/test_browser_parity.py +353 -0
- kolega_code/agent/tests/services/test_file_system.py +699 -0
- kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
- kolega_code/agent/tests/services/test_terminal.py +154 -0
- kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
- kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
- kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
- kolega_code/agent/tests/test_base_agent.py +1942 -0
- kolega_code/agent/tests/test_coder_attachments.py +330 -0
- kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
- kolega_code/agent/tests/test_commands.py +179 -0
- kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
- kolega_code/agent/tests/test_empty_message_handling.py +48 -0
- kolega_code/agent/tests/test_general_agent.py +242 -0
- kolega_code/agent/tests/test_html.py +320 -0
- kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
- kolega_code/agent/tests/test_planning_agent.py +227 -0
- kolega_code/agent/tests/test_prompt_provider.py +271 -0
- kolega_code/agent/tests/test_tool_registry.py +102 -0
- kolega_code/agent/tests/test_tools.py +549 -0
- kolega_code/agent/tests/tool_backend/__init__.py +0 -0
- kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
- kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
- kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
- kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
- kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
- kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
- kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
- kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
- kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
- kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
- kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
- kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
- kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
- kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
- kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
- kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
- kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
- kolega_code/agent/tool_backend/agent_tool.py +414 -0
- kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
- kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
- kolega_code/agent/tool_backend/base_tool.py +217 -0
- kolega_code/agent/tool_backend/browser_tool.py +271 -0
- kolega_code/agent/tool_backend/build_tool.py +93 -0
- kolega_code/agent/tool_backend/create_file_tool.py +52 -0
- kolega_code/agent/tool_backend/glob_tool.py +323 -0
- kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
- kolega_code/agent/tool_backend/memory_tool.py +79 -0
- kolega_code/agent/tool_backend/read_file_tool.py +119 -0
- kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
- kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
- kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
- kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
- kolega_code/agent/tool_backend/streaming_tool.py +47 -0
- kolega_code/agent/tool_backend/terminal_tool.py +643 -0
- kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
- kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
- kolega_code/agent/tools.py +1704 -0
- kolega_code/agent/utils/commands.py +94 -0
- kolega_code/cli/__init__.py +1 -0
- kolega_code/cli/app.py +2756 -0
- kolega_code/cli/config.py +280 -0
- kolega_code/cli/connection.py +49 -0
- kolega_code/cli/file_index.py +147 -0
- kolega_code/cli/main.py +564 -0
- kolega_code/cli/mentions.py +155 -0
- kolega_code/cli/messages.py +89 -0
- kolega_code/cli/provider_registry.py +96 -0
- kolega_code/cli/session_store.py +207 -0
- kolega_code/cli/settings.py +87 -0
- kolega_code/cli/skills.py +409 -0
- kolega_code/cli/slash_commands.py +108 -0
- kolega_code/cli/tests/__init__.py +1 -0
- kolega_code/cli/tests/test_app.py +4251 -0
- kolega_code/cli/tests/test_cli_config.py +171 -0
- kolega_code/cli/tests/test_connection.py +26 -0
- kolega_code/cli/tests/test_file_index.py +103 -0
- kolega_code/cli/tests/test_main.py +455 -0
- kolega_code/cli/tests/test_mentions.py +108 -0
- kolega_code/cli/tests/test_session_store.py +67 -0
- kolega_code/cli/tests/test_settings.py +62 -0
- kolega_code/cli/tests/test_skills.py +157 -0
- kolega_code/cli/tests/test_slash_commands.py +88 -0
- kolega_code/cli/theme.py +180 -0
- kolega_code/config.py +154 -0
- kolega_code/events.py +202 -0
- kolega_code/llm/client.py +300 -0
- kolega_code/llm/exceptions.py +285 -0
- kolega_code/llm/instrumented_client.py +520 -0
- kolega_code/llm/models.py +1368 -0
- kolega_code/llm/providers/__init__.py +0 -0
- kolega_code/llm/providers/anthropic.py +387 -0
- kolega_code/llm/providers/base.py +71 -0
- kolega_code/llm/providers/google.py +157 -0
- kolega_code/llm/providers/models.py +37 -0
- kolega_code/llm/providers/openai.py +363 -0
- kolega_code/llm/ratelimit.py +40 -0
- kolega_code/llm/specs.py +67 -0
- kolega_code/llm/tool_execution_ids.py +18 -0
- kolega_code/models/__init__.py +9 -0
- kolega_code/models/sandbox_terminal_state.py +47 -0
- kolega_code/runtime.py +50 -0
- kolega_code/sandbox/README.md +200 -0
- kolega_code/sandbox/__init__.py +21 -0
- kolega_code/sandbox/async_filesystem.py +475 -0
- kolega_code/sandbox/base.py +297 -0
- kolega_code/sandbox/browser.py +25 -0
- kolega_code/sandbox/event_loop.py +43 -0
- kolega_code/sandbox/filesystem.py +341 -0
- kolega_code/sandbox/local.py +118 -0
- kolega_code/sandbox/serializer.py +175 -0
- kolega_code/sandbox/terminal.py +868 -0
- kolega_code/sandbox/utils.py +216 -0
- kolega_code/services/base.py +255 -0
- kolega_code/services/browser.py +444 -0
- kolega_code/services/file_system.py +749 -0
- kolega_code/services/html.py +221 -0
- kolega_code/services/terminal.py +903 -0
- kolega_code/tools/__init__.py +22 -0
- kolega_code/tools/core.py +33 -0
- kolega_code/tools/definitions.py +81 -0
- kolega_code/tools/registry.py +73 -0
- kolega_code-0.1.0.dist-info/METADATA +157 -0
- kolega_code-0.1.0.dist-info/RECORD +171 -0
- kolega_code-0.1.0.dist-info/WHEEL +4 -0
- kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
- kolega_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import inspect
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Union, Optional
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from kolega_code.config import AgentConfig
|
|
9
|
+
from kolega_code.events import AgentEvent
|
|
10
|
+
from .base_tool import BaseTool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentTool(BaseTool):
|
|
14
|
+
"""
|
|
15
|
+
Unified tool for dispatching all types of sub-agents.
|
|
16
|
+
|
|
17
|
+
This tool provides a consistent interface for creating and managing sub-agents
|
|
18
|
+
with proper interrupt handling, error management, and cleanup.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
project_path: Union[str, Path],
|
|
24
|
+
workspace_id: str,
|
|
25
|
+
thread_id: str,
|
|
26
|
+
connection_manager,
|
|
27
|
+
config: AgentConfig,
|
|
28
|
+
caller,
|
|
29
|
+
filesystem=None,
|
|
30
|
+
terminal_manager=None,
|
|
31
|
+
browser_manager=None,
|
|
32
|
+
langfuse_client=None,
|
|
33
|
+
):
|
|
34
|
+
super().__init__(
|
|
35
|
+
project_path,
|
|
36
|
+
workspace_id,
|
|
37
|
+
thread_id,
|
|
38
|
+
connection_manager,
|
|
39
|
+
config,
|
|
40
|
+
caller,
|
|
41
|
+
filesystem,
|
|
42
|
+
terminal_manager=terminal_manager,
|
|
43
|
+
browser_manager=browser_manager,
|
|
44
|
+
)
|
|
45
|
+
self.agents = {}
|
|
46
|
+
self.langfuse_client = langfuse_client
|
|
47
|
+
self.sub_agent_recorder = getattr(caller, "sub_agent_recorder", None) if caller else None
|
|
48
|
+
# No need to store these separately since they're already in the parent class
|
|
49
|
+
# self.terminal_manager = terminal_manager
|
|
50
|
+
# self.browser_manager = browser_manager
|
|
51
|
+
|
|
52
|
+
async def _maybe_await(self, value):
|
|
53
|
+
if inspect.isawaitable(value):
|
|
54
|
+
return await value
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
async def _call_recorder(self, method_name: str, *args, **kwargs):
|
|
58
|
+
"""Call an optional host-provided sub-agent recorder method."""
|
|
59
|
+
if not self.sub_agent_recorder:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
method = getattr(self.sub_agent_recorder, method_name, None)
|
|
63
|
+
if method is None:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
return await self._maybe_await(method(*args, **kwargs))
|
|
67
|
+
|
|
68
|
+
async def _start_conversation(
|
|
69
|
+
self,
|
|
70
|
+
tool_call_id: str,
|
|
71
|
+
agent_name: str,
|
|
72
|
+
class_name: str,
|
|
73
|
+
agent_id: str,
|
|
74
|
+
task: str,
|
|
75
|
+
) -> Optional[str]:
|
|
76
|
+
payload = {
|
|
77
|
+
"parent_thread_id": self.thread_id,
|
|
78
|
+
"parent_tool_call_id": tool_call_id,
|
|
79
|
+
"agent_name": agent_name,
|
|
80
|
+
"agent_type": class_name,
|
|
81
|
+
"agent_id": agent_id,
|
|
82
|
+
"initial_task": task,
|
|
83
|
+
}
|
|
84
|
+
return await self._call_recorder("start_conversation", payload)
|
|
85
|
+
|
|
86
|
+
async def _record_message(self, conversation_id: str, message: dict, sequence: int) -> None:
|
|
87
|
+
await self._call_recorder("record_message", conversation_id, message, sequence)
|
|
88
|
+
|
|
89
|
+
async def _complete_conversation(self, conversation_id: str, update_data: dict) -> None:
|
|
90
|
+
await self._call_recorder("complete_conversation", conversation_id, update_data)
|
|
91
|
+
|
|
92
|
+
async def _fail_conversation(self, conversation_id: str, update_data: dict) -> None:
|
|
93
|
+
await self._call_recorder("fail_conversation", conversation_id, update_data)
|
|
94
|
+
|
|
95
|
+
async def _interrupt_conversation(self, conversation_id: str, update_data: dict) -> None:
|
|
96
|
+
await self._call_recorder("interrupt_conversation", conversation_id, update_data)
|
|
97
|
+
|
|
98
|
+
async def _send_status_event(self, status: str, message: str, sub_agent_info: Optional[dict] = None) -> None:
|
|
99
|
+
"""Helper method to send status events."""
|
|
100
|
+
event = AgentEvent(
|
|
101
|
+
event_type="chat_message",
|
|
102
|
+
content={"status": status, "message": message},
|
|
103
|
+
sender=self.caller.agent_name if self.caller else "agent-tool",
|
|
104
|
+
sub_agent_info=sub_agent_info,
|
|
105
|
+
)
|
|
106
|
+
await self.connection_manager.broadcast_event(event, self.workspace_id, self.thread_id)
|
|
107
|
+
|
|
108
|
+
async def _dispatch_agent(self, agent_class_import: str, task: str) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Generic method to dispatch any agent type.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
agent_class_import: Full import path to agent class (e.g., "kolega_code.agent.investigationagent.InvestigationAgent")
|
|
114
|
+
task: Task description for the agent
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The agent's recap of its work
|
|
118
|
+
"""
|
|
119
|
+
# Extract the agent name from the class
|
|
120
|
+
module_path, class_name = agent_class_import.rsplit(".", 1)
|
|
121
|
+
|
|
122
|
+
# Import the module and get the class
|
|
123
|
+
module = __import__(module_path, fromlist=[class_name])
|
|
124
|
+
agent_class = getattr(module, class_name)
|
|
125
|
+
agent_name = agent_class.agent_name
|
|
126
|
+
|
|
127
|
+
# Create a unique agent ID
|
|
128
|
+
agent_id = str(uuid.uuid4())
|
|
129
|
+
|
|
130
|
+
# Use the app's unique execution ID for DB/UI links, not the provider's tool-use ID.
|
|
131
|
+
tool_call_id = getattr(self.caller, "current_tool_execution_id", None)
|
|
132
|
+
if not isinstance(tool_call_id, str):
|
|
133
|
+
tool_call_id = getattr(self.caller, "current_tool_call_id", None)
|
|
134
|
+
if not isinstance(tool_call_id, str):
|
|
135
|
+
tool_call_id = None
|
|
136
|
+
conversation_id = None
|
|
137
|
+
start_time = time.time()
|
|
138
|
+
|
|
139
|
+
# Create sub-agent conversation record if the host application supplied a recorder.
|
|
140
|
+
if tool_call_id:
|
|
141
|
+
conversation_id = await self._start_conversation(
|
|
142
|
+
tool_call_id=tool_call_id,
|
|
143
|
+
agent_name=agent_name,
|
|
144
|
+
class_name=class_name,
|
|
145
|
+
agent_id=agent_id,
|
|
146
|
+
task=task,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Calculate depth based on whether the caller is also a sub-agent
|
|
150
|
+
parent_depth = 0
|
|
151
|
+
if hasattr(self.caller, "sub_agent") and self.caller.sub_agent:
|
|
152
|
+
# If the caller is a sub-agent, get its depth
|
|
153
|
+
# For now, we'll increment from 1, but ideally we'd track this
|
|
154
|
+
parent_depth = 1
|
|
155
|
+
|
|
156
|
+
# Attached to every event from this dispatch so the UI can group and
|
|
157
|
+
# disambiguate concurrently running sub-agents.
|
|
158
|
+
sub_agent_info = {
|
|
159
|
+
"agent_id": agent_id,
|
|
160
|
+
"agent_name": agent_name,
|
|
161
|
+
"task": task[:120],
|
|
162
|
+
"conversation_id": conversation_id,
|
|
163
|
+
"parent_tool_call_id": tool_call_id,
|
|
164
|
+
"depth": parent_depth + 1,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Send start status
|
|
168
|
+
await self._send_status_event("GENERATING", f"Starting {agent_name} task", sub_agent_info=sub_agent_info)
|
|
169
|
+
conversation_finished = False
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
# Create the agent instance
|
|
173
|
+
agent = agent_class(
|
|
174
|
+
project_path=self.project_path,
|
|
175
|
+
workspace_id=self.workspace_id,
|
|
176
|
+
thread_id=self.thread_id,
|
|
177
|
+
connection_manager=self.connection_manager,
|
|
178
|
+
config=self.config,
|
|
179
|
+
sub_agent=True,
|
|
180
|
+
filesystem=self.filesystem,
|
|
181
|
+
terminal_manager=self.terminal_manager,
|
|
182
|
+
browser_manager=self.browser_manager,
|
|
183
|
+
langfuse_client=self.langfuse_client,
|
|
184
|
+
user_id=getattr(self.caller, "user_id", None) if self.caller else None,
|
|
185
|
+
user_email=getattr(self.caller, "user_email", None) if self.caller else None,
|
|
186
|
+
project_template_slug=getattr(self.caller, "project_template_slug", None) if self.caller else None,
|
|
187
|
+
protected_files=getattr(self.caller, "protected_files", None) if self.caller else None,
|
|
188
|
+
agent_mode=getattr(self.caller, "agent_mode", None) if self.caller else None,
|
|
189
|
+
workspace_env_var_descriptions=getattr(self.caller, "workspace_env_var_descriptions", None)
|
|
190
|
+
if self.caller
|
|
191
|
+
else None,
|
|
192
|
+
workspace_memories=getattr(self.caller, "workspace_memories", None) if self.caller else None,
|
|
193
|
+
prompt_extensions=getattr(self.caller, "prompt_extensions", None) if self.caller else None,
|
|
194
|
+
tool_extensions=getattr(self.caller, "tool_extensions", None) if self.caller else None,
|
|
195
|
+
usage_recorder=getattr(self.caller, "usage_recorder", None) if self.caller else None,
|
|
196
|
+
sub_agent_recorder=getattr(self.caller, "sub_agent_recorder", None) if self.caller else None,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Store agent reference
|
|
200
|
+
self.agents[agent_id] = agent
|
|
201
|
+
|
|
202
|
+
# Set parent context so the agent's own events carry sub_agent_info
|
|
203
|
+
agent.parent_tool_call_id = tool_call_id
|
|
204
|
+
agent.conversation_id = conversation_id
|
|
205
|
+
agent.sub_agent_context = sub_agent_info
|
|
206
|
+
|
|
207
|
+
# Track messages and their sequence
|
|
208
|
+
last_saved_index = -1 # Track what we've already saved
|
|
209
|
+
streamed_messages = {} # Track messages by UUID for assembly
|
|
210
|
+
|
|
211
|
+
# Process the task and stream messages
|
|
212
|
+
async for msg in agent.process_message_stream(task):
|
|
213
|
+
# Extract message details
|
|
214
|
+
message_type = msg.get("type", "agent")
|
|
215
|
+
content = msg.get("content", "")
|
|
216
|
+
complete = msg.get("complete", False)
|
|
217
|
+
msg_uuid = msg.get("uuid", str(uuid.uuid4()))
|
|
218
|
+
timestamp = datetime.now().isoformat()
|
|
219
|
+
|
|
220
|
+
content_payload = {"text": content}
|
|
221
|
+
if message_type != "response":
|
|
222
|
+
content_payload["message_type"] = message_type
|
|
223
|
+
|
|
224
|
+
evt = AgentEvent(
|
|
225
|
+
event_type="chat_message",
|
|
226
|
+
content=content_payload,
|
|
227
|
+
sender=agent_name,
|
|
228
|
+
timestamp=timestamp,
|
|
229
|
+
is_streaming=(message_type in ["response", "thinking"] and not complete),
|
|
230
|
+
uuid=msg_uuid,
|
|
231
|
+
sub_agent_info=sub_agent_info,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Broadcast to connection manager
|
|
235
|
+
await self.connection_manager.broadcast_event(evt, self.workspace_id, self.thread_id)
|
|
236
|
+
|
|
237
|
+
# Track streaming messages
|
|
238
|
+
if msg_uuid not in streamed_messages:
|
|
239
|
+
streamed_messages[msg_uuid] = {
|
|
240
|
+
"content": "",
|
|
241
|
+
"type": message_type,
|
|
242
|
+
"uuid": msg_uuid,
|
|
243
|
+
"complete": False,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
streamed_messages[msg_uuid]["content"] += content
|
|
247
|
+
streamed_messages[msg_uuid]["complete"] = complete
|
|
248
|
+
|
|
249
|
+
# Save new messages when a message completes
|
|
250
|
+
if conversation_id and complete:
|
|
251
|
+
# Get current complete history from agent
|
|
252
|
+
current_history = agent.dump_message_history()
|
|
253
|
+
|
|
254
|
+
# Only save messages we haven't saved yet
|
|
255
|
+
for i in range(last_saved_index + 1, len(current_history)):
|
|
256
|
+
hist_msg = current_history[i]
|
|
257
|
+
await self._record_message(
|
|
258
|
+
conversation_id,
|
|
259
|
+
{
|
|
260
|
+
"role": hist_msg.get("role", "assistant"),
|
|
261
|
+
"content": hist_msg.get("content", []),
|
|
262
|
+
"stream_uuid": None,
|
|
263
|
+
},
|
|
264
|
+
i + 1,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Update last saved index
|
|
268
|
+
last_saved_index = len(current_history) - 1
|
|
269
|
+
|
|
270
|
+
# Mark streamed message as saved
|
|
271
|
+
streamed_messages[msg_uuid]["saved"] = True
|
|
272
|
+
|
|
273
|
+
# Get final history and save any remaining messages
|
|
274
|
+
final_history = agent.dump_message_history()
|
|
275
|
+
|
|
276
|
+
if conversation_id:
|
|
277
|
+
# Save any messages we haven't saved yet
|
|
278
|
+
for i in range(last_saved_index + 1, len(final_history)):
|
|
279
|
+
hist_msg = final_history[i]
|
|
280
|
+
await self._record_message(
|
|
281
|
+
conversation_id,
|
|
282
|
+
{
|
|
283
|
+
"role": hist_msg.get("role", "assistant"),
|
|
284
|
+
"content": hist_msg.get("content", []),
|
|
285
|
+
"stream_uuid": None,
|
|
286
|
+
},
|
|
287
|
+
i + 1,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Get agent recap
|
|
291
|
+
result = await agent.recap_agent_outcome()
|
|
292
|
+
|
|
293
|
+
# Update conversation with completion status
|
|
294
|
+
if conversation_id:
|
|
295
|
+
execution_time = time.time() - start_time
|
|
296
|
+
|
|
297
|
+
update_data = {
|
|
298
|
+
"status": "completed",
|
|
299
|
+
"completed_at": datetime.now(timezone.utc),
|
|
300
|
+
"recap": result,
|
|
301
|
+
"message_count": len(final_history),
|
|
302
|
+
"execution_time_seconds": execution_time,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Try to get token count if available
|
|
306
|
+
if hasattr(agent, "total_tokens_used"):
|
|
307
|
+
update_data["total_tokens"] = agent.total_tokens_used
|
|
308
|
+
|
|
309
|
+
await self._complete_conversation(conversation_id, update_data)
|
|
310
|
+
conversation_finished = True
|
|
311
|
+
|
|
312
|
+
# Send completion status
|
|
313
|
+
await self._send_status_event(
|
|
314
|
+
"STOPPED", f"Completed {agent_name} task", sub_agent_info=sub_agent_info
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return result
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
# Update conversation with error status
|
|
321
|
+
if conversation_id:
|
|
322
|
+
execution_time = time.time() - start_time
|
|
323
|
+
await self._fail_conversation(
|
|
324
|
+
conversation_id,
|
|
325
|
+
{
|
|
326
|
+
"status": "failed",
|
|
327
|
+
"completed_at": datetime.now(timezone.utc),
|
|
328
|
+
"error": str(e),
|
|
329
|
+
"execution_time_seconds": execution_time,
|
|
330
|
+
},
|
|
331
|
+
)
|
|
332
|
+
conversation_finished = True
|
|
333
|
+
|
|
334
|
+
# Log and re-raise the error
|
|
335
|
+
await self.log_error(f"Error in {agent_name}: {str(e)}", sender="AgentTool")
|
|
336
|
+
await self._send_status_event("ERROR", f"Error in {agent_name}: {str(e)}", sub_agent_info=sub_agent_info)
|
|
337
|
+
raise
|
|
338
|
+
|
|
339
|
+
finally:
|
|
340
|
+
# Handle interrupted conversations
|
|
341
|
+
if conversation_id and not conversation_finished:
|
|
342
|
+
execution_time = time.time() - start_time
|
|
343
|
+
await self._interrupt_conversation(
|
|
344
|
+
conversation_id,
|
|
345
|
+
{
|
|
346
|
+
"status": "interrupted",
|
|
347
|
+
"completed_at": datetime.now(timezone.utc),
|
|
348
|
+
"execution_time_seconds": execution_time,
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Clean up agent reference
|
|
353
|
+
if agent_id in self.agents:
|
|
354
|
+
del self.agents[agent_id]
|
|
355
|
+
|
|
356
|
+
async def dispatch_investigation_agent(self, task: str) -> str:
|
|
357
|
+
"""
|
|
358
|
+
Dispatch an investigation agent to perform a specific task with read-only access to the codebase.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
task: A detailed description of the investigation task to perform
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
A comprehensive report of the investigation findings
|
|
365
|
+
"""
|
|
366
|
+
return await self._dispatch_agent(
|
|
367
|
+
agent_class_import="kolega_code.agent.investigationagent.InvestigationAgent",
|
|
368
|
+
task=task,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
async def dispatch_browser_agent(self, task: str) -> str:
|
|
372
|
+
"""
|
|
373
|
+
Dispatch a browser agent to perform web-based tasks and interactions.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
task: A detailed description of the browser task to perform
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
A comprehensive report of the browser agent's findings and actions
|
|
380
|
+
"""
|
|
381
|
+
return await self._dispatch_agent(
|
|
382
|
+
agent_class_import="kolega_code.agent.browseragent.BrowserAgent",
|
|
383
|
+
task=task,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
async def dispatch_coding_agent(self, task: str) -> str:
|
|
387
|
+
"""
|
|
388
|
+
Dispatch a coding agent for processing coding-related tasks with streaming output.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
task: A detailed description of the coding task to perform
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
A summary of the coding process outcome
|
|
395
|
+
"""
|
|
396
|
+
return await self._dispatch_agent(
|
|
397
|
+
agent_class_import="kolega_code.agent.coder.CoderAgent",
|
|
398
|
+
task=task,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
async def dispatch_general_agent(self, task: str) -> str:
|
|
402
|
+
"""
|
|
403
|
+
Dispatch a general-purpose agent to autonomously complete a self-contained task.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
task: A detailed, self-contained description of the task to perform
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
The agent's final report on the completed task
|
|
410
|
+
"""
|
|
411
|
+
return await self._dispatch_agent(
|
|
412
|
+
agent_class_import="kolega_code.agent.generalagent.GeneralAgent",
|
|
413
|
+
task=task,
|
|
414
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from .. import prompts
|
|
2
|
+
from kolega_code.llm.client import LLMClient
|
|
3
|
+
from kolega_code.llm.models import Message, MessageHistory, TextBlock
|
|
4
|
+
from kolega_code.llm.specs import get_model_specs
|
|
5
|
+
from .base_tool import BaseTool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApplyEditTool(BaseTool):
|
|
9
|
+
|
|
10
|
+
async def edit_file(self, relative_path: str, instructions: str, code_edit: str) -> str:
|
|
11
|
+
await self.log_info(f"Applying edits to: {relative_path}", sender=self.caller.agent_name)
|
|
12
|
+
|
|
13
|
+
provider = self.config.edit_model_config.provider
|
|
14
|
+
api_key = self.config.get_api_key(provider)
|
|
15
|
+
rate_limits = self.config.edit_model_config.rate_limits
|
|
16
|
+
model_specs = get_model_specs(self.config.edit_model_config.provider, self.config.edit_model_config.model)
|
|
17
|
+
|
|
18
|
+
client = LLMClient(
|
|
19
|
+
provider=provider.value,
|
|
20
|
+
api_key=api_key,
|
|
21
|
+
max_retries=rate_limits.max_retries,
|
|
22
|
+
requests_per_minute=rate_limits.requests_per_minute,
|
|
23
|
+
tokens_per_minute=rate_limits.tokens_per_minute,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# Read the original file content
|
|
28
|
+
if not self.filesystem.exists(relative_path):
|
|
29
|
+
raise FileNotFoundError(f"File not found: {relative_path}")
|
|
30
|
+
|
|
31
|
+
if not self.filesystem.is_file(relative_path):
|
|
32
|
+
raise ValueError(f"Not a file: {relative_path}")
|
|
33
|
+
|
|
34
|
+
# Read the original code from the file
|
|
35
|
+
original_code = self.filesystem.read_text(relative_path)
|
|
36
|
+
|
|
37
|
+
system_prompt = prompts.APPLY_EDIT_USER_PROMPT
|
|
38
|
+
user_prompt = prompts.APPLY_EDIT_USER_PROMPT.format(
|
|
39
|
+
original_code=original_code, code_edit=code_edit, instructions=instructions
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
system_message = Message(role="system", content=[TextBlock(text=system_prompt)])
|
|
43
|
+
|
|
44
|
+
messages = MessageHistory([Message(role="user", content=[TextBlock(text=user_prompt)])])
|
|
45
|
+
|
|
46
|
+
# Count tokens in the messages to ensure they're within model limits
|
|
47
|
+
tokens = await client.count_tokens(messages=messages, system=system_message)
|
|
48
|
+
|
|
49
|
+
if tokens.input_tokens > model_specs["max_completion_tokens"]:
|
|
50
|
+
await self.log_warning(
|
|
51
|
+
f"The input tokens are higher than the max completion tokens in the model. ({tokens} vs {model_specs['max_completion_tokens']})",
|
|
52
|
+
sender=self.caller.agent_name,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
response = await client.generate(
|
|
56
|
+
model=self.config.edit_model_config.model,
|
|
57
|
+
max_completion_tokens=model_specs["max_completion_tokens"],
|
|
58
|
+
system=system_message,
|
|
59
|
+
messages=messages,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
response_text = response.get_text_content()
|
|
63
|
+
|
|
64
|
+
if "<updated-code>" not in response_text:
|
|
65
|
+
raise ValueError("Malformed LLM response.")
|
|
66
|
+
|
|
67
|
+
updated_code = response_text.split("<updated-code>\n")[1].split("</updated-code>")[0]
|
|
68
|
+
if not updated_code:
|
|
69
|
+
raise ValueError("Updated code is empty.")
|
|
70
|
+
|
|
71
|
+
# If we are here we have the updated code.
|
|
72
|
+
# Write the updated code to the file (with vibe policy enforcement)
|
|
73
|
+
try:
|
|
74
|
+
blocked_msg = self._enforce_vibe_edit_policy(relative_path)
|
|
75
|
+
if blocked_msg:
|
|
76
|
+
return blocked_msg
|
|
77
|
+
self.filesystem.write_text(relative_path, updated_code)
|
|
78
|
+
success_msg = f"Successfully updated file: {relative_path}"
|
|
79
|
+
await self.log_info(success_msg, sender=self.caller.agent_name)
|
|
80
|
+
return f"# {relative_path} has been updated.\n\n```\n{updated_code}\n```"
|
|
81
|
+
except PermissionError:
|
|
82
|
+
error_msg = f"Permission denied when writing to file: {relative_path}"
|
|
83
|
+
await self.log_error(error_msg, sender=self.caller.agent_name)
|
|
84
|
+
raise
|
|
85
|
+
except Exception as e:
|
|
86
|
+
error_msg = f"Failed to write to file {relative_path}: {str(e)}"
|
|
87
|
+
await self.log_error(error_msg, sender=self.caller.agent_name)
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
error_message = f"Error while applying edit: {str(e)}"
|
|
92
|
+
await self.log_error(error_message, sender=self.caller.agent_name)
|
|
93
|
+
|
|
94
|
+
import traceback
|
|
95
|
+
|
|
96
|
+
traceback_str = traceback.format_exc()
|
|
97
|
+
print(f"Traceback:\n{traceback_str}")
|
|
98
|
+
return error_message
|