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,377 @@
1
+ import re
2
+ import os
3
+ from pathlib import Path
4
+ from typing import List, Tuple, Optional, Dict, Any
5
+
6
+ from .base_tool import BaseTool
7
+
8
+
9
+ class SearchCodebaseTool(BaseTool):
10
+ # Define binary extensions to skip without checking content
11
+ BINARY_EXTENSIONS = {
12
+ ".pyc",
13
+ ".so",
14
+ ".dll",
15
+ ".exe",
16
+ ".bin",
17
+ ".jar",
18
+ ".war",
19
+ ".jpg",
20
+ ".jpeg",
21
+ ".png",
22
+ ".gif",
23
+ ".bmp",
24
+ ".ico",
25
+ ".svg",
26
+ ".pdf",
27
+ ".zip",
28
+ ".tar",
29
+ ".gz",
30
+ ".tgz",
31
+ ".rar",
32
+ ".7z",
33
+ ".mp3",
34
+ ".mp4",
35
+ ".avi",
36
+ ".mov",
37
+ ".mkv",
38
+ ".wav",
39
+ ".o",
40
+ ".obj",
41
+ ".class",
42
+ ".binary",
43
+ ".wasm",
44
+ ".node",
45
+ }
46
+
47
+ # Define directories to exclude
48
+ EXCLUDE_DIRS = {
49
+ ".git",
50
+ ".svn",
51
+ ".hg",
52
+ ".idea",
53
+ ".vscode",
54
+ "__pycache__",
55
+ "node_modules",
56
+ "venv",
57
+ "env",
58
+ ".env",
59
+ "dist",
60
+ "build",
61
+ "target",
62
+ "bin",
63
+ "obj",
64
+ ".next",
65
+ ".nuxt",
66
+ "coverage",
67
+ }
68
+
69
+ async def search_codebase(self, pattern: str, file_pattern: str = "*", case_sensitive: bool = False, literal: bool = True) -> str:
70
+ """
71
+ Search the codebase for files containing a specific pattern (grep functionality).
72
+
73
+ Uses grep command in sandbox environments for maximum efficiency (single command).
74
+ Uses optimized Python implementation for local filesystems.
75
+
76
+ Args:
77
+ pattern: The pattern to search for in files
78
+ file_pattern: Optional glob pattern to filter which files to search (default: all files)
79
+ case_sensitive: Whether the search should be case-sensitive (default: False)
80
+ literal: Whether to treat the pattern as literal text (True) or as a regular expression (False) (default: True)
81
+
82
+ Returns:
83
+ Markdown formatted list of files and matches, limited to 128 results
84
+
85
+ Raises:
86
+ Exception: If any error occurs during the search operation
87
+ """
88
+ try:
89
+ await self.log_info(f"Searching codebase for pattern: '{pattern}'", sender=self.caller.agent_name)
90
+
91
+ # If literal search, escape special regex characters
92
+ search_pattern = re.escape(pattern) if literal else pattern
93
+
94
+ # Compile the regex pattern to validate it
95
+ flags = 0 if case_sensitive else re.IGNORECASE
96
+ try:
97
+ regex = re.compile(search_pattern, flags)
98
+ except re.error as e:
99
+ error_msg = f"Invalid regular expression: {str(e)}"
100
+ await self.log_error(error_msg, sender=self.caller.agent_name)
101
+ return f"Error: {error_msg}"
102
+
103
+ # Use grep for sandbox environments (single command execution)
104
+ if hasattr(self.filesystem, "sandbox"):
105
+ return await self._search_with_grep_sandbox(search_pattern, file_pattern, case_sensitive, pattern, literal)
106
+ else:
107
+ # Use optimized Python implementation for local filesystem
108
+ return await self._search_with_python(search_pattern, file_pattern, case_sensitive, regex, pattern)
109
+
110
+ except Exception as e:
111
+ error_msg = f"Error searching codebase: {str(e)}"
112
+ await self.log_error(error_msg, sender=self.caller.agent_name)
113
+ return f"Error: {error_msg}"
114
+
115
+ async def _search_with_grep_sandbox(self, pattern: str, file_pattern: str, case_sensitive: bool, original_pattern: str, literal: bool) -> str:
116
+ """Use single grep command for sandbox environments - most efficient approach"""
117
+
118
+ # Build grep command
119
+ grep_flags = [
120
+ "-r", # Recursive
121
+ "-n", # Show line numbers
122
+ "--binary-files=without-match", # Skip binary files
123
+ ]
124
+
125
+ # Case sensitivity
126
+ if not case_sensitive:
127
+ grep_flags.append("-i")
128
+
129
+ # Use fixed string matching for literal searches, extended regex otherwise
130
+ if literal:
131
+ grep_flags.append("-F") # Fixed string matching
132
+ else:
133
+ grep_flags.append("-E") # Extended regex
134
+
135
+ # File pattern
136
+ if file_pattern != "*":
137
+ grep_flags.append(f'--include={file_pattern}')
138
+
139
+ # Exclude directories
140
+ for exclude_dir in self.EXCLUDE_DIRS:
141
+ grep_flags.append(f"--exclude-dir={exclude_dir}")
142
+
143
+ # Exclude binary extensions
144
+ for ext in self.BINARY_EXTENSIONS:
145
+ grep_flags.append(f'--exclude=*{ext}')
146
+
147
+ # Build command with awk processing for formatting
148
+ # Use the original pattern for grep when literal=True (no escaping needed with -F flag)
149
+ grep_pattern = original_pattern if literal else pattern
150
+ grep_cmd = f"grep {' '.join(grep_flags)} '{grep_pattern}' . 2>/dev/null"
151
+
152
+ # AWK script to format output exactly like the original
153
+ awk_script = r"""| awk '
154
+ BEGIN {
155
+ file_count = 0;
156
+ current_file = "";
157
+ match_count = 0;
158
+ lines = "";
159
+ lines_shown = 0;
160
+ max_lines_per_file = 5;
161
+ max_files = 128;
162
+ max_line_length = 200;
163
+ }
164
+ {
165
+ # Parse grep output: filename:line_number:content
166
+ colon1 = index($0, ":");
167
+ colon2 = index(substr($0, colon1 + 1), ":") + colon1;
168
+
169
+ file = substr($0, 1, colon1 - 1);
170
+ line_num = substr($0, colon1 + 1, colon2 - colon1 - 1);
171
+ line_content = substr($0, colon2 + 1);
172
+
173
+ # Remove leading/trailing whitespace from content
174
+ gsub(/^[ \t]+|[ \t]+$/, "", line_content);
175
+
176
+ # Truncate long lines to 200 characters
177
+ if (length(line_content) > max_line_length) {
178
+ line_content = substr(line_content, 1, max_line_length) "...";
179
+ }
180
+
181
+ if (file != current_file) {
182
+ # Print previous file results if any
183
+ if (current_file != "") {
184
+ print "- **" current_file "** (" match_count " matches)";
185
+ print substr(lines, 1, length(lines)-1); # Remove trailing newline
186
+ if (lines_shown < match_count && lines_shown >= max_lines_per_file) {
187
+ print " ... and " (match_count - max_lines_per_file) " more matches";
188
+ }
189
+ print "";
190
+ }
191
+
192
+ # Check if we reached the file limit
193
+ file_count++;
194
+ if (file_count > max_files) {
195
+ reached_limit = 1;
196
+ exit;
197
+ }
198
+
199
+ # Start new file
200
+ current_file = file;
201
+ match_count = 0;
202
+ lines = "";
203
+ lines_shown = 0;
204
+ }
205
+
206
+ match_count++;
207
+ if (lines_shown < max_lines_per_file) {
208
+ lines = lines " Line " line_num ": " line_content "\n";
209
+ lines_shown++;
210
+ }
211
+ }
212
+ END {
213
+ # Print last file
214
+ if (current_file != "" && file_count <= max_files) {
215
+ print "- **" current_file "** (" match_count " matches)";
216
+ print substr(lines, 1, length(lines)-1); # Remove trailing newline
217
+ if (lines_shown < match_count && lines_shown >= max_lines_per_file) {
218
+ print " ... and " (match_count - max_lines_per_file) " more matches";
219
+ }
220
+ }
221
+
222
+ # Add warning if limit reached
223
+ if (file_count > max_files || reached_limit) {
224
+ print "";
225
+ print "⚠️ **Note:** Showing only the first " max_files " results. There are more matches in the codebase.";
226
+ }
227
+ }'
228
+ """
229
+
230
+ full_cmd = f"cd {self.filesystem.root_path} && {grep_cmd} {awk_script}"
231
+
232
+ # Execute the single command - sandbox is always async
233
+ result = await self.filesystem.sandbox.commands.run(full_cmd)
234
+
235
+ if result.exit_code != 0 or not result.stdout.strip():
236
+ return f"No matches found for pattern '{original_pattern}'"
237
+
238
+ # Format the final output
239
+ output = f"# Search Results for '{original_pattern}'\n\n"
240
+ output += result.stdout.strip()
241
+
242
+ return output
243
+
244
+ async def _search_with_python(self, pattern: str, file_pattern: str, case_sensitive: bool, regex, original_pattern: str) -> str:
245
+ """Optimized Python implementation for local filesystems"""
246
+
247
+ # Get files with their info
248
+ files_with_info = await self._get_files_batch_local(file_pattern)
249
+
250
+ # Search through files
251
+ results = []
252
+ total_matches = 0
253
+ max_results = 128
254
+ max_file_size = 10 * 1024 * 1024 # 10MB
255
+
256
+ for file_path, file_size in files_with_info:
257
+ # Skip files that are too large
258
+ if file_size > max_file_size:
259
+ continue
260
+
261
+ # Skip binary files by extension
262
+ if self._is_likely_binary_by_extension(file_path):
263
+ continue
264
+
265
+ try:
266
+ # Read file content once
267
+ content = self.filesystem.read_text(file_path)
268
+
269
+ # Quick binary check on content (first 1024 bytes)
270
+ if "\x00" in content[:1024]:
271
+ continue
272
+
273
+ # Search for matches
274
+ matches = list(regex.finditer(content))
275
+
276
+ if matches:
277
+ # Get matching lines with context
278
+ matching_lines = self._extract_matching_lines(content, matches, max_lines=5)
279
+
280
+ # Add to results
281
+ results.append(f"- **{file_path}** ({len(matches)} matches)\n" + "\n".join(matching_lines))
282
+
283
+ total_matches += 1
284
+ if total_matches >= max_results:
285
+ break
286
+
287
+ except Exception:
288
+ # Skip files that can't be read
289
+ continue
290
+
291
+ # Format results
292
+ if results:
293
+ result_text = f"# Search Results for '{original_pattern}'\n\n"
294
+ if total_matches >= max_results:
295
+ result_text += f"⚠️ **Note:** Showing only the first {max_results} results. There are more matches in the codebase.\n\n"
296
+ result_text += "\n\n".join(results)
297
+ return result_text
298
+ else:
299
+ return f"No matches found for pattern '{original_pattern}'"
300
+
301
+ async def _get_files_batch_local(self, file_pattern: str) -> List[Tuple[str, int]]:
302
+ """
303
+ Get files for local filesystem with minimal stat calls.
304
+ """
305
+ # Get files using glob
306
+ if file_pattern == "*":
307
+ files = self.filesystem.glob("**/*")
308
+ else:
309
+ files = self.filesystem.glob(f"**/{file_pattern}")
310
+
311
+ files_with_info = []
312
+ for file_path in files:
313
+ # Quick path-based exclusions
314
+ if self._should_exclude_by_path(file_path):
315
+ continue
316
+
317
+ # Check if it's a file and get size in one stat call
318
+ try:
319
+ if self.filesystem.is_file(file_path):
320
+ path_obj = self.filesystem.get_path(file_path)
321
+ # Use the existing _should_exclude_file logic from base class
322
+ if self._should_exclude_file(path_obj):
323
+ continue
324
+
325
+ # Get size from stat
326
+ try:
327
+ stat_info = path_obj.stat()
328
+ size = stat_info.st_size
329
+ except:
330
+ size = 0
331
+
332
+ files_with_info.append((file_path, size))
333
+ except Exception:
334
+ continue
335
+
336
+ return files_with_info
337
+
338
+ def _should_exclude_by_path(self, file_path: str) -> bool:
339
+ """
340
+ Check if file should be excluded based on its path alone.
341
+ """
342
+ path_parts = Path(file_path).parts
343
+ return any(part in self.EXCLUDE_DIRS for part in path_parts)
344
+
345
+ def _is_likely_binary_by_extension(self, file_path: str) -> bool:
346
+ """
347
+ Quick binary check based on file extension.
348
+ """
349
+ return Path(file_path).suffix.lower() in self.BINARY_EXTENSIONS
350
+
351
+ def _extract_matching_lines(self, content: str, matches: List, max_lines: int = 5) -> List[str]:
352
+ """
353
+ Extract lines containing matches with line numbers.
354
+ """
355
+ lines = content.splitlines()
356
+ line_matches = {}
357
+
358
+ # Find which lines have matches
359
+ for match in matches:
360
+ line_num = content[: match.start()].count("\n") + 1
361
+ if line_num not in line_matches:
362
+ line_content = lines[line_num - 1].strip()
363
+ # Truncate long lines to 200 characters
364
+ if len(line_content) > 200:
365
+ line_content = line_content[:200] + "..."
366
+ line_matches[line_num] = line_content
367
+
368
+ # Format output - matching original format exactly
369
+ matching_lines = []
370
+ for line_num in sorted(line_matches.keys())[:max_lines]:
371
+ matching_lines.append(f" Line {line_num}: {line_matches[line_num]}")
372
+
373
+ # Important: show total matches minus shown lines, not line count
374
+ if len(line_matches) > max_lines:
375
+ matching_lines.append(f" ... and {len(matches) - max_lines} more matches")
376
+
377
+ return matching_lines
@@ -0,0 +1,47 @@
1
+ """Base class for tools that support streaming responses."""
2
+
3
+ from .base_tool import BaseTool
4
+ from kolega_code.events import AgentEvent
5
+
6
+
7
+ class StreamingTool(BaseTool):
8
+ """Base class for tools that support streaming responses."""
9
+
10
+ async def send_streaming_update(
11
+ self,
12
+ content: str,
13
+ tool_call_id: str,
14
+ tool_name: str,
15
+ is_complete: bool = False,
16
+ stream_mode: str = "replace",
17
+ ):
18
+ """Send a streaming update for this tool's execution.
19
+
20
+ Args:
21
+ content: The partial or complete content to stream
22
+ tool_call_id: The ID of the tool call this update belongs to
23
+ tool_name: The name of the tool being executed
24
+ is_complete: Whether this is the final update
25
+ stream_mode: Whether incomplete updates should replace or append to the visible stream
26
+ """
27
+ # Attach dispatch metadata when the calling agent is a sub-agent so the
28
+ # UI can route this stream to the right sub-agent display.
29
+ sub_agent_info = None
30
+ sub_agent_context = getattr(self.caller, "sub_agent_context", None)
31
+ if getattr(self.caller, "sub_agent", False) is True and isinstance(sub_agent_context, dict):
32
+ sub_agent_info = dict(sub_agent_context)
33
+
34
+ event = AgentEvent(
35
+ sender=self.caller.agent_name,
36
+ event_type="tool_streaming_update",
37
+ content={
38
+ "text": content,
39
+ "tool_call_id": tool_call_id,
40
+ "tool_name": tool_name,
41
+ "is_complete": is_complete,
42
+ "stream_mode": stream_mode,
43
+ },
44
+ is_streaming=not is_complete,
45
+ sub_agent_info=sub_agent_info,
46
+ )
47
+ await self.connection_manager.broadcast_event(event, self.workspace_id, self.thread_id)