kolega-code 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kolega_code/__init__.py +151 -0
- kolega_code/agent/__init__.py +42 -0
- kolega_code/agent/baseagent.py +998 -0
- kolega_code/agent/browseragent.py +123 -0
- kolega_code/agent/coder.py +157 -0
- kolega_code/agent/common.py +41 -0
- kolega_code/agent/compression.py +81 -0
- kolega_code/agent/context.py +112 -0
- kolega_code/agent/conversation.py +408 -0
- kolega_code/agent/generalagent.py +146 -0
- kolega_code/agent/investigationagent.py +123 -0
- kolega_code/agent/planningagent.py +187 -0
- kolega_code/agent/prompt_provider.py +196 -0
- kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
- kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
- kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
- kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
- kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
- kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
- kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
- kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
- kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
- kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
- kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
- kolega_code/agent/prompts.py +192 -0
- kolega_code/agent/tests/__init__.py +0 -0
- kolega_code/agent/tests/llm/__init__.py +0 -0
- kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
- kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
- kolega_code/agent/tests/llm/test_client.py +773 -0
- kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
- kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
- kolega_code/agent/tests/llm/test_exceptions.py +249 -0
- kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
- kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
- kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
- kolega_code/agent/tests/llm/test_model_specs.py +17 -0
- kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
- kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
- kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
- kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
- kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
- kolega_code/agent/tests/services/__init__.py +1 -0
- kolega_code/agent/tests/services/test_browser.py +447 -0
- kolega_code/agent/tests/services/test_browser_parity.py +353 -0
- kolega_code/agent/tests/services/test_file_system.py +699 -0
- kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
- kolega_code/agent/tests/services/test_terminal.py +154 -0
- kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
- kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
- kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
- kolega_code/agent/tests/test_base_agent.py +1942 -0
- kolega_code/agent/tests/test_coder_attachments.py +330 -0
- kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
- kolega_code/agent/tests/test_commands.py +179 -0
- kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
- kolega_code/agent/tests/test_empty_message_handling.py +48 -0
- kolega_code/agent/tests/test_general_agent.py +242 -0
- kolega_code/agent/tests/test_html.py +320 -0
- kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
- kolega_code/agent/tests/test_planning_agent.py +227 -0
- kolega_code/agent/tests/test_prompt_provider.py +271 -0
- kolega_code/agent/tests/test_tool_registry.py +102 -0
- kolega_code/agent/tests/test_tools.py +549 -0
- kolega_code/agent/tests/tool_backend/__init__.py +0 -0
- kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
- kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
- kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
- kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
- kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
- kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
- kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
- kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
- kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
- kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
- kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
- kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
- kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
- kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
- kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
- kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
- kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
- kolega_code/agent/tool_backend/agent_tool.py +414 -0
- kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
- kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
- kolega_code/agent/tool_backend/base_tool.py +217 -0
- kolega_code/agent/tool_backend/browser_tool.py +271 -0
- kolega_code/agent/tool_backend/build_tool.py +93 -0
- kolega_code/agent/tool_backend/create_file_tool.py +52 -0
- kolega_code/agent/tool_backend/glob_tool.py +323 -0
- kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
- kolega_code/agent/tool_backend/memory_tool.py +79 -0
- kolega_code/agent/tool_backend/read_file_tool.py +119 -0
- kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
- kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
- kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
- kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
- kolega_code/agent/tool_backend/streaming_tool.py +47 -0
- kolega_code/agent/tool_backend/terminal_tool.py +643 -0
- kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
- kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
- kolega_code/agent/tools.py +1704 -0
- kolega_code/agent/utils/commands.py +94 -0
- kolega_code/cli/__init__.py +1 -0
- kolega_code/cli/app.py +2756 -0
- kolega_code/cli/config.py +280 -0
- kolega_code/cli/connection.py +49 -0
- kolega_code/cli/file_index.py +147 -0
- kolega_code/cli/main.py +564 -0
- kolega_code/cli/mentions.py +155 -0
- kolega_code/cli/messages.py +89 -0
- kolega_code/cli/provider_registry.py +96 -0
- kolega_code/cli/session_store.py +207 -0
- kolega_code/cli/settings.py +87 -0
- kolega_code/cli/skills.py +409 -0
- kolega_code/cli/slash_commands.py +108 -0
- kolega_code/cli/tests/__init__.py +1 -0
- kolega_code/cli/tests/test_app.py +4251 -0
- kolega_code/cli/tests/test_cli_config.py +171 -0
- kolega_code/cli/tests/test_connection.py +26 -0
- kolega_code/cli/tests/test_file_index.py +103 -0
- kolega_code/cli/tests/test_main.py +455 -0
- kolega_code/cli/tests/test_mentions.py +108 -0
- kolega_code/cli/tests/test_session_store.py +67 -0
- kolega_code/cli/tests/test_settings.py +62 -0
- kolega_code/cli/tests/test_skills.py +157 -0
- kolega_code/cli/tests/test_slash_commands.py +88 -0
- kolega_code/cli/theme.py +180 -0
- kolega_code/config.py +154 -0
- kolega_code/events.py +202 -0
- kolega_code/llm/client.py +300 -0
- kolega_code/llm/exceptions.py +285 -0
- kolega_code/llm/instrumented_client.py +520 -0
- kolega_code/llm/models.py +1368 -0
- kolega_code/llm/providers/__init__.py +0 -0
- kolega_code/llm/providers/anthropic.py +387 -0
- kolega_code/llm/providers/base.py +71 -0
- kolega_code/llm/providers/google.py +157 -0
- kolega_code/llm/providers/models.py +37 -0
- kolega_code/llm/providers/openai.py +363 -0
- kolega_code/llm/ratelimit.py +40 -0
- kolega_code/llm/specs.py +67 -0
- kolega_code/llm/tool_execution_ids.py +18 -0
- kolega_code/models/__init__.py +9 -0
- kolega_code/models/sandbox_terminal_state.py +47 -0
- kolega_code/runtime.py +50 -0
- kolega_code/sandbox/README.md +200 -0
- kolega_code/sandbox/__init__.py +21 -0
- kolega_code/sandbox/async_filesystem.py +475 -0
- kolega_code/sandbox/base.py +297 -0
- kolega_code/sandbox/browser.py +25 -0
- kolega_code/sandbox/event_loop.py +43 -0
- kolega_code/sandbox/filesystem.py +341 -0
- kolega_code/sandbox/local.py +118 -0
- kolega_code/sandbox/serializer.py +175 -0
- kolega_code/sandbox/terminal.py +868 -0
- kolega_code/sandbox/utils.py +216 -0
- kolega_code/services/base.py +255 -0
- kolega_code/services/browser.py +444 -0
- kolega_code/services/file_system.py +749 -0
- kolega_code/services/html.py +221 -0
- kolega_code/services/terminal.py +903 -0
- kolega_code/tools/__init__.py +22 -0
- kolega_code/tools/core.py +33 -0
- kolega_code/tools/definitions.py +81 -0
- kolega_code/tools/registry.py +73 -0
- kolega_code-0.1.0.dist-info/METADATA +157 -0
- kolega_code-0.1.0.dist-info/RECORD +171 -0
- kolega_code-0.1.0.dist-info/WHEEL +4 -0
- kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
- kolega_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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 = {}
|