kolega-code 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. kolega_code/__init__.py +151 -0
  2. kolega_code/agent/__init__.py +42 -0
  3. kolega_code/agent/baseagent.py +998 -0
  4. kolega_code/agent/browseragent.py +123 -0
  5. kolega_code/agent/coder.py +157 -0
  6. kolega_code/agent/common.py +41 -0
  7. kolega_code/agent/compression.py +81 -0
  8. kolega_code/agent/context.py +112 -0
  9. kolega_code/agent/conversation.py +408 -0
  10. kolega_code/agent/generalagent.py +146 -0
  11. kolega_code/agent/investigationagent.py +123 -0
  12. kolega_code/agent/planningagent.py +187 -0
  13. kolega_code/agent/prompt_provider.py +196 -0
  14. kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
  15. kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
  16. kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
  17. kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
  18. kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
  19. kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
  20. kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
  21. kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
  22. kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
  23. kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
  24. kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
  25. kolega_code/agent/prompts.py +192 -0
  26. kolega_code/agent/tests/__init__.py +0 -0
  27. kolega_code/agent/tests/llm/__init__.py +0 -0
  28. kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
  29. kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
  30. kolega_code/agent/tests/llm/test_client.py +773 -0
  31. kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
  32. kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
  33. kolega_code/agent/tests/llm/test_exceptions.py +249 -0
  34. kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
  35. kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
  36. kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
  37. kolega_code/agent/tests/llm/test_model_specs.py +17 -0
  38. kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
  39. kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
  40. kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
  41. kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
  42. kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
  43. kolega_code/agent/tests/services/__init__.py +1 -0
  44. kolega_code/agent/tests/services/test_browser.py +447 -0
  45. kolega_code/agent/tests/services/test_browser_parity.py +353 -0
  46. kolega_code/agent/tests/services/test_file_system.py +699 -0
  47. kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
  48. kolega_code/agent/tests/services/test_terminal.py +154 -0
  49. kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
  50. kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
  51. kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
  52. kolega_code/agent/tests/test_base_agent.py +1942 -0
  53. kolega_code/agent/tests/test_coder_attachments.py +330 -0
  54. kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
  55. kolega_code/agent/tests/test_commands.py +179 -0
  56. kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
  57. kolega_code/agent/tests/test_empty_message_handling.py +48 -0
  58. kolega_code/agent/tests/test_general_agent.py +242 -0
  59. kolega_code/agent/tests/test_html.py +320 -0
  60. kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
  61. kolega_code/agent/tests/test_planning_agent.py +227 -0
  62. kolega_code/agent/tests/test_prompt_provider.py +271 -0
  63. kolega_code/agent/tests/test_tool_registry.py +102 -0
  64. kolega_code/agent/tests/test_tools.py +549 -0
  65. kolega_code/agent/tests/tool_backend/__init__.py +0 -0
  66. kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
  67. kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
  68. kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
  69. kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
  70. kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
  71. kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
  72. kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
  73. kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
  74. kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
  75. kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
  76. kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
  77. kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
  78. kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
  79. kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
  80. kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
  81. kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
  82. kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
  83. kolega_code/agent/tool_backend/agent_tool.py +414 -0
  84. kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
  85. kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
  86. kolega_code/agent/tool_backend/base_tool.py +217 -0
  87. kolega_code/agent/tool_backend/browser_tool.py +271 -0
  88. kolega_code/agent/tool_backend/build_tool.py +93 -0
  89. kolega_code/agent/tool_backend/create_file_tool.py +52 -0
  90. kolega_code/agent/tool_backend/glob_tool.py +323 -0
  91. kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
  92. kolega_code/agent/tool_backend/memory_tool.py +79 -0
  93. kolega_code/agent/tool_backend/read_file_tool.py +119 -0
  94. kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
  95. kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
  96. kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
  97. kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
  98. kolega_code/agent/tool_backend/streaming_tool.py +47 -0
  99. kolega_code/agent/tool_backend/terminal_tool.py +643 -0
  100. kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
  101. kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
  102. kolega_code/agent/tools.py +1704 -0
  103. kolega_code/agent/utils/commands.py +94 -0
  104. kolega_code/cli/__init__.py +1 -0
  105. kolega_code/cli/app.py +2756 -0
  106. kolega_code/cli/config.py +280 -0
  107. kolega_code/cli/connection.py +49 -0
  108. kolega_code/cli/file_index.py +147 -0
  109. kolega_code/cli/main.py +564 -0
  110. kolega_code/cli/mentions.py +155 -0
  111. kolega_code/cli/messages.py +89 -0
  112. kolega_code/cli/provider_registry.py +96 -0
  113. kolega_code/cli/session_store.py +207 -0
  114. kolega_code/cli/settings.py +87 -0
  115. kolega_code/cli/skills.py +409 -0
  116. kolega_code/cli/slash_commands.py +108 -0
  117. kolega_code/cli/tests/__init__.py +1 -0
  118. kolega_code/cli/tests/test_app.py +4251 -0
  119. kolega_code/cli/tests/test_cli_config.py +171 -0
  120. kolega_code/cli/tests/test_connection.py +26 -0
  121. kolega_code/cli/tests/test_file_index.py +103 -0
  122. kolega_code/cli/tests/test_main.py +455 -0
  123. kolega_code/cli/tests/test_mentions.py +108 -0
  124. kolega_code/cli/tests/test_session_store.py +67 -0
  125. kolega_code/cli/tests/test_settings.py +62 -0
  126. kolega_code/cli/tests/test_skills.py +157 -0
  127. kolega_code/cli/tests/test_slash_commands.py +88 -0
  128. kolega_code/cli/theme.py +180 -0
  129. kolega_code/config.py +154 -0
  130. kolega_code/events.py +202 -0
  131. kolega_code/llm/client.py +300 -0
  132. kolega_code/llm/exceptions.py +285 -0
  133. kolega_code/llm/instrumented_client.py +520 -0
  134. kolega_code/llm/models.py +1368 -0
  135. kolega_code/llm/providers/__init__.py +0 -0
  136. kolega_code/llm/providers/anthropic.py +387 -0
  137. kolega_code/llm/providers/base.py +71 -0
  138. kolega_code/llm/providers/google.py +157 -0
  139. kolega_code/llm/providers/models.py +37 -0
  140. kolega_code/llm/providers/openai.py +363 -0
  141. kolega_code/llm/ratelimit.py +40 -0
  142. kolega_code/llm/specs.py +67 -0
  143. kolega_code/llm/tool_execution_ids.py +18 -0
  144. kolega_code/models/__init__.py +9 -0
  145. kolega_code/models/sandbox_terminal_state.py +47 -0
  146. kolega_code/runtime.py +50 -0
  147. kolega_code/sandbox/README.md +200 -0
  148. kolega_code/sandbox/__init__.py +21 -0
  149. kolega_code/sandbox/async_filesystem.py +475 -0
  150. kolega_code/sandbox/base.py +297 -0
  151. kolega_code/sandbox/browser.py +25 -0
  152. kolega_code/sandbox/event_loop.py +43 -0
  153. kolega_code/sandbox/filesystem.py +341 -0
  154. kolega_code/sandbox/local.py +118 -0
  155. kolega_code/sandbox/serializer.py +175 -0
  156. kolega_code/sandbox/terminal.py +868 -0
  157. kolega_code/sandbox/utils.py +216 -0
  158. kolega_code/services/base.py +255 -0
  159. kolega_code/services/browser.py +444 -0
  160. kolega_code/services/file_system.py +749 -0
  161. kolega_code/services/html.py +221 -0
  162. kolega_code/services/terminal.py +903 -0
  163. kolega_code/tools/__init__.py +22 -0
  164. kolega_code/tools/core.py +33 -0
  165. kolega_code/tools/definitions.py +81 -0
  166. kolega_code/tools/registry.py +73 -0
  167. kolega_code-0.1.0.dist-info/METADATA +157 -0
  168. kolega_code-0.1.0.dist-info/RECORD +171 -0
  169. kolega_code-0.1.0.dist-info/WHEEL +4 -0
  170. kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
  171. kolega_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,216 @@
1
+ """Utility functions for sandbox operations."""
2
+
3
+ import logging
4
+ import shlex
5
+ from typing import List, Optional, Dict, Any
6
+ from .base import SandboxManager, ProjectManifest
7
+ import yaml
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ WORKSPACE_PATH = "/home/user/workspace"
12
+
13
+
14
+ def parse_git_status_output(status_output: str) -> List[str]:
15
+ """Parse git status --porcelain output and return list of files to add.
16
+
17
+ This is a shared parsing function used by both direct E2B sandbox operations
18
+ and SandboxManager-based operations to ensure consistent git status parsing.
19
+
20
+ Args:
21
+ status_output: Raw output from 'git -c core.quotePath=false status --porcelain'
22
+
23
+ Returns:
24
+ List of file paths, excluding deleted files and handling renames correctly
25
+ """
26
+ logger.debug(f"Parsing git status output: {status_output!r}")
27
+
28
+ if not status_output or not status_output.strip():
29
+ return []
30
+
31
+ modified_files = []
32
+ for line in status_output.split("\n"):
33
+ if not line or len(line) < 3:
34
+ continue
35
+
36
+ # Git status format: XY filename
37
+ # X = staging area status, Y = working tree status
38
+ # Status codes (per git-status --porcelain):
39
+ # ' ' = unmodified
40
+ # M = modified
41
+ # A = added
42
+ # D = deleted
43
+ # R = renamed
44
+ # C = copied
45
+ # U = updated but unmerged
46
+ # ? = untracked
47
+ # ! = ignored
48
+ status = line[:2] # First 2 characters are status codes
49
+ file_path = line[3:].strip() # Everything after "XY "
50
+
51
+ # Skip STAGED deletions (D at position 0) - these are already removed from git index
52
+ # These will fail with "pathspec did not match any files" error
53
+ # Working tree deletions (" D" at position 1, like " D", "MD", "AD") can still be staged with git add, so we include them
54
+ if status[0] == "D":
55
+ logger.debug(f"Skipping staged deletion (not in git index): {file_path}")
56
+ continue
57
+
58
+ # Handle renamed files: "R old -> new"
59
+ # Git quotes filenames containing " -> ": R "old -> a.txt" -> "new -> b.txt"
60
+ if status.startswith("R"):
61
+ logger.debug(f"Processing renamed file - status: {status!r}, file_path: {file_path!r}")
62
+ if " -> " in file_path:
63
+ # Find the separator " -> " that's NOT inside quotes
64
+ in_quotes = False
65
+ for i in range(len(file_path) - 4): # -4 for " -> " (4 chars)
66
+ if file_path[i] == '"':
67
+ in_quotes = not in_quotes
68
+ elif not in_quotes and file_path[i:i+4] == ' -> ':
69
+ # Found the real separator between old and new filenames
70
+ file_path = file_path[i+4:].strip()
71
+ break
72
+
73
+ # Remove quotes if present (git quotes filenames with special chars or " -> ")
74
+ if file_path.startswith('"') and file_path.endswith('"'):
75
+ file_path = file_path[1:-1]
76
+ # Unescape git's quote escaping (\" becomes ")
77
+ file_path = file_path.replace('\\"', '"')
78
+
79
+ modified_files.append(file_path)
80
+
81
+ return modified_files
82
+
83
+
84
+ async def get_modified_files_from_sandbox(sandbox_manager: SandboxManager, sandbox_id: str) -> List[str]:
85
+ """
86
+ Get list of modified files from sandbox using git status.
87
+
88
+ Args:
89
+ sandbox_manager: The sandbox manager instance
90
+ sandbox_id: ID of the sandbox
91
+
92
+ Returns:
93
+ List of modified file paths
94
+ """
95
+ try:
96
+ # Get the sandbox's terminal manager
97
+ terminal_manager = sandbox_manager.get_terminal_manager(sandbox_id)
98
+
99
+ # Run git status with core.quotePath=false to get raw filenames without escaping
100
+ # This prevents issues with special characters (e.g., em-dash — being escaped as \342\200\224)
101
+ result = await terminal_manager.run_command("git -c core.quotePath=false status --porcelain", cwd=WORKSPACE_PATH)
102
+
103
+ # Use shared parsing function
104
+ return parse_git_status_output(result)
105
+
106
+ except Exception as e:
107
+ logger.error(f"Error getting modified files from sandbox {sandbox_id}: {e}")
108
+ return []
109
+
110
+
111
+ async def get_git_diff_from_sandbox(
112
+ sandbox_manager: SandboxManager, sandbox_id: str, files: Optional[List[str]] = None
113
+ ) -> str:
114
+ """
115
+ Get git diff from sandbox.
116
+
117
+ Args:
118
+ sandbox_manager: The sandbox manager instance
119
+ sandbox_id: ID of the sandbox
120
+ files: Optional list of specific files to diff
121
+
122
+ Returns:
123
+ Git diff output as string
124
+ """
125
+ try:
126
+ # Get the sandbox's terminal manager
127
+ terminal_manager = sandbox_manager.get_terminal_manager(sandbox_id)
128
+
129
+ # Include untracked files in the diff without staging their contents.
130
+ try:
131
+ await terminal_manager.run_command("git add -N .", cwd=WORKSPACE_PATH)
132
+ except Exception as add_error:
133
+ logger.warning(f"Failed to mark untracked files for diff in sandbox {sandbox_id}: {add_error}")
134
+
135
+ # Build git diff command
136
+ if files:
137
+ # Diff specific files
138
+ files_arg = " ".join(shlex.quote(f) for f in files)
139
+ cmd = f"git diff HEAD -- {files_arg}"
140
+ else:
141
+ # Diff all changes
142
+ cmd = "git diff HEAD"
143
+
144
+ # Run git diff
145
+ result = await terminal_manager.run_command(cmd, cwd=WORKSPACE_PATH)
146
+
147
+ return result or ""
148
+
149
+ except Exception as e:
150
+ logger.error(f"Error getting git diff from sandbox {sandbox_id}: {e}")
151
+ return ""
152
+
153
+
154
+ async def run_project_tests_in_sandbox(
155
+ sandbox_manager: SandboxManager, sandbox_id: str, config: Optional[dict] = None
156
+ ) -> dict:
157
+ """
158
+ Run project tests in sandbox based on manifest configuration.
159
+
160
+ Args:
161
+ sandbox_manager: The sandbox manager instance
162
+ sandbox_id: ID of the sandbox
163
+ config: Optional project configuration
164
+
165
+ Returns:
166
+ Dictionary with test results
167
+ """
168
+ try:
169
+ # Get the terminal manager
170
+ terminal_manager = sandbox_manager.get_terminal_manager(sandbox_id)
171
+
172
+ # Try to read project manifest
173
+ manifest_path = f"{WORKSPACE_PATH}/.kolega-manifest.yaml"
174
+
175
+ try:
176
+ filesystem = sandbox_manager.get_filesystem(sandbox_id)
177
+ manifest_content = await filesystem.read_text(manifest_path)
178
+ manifest_data = yaml.safe_load(manifest_content)
179
+ manifest = ProjectManifest(**manifest_data)
180
+ test_commands = manifest.test_commands or []
181
+ except Exception:
182
+ # Fallback to common test commands if no manifest
183
+ test_commands = ["npm test", "pytest", "python -m pytest", "cargo test", "go test ./...", "mvn test"]
184
+
185
+ if not test_commands:
186
+ return {"success": False, "error": "No test commands found in project configuration"}
187
+
188
+ # Run test commands
189
+ results = []
190
+ for cmd in test_commands:
191
+ try:
192
+ logger.info(f"Running test command: {cmd}")
193
+ result = await terminal_manager.run_command(
194
+ cmd, cwd=WORKSPACE_PATH, timeout=300 # 5 minute timeout for tests
195
+ )
196
+
197
+ # Determine if command succeeded (this is a simple check)
198
+ success = "error" not in result.lower() and "failed" not in result.lower()
199
+
200
+ results.append({"command": cmd, "success": success, "output": result})
201
+
202
+ if success:
203
+ # If one test command succeeds, we can stop
204
+ break
205
+
206
+ except Exception as e:
207
+ results.append({"command": cmd, "success": False, "error": str(e)})
208
+
209
+ # Determine overall success
210
+ overall_success = any(r.get("success", False) for r in results)
211
+
212
+ return {"success": overall_success, "results": results}
213
+
214
+ except Exception as e:
215
+ logger.error(f"Error running tests in sandbox {sandbox_id}: {e}")
216
+ return {"success": False, "error": str(e)}
@@ -0,0 +1,255 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict, List, Optional
3
+
4
+
5
+ class TerminalManager(ABC):
6
+ """
7
+ Abstract base class for terminal managers.
8
+ Implementations should handle creation, management, and cleanup of terminal instances.
9
+ """
10
+
11
+ @abstractmethod
12
+ async def launch_terminal(self, terminal_id: Optional[str] = None, **terminal_kwargs) -> str:
13
+ """
14
+ Create a new terminal instance with the given ID or generate a random ID.
15
+
16
+ Args:
17
+ terminal_id: Optional identifier for the terminal (random UUID if not provided)
18
+ **terminal_kwargs: Arguments to pass to terminal constructor
19
+
20
+ Returns:
21
+ The ID of the created terminal
22
+ """
23
+
24
+ @abstractmethod
25
+ async def send_command(
26
+ self, term_id: str, command: str, purpose: Optional[str] = None, timeout: Optional[int] = None
27
+ ) -> bool:
28
+ """
29
+ Send a command to a specific terminal.
30
+
31
+ Args:
32
+ term_id: ID of the terminal to send command to
33
+ command: The command to execute
34
+ purpose: Optional description of command purpose
35
+ timeout: Optional timeout in seconds (0 or None for no timeout)
36
+
37
+ Returns:
38
+ True if command was sent successfully
39
+
40
+ Raises:
41
+ KeyError: If terminal_id doesn't exist
42
+ """
43
+
44
+ @abstractmethod
45
+ async def send_input(
46
+ self, term_id: str, text: str, submit: bool = True, command_id: Optional[str] = None
47
+ ) -> bool:
48
+ """
49
+ Send input to a command that is already running in a terminal.
50
+
51
+ Args:
52
+ term_id: ID of the terminal to send input to
53
+ text: Input text to send
54
+ submit: Whether to append a newline before sending
55
+ command_id: Optional active command ID when the backend requires disambiguation
56
+
57
+ Returns:
58
+ True if input was sent successfully
59
+
60
+ Raises:
61
+ KeyError: If terminal_id doesn't exist
62
+ ValueError: If no running command can receive input
63
+ """
64
+
65
+ @abstractmethod
66
+ async def get_output(self, terminal_id: str, **kwargs) -> str:
67
+ """
68
+ Get output from a specific terminal.
69
+
70
+ Args:
71
+ terminal_id: ID of the terminal to get output from
72
+ **kwargs: Arguments to pass to get output method
73
+
74
+ Returns:
75
+ Output from the specified terminal
76
+
77
+ Raises:
78
+ KeyError: If terminal_id doesn't exist
79
+ """
80
+
81
+ @abstractmethod
82
+ async def close_terminal(self, terminal_id: str) -> None:
83
+ """
84
+ Close a specific terminal.
85
+
86
+ Args:
87
+ terminal_id: ID of the terminal to close
88
+
89
+ Raises:
90
+ KeyError: If terminal_id doesn't exist
91
+ """
92
+
93
+ @abstractmethod
94
+ async def close_all(self) -> None:
95
+ """Close all terminal instances."""
96
+
97
+ @abstractmethod
98
+ async def list_terminals(self) -> Dict[str, Any]:
99
+ """
100
+ Get information about all terminals.
101
+
102
+ Returns:
103
+ Dictionary mapping terminal IDs to terminal information
104
+ """
105
+
106
+ @abstractmethod
107
+ async def run_command(self, command: str, cwd: Optional[str] = None, timeout: Optional[int] = None) -> str:
108
+ """
109
+ Run a command directly (convenience method for utilities).
110
+
111
+ Args:
112
+ command: Command to execute
113
+ cwd: Optional working directory
114
+ timeout: Optional timeout in seconds
115
+
116
+ Returns:
117
+ Command output as string
118
+ """
119
+
120
+
121
+ class BrowserManager(ABC):
122
+ """
123
+ Abstract base class for browser management.
124
+ Defines the interface for browser operations.
125
+ """
126
+
127
+ @abstractmethod
128
+ async def launch_browser(self, url: str) -> str:
129
+ """
130
+ Launch a new browser instance and navigate to the specified URL.
131
+
132
+ Args:
133
+ url: The URL to navigate to
134
+
135
+ Returns:
136
+ Browser ID string
137
+
138
+ Raises:
139
+ Exception: If browser launch fails
140
+ """
141
+
142
+ @abstractmethod
143
+ async def list_browsers(self) -> dict:
144
+ """
145
+ Get information about all active browser instances.
146
+
147
+ Returns:
148
+ Dictionary mapping browser IDs to browser information
149
+ """
150
+
151
+ @abstractmethod
152
+ async def get_browser_console_logs(
153
+ self,
154
+ browser_id: str,
155
+ max_logs: int = 50,
156
+ log_types: Optional[List[str]] = None,
157
+ minutes_back: Optional[int] = None,
158
+ max_chars: Optional[int] = 8000,
159
+ ) -> dict:
160
+ """
161
+ Get console logs from a specific browser with configurable filtering.
162
+
163
+ Args:
164
+ browser_id: ID of the browser to get logs from
165
+ max_logs: Maximum number of logs to return (most recent)
166
+ log_types: List of log types to include (e.g., ['error', 'warning', 'assert'])
167
+ minutes_back: Only return logs from the last N minutes
168
+ max_chars: Maximum total character count for all log messages combined
169
+
170
+ Returns:
171
+ Dictionary containing filtered console logs and metadata
172
+
173
+ Raises:
174
+ KeyError: If browser_id doesn't exist
175
+ """
176
+
177
+ @abstractmethod
178
+ async def get_browser_interactive_elements(self, browser_id: str) -> list:
179
+ """
180
+ Get interactive elements from a specific browser.
181
+
182
+ Args:
183
+ browser_id: ID of the browser to get elements from
184
+
185
+ Returns:
186
+ Dictionary containing current URL, title, and interactive elements
187
+
188
+ Raises:
189
+ KeyError: If browser_id doesn't exist
190
+ """
191
+
192
+ @abstractmethod
193
+ async def take_browser_screenshot(self, browser_id: str) -> dict:
194
+ """
195
+ Take a screenshot of a specific browser.
196
+
197
+ Args:
198
+ browser_id: ID of the browser to take screenshot of
199
+
200
+ Returns:
201
+ Dictionary containing current URL, title, and base64-encoded screenshot
202
+
203
+ Raises:
204
+ KeyError: If browser_id doesn't exist
205
+ """
206
+
207
+ @abstractmethod
208
+ async def interact_with_browser(self, browser_id: str, action: str, selector: str, text: str, scroll_px) -> dict:
209
+ """
210
+ Interact with a specific browser.
211
+
212
+ Args:
213
+ browser_id: ID of the browser to interact with
214
+ action: Type of interaction (click, type, scroll, navigate)
215
+ selector: CSS selector for the element to interact with
216
+ text: Text to type or URL to navigate to
217
+ scroll_px: Number of pixels to scroll
218
+
219
+ Returns:
220
+ Dictionary containing status and interaction details
221
+
222
+ Raises:
223
+ KeyError: If browser_id doesn't exist
224
+ ValueError: If action is unknown
225
+ """
226
+
227
+ @abstractmethod
228
+ async def set_select_value(self, browser_id: str, selector: str, value: str) -> dict:
229
+ """
230
+ Set the value of a select box in a specific browser.
231
+
232
+ Args:
233
+ browser_id: ID of the browser to interact with
234
+ selector: CSS selector for the select element
235
+ value: The value to set for the select option
236
+
237
+ Returns:
238
+ Dictionary containing status, current URL, and selected value
239
+
240
+ Raises:
241
+ KeyError: If browser_id doesn't exist
242
+ ValueError: If the element is not a select box or value doesn't exist
243
+ """
244
+
245
+ @abstractmethod
246
+ async def close_browser(self, browser_id: str) -> None:
247
+ """
248
+ Close a specific browser.
249
+
250
+ Args:
251
+ browser_id: ID of the browser to close
252
+
253
+ Raises:
254
+ KeyError: If browser_id doesn't exist
255
+ """