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,1704 @@
1
+ import asyncio
2
+ import inspect
3
+ import re
4
+ from pathlib import Path
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Callable, List, Optional, Union
7
+
8
+ from .common import LogMixin
9
+ from kolega_code.config import AgentConfig
10
+ from kolega_code.llm.models import ImageBlock, ToolDefinition, ToolParameter
11
+ from kolega_code.tools import Tool, ToolRegistry, tool_definition_from_callable
12
+ from kolega_code.services.file_system import FileSystem, LocalFileSystem
13
+ from kolega_code.services.base import TerminalManager, BrowserManager
14
+ from kolega_code.services.terminal import LocalTerminalManager
15
+ from kolega_code.services.browser import PlaywrightBrowserManager
16
+ from .tool_backend.agent_tool import AgentTool
17
+ from .tool_backend.apply_edit_tool import ApplyEditTool
18
+ from .tool_backend.apply_patch_tool import APPLY_PATCH_TOOL_DESC, ApplyPatchTool
19
+ from .tool_backend.browser_tool import BrowserTool
20
+ from .tool_backend.create_file_tool import CreateFileTool
21
+ from .tool_backend.glob_tool import GlobTool
22
+ from .tool_backend.list_directory_tool import ListDirectoryTool
23
+ from .tool_backend.memory_tool import MemoryTool
24
+ from .tool_backend.read_file_tool import ReadFileTool
25
+ from .tool_backend.replace_entire_file_tool import (
26
+ ReplaceEntireFileTool,
27
+ )
28
+ from .tool_backend.replace_lines_tool import ReplaceLinesTool
29
+ from .tool_backend.search_and_replace_tool import SearchAndReplaceTool
30
+ from .tool_backend.search_codebase_tool import SearchCodebaseTool
31
+ from .tool_backend.web_fetch_tool import WebFetchTool
32
+ from .tool_backend.terminal_tool import TerminalTool
33
+ from .tool_backend.think_hard_tool import ThinkHardTool
34
+
35
+ # Import additional tools for consolidated functionality
36
+ from .tool_backend.build_tool import BuildTool
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class ToolExtension:
41
+ """Host-provided tool callbacks and named groups."""
42
+
43
+ name: str
44
+ tools: dict[str, Callable[..., Any]]
45
+ tool_groups: dict[str, List[str]] = field(default_factory=dict)
46
+
47
+
48
+ class ToolCollectionConfig:
49
+ """Configuration class for customizing tool availability per agent type."""
50
+
51
+ def __init__(
52
+ self,
53
+ read_only: bool = False,
54
+ browser_only: bool = False,
55
+ include_agent_dispatch_tools: bool = False,
56
+ include_memory_tools: bool = False,
57
+ tool_exclusions: List[str] = None,
58
+ custom_tool_groups: List[str] = None,
59
+ enabled_tool_groups: List[str] = None,
60
+ restrict_to_tool_groups: bool = False,
61
+ ):
62
+ """
63
+ Initialize tool collection configuration.
64
+
65
+ Args:
66
+ read_only: Whether to restrict to read-only tools
67
+ browser_only: Whether to only include browser tools
68
+ include_agent_dispatch_tools: Whether to include agent dispatch tools (investigation, browser, coding)
69
+ include_memory_tools: Whether to include memory management tools
70
+ tool_exclusions: List of method names to exclude from tool list
71
+ custom_tool_groups: Additional custom tool groups to include
72
+ enabled_tool_groups: Additional custom tool groups to include
73
+ restrict_to_tool_groups: If True, ONLY include tools from specified groups, excluding all other core tools
74
+ """
75
+ self.read_only = read_only
76
+ self.browser_only = browser_only
77
+ self.include_agent_dispatch_tools = include_agent_dispatch_tools
78
+ self.include_memory_tools = include_memory_tools
79
+ self.tool_exclusions = tool_exclusions or []
80
+ self.custom_tool_groups = list(dict.fromkeys((custom_tool_groups or []) + (enabled_tool_groups or [])))
81
+ self.restrict_to_tool_groups = restrict_to_tool_groups
82
+
83
+
84
+ class ToolCollection(LogMixin):
85
+ """
86
+ A collection of tools for interacting with the project workspace.
87
+
88
+ Provides utilities for file operations and workspace management.
89
+ """
90
+
91
+ read_only_tools = [
92
+ "list_directory",
93
+ "read_entire_file",
94
+ "read_file_section",
95
+ "read_memory",
96
+ "search_codebase",
97
+ "find_files_by_pattern",
98
+ "think_hard",
99
+ "web_fetch",
100
+ "sleep",
101
+ ]
102
+
103
+ browser_tools = [
104
+ "launch_browser",
105
+ "list_browsers",
106
+ "get_browser_interactive_elements",
107
+ "get_browser_console_logs",
108
+ "take_browser_screenshot",
109
+ "interact_with_browser",
110
+ "set_browser_select_value",
111
+ "close_browser",
112
+ ]
113
+
114
+ # Agent dispatch tools group - includes all agent dispatch functionality
115
+ agent_dispatch_tools = [
116
+ "dispatch_investigation_agent",
117
+ "dispatch_browser_agent",
118
+ "dispatch_coding_agent",
119
+ "dispatch_general_agent",
120
+ ]
121
+
122
+ # Legacy name for backward compatibility
123
+ investigation_agent_tools = agent_dispatch_tools
124
+
125
+ # CoderAgent specific dispatch tools
126
+ coder_agent_tools = [
127
+ "dispatch_investigation_agent",
128
+ "dispatch_browser_agent",
129
+ "dispatch_general_agent",
130
+ ]
131
+
132
+ # Memory tools group
133
+ memory_tools = [
134
+ "read_memory",
135
+ "write_memory",
136
+ ]
137
+
138
+ def __init__(
139
+ self,
140
+ project_path: Union[str, Path],
141
+ workspace_id: str,
142
+ thread_id: str,
143
+ connection_manager,
144
+ config: AgentConfig,
145
+ caller,
146
+ tool_config: Optional[ToolCollectionConfig] = None,
147
+ read_only: bool = False, # Keep for backward compatibility
148
+ browser_only: bool = False, # Keep for backward compatibility
149
+ filesystem: Optional[FileSystem] = None,
150
+ terminal_manager: Optional[TerminalManager] = None,
151
+ browser_manager: Optional[BrowserManager] = None,
152
+ langfuse_client=None,
153
+ tool_extensions: Optional[List[ToolExtension]] = None,
154
+ ) -> None:
155
+ """
156
+ Initialize a new ToolCollection instance.
157
+
158
+ Args:
159
+ project_path: File system path to the project root directory
160
+ workspace_id: Unique identifier for the workspace
161
+ thread_id: Unique identifier for the thread
162
+ connection_manager: Connection manager for agent communication
163
+ config: Agent configuration
164
+ caller: The calling agent instance
165
+ tool_config: Configuration for which tools to include (takes precedence over legacy flags)
166
+ read_only: Whether tools should be read-only (legacy, use tool_config instead)
167
+ browser_only: Whether to only include browser tools (legacy, use tool_config instead)
168
+ filesystem: Optional filesystem implementation. If None, creates LocalFileSystem with project_path as root
169
+ terminal_manager: Optional terminal manager implementation. If None, creates LocalTerminalManager
170
+ browser_manager: Optional browser manager implementation. If None, creates PlaywrightBrowserManager
171
+ tool_extensions: Host-provided tools and groups
172
+ """
173
+ # Handle backward compatibility - create tool_config from legacy parameters if not provided
174
+ if tool_config is None:
175
+ tool_config = ToolCollectionConfig(read_only=read_only, browser_only=browser_only)
176
+
177
+ self.tool_config = tool_config
178
+ self.workspace_id = workspace_id
179
+ self.thread_id = thread_id
180
+
181
+ # Convert string path to Path object if needed
182
+ self.project_path = Path(project_path) if isinstance(project_path, str) else project_path
183
+
184
+ # Create filesystem instance if not provided
185
+ if filesystem is None:
186
+ self.filesystem = LocalFileSystem(root_path=self.project_path)
187
+ else:
188
+ self.filesystem = filesystem
189
+
190
+ # Create terminal manager instance if not provided
191
+ if terminal_manager is None:
192
+ self.terminal_manager = LocalTerminalManager(workspace_id, thread_id, connection_manager)
193
+ else:
194
+ self.terminal_manager = terminal_manager
195
+
196
+ # Create browser manager instance if not provided
197
+ if browser_manager is None:
198
+ self.browser_manager = PlaywrightBrowserManager()
199
+ else:
200
+ self.browser_manager = browser_manager
201
+
202
+ # Validate the filesystem root. Local filesystems check the directory
203
+ # eagerly; sandbox filesystems are provisioned by their manager and no-op.
204
+ self.filesystem.validate_root()
205
+
206
+ self.connection_manager = connection_manager
207
+ self.config = config
208
+ self.caller = caller
209
+ self.langfuse_client = langfuse_client
210
+ self.tool_extensions = tool_extensions or []
211
+ self.extension_callbacks = {}
212
+ self._extension_group_names = set()
213
+
214
+ # Set legacy attributes for backward compatibility
215
+ self.read_only = tool_config.read_only
216
+ self.browser_only = tool_config.browser_only
217
+
218
+ # Build tool exclusions list from config
219
+ self.tool_exclusions = [
220
+ "read_memory",
221
+ "write_memory",
222
+ "execute_terminal_command",
223
+ "replace_lines",
224
+ "apply_patch",
225
+ "edit_file",
226
+ "get_tool_list",
227
+ "log_error",
228
+ "log_warning",
229
+ "log_info",
230
+ "run_command", # Disabled: unreliable completion detection, use run_command_tracked instead
231
+ ]
232
+ self.tool_exclusions.extend(tool_config.tool_exclusions)
233
+
234
+ # Initialize tool backends
235
+ self._initialize_tools()
236
+ self._register_tool_extensions()
237
+
238
+ def _register_tool_extensions(self):
239
+ """Bind host-provided extension callbacks onto this collection."""
240
+ for extension in self.tool_extensions:
241
+ for tool_name, callback in extension.tools.items():
242
+ if hasattr(self, tool_name):
243
+ raise ValueError(f"Tool extension '{extension.name}' conflicts with existing tool '{tool_name}'")
244
+ setattr(self, tool_name, callback)
245
+ self.extension_callbacks[tool_name] = callback
246
+
247
+ for group_name, tool_names in extension.tool_groups.items():
248
+ existing_group = list(getattr(self, group_name, []))
249
+ merged_group = list(dict.fromkeys(existing_group + list(tool_names)))
250
+ setattr(self, group_name, merged_group)
251
+ self._extension_group_names.add(group_name)
252
+
253
+ def _initialize_tools(self):
254
+ """Initialize all tool backends based on configuration."""
255
+ # Core tool backends (always available)
256
+ self.think_hard_tool = ThinkHardTool(
257
+ self.project_path,
258
+ self.workspace_id,
259
+ self.thread_id,
260
+ self.connection_manager,
261
+ self.config,
262
+ self.caller,
263
+ self.filesystem,
264
+ )
265
+ self.apply_edit_tool = ApplyEditTool(
266
+ self.project_path,
267
+ self.workspace_id,
268
+ self.thread_id,
269
+ self.connection_manager,
270
+ self.config,
271
+ self.caller,
272
+ self.filesystem,
273
+ )
274
+ self.search_and_replace_tool = SearchAndReplaceTool(
275
+ self.project_path,
276
+ self.workspace_id,
277
+ self.thread_id,
278
+ self.connection_manager,
279
+ self.config,
280
+ self.caller,
281
+ self.filesystem,
282
+ )
283
+ self.list_directory_tool = ListDirectoryTool(
284
+ self.project_path,
285
+ self.workspace_id,
286
+ self.thread_id,
287
+ self.connection_manager,
288
+ self.config,
289
+ self.caller,
290
+ self.filesystem,
291
+ )
292
+ self.terminal_tool = TerminalTool(
293
+ self.project_path,
294
+ self.workspace_id,
295
+ self.thread_id,
296
+ self.connection_manager,
297
+ self.config,
298
+ self.caller,
299
+ self.filesystem,
300
+ terminal_manager=self.terminal_manager,
301
+ )
302
+ self.memory_tool = MemoryTool(
303
+ self.project_path,
304
+ self.workspace_id,
305
+ self.thread_id,
306
+ self.connection_manager,
307
+ self.config,
308
+ self.caller,
309
+ self.filesystem,
310
+ )
311
+ self.search_codebase_tool = SearchCodebaseTool(
312
+ self.project_path,
313
+ self.workspace_id,
314
+ self.thread_id,
315
+ self.connection_manager,
316
+ self.config,
317
+ self.caller,
318
+ self.filesystem,
319
+ )
320
+ self.web_fetch_tool = WebFetchTool(
321
+ self.project_path,
322
+ self.workspace_id,
323
+ self.thread_id,
324
+ self.connection_manager,
325
+ self.config,
326
+ self.caller,
327
+ self.filesystem,
328
+ )
329
+ self.glob_tool = GlobTool(
330
+ self.project_path,
331
+ self.workspace_id,
332
+ self.thread_id,
333
+ self.connection_manager,
334
+ self.config,
335
+ self.caller,
336
+ self.filesystem,
337
+ )
338
+ self.read_file_tool = ReadFileTool(
339
+ self.project_path,
340
+ self.workspace_id,
341
+ self.thread_id,
342
+ self.connection_manager,
343
+ self.config,
344
+ self.caller,
345
+ self.filesystem,
346
+ )
347
+ self.create_file_tool = CreateFileTool(
348
+ self.project_path,
349
+ self.workspace_id,
350
+ self.thread_id,
351
+ self.connection_manager,
352
+ self.config,
353
+ self.caller,
354
+ self.filesystem,
355
+ )
356
+ self.replace_entire_file_tool = ReplaceEntireFileTool(
357
+ self.project_path,
358
+ self.workspace_id,
359
+ self.thread_id,
360
+ self.connection_manager,
361
+ self.config,
362
+ self.caller,
363
+ self.filesystem,
364
+ )
365
+ self.replace_lines_tool = ReplaceLinesTool(
366
+ self.project_path,
367
+ self.workspace_id,
368
+ self.thread_id,
369
+ self.connection_manager,
370
+ self.config,
371
+ self.caller,
372
+ self.filesystem,
373
+ )
374
+ self.apply_patch_tool = ApplyPatchTool(
375
+ self.project_path,
376
+ self.workspace_id,
377
+ self.thread_id,
378
+ self.connection_manager,
379
+ self.config,
380
+ self.caller,
381
+ self.filesystem,
382
+ )
383
+ self.agent_tool = AgentTool(
384
+ self.project_path,
385
+ self.workspace_id,
386
+ self.thread_id,
387
+ self.connection_manager,
388
+ self.config,
389
+ self.caller,
390
+ self.filesystem,
391
+ terminal_manager=self.terminal_manager,
392
+ browser_manager=self.browser_manager,
393
+ langfuse_client=self.langfuse_client,
394
+ )
395
+ self.browser_tool = BrowserTool(
396
+ self.project_path,
397
+ self.workspace_id,
398
+ self.thread_id,
399
+ self.connection_manager,
400
+ self.config,
401
+ self.caller,
402
+ self.filesystem,
403
+ browser_manager=self.browser_manager,
404
+ )
405
+
406
+ # Build tool
407
+ self.build_tool = BuildTool(
408
+ self.project_path,
409
+ self.workspace_id,
410
+ self.thread_id,
411
+ self.connection_manager,
412
+ self.config,
413
+ self.caller,
414
+ self.filesystem,
415
+ terminal_manager=self.terminal_manager,
416
+ )
417
+
418
+ async def launch_browser(self, url: str) -> str:
419
+ """
420
+ Launch a browser and navigate to a specified URL.
421
+
422
+ This tool opens a new browser window, navigates to the provided URL,
423
+ and returns a unique browser ID that can be used to interact with this browser instance
424
+ through other browser-related tools.
425
+
426
+ When to use this tool:
427
+ - When you need to visit a website to gather information
428
+ - When you need to interact with web applications
429
+ - When you need to test web functionality
430
+ - When you need to demonstrate web-based features to the user
431
+
432
+ Usage notes:
433
+ 1. The browser uses a standard viewport size (1280x720) and Chrome user agent
434
+ 2. The returned browser ID must be saved if you plan to interact with this browser later
435
+ 3. Each call creates a new browser instance - use judiciously to avoid resource consumption
436
+
437
+ Args:
438
+ url: The complete URL to navigate to (must include http:// or https://)
439
+
440
+ Returns:
441
+ A confirmation message with the unique browser ID for future reference
442
+ """
443
+
444
+ return await self.browser_tool.launch_browser(url)
445
+
446
+ async def list_browsers(self) -> str:
447
+ """
448
+ List all currently running browser instances.
449
+
450
+ This tool provides a formatted overview of all active browser sessions, displaying
451
+ their unique browser IDs, the URLs they're currently visiting, and when they were launched.
452
+
453
+ When to use this tool:
454
+ - When you need to check which browser instances are currently active
455
+ - When you need to retrieve a browser ID for use with other browser tools
456
+ - When you want to see which URLs are currently being accessed
457
+ - When you need to manage multiple browser sessions
458
+
459
+ Usage notes:
460
+ 1. The output is formatted as a markdown table for easy readability
461
+ 2. If no browsers are running, the tool will indicate this
462
+ 3. Browser IDs can be used with other browser tools like close_browser
463
+ 4. This tool is useful for cleanup to ensure all browser instances are properly closed
464
+
465
+ Returns:
466
+ A markdown-formatted table listing all active browser instances with their details
467
+ """
468
+ return await self.browser_tool.list_browsers()
469
+
470
+ async def get_browser_console_logs(
471
+ self,
472
+ browser_id: str,
473
+ max_logs: int = 50,
474
+ log_types: list = None,
475
+ minutes_back: int = None,
476
+ max_chars: int = 8000,
477
+ ) -> str:
478
+ """
479
+ Retrieve filtered console logs from a browser instance by its browser ID.
480
+
481
+ This tool captures console messages (info, warnings, errors) that have been logged
482
+ in the browser's JavaScript console and returns them in a formatted markdown document.
483
+ The logs are filtered to prevent context window overflow while focusing on the most relevant information.
484
+
485
+ When to use this tool:
486
+ - When you need to debug JavaScript errors on a webpage
487
+ - When you want to see application messages logged to the console
488
+ - When you need to diagnose network or rendering issues
489
+ - When you're working with web applications that use console logging
490
+
491
+ Usage notes:
492
+ 1. You must provide a valid browser_id from a previous launch_browser call
493
+ 2. Console logs are filtered by default to show only errors, warnings, and assertions
494
+ 3. By default, only the most recent 50 logs are returned with a character limit of 8000
495
+ 4. Each log entry includes its type, timestamp, and message text
496
+ 5. Use this after interacting with a page to see what messages were generated
497
+
498
+ Args:
499
+ browser_id: The unique identifier of the browser instance to get console logs from
500
+ max_logs: Maximum number of logs to return (default: 50, most recent)
501
+ log_types: List of log types to include (default: ['error', 'warning', 'assert'])
502
+ minutes_back: Only return logs from the last N minutes (optional)
503
+ max_chars: Maximum total character count for all log messages (default: 8000)
504
+
505
+ Returns:
506
+ A markdown-formatted document containing the filtered browser console logs
507
+ """
508
+ return await self.browser_tool.get_browser_console_logs(
509
+ browser_id, max_logs=max_logs, log_types=log_types, minutes_back=minutes_back, max_chars=max_chars
510
+ )
511
+
512
+ async def get_browser_interactive_elements(self, browser_id: str) -> str:
513
+ """
514
+ Identify and extract all interactive elements from a browser page.
515
+
516
+ This tool analyzes the current state of a browser page and identifies all interactive elements
517
+ such as buttons, links, form inputs, and other clickable components, returning them in a
518
+ structured markdown format with their selectors and attributes.
519
+
520
+ When to use this tool:
521
+ - When you need to discover what actions are possible on a webpage
522
+ - When you need to find specific interactive elements to interact with
523
+ - When you're exploring a new website and need to understand its interface
524
+ - When you need to automate interactions with a webpage
525
+ - When you need precise selectors for use with the interact_with_browser or set_browser_select_value tools
526
+
527
+ Usage notes:
528
+ 1. You must provide a valid browser_id from a previous launch_browser call
529
+ 2. The tool returns a comprehensive list of all interactive elements with their types, text content, and selectors
530
+ 3. The selector column provides CSS selectors that can be used with interact_with_browser or set_browser_select_value
531
+ 4. The attributes column provides additional information about each element
532
+ 5. Use this tool before performing interactions to identify the correct elements to target
533
+
534
+ Args:
535
+ browser_id: The unique identifier of the browser instance to analyze
536
+
537
+ Returns:
538
+ A markdown-formatted document listing all interactive elements on the page with their details
539
+ """
540
+ return await self.browser_tool.get_browser_interactive_elements(browser_id)
541
+
542
+ async def take_browser_screenshot(self, browser_id: str) -> str:
543
+ """
544
+ Take a screenshot of the current browser page.
545
+
546
+ This tool captures the current visual state of a browser page and returns it as an image,
547
+ along with relevant metadata such as the current URL and page title.
548
+
549
+ When to use this tool:
550
+ - When you need to visually inspect the current state of a webpage
551
+ - When you need to capture visual evidence of a web application's behavior
552
+ - When text-based content extraction is insufficient to understand the page layout
553
+ - When you need to verify the visual appearance of a web interface
554
+
555
+ Usage notes:
556
+ 1. You must provide a valid browser_id from a previous launch_browser call
557
+ 2. The screenshot captures the entire visible viewport of the browser
558
+ 3. The returned image is in base64-encoded format
559
+ 4. The tool also returns metadata about the page including title and URL
560
+
561
+ Args:
562
+ browser_id: The unique identifier of the browser instance to screenshot
563
+
564
+ Returns:
565
+ A list containing a text description and the screenshot image
566
+ """
567
+ result = await self.browser_tool.take_browser_screenshot(browser_id)
568
+
569
+ # Create an image block with the screenshot data
570
+ image_block = ImageBlock(image_type="base64", media_type="image/png", data=result["screenshot"])
571
+
572
+ return [image_block]
573
+
574
+ async def interact_with_browser(
575
+ self, browser_id: str, action: str, selector: str, text: str, scroll_px: int
576
+ ) -> str:
577
+ """
578
+ Interact with a browser by performing actions on web elements.
579
+
580
+ This tool allows you to control a browser programmatically by executing common actions
581
+ like clicking elements, typing text, or navigating to new URLs. It provides a way to
582
+ automate web interactions within an existing browser session.
583
+
584
+ When to use this tool:
585
+ - When you need to click buttons, links, or other interactive elements on a webpage
586
+ - When you need to fill out forms by typing text into input fields
587
+ - When you need to navigate to a different URL within an existing browser session
588
+ - When you need to automate a sequence of interactions with a web application
589
+
590
+ When NOT to use this tool:
591
+ - When you need to interact with a dropdown or select input. Use set_browser_select_value for that.
592
+
593
+ Usage notes:
594
+ 1. You must provide a valid browser_id from a previous launch_browser call
595
+ 2. The action parameter must be one of: 'click', 'type', 'scroll' or 'navigate'
596
+ 3. For 'click' actions, provide a CSS or XPath selector that identifies the element to click
597
+ 4. For 'type' actions, provide both a selector for the input field and the text to type
598
+ 5. For 'scroll' actions, provide a scroll_px (positive to scroll down the page, negative to scroll up)
599
+ 5. For 'navigate' actions, provide the URL in the text parameter (selector can be empty)
600
+ 6. The tool waits for the page to stabilize after the action before returning
601
+ 7. The return value includes the current URL after the action is performed
602
+
603
+ Args:
604
+ browser_id: The unique identifier of the browser instance to interact with
605
+ action: The type of interaction to perform ('click', 'type', or 'navigate')
606
+ selector: CSS or XPath selector identifying the element to interact with
607
+ text: Text to type (for 'type' action) or URL to navigate to (for 'navigate' action)
608
+
609
+ Returns:
610
+ A markdown-formatted report of the interaction result, including the current URL
611
+ """
612
+ return await self.browser_tool.interact_with_browser(browser_id, action, selector, text, scroll_px)
613
+
614
+ async def set_browser_select_value(self, browser_id: str, selector: str, value: str) -> str:
615
+ """
616
+ Set the value of a select box (dropdown) in a browser page.
617
+
618
+ This tool allows you to programmatically select an option from a dropdown menu (select element)
619
+ on a webpage. It validates that the element is indeed a select box and that the specified value
620
+ exists among the available options before making the selection.
621
+
622
+ When to use this tool:
623
+ - When you need to select an option from a dropdown menu on a form
624
+ - When you need to change the selected value in a select box
625
+ - When automating form filling that includes dropdown selections
626
+ - When you need to test different options in a select element
627
+
628
+ Usage notes:
629
+ 1. You must provide a valid browser_id from a previous launch_browser call
630
+ 2. The selector must identify a <select> HTML element - the tool will fail if used on other element types
631
+ 3. The value parameter should match the 'value' attribute of the <option> you want to select, not the visible text
632
+ 4. Use get_browser_interactive_elements first to find the correct selector and see available option values
633
+ 5. The tool validates that the specified value exists in the select options before attempting to set it
634
+ 6. The response will confirm whether the selection was successful and show the actual selected value
635
+
636
+ Args:
637
+ browser_id: The unique identifier of the browser instance to interact with
638
+ selector: CSS selector that uniquely identifies the select element
639
+ value: The value attribute of the option to select (not the display text)
640
+
641
+ Returns:
642
+ A markdown-formatted report showing the result of the selection, including success/error status
643
+ """
644
+ return await self.browser_tool.set_browser_select_value(browser_id, selector, value)
645
+
646
+ async def close_browser(self, browser_id: str) -> str:
647
+ """
648
+ Close a specific browser instance by its ID.
649
+
650
+ This tool terminates a browser session that was previously launched with the launch_browser tool,
651
+ freeing up system resources and cleaning up the browser process.
652
+
653
+ When to use this tool:
654
+ - When you've completed tasks in a specific browser instance
655
+ - When you need to clean up resources after web-based operations
656
+ - When you want to start fresh with a new browser session
657
+ - When you're managing multiple browser instances and need to close specific ones
658
+
659
+ Usage notes:
660
+ 1. You must provide a valid browser ID that was returned from a previous launch_browser call
661
+ 2. Once closed, the browser ID becomes invalid and cannot be used again
662
+ 3. It's good practice to close browsers when you're done with them to free up resources
663
+ 4. If you're unsure which browser IDs are available, use the list_browsers tool first
664
+
665
+ Args:
666
+ browser_id: The unique identifier of the browser instance to close
667
+
668
+ Returns:
669
+ A confirmation message indicating the browser has been closed
670
+ """
671
+ return await self.browser_tool.close_browser(browser_id)
672
+
673
+ async def build_backend(self) -> str:
674
+ """
675
+ Build the backend defined by the project manifest (.kolega-manifest.yaml).
676
+
677
+ When to use this tool:
678
+ - When you need to compile, bundle, or otherwise build the backend for the current workspace
679
+ - When verifying that the backend build still succeeds after code changes
680
+
681
+ Guidance:
682
+ - Prefer this tool over manually running build commands in a terminal; it automatically selects the correct
683
+ command from the manifest and works in both local and sandbox environments with standardized output
684
+
685
+ Returns:
686
+ Build output as markdown (combined stdout/stderr)
687
+ """
688
+ return await self.build_tool.build_backend()
689
+
690
+ async def build_frontend(self) -> str:
691
+ """
692
+ Build the frontend defined by the project manifest (.kolega-manifest.yaml).
693
+
694
+ When to use this tool:
695
+ - When you need to compile, bundle, or otherwise build the frontend application
696
+ - When you want a consistent build execution that adapts to local or sandbox contexts
697
+
698
+ Guidance:
699
+ - Prefer this tool over manually running build commands in a terminal; it reads the manifest to choose the
700
+ correct command and standardizes execution and output across environments
701
+
702
+ Returns:
703
+ Build output as markdown (combined stdout/stderr)
704
+ """
705
+ return await self.build_tool.build_frontend()
706
+
707
+ # Agent Dispatch Tools (available when include_agent_dispatch_tools is True)
708
+ async def dispatch_investigation_agent(self, task: str) -> str:
709
+ """
710
+ Dispatch an investigation agent to perform a specific task with read-only access to the codebase.
711
+
712
+ This tool launches a specialized agent that can analyze code, search for patterns, and investigate
713
+ issues without modifying any files. The investigation agent has access to all read-only tools
714
+ and will return a comprehensive report on its findings.
715
+
716
+ When to use this tool:
717
+ - When you need to perform complex searches across multiple files
718
+ - When you need to analyze code patterns or understand how components interact
719
+ - When you need to trace through code execution paths
720
+ - When you need to gather information from multiple parts of the codebase
721
+
722
+ Usage notes:
723
+ 1. Provide a detailed task description with specific questions or objectives for the agent
724
+ 2. The agent will work autonomously and return a single comprehensive report
725
+ 3. The agent cannot modify any files - it has read-only access to the codebase
726
+ 4. For best results, specify exactly what information you want the agent to find and include in its report
727
+ 5. The agent's report is not automatically shown to the user - you should summarize key findings
728
+
729
+ IMPORTANT: The agent can only use these tools:
730
+ - list_directory
731
+ - read_entire_file
732
+ - read_file_section
733
+ - read_memory
734
+ - search_codebase
735
+ - find_files_by_pattern
736
+ - think_hard
737
+ If you need to do something that requires any other tool, you should call the tool directly.
738
+
739
+ Args:
740
+ task: A detailed description of the investigation task to perform
741
+
742
+ Returns:
743
+ A comprehensive report of the investigation findings
744
+ """
745
+ return await self.agent_tool.dispatch_investigation_agent(task)
746
+
747
+ async def dispatch_browser_agent(self, task: str) -> str:
748
+ """
749
+ Dispatch a browser agent to perform web-based tasks and interactions.
750
+
751
+ This tool launches a specialized agent that can navigate websites, interact with web elements,
752
+ and extract information from web pages. The browser agent has access to all browser-related tools
753
+ and will return a comprehensive report on its findings and actions.
754
+
755
+ Use this ONLY when the user explicitly asks to browse, open, visit, or interact with a web page/URL,
756
+ or explicitly requests a screenshot or web UI action. Do NOT use this for general research, docs lookup,
757
+ or exploration unless the user clearly requests browsing.
758
+
759
+
760
+ When to use this tool:
761
+ - When you need to navigate and interact with websites
762
+ - When you need to extract information from web pages
763
+ - When you need to test web applications or interfaces
764
+ - When you need to automate web-based workflows
765
+
766
+ Usage notes:
767
+ 1. Provide a detailed task description with specific objectives for the browser agent
768
+ 2. The agent will work autonomously and return a single comprehensive report
769
+ 3. The agent can launch browsers, navigate pages, click elements, fill forms, and extract content
770
+ 4. For best results, specify exactly what information you want the agent to find or what actions to perform
771
+ 5. The agent's report is not automatically shown to the user - you should summarize key findings
772
+
773
+ IMPORTANT: The browser agent specializes in these tools:
774
+ - launch_browser
775
+ - list_browsers
776
+ - get_browser_content
777
+ - get_browser_console_logs
778
+ - take_browser_screenshot
779
+ - interact_with_browser
780
+ - set_browser_select_value
781
+ - close_browser
782
+
783
+ Args:
784
+ task: A detailed description of the browser task to perform
785
+
786
+ Returns:
787
+ A comprehensive report of the browser agent's findings and actions
788
+ """
789
+ return await self.agent_tool.dispatch_browser_agent(task)
790
+
791
+ async def dispatch_coding_agent(self, task: str) -> str:
792
+ """
793
+ Dispatch a coding agent for processing coding-related tasks with streaming output.
794
+
795
+ Args:
796
+ task: A detailed description of the coding task to perform
797
+
798
+ Returns:
799
+ A summary of the coding process outcome
800
+ """
801
+ return await self.agent_tool.dispatch_coding_agent(task)
802
+
803
+ async def dispatch_general_agent(self, task: str) -> str:
804
+ """
805
+ Dispatch an autonomous general-purpose agent to complete a self-contained task.
806
+
807
+ This tool launches a sub-agent with the full set of workspace tools (read, search,
808
+ edit files, run commands). It works autonomously on the task you give it and returns
809
+ a single final report. You will not see its intermediate steps, and you cannot send
810
+ it follow-up messages, so the task description must contain everything it needs.
811
+
812
+ PARALLEL EXECUTION: If you issue multiple dispatch_general_agent calls in a single
813
+ response, the agents run CONCURRENTLY. Use this to fan out work that can proceed
814
+ independently (e.g., "update module A's tests" and "update module B's tests").
815
+
816
+ When to use this tool:
817
+ - The work splits into independent subtasks that do not touch the same files
818
+ - A subtask is large or noisy (broad searches, mechanical multi-file edits) and you
819
+ only need the outcome, not every intermediate step
820
+ - You want several independent investigations or changes done at once
821
+
822
+ When NOT to use this tool:
823
+ - Tasks that depend on each other's output or edit the same files - do those
824
+ yourself sequentially, or dispatch them one at a time
825
+ - Small tasks you can do directly with one or two tool calls
826
+ - Anything requiring back-and-forth with the user
827
+
828
+ Usage notes:
829
+ 1. Each task must be INDEPENDENT and SELF-CONTAINED: include the goal, relevant
830
+ file paths, constraints, and exactly what the final report should contain.
831
+ 2. Never dispatch two parallel agents whose work could overlap on the same files.
832
+ 3. The agent cannot spawn further sub-agents.
833
+ 4. The agent's report is not automatically shown to the user - you should summarize
834
+ the key results.
835
+
836
+ Args:
837
+ task: A detailed, self-contained description of the task to perform
838
+
839
+ Returns:
840
+ The agent's final report on the completed task
841
+ """
842
+ return await self.agent_tool.dispatch_general_agent(task)
843
+
844
+ async def think_hard(self, problem_statement: str) -> str:
845
+ """
846
+ Uses Claude 3.7 Sonnet in extended thinking mode to analyze a problem deeply.
847
+
848
+ This tool leverages Claude's extended thinking capabilities to perform in-depth
849
+ analysis on complex problems. It sends the problem statement to the Claude API
850
+ with specific parameters to enable extended thinking and returns the detailed response.
851
+
852
+ Args:
853
+ problem_statement: A clear statement of the problem to be analyzed, including ALL relevant details.
854
+
855
+ Returns:
856
+ The detailed analysis from Claude, including its extended thinking process
857
+ """
858
+ return await self.think_hard_tool.think_hard(problem_statement)
859
+
860
+ async def sleep(self, seconds: float) -> str:
861
+ """
862
+ Pause execution for a specified number of seconds.
863
+
864
+ This tool introduces a deliberate delay in execution, allowing time for external processes
865
+ to complete, systems to stabilize, or operations to finish processing. It's particularly
866
+ useful when working with asynchronous operations or waiting for long-running commands.
867
+
868
+ When to use this tool:
869
+ - After starting a long-running test suite and wanting to wait before checking results
870
+ - When waiting for a development server or application to fully start up
871
+ - After triggering a build process that needs time to complete
872
+ - When waiting for file system operations to propagate (especially on networked drives)
873
+ - After making configuration changes that need time to take effect
874
+ - When working with rate-limited APIs and need to respect timing constraints
875
+
876
+ Usage notes:
877
+ 1. Use this tool judiciously - unnecessary delays slow down overall task completion
878
+ 2. Consider checking process status first rather than using arbitrary wait times
879
+ 3. For very long operations (>5 minutes), consider breaking into smaller check intervals
880
+ 4. The tool accepts decimal values for sub-second precision (e.g., 0.5 for half a second)
881
+ 5. Maximum recommended sleep time is 300 seconds (5 minutes) to avoid excessive delays
882
+ 6. Use read_terminal or other status-checking tools after sleeping to verify completion
883
+ 7. Consider using shorter initial sleeps and checking status rather than one long sleep
884
+
885
+ Args:
886
+ seconds: Number of seconds to sleep (must be positive, supports decimal values)
887
+
888
+ Returns:
889
+ A confirmation message indicating how long the execution was paused
890
+
891
+ Raises:
892
+ ValueError: If seconds is negative or exceeds the maximum allowed duration
893
+ """
894
+ if seconds <= 0:
895
+ raise ValueError("Sleep duration must be positive")
896
+
897
+ if seconds > 300: # 5 minutes maximum
898
+ raise ValueError("Sleep duration cannot exceed 300 seconds (5 minutes)")
899
+
900
+ await asyncio.sleep(seconds)
901
+
902
+ if seconds == 1:
903
+ return f"✅ Paused execution for {seconds} second"
904
+ else:
905
+ return f"✅ Paused execution for {seconds} seconds"
906
+
907
+ async def edit_file(self, relative_path: str, instructions: str, code_edit: str) -> str:
908
+ """
909
+ Use this tool to propose an edit to an existing file.
910
+
911
+ This will be read by a less intelligent model, which will quickly apply the edit.
912
+
913
+ You should make it clear what the edit is, while also minimizing the unchanged code you write.
914
+
915
+ When writing the edit, you should specify each edit in sequence, with the special comment `// ... existing code ...` to represent unchanged code in between edited lines.
916
+
917
+ For example:
918
+
919
+ ```
920
+ // ... existing code ...
921
+ FIRST_EDIT
922
+ // ... existing code ...
923
+ SECOND_EDIT
924
+ // ... existing code ...
925
+ THIRD_EDIT
926
+ // ... existing code ...
927
+ ```
928
+
929
+ You should still bias towards repeating as few lines of the original file as possible to convey the change.
930
+
931
+ But, each edit should contain sufficient context of unchanged lines around the code you're editing to resolve ambiguity.
932
+
933
+ DO NOT omit spans of pre-existing code (or comments) without using the `// ... existing code ...` comment to indicate its absence.
934
+
935
+ If you omit the existing code comment, the model may inadvertently delete these lines.
936
+
937
+ Make sure it is clear what the edit should be, and where it should be applied.
938
+
939
+ Args:
940
+ relative_path: Path to the file to edit, relative to the project root
941
+ instructions: A single sentence instruction describing what you are going to do for the sketched edit. This is used to assist the less intelligent model in applying the edit. Please use the first person to describe what you are going to do. Dont repeat what you have said previously in normal messages. And use it to disambiguate uncertainty in the edit.
942
+ code_edit: Specify ONLY the precise lines of code that you wish to edit. **NEVER specify or write out unchanged code**. Instead, represent all unchanged code using the comment of the language you're editing in - example: `// ... existing code ...`
943
+ """
944
+ return await self.apply_edit_tool.edit_file(relative_path, instructions, code_edit)
945
+
946
+ async def search_and_replace(self, relative_path: str, block: str) -> str:
947
+ """
948
+ Edit a file using a search and replace block.
949
+
950
+ The block should be formatted as follows:
951
+ ```
952
+ <<<<<<< SEARCH
953
+ [original code to find]
954
+ =======
955
+ [new code to replace with]
956
+ >>>>>>> REPLACE
957
+ ```
958
+
959
+ Before using this tool:
960
+
961
+ 1. Use the read_entire_file tool to understand the file's contents and context
962
+
963
+ To make a file edit, provide the following:
964
+ 1. relative_path: The absolute path to the file to modify (must be absolute, not relative)
965
+ 2. block: The search and replace block, as specified above. The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation)
966
+
967
+ The tool will replace ONE occurrence of old_string with new_string in the specified file.
968
+
969
+ CRITICAL REQUIREMENTS FOR USING THIS TOOL:
970
+
971
+ 1. UNIQUENESS: The old_string MUST uniquely identify the specific instance you want to change. This means:
972
+ - Include AT LEAST 3-5 lines of context BEFORE the change point
973
+ - Include AT LEAST 3-5 lines of context AFTER the change point
974
+ - Include all whitespace, indentation, and surrounding code exactly as it appears in the file
975
+
976
+ 2. SINGLE INSTANCE: This tool can only change ONE instance at a time. If you need to change multiple instances:
977
+ - Make separate calls to this tool for each instance
978
+ - Each call must uniquely identify its specific instance using extensive context
979
+
980
+ 3. VERIFICATION: Before using this tool:
981
+ - Check how many instances of the target text exist in the file
982
+ - If multiple instances exist, gather enough context to uniquely identify each one
983
+ - Plan separate tool calls for each instance
984
+
985
+ WARNING: If you do not follow these requirements:
986
+ - The tool will fail if block matches multiple locations
987
+ - The tool will fail if block doesn't match exactly (including whitespace)
988
+ - You may change the wrong instance if you don't include enough context
989
+
990
+ When making edits:
991
+ - Ensure the edit results in idiomatic, correct code
992
+ - Do not leave the code in a broken state
993
+
994
+ If you want to create a new file, use the create_file tool.
995
+
996
+ THE INDENTATION IN THE SEARCH BLOCK MUST BE IDENTICAL TO THE EXISTING FILE.
997
+
998
+ Args:
999
+ relative_path: Path to the file to edit, relative to the project root
1000
+ block: A single search and replace blocks formatted as shown above
1001
+
1002
+ Returns:
1003
+ A summary of the update made to the file
1004
+
1005
+ Raises:
1006
+ FileNotFoundError: If the file doesn't exist
1007
+ ValueError: If the search block doesn't match any content in the file
1008
+ ValueError: If the block is malformed or incorrectly formatted
1009
+ ValueError: If the block matches more than one place in the file
1010
+ PermissionError: If the file cannot be written to
1011
+ """
1012
+ return await self.search_and_replace_tool.search_and_replace(relative_path, block)
1013
+
1014
+ async def list_directory(self, relative_path: str = "") -> str:
1015
+ """
1016
+ List files and directories at the specified path.
1017
+
1018
+ Args:
1019
+ relative_path: Path to list, relative to the project root
1020
+
1021
+ Returns:
1022
+ Markdown formatted list of files and directories with details
1023
+
1024
+ Raises:
1025
+ NotADirectoryError: If the path is not a directory
1026
+ """
1027
+ return await self.list_directory_tool.list_directory(relative_path)
1028
+
1029
+ async def execute_terminal_command(self, command: str) -> str:
1030
+ """Execute a command and display output in terminal."""
1031
+ return await self.terminal_tool.execute_terminal_command(command)
1032
+
1033
+ async def launch_terminal(self, terminal_id: Optional[str] = None) -> str:
1034
+ """
1035
+ Launch a new terminal session.
1036
+
1037
+ This tool creates a new terminal instance that can be used to execute commands.
1038
+ The terminal persists between commands, maintaining environment variables,
1039
+ working directory, and other state.
1040
+
1041
+ Args:
1042
+ terminal_id: Optional ID for the terminal. If not provided, a random UUID will be generated.
1043
+ Use this ID with run_command, read_terminal and close_terminal to interact with
1044
+ this specific terminal.
1045
+
1046
+ Returns:
1047
+ The ID of the created terminal that can be used in subsequent terminal operations
1048
+ """
1049
+ return await self.terminal_tool.launch_terminal(terminal_id)
1050
+
1051
+ async def run_command(self, terminal_id: str, command: str, purpose: str) -> str:
1052
+ """
1053
+ Run a command in a specific terminal session and wait for output.
1054
+
1055
+ This tool sends a command to an existing terminal session and returns true or false
1056
+ if the command was accepted. The tool does not return the terminal output. You must call
1057
+ read_terminal to get the output.
1058
+
1059
+ This tool can be used to run long-running processes that do not exit, such as a development server.
1060
+
1061
+ CRITICAL WARNINGS:
1062
+ 1. **NEVER** send commands to a terminal that has a process still running (e.g., dev server, watch mode, etc.)
1063
+ 2. If a terminal is running jest --watch, npm run dev, or any other persistent process, that terminal is BLOCKED
1064
+ 3. To check if a terminal is blocked, use read_terminal first - if it shows an active process, DO NOT send commands
1065
+ 4. For new commands while something is running, you MUST launch a new terminal with launch_terminal
1066
+
1067
+ IMPORTANT NOTES:
1068
+ - If you change directory using cd in a terminal, the terminal will remain in that directory
1069
+ - Start a new terminal if you want to be sure of being in the project directory
1070
+ - Use list_terminals to see all active terminals and their last commands
1071
+
1072
+ Args:
1073
+ terminal_id: The ID of the terminal session to use (must be created with launch_terminal first)
1074
+ command: The command to execute in the terminal
1075
+ purpose: The reason to run the command including what information you hope to get when you read the terminal
1076
+
1077
+ Returns:
1078
+ Success message if command was accepted, or error if terminal is blocked/unavailable.
1079
+ """
1080
+ return await self.terminal_tool.run_command(terminal_id, command, purpose=purpose)
1081
+
1082
+ async def read_terminal(self, terminal_id: str, num_chars: int = 1024, offset: int = 0) -> str:
1083
+ """
1084
+ Read the output from a specific terminal session.
1085
+
1086
+ This tool retrieves output from a terminal session's persistent buffer, reading
1087
+ the most recent characters up to the specified limit. It does not wait for command
1088
+ completion - it returns whatever output is currently available.
1089
+
1090
+ Args:
1091
+ terminal_id: The ID of the terminal session to read from (must be created with launch_terminal first)
1092
+ num_chars: Number of characters to read from the output buffer (default: 1024).
1093
+ If buffer is smaller than num_chars, returns entire buffer.
1094
+ offset: Number of characters from the end to start reading from (default: 0).
1095
+ If offset is 0, reads the last num_chars characters.
1096
+ If offset is > 0, reads num_chars characters starting from that offset from the end.
1097
+ Note: When offset > 0, compression is bypassed to allow reading specific portions.
1098
+
1099
+ Returns:
1100
+ The terminal output as a string, formatted in markdown code blocks
1101
+ """
1102
+ return await self.terminal_tool.read_terminal(terminal_id, num_chars=num_chars, offset=offset)
1103
+
1104
+ async def close_terminal(self, terminal_id: str) -> str:
1105
+ """
1106
+ Close a specific terminal session.
1107
+
1108
+ This tool terminates a terminal session that was previously created with launch_terminal.
1109
+ It will kill any running processes in that terminal and clean up associated resources.
1110
+ Once closed, the terminal ID cannot be used again and a new terminal must be launched
1111
+ if needed.
1112
+
1113
+ Args:
1114
+ terminal_id: The ID of the terminal session to close
1115
+
1116
+ Returns:
1117
+ A confirmation message indicating the terminal was successfully closed
1118
+ """
1119
+ return await self.terminal_tool.close_terminal(terminal_id)
1120
+
1121
+ async def list_terminals(self) -> str:
1122
+ """
1123
+ List all active terminal sessions and their status.
1124
+
1125
+ This tool provides information about all currently active terminal sessions,
1126
+ including their IDs, running status, and the last command executed in each terminal.
1127
+ Use this to keep track of multiple terminal sessions and their state.
1128
+
1129
+ Returns:
1130
+ A formatted string containing a table of all terminal sessions with their IDs,
1131
+ status (Running/Stopped), and last executed command
1132
+ """
1133
+ return await self.terminal_tool.list_terminals()
1134
+
1135
+ async def run_command_tracked(self, terminal_id: str, command: str, purpose: str) -> str:
1136
+ """
1137
+ Run a command in a terminal with completion tracking.
1138
+
1139
+ This is the standard tool for executing commands in terminals. It provides
1140
+ a command ID that allows you to monitor completion status and ensures
1141
+ reliable execution for both quick and long-running commands.
1142
+
1143
+ The returned command ID enables you to:
1144
+ - Check if the command has finished with check_command_status
1145
+ - Wait for completion with wait_for_command_completion
1146
+ - Monitor progress for long-running operations
1147
+
1148
+ Best practices:
1149
+ - Provide a clear purpose describing what you expect the command to accomplish
1150
+ - Save the returned command ID for monitoring if needed
1151
+ - Use wait_for_command_completion for commands that subsequent steps depend on
1152
+
1153
+ Args:
1154
+ terminal_id: The ID of the terminal to run the command in
1155
+ command: The command to execute in the terminal
1156
+ purpose: Description of what the command is meant to accomplish
1157
+
1158
+ Returns:
1159
+ Command ID for tracking completion, or error message if command couldn't be started
1160
+ """
1161
+ return await self.terminal_tool.run_command_tracked(terminal_id, command, purpose)
1162
+
1163
+ async def send_terminal_input(
1164
+ self, terminal_id: str, text: str, submit: bool = True, command_id: Optional[str] = None
1165
+ ) -> str:
1166
+ """
1167
+ Send input to an already-running terminal command.
1168
+
1169
+ Use this tool when a command started with run_command_tracked is waiting for
1170
+ interactive input, such as a confirmation prompt or a password prompt. Read
1171
+ the terminal first to confirm it is waiting, then send the exact response.
1172
+
1173
+ This tool does not start a new command and does not echo or store the input
1174
+ text in terminal output.
1175
+
1176
+ Args:
1177
+ terminal_id: The ID of the terminal where the command is running
1178
+ text: Text to send to the running process
1179
+ submit: Whether to append a newline before sending (default: true)
1180
+ command_id: Optional command ID when more than one command is active
1181
+
1182
+ Returns:
1183
+ Confirmation that input was sent, or an error explaining why it could not be sent
1184
+ """
1185
+ return await self.terminal_tool.send_terminal_input(terminal_id, text, submit=submit, command_id=command_id)
1186
+
1187
+ async def check_command_status(self, terminal_id: str, command_id: str) -> str:
1188
+ """
1189
+ Check if a command has finished running and get its results.
1190
+
1191
+ Use this tool to check the status of commands started with run_command_tracked.
1192
+ The status will show whether the command is still running, completed successfully,
1193
+ or terminated with an error.
1194
+
1195
+ Typical workflow:
1196
+ 1. Start a command with run_command_tracked to get a command ID
1197
+ 2. Use this tool to check if the command has finished
1198
+ 3. Read the terminal output once the command is complete
1199
+
1200
+ Args:
1201
+ terminal_id: The ID of the terminal where the command is running
1202
+ command_id: The command ID returned from run_command_tracked
1203
+
1204
+ Returns:
1205
+ Formatted status showing completion state, duration, and exit code
1206
+ """
1207
+ return await self.terminal_tool.check_command_status(terminal_id, command_id)
1208
+
1209
+ async def check_terminal_status(self, terminal_id: str) -> str:
1210
+ """
1211
+ Get an overview of a terminal's current state and active commands.
1212
+
1213
+ Use this tool to see what commands are currently running in a terminal
1214
+ and whether the terminal is ready to accept new commands. This helps
1215
+ you avoid conflicts when managing multiple concurrent operations.
1216
+
1217
+ When to use:
1218
+ - Before starting new commands to ensure the terminal is available
1219
+ - To see all active commands and their progress
1220
+ - To troubleshoot why a terminal might not be responding
1221
+ - To get an overview of terminal activity
1222
+
1223
+ Args:
1224
+ terminal_id: The ID of the terminal to check
1225
+
1226
+ Returns:
1227
+ Formatted report showing terminal status and all active commands with their progress
1228
+ """
1229
+ return await self.terminal_tool.check_terminal_status(terminal_id)
1230
+
1231
+ async def wait_for_command_completion(self, terminal_id: str, command_id: str, timeout: Optional[int] = 120) -> str:
1232
+ """
1233
+ Wait for a command to finish before continuing with other tasks.
1234
+
1235
+ Use this tool when you need to ensure a command completes before proceeding.
1236
+ This is essential for workflows where subsequent steps depend on the command
1237
+ results, such as running tests before deployment or building before serving.
1238
+
1239
+ The tool will block execution until the command finishes or the timeout expires.
1240
+ Timeout defaults to 120 seconds and is capped at 300 seconds. If the timeout
1241
+ expires, the command is left running in the terminal and the response tells you
1242
+ how to check it again with check_command_status.
1243
+ After completion, you can read the terminal output to see the results.
1244
+
1245
+ Common scenarios:
1246
+ - Wait for test suites to complete before analyzing results
1247
+ - Wait for build processes to finish before starting servers
1248
+ - Ensure setup commands complete before running the main application
1249
+ - Wait for package installations to finish before using new dependencies
1250
+
1251
+ Args:
1252
+ terminal_id: The ID of the terminal where the command is running
1253
+ command_id: The command ID returned from run_command_tracked
1254
+ timeout: Maximum time to wait in seconds (default: 120, capped at 300)
1255
+
1256
+ Returns:
1257
+ Completion status message or timeout notification with follow-up status-check guidance
1258
+ """
1259
+ return await self.terminal_tool.wait_for_command_completion(terminal_id, command_id, timeout)
1260
+
1261
+ async def read_entire_file(self, relative_path: str) -> str:
1262
+ """
1263
+ Read the contents of a file in the project.
1264
+
1265
+ Note: Files exceeding 2000 lines will be truncated with a warning message.
1266
+ Use read_file_section to read specific portions of large files.
1267
+
1268
+ Args:
1269
+ relative_path: Path to the file, relative to the project root
1270
+
1271
+ Returns:
1272
+ The contents of the file as a string formatted as markdown
1273
+
1274
+ Raises:
1275
+ FileNotFoundError: If the file doesn't exist
1276
+ """
1277
+ return await self.read_file_tool.read_entire_file(relative_path)
1278
+
1279
+ async def read_file_section(self, relative_path: str, start_line: int, end_line: int) -> str:
1280
+ """
1281
+ Read a specific section of a file in the project from start_line to end_line (inclusive).
1282
+
1283
+ Args:
1284
+ relative_path: Path to the file, relative to the project root
1285
+ start_line: The line number to start reading from (1-indexed)
1286
+ end_line: The line number to stop reading at (1-indexed, inclusive)
1287
+
1288
+ Returns:
1289
+ The specified section of the file as a string formatted as markdown
1290
+
1291
+ Raises:
1292
+ FileNotFoundError: If the file doesn't exist
1293
+ ValueError: If start_line or end_line are invalid
1294
+ """
1295
+ return await self.read_file_tool.read_file_section(relative_path, start_line, end_line)
1296
+
1297
+ async def create_file(self, relative_path: str, content: str) -> str:
1298
+ """
1299
+ Create a new file in the project with the given content.
1300
+
1301
+ Args:
1302
+ relative_path: Path to the file to create, relative to the project root
1303
+ content: Content to write to the new file
1304
+
1305
+ Returns:
1306
+ The contents of the created file as a string formatted as markdown
1307
+
1308
+ Raises:
1309
+ FileExistsError: If the file already exists
1310
+ ValueError: If the parent directory doesn't exist
1311
+ PermissionError: If the file cannot be created due to permissions
1312
+ """
1313
+ return await self.create_file_tool.create_file(relative_path, content)
1314
+
1315
+ async def replace_entire_file(self, relative_path: str, content: str) -> str:
1316
+ """
1317
+ Replace the entire contents of a file in the project.
1318
+
1319
+ Args:
1320
+ relative_path: Path to the file, relative to the project root
1321
+ content: New content to write to the file
1322
+
1323
+ Returns:
1324
+ The updated contents of the file as a string formatted as markdown
1325
+
1326
+ Raises:
1327
+ FileNotFoundError: If the file doesn't exist
1328
+ PermissionError: If the file cannot be written to
1329
+ """
1330
+ return await self.replace_entire_file_tool.replace_entire_file(relative_path, content)
1331
+
1332
+ async def replace_lines(self, relative_path: str, start_line: int, end_line: int, new_content: str) -> str:
1333
+ """
1334
+ Replace a range of lines in a file with new content.
1335
+
1336
+ Args:
1337
+ relative_path: Path to the file, relative to the project root
1338
+ start_line: The starting line number (1-indexed)
1339
+ end_line: The ending line number (1-indexed, inclusive)
1340
+ new_content: The new content to replace the specified lines with
1341
+
1342
+ Returns:
1343
+ The updated contents of the file as a string formatted as markdown
1344
+
1345
+ Raises:
1346
+ FileNotFoundError: If the file doesn't exist
1347
+ ValueError: If the line range is invalid
1348
+ PermissionError: If the file cannot be written to
1349
+ """
1350
+ return await self.replace_lines_tool.replace_lines(relative_path, start_line, end_line, new_content)
1351
+
1352
+ async def apply_patch(self, input: str) -> str:
1353
+ """
1354
+ This is a custom utility that makes it more convenient to add, remove, move, or edit code files. `apply_patch` effectively allows you to execute a diff/patch against a file, but the format of the diff specification is unique to this task, so pay careful attention to these instructions. To use the `apply_patch` command, you should pass a message of the following structure as "input":
1355
+
1356
+ %%bash
1357
+ apply_patch <<"EOF"
1358
+ *** Begin Patch
1359
+ [YOUR_PATCH]
1360
+ *** End Patch
1361
+ EOF
1362
+
1363
+ Where [YOUR_PATCH] is the actual content of your patch, specified in the following V4A diff format.
1364
+
1365
+ *** [ACTION] File: [path/to/file] -> ACTION can be one of Add, Update, or Delete.
1366
+ For each snippet of code that needs to be changed, repeat the following:
1367
+ [context_before] -> See below for further instructions on context.
1368
+ - [old_code] -> Precede the old code with a minus sign.
1369
+ + [new_code] -> Precede the new, replacement code with a plus sign.
1370
+ [context_after] -> See below for further instructions on context.
1371
+
1372
+ For instructions on [context_before] and [context_after]:
1373
+ - By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change's [context_after] lines in the second change's [context_before] lines.
1374
+ - If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
1375
+ @@ class BaseClass
1376
+ [3 lines of pre-context]
1377
+ - [old_code]
1378
+ + [new_code]
1379
+ [3 lines of post-context]
1380
+
1381
+ - If a code block is repeated so many times in a class or function such that even a single @@ statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance:
1382
+
1383
+ @@ class BaseClass
1384
+ @@ def method():
1385
+ [3 lines of pre-context]
1386
+ - [old_code]
1387
+ + [new_code]
1388
+ [3 lines of post-context]
1389
+
1390
+ Note, then, that we do not use line numbers in this diff format, as the context is enough to uniquely identify code. An example of a message that you might pass as "input" to this function, in order to apply a patch, is shown below.
1391
+
1392
+ %%bash
1393
+ apply_patch <<"EOF"
1394
+ *** Begin Patch
1395
+ *** Update File: pygorithm/searching/binary_search.py
1396
+ @@ class BaseClass
1397
+ @@ def search():
1398
+ - pass
1399
+ + raise NotImplementedError()
1400
+
1401
+ @@ class Subclass
1402
+ @@ def search():
1403
+ - pass
1404
+ + raise NotImplementedError()
1405
+
1406
+ *** End Patch
1407
+ EOF
1408
+ """
1409
+ return await self.apply_patch_tool.apply_patch(input)
1410
+
1411
+ async def read_memory(self) -> str:
1412
+ """
1413
+ Read the contents of the KOLEGA.md file which serves as the agent's memory.
1414
+
1415
+ Returns:
1416
+ The contents of the KOLEGA.md file as a string
1417
+
1418
+ Raises:
1419
+ FileNotFoundError: If the KOLEGA.md file doesn't exist
1420
+ """
1421
+ return await self.memory_tool.read_memory()
1422
+
1423
+ async def write_memory(self, memory_content: str) -> str:
1424
+ """
1425
+ Write a new memory to the KOLEGA.md file which serves as the agent's memory.
1426
+
1427
+ The memory is added as a markdown bullet point to the file.
1428
+
1429
+ Args:
1430
+ memory_content: The memory content to add to the file
1431
+
1432
+ Returns:
1433
+ A confirmation message indicating success
1434
+
1435
+ Raises:
1436
+ PermissionError: If the file cannot be written to
1437
+ Exception: If any other error occurs during writing
1438
+ """
1439
+ return await self.memory_tool.write_memory(memory_content)
1440
+
1441
+ async def search_codebase(
1442
+ self, pattern: str, file_pattern: str = "*", case_sensitive: bool = False, literal: bool = True
1443
+ ) -> str:
1444
+ """
1445
+ Search the codebase for files containing a specific pattern (grep functionality).
1446
+
1447
+ Args:
1448
+ pattern: The pattern to search for in files
1449
+ file_pattern: Optional glob pattern to filter which files to search (default: all files)
1450
+ case_sensitive: Whether the search should be case-sensitive (default: False)
1451
+ literal: Whether to treat the pattern as literal text (True) or as a regular expression (False) (default: True)
1452
+
1453
+ Returns:
1454
+ Markdown formatted list of files and matches, limited to 128 results
1455
+
1456
+ Raises:
1457
+ Exception: If any error occurs during the search operation
1458
+ """
1459
+ return await self.search_codebase_tool.search_codebase(
1460
+ pattern, file_pattern=file_pattern, case_sensitive=case_sensitive, literal=literal
1461
+ )
1462
+
1463
+ async def web_fetch(self, url: str, instruction: str) -> str:
1464
+ """
1465
+ Fetch web page content, follow an instruction, and return a concise response.
1466
+
1467
+ This tool downloads the specified URL, extracts readable text using Trafilatura,
1468
+ and asks the fast LLM model to apply the provided instruction. Useful for gathering
1469
+ information from public web pages without launching an interactive browser session.
1470
+
1471
+ Args:
1472
+ url: Full http(s) URL to fetch.
1473
+ instruction: Guidance for how to use the extracted content.
1474
+
1475
+ Returns:
1476
+ The model's response derived from the fetched content, truncated to an internal character limit if needed.
1477
+ """
1478
+ return await self.web_fetch_tool.web_fetch(url, instruction)
1479
+
1480
+ async def find_files_by_pattern(
1481
+ self, pattern: str, include_directories: bool = True, show_details: bool = True
1482
+ ) -> str:
1483
+ """
1484
+ Find files by glob pattern in the project directory.
1485
+
1486
+ Behavior:
1487
+ - Supports patterns like '*.py', 'src/**/*.js'. Leading '/' is ignored.
1488
+ - Bare filenames without wildcards or '/' (e.g., 'README.md') are treated as '**/README.md'.
1489
+ - include_directories=True (default) shows directories as well as files.
1490
+ - Returns 128 results max.
1491
+
1492
+ Args:
1493
+ pattern: Glob pattern or filename to search for
1494
+ include_directories: Include directories in results (default: True)
1495
+ show_details: Include size/mtime/type metadata (default: True)
1496
+
1497
+ Returns:
1498
+ Markdown with the matching items (max 128)
1499
+ """
1500
+ return await self.glob_tool.find_files_by_pattern(
1501
+ pattern, include_directories=include_directories, show_details=show_details
1502
+ )
1503
+
1504
+ async def get_host(self, port: int) -> str:
1505
+ """
1506
+ Get the hostname for accessing a service on the specified port.
1507
+
1508
+ This tool returns the appropriate hostname based on the environment:
1509
+ - In local development: returns 'localhost:PORT'
1510
+ - In cloud sandbox (e2b): returns the sandbox-specific hostname
1511
+
1512
+ When to use this tool:
1513
+ - Before accessing any web service or development server
1514
+ - When constructing URLs for HTTP requests
1515
+ - When providing URLs to users or other tools
1516
+ - When launching browsers to access local services
1517
+
1518
+ Usage notes:
1519
+ 1. Always call this tool before making HTTP requests to local services
1520
+ 2. The port parameter is required - specify the port your service is running on
1521
+ 3. Use the returned hostname to construct full URLs (e.g., http://{host}/api/endpoint)
1522
+ 4. This ensures your code works in both local and cloud sandbox environments
1523
+
1524
+ Args:
1525
+ port: The port number where the service is running
1526
+
1527
+ Returns:
1528
+ The full hostname including port (e.g., 'localhost:3000' or 'xxxx.e2b.dev')
1529
+ """
1530
+ # Check if we're using a SandboxTerminalManager (indicates sandbox mode)
1531
+ if hasattr(self.terminal_manager, "sandbox") and self.terminal_manager.sandbox:
1532
+ # We're in sandbox mode, get the host from the sandbox
1533
+ sandbox = self.terminal_manager.sandbox
1534
+ # E2B AsyncSandbox has a get_host method that takes a port
1535
+ # The method is synchronous and returns a string directly
1536
+ host = sandbox.get_host(port)
1537
+ return host
1538
+ else:
1539
+ # Local mode, return localhost
1540
+ return f"localhost:{port}"
1541
+
1542
+ def _tool_definition_from_callable(self, method_name: str, method: Callable[..., Any]) -> ToolDefinition:
1543
+ """Build a provider-agnostic tool definition from a Python callable."""
1544
+ return tool_definition_from_callable(
1545
+ method_name, method, description_overrides={"apply_patch": APPLY_PATCH_TOOL_DESC}
1546
+ )
1547
+
1548
+ def _groups_for(self, method_name: str) -> frozenset:
1549
+ """Group tags for a tool, from the core group lists plus extension groups."""
1550
+ group_attrs = {
1551
+ "read_only_tools",
1552
+ "browser_tools",
1553
+ "agent_dispatch_tools",
1554
+ "coder_agent_tools",
1555
+ "memory_tools",
1556
+ *self._extension_group_names,
1557
+ }
1558
+ return frozenset(
1559
+ group_name for group_name in group_attrs if method_name in (getattr(self, group_name, None) or [])
1560
+ )
1561
+
1562
+ def registry(self) -> ToolRegistry:
1563
+ """
1564
+ Build the ToolRegistry of currently enabled tools.
1565
+
1566
+ Rebuilt per call (matching the previous dynamic get_tool_list behavior)
1567
+ so tools added by subclasses or extensions after construction are seen.
1568
+ """
1569
+ registry = ToolRegistry()
1570
+
1571
+ for method_name, method in inspect.getmembers(self, predicate=inspect.ismethod):
1572
+ if method_name.startswith("_") or method_name in self.tool_exclusions:
1573
+ continue
1574
+ if not self._should_include_tool(method_name):
1575
+ continue
1576
+ registry.add(self._build_tool(method_name, method))
1577
+
1578
+ for method_name, method in self.extension_callbacks.items():
1579
+ if method_name in registry or method_name in self.tool_exclusions:
1580
+ continue
1581
+ if not self._should_include_tool(method_name):
1582
+ continue
1583
+ registry.add(self._build_tool(method_name, method))
1584
+
1585
+ return registry
1586
+
1587
+ def _build_tool(self, method_name: str, method: Callable[..., Any]) -> Tool:
1588
+ return Tool(
1589
+ name=method_name,
1590
+ definition=self._tool_definition_from_callable(method_name, method),
1591
+ handler=method,
1592
+ groups=self._groups_for(method_name),
1593
+ # Read-only tools have no side effects and agent dispatches operate
1594
+ # on independent sub-agents, so these may run concurrently.
1595
+ parallel_safe=(
1596
+ method_name in (self.read_only_tools or []) or method_name in (self.agent_dispatch_tools or [])
1597
+ ),
1598
+ )
1599
+
1600
+ def has_tool(self, name: str) -> bool:
1601
+ """True if the named tool is currently enabled."""
1602
+ return name in self.registry()
1603
+
1604
+ async def call(self, name: str, **inputs: Any) -> Any:
1605
+ """Dispatch an enabled tool by name."""
1606
+ return await self.registry().call(name, **inputs)
1607
+
1608
+ def get_tool_list(self) -> List[ToolDefinition]:
1609
+ """
1610
+ Returns a list of tool definitions in the format required by the Anthropic API.
1611
+
1612
+ Definitions are generated from the enabled tools' signatures and
1613
+ docstrings; the last definition carries the prompt-cache checkpoint.
1614
+ """
1615
+ return self.registry().definitions()
1616
+
1617
+ def _should_include_tool(self, method_name: str) -> bool:
1618
+ """
1619
+ Determine if a tool method should be included based on the configuration.
1620
+
1621
+ Args:
1622
+ method_name: Name of the method/tool to check
1623
+
1624
+ Returns:
1625
+ True if the tool should be included, False otherwise
1626
+ """
1627
+ # Check custom tool groups first
1628
+ if self.tool_config.custom_tool_groups:
1629
+ for group_name in self.tool_config.custom_tool_groups:
1630
+ if hasattr(self, group_name):
1631
+ group_tools = getattr(self, group_name)
1632
+ if method_name in group_tools:
1633
+ return True
1634
+
1635
+ # If restrict_to_tool_groups is True, only include tools from explicitly enabled groups
1636
+ if self.tool_config.restrict_to_tool_groups:
1637
+ # Check if tool belongs to any enabled group
1638
+ if method_name in self.agent_dispatch_tools and self.tool_config.include_agent_dispatch_tools:
1639
+ return True
1640
+ if method_name in self.memory_tools and self.tool_config.include_memory_tools:
1641
+ return method_name not in self.tool_exclusions
1642
+ if method_name in self.browser_tools and self.tool_config.browser_only:
1643
+ return True
1644
+ if method_name in self.read_only_tools and self.tool_config.read_only:
1645
+ return True
1646
+ # Tool doesn't belong to any enabled group
1647
+ return False
1648
+
1649
+ # Original behavior for non-restricted mode
1650
+ # Handle legacy read-only filtering
1651
+ if self.tool_config.read_only and method_name not in self.read_only_tools:
1652
+ return False
1653
+
1654
+ # Handle legacy browser-only filtering
1655
+ if self.tool_config.browser_only and method_name not in self.browser_tools:
1656
+ return False
1657
+
1658
+ # Exclude browser tools unless this is a browser-only agent or investigation tools are enabled
1659
+ if (
1660
+ not self.tool_config.browser_only
1661
+ and not self.tool_config.include_agent_dispatch_tools
1662
+ and method_name in self.browser_tools
1663
+ ):
1664
+ return False
1665
+
1666
+ # Check investigation agent tools
1667
+ if method_name in self.agent_dispatch_tools:
1668
+ return self.tool_config.include_agent_dispatch_tools
1669
+
1670
+ # Check memory tools
1671
+ if method_name in self.memory_tools:
1672
+ # Include memory tools if explicitly enabled, or if memory tools are not excluded
1673
+ return self.tool_config.include_memory_tools or method_name not in self.tool_exclusions
1674
+
1675
+ # Include all other core tools by default
1676
+ return True
1677
+
1678
+ async def cleanup(self):
1679
+ """Clean up all tool resources"""
1680
+ try:
1681
+ # Clean up terminal resources
1682
+ if hasattr(self, "terminal_tool") and hasattr(self.terminal_tool, "terminal_manager"):
1683
+ await self.terminal_tool.terminal_manager.cleanup_all()
1684
+ print("Cleaned up terminal resources")
1685
+
1686
+ # Clean up any browser resources
1687
+ if hasattr(self, "browser_tool") and hasattr(self.browser_tool, "cleanup"):
1688
+ await self.browser_tool.cleanup()
1689
+ print("Cleaned up browser resources")
1690
+
1691
+ # Clean up any sub-agents
1692
+ if hasattr(self, "agent_tool") and hasattr(self.agent_tool, "agents"):
1693
+ for agent_id, agent in list(self.agent_tool.agents.items()):
1694
+ if hasattr(agent, "cleanup"):
1695
+ try:
1696
+ await agent.cleanup()
1697
+ print(f"Cleaned up sub-agent: {agent_id}")
1698
+ except Exception as e:
1699
+ print(f"Error cleaning up sub-agent {agent_id}: {e}")
1700
+
1701
+ except Exception as e:
1702
+ await self.log_error(f"Error during tool cleanup: {str(e)}", sender="ToolCollection")
1703
+
1704
+ self.extension_callbacks = {}