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.
Files changed (171) hide show
  1. kolega_code/__init__.py +151 -0
  2. kolega_code/agent/__init__.py +42 -0
  3. kolega_code/agent/baseagent.py +998 -0
  4. kolega_code/agent/browseragent.py +123 -0
  5. kolega_code/agent/coder.py +157 -0
  6. kolega_code/agent/common.py +41 -0
  7. kolega_code/agent/compression.py +81 -0
  8. kolega_code/agent/context.py +112 -0
  9. kolega_code/agent/conversation.py +408 -0
  10. kolega_code/agent/generalagent.py +146 -0
  11. kolega_code/agent/investigationagent.py +123 -0
  12. kolega_code/agent/planningagent.py +187 -0
  13. kolega_code/agent/prompt_provider.py +196 -0
  14. kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
  15. kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
  16. kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
  17. kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
  18. kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
  19. kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
  20. kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
  21. kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
  22. kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
  23. kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
  24. kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
  25. kolega_code/agent/prompts.py +192 -0
  26. kolega_code/agent/tests/__init__.py +0 -0
  27. kolega_code/agent/tests/llm/__init__.py +0 -0
  28. kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
  29. kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
  30. kolega_code/agent/tests/llm/test_client.py +773 -0
  31. kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
  32. kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
  33. kolega_code/agent/tests/llm/test_exceptions.py +249 -0
  34. kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
  35. kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
  36. kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
  37. kolega_code/agent/tests/llm/test_model_specs.py +17 -0
  38. kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
  39. kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
  40. kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
  41. kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
  42. kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
  43. kolega_code/agent/tests/services/__init__.py +1 -0
  44. kolega_code/agent/tests/services/test_browser.py +447 -0
  45. kolega_code/agent/tests/services/test_browser_parity.py +353 -0
  46. kolega_code/agent/tests/services/test_file_system.py +699 -0
  47. kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
  48. kolega_code/agent/tests/services/test_terminal.py +154 -0
  49. kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
  50. kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
  51. kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
  52. kolega_code/agent/tests/test_base_agent.py +1942 -0
  53. kolega_code/agent/tests/test_coder_attachments.py +330 -0
  54. kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
  55. kolega_code/agent/tests/test_commands.py +179 -0
  56. kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
  57. kolega_code/agent/tests/test_empty_message_handling.py +48 -0
  58. kolega_code/agent/tests/test_general_agent.py +242 -0
  59. kolega_code/agent/tests/test_html.py +320 -0
  60. kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
  61. kolega_code/agent/tests/test_planning_agent.py +227 -0
  62. kolega_code/agent/tests/test_prompt_provider.py +271 -0
  63. kolega_code/agent/tests/test_tool_registry.py +102 -0
  64. kolega_code/agent/tests/test_tools.py +549 -0
  65. kolega_code/agent/tests/tool_backend/__init__.py +0 -0
  66. kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
  67. kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
  68. kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
  69. kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
  70. kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
  71. kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
  72. kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
  73. kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
  74. kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
  75. kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
  76. kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
  77. kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
  78. kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
  79. kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
  80. kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
  81. kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
  82. kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
  83. kolega_code/agent/tool_backend/agent_tool.py +414 -0
  84. kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
  85. kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
  86. kolega_code/agent/tool_backend/base_tool.py +217 -0
  87. kolega_code/agent/tool_backend/browser_tool.py +271 -0
  88. kolega_code/agent/tool_backend/build_tool.py +93 -0
  89. kolega_code/agent/tool_backend/create_file_tool.py +52 -0
  90. kolega_code/agent/tool_backend/glob_tool.py +323 -0
  91. kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
  92. kolega_code/agent/tool_backend/memory_tool.py +79 -0
  93. kolega_code/agent/tool_backend/read_file_tool.py +119 -0
  94. kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
  95. kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
  96. kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
  97. kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
  98. kolega_code/agent/tool_backend/streaming_tool.py +47 -0
  99. kolega_code/agent/tool_backend/terminal_tool.py +643 -0
  100. kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
  101. kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
  102. kolega_code/agent/tools.py +1704 -0
  103. kolega_code/agent/utils/commands.py +94 -0
  104. kolega_code/cli/__init__.py +1 -0
  105. kolega_code/cli/app.py +2756 -0
  106. kolega_code/cli/config.py +280 -0
  107. kolega_code/cli/connection.py +49 -0
  108. kolega_code/cli/file_index.py +147 -0
  109. kolega_code/cli/main.py +564 -0
  110. kolega_code/cli/mentions.py +155 -0
  111. kolega_code/cli/messages.py +89 -0
  112. kolega_code/cli/provider_registry.py +96 -0
  113. kolega_code/cli/session_store.py +207 -0
  114. kolega_code/cli/settings.py +87 -0
  115. kolega_code/cli/skills.py +409 -0
  116. kolega_code/cli/slash_commands.py +108 -0
  117. kolega_code/cli/tests/__init__.py +1 -0
  118. kolega_code/cli/tests/test_app.py +4251 -0
  119. kolega_code/cli/tests/test_cli_config.py +171 -0
  120. kolega_code/cli/tests/test_connection.py +26 -0
  121. kolega_code/cli/tests/test_file_index.py +103 -0
  122. kolega_code/cli/tests/test_main.py +455 -0
  123. kolega_code/cli/tests/test_mentions.py +108 -0
  124. kolega_code/cli/tests/test_session_store.py +67 -0
  125. kolega_code/cli/tests/test_settings.py +62 -0
  126. kolega_code/cli/tests/test_skills.py +157 -0
  127. kolega_code/cli/tests/test_slash_commands.py +88 -0
  128. kolega_code/cli/theme.py +180 -0
  129. kolega_code/config.py +154 -0
  130. kolega_code/events.py +202 -0
  131. kolega_code/llm/client.py +300 -0
  132. kolega_code/llm/exceptions.py +285 -0
  133. kolega_code/llm/instrumented_client.py +520 -0
  134. kolega_code/llm/models.py +1368 -0
  135. kolega_code/llm/providers/__init__.py +0 -0
  136. kolega_code/llm/providers/anthropic.py +387 -0
  137. kolega_code/llm/providers/base.py +71 -0
  138. kolega_code/llm/providers/google.py +157 -0
  139. kolega_code/llm/providers/models.py +37 -0
  140. kolega_code/llm/providers/openai.py +363 -0
  141. kolega_code/llm/ratelimit.py +40 -0
  142. kolega_code/llm/specs.py +67 -0
  143. kolega_code/llm/tool_execution_ids.py +18 -0
  144. kolega_code/models/__init__.py +9 -0
  145. kolega_code/models/sandbox_terminal_state.py +47 -0
  146. kolega_code/runtime.py +50 -0
  147. kolega_code/sandbox/README.md +200 -0
  148. kolega_code/sandbox/__init__.py +21 -0
  149. kolega_code/sandbox/async_filesystem.py +475 -0
  150. kolega_code/sandbox/base.py +297 -0
  151. kolega_code/sandbox/browser.py +25 -0
  152. kolega_code/sandbox/event_loop.py +43 -0
  153. kolega_code/sandbox/filesystem.py +341 -0
  154. kolega_code/sandbox/local.py +118 -0
  155. kolega_code/sandbox/serializer.py +175 -0
  156. kolega_code/sandbox/terminal.py +868 -0
  157. kolega_code/sandbox/utils.py +216 -0
  158. kolega_code/services/base.py +255 -0
  159. kolega_code/services/browser.py +444 -0
  160. kolega_code/services/file_system.py +749 -0
  161. kolega_code/services/html.py +221 -0
  162. kolega_code/services/terminal.py +903 -0
  163. kolega_code/tools/__init__.py +22 -0
  164. kolega_code/tools/core.py +33 -0
  165. kolega_code/tools/definitions.py +81 -0
  166. kolega_code/tools/registry.py +73 -0
  167. kolega_code-0.1.0.dist-info/METADATA +157 -0
  168. kolega_code-0.1.0.dist-info/RECORD +171 -0
  169. kolega_code-0.1.0.dist-info/WHEEL +4 -0
  170. kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
  171. 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