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,271 @@
1
+ """
2
+ Unit tests for the PromptProvider class.
3
+ """
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from kolega_code.agent.prompt_provider import (
10
+ AgentMode,
11
+ AgentType,
12
+ MissingPromptTemplateError,
13
+ PromptContext,
14
+ PromptExtension,
15
+ PromptProvider,
16
+ )
17
+
18
+
19
+ class TestPromptProvider:
20
+ """Test suite for PromptProvider functionality."""
21
+
22
+ @pytest.fixture
23
+ def prompt_provider(self):
24
+ """Create a PromptProvider instance."""
25
+ return PromptProvider()
26
+
27
+ @pytest.fixture
28
+ def prompt_context(self):
29
+ """Create a test prompt context."""
30
+ return PromptContext(
31
+ system_name="Kolega Code",
32
+ project_path="/test/project",
33
+ is_git_repo=True,
34
+ platform="Darwin",
35
+ date_today="2024-01-15",
36
+ model_name="claude-3-5-sonnet",
37
+ available_ports="9001-9999",
38
+ kolega_md="Test project documentation",
39
+ workspace_id="test-workspace-123",
40
+ )
41
+
42
+ def test_coder_agent_cli_mode(self, prompt_provider, prompt_context):
43
+ """Test coder agent with public CLI mode."""
44
+ prompt = prompt_provider.get_system_prompt(
45
+ agent_type=AgentType.CODER, mode=AgentMode.CLI, context=prompt_context
46
+ )
47
+
48
+ assert prompt is not None
49
+ assert "local developer CLI" in prompt
50
+ assert "Kolega Code" in prompt
51
+ assert "/test/project" in prompt
52
+ assert "Test project documentation" in prompt
53
+ assert len(prompt) > 0
54
+
55
+ @pytest.mark.parametrize("mode", [AgentMode.CODE, AgentMode.VIBE, AgentMode.FIX])
56
+ def test_hosted_coder_modes_require_host_template(self, prompt_provider, prompt_context, mode):
57
+ """Hosted coder modes are private and require host-owned prompt templates."""
58
+ with pytest.raises(MissingPromptTemplateError) as exc_info:
59
+ prompt_provider.get_system_prompt(agent_type=AgentType.CODER, mode=mode, context=prompt_context)
60
+
61
+ assert mode.value in str(exc_info.value)
62
+ assert "host-owned template_dirs" in str(exc_info.value)
63
+
64
+ def test_cli_mode_prompt_omits_platform_memory_bank_instructions(self, prompt_provider, prompt_context):
65
+ """CLI mode should not include platform memory-bank or hosted-agent instructions."""
66
+ prompt = prompt_provider.get_system_prompt(
67
+ agent_type=AgentType.CODER, mode=AgentMode.CLI, context=prompt_context
68
+ )
69
+ prompt_lower = prompt.lower()
70
+
71
+ assert "memory bank" not in prompt_lower
72
+ assert "kolega-memory-bank" not in prompt
73
+ assert "dispatch_investigation_agent" not in prompt
74
+ assert "Test Task Detection" not in prompt
75
+ assert "Scope Boundary Management" not in prompt
76
+
77
+ def test_investigation_agent_prompt_generation(self, prompt_provider, prompt_context):
78
+ """Test that investigation agent prompts can be generated."""
79
+ prompt = prompt_provider.get_system_prompt(agent_type=AgentType.INVESTIGATION, context=prompt_context)
80
+
81
+ assert prompt is not None
82
+ assert len(prompt) > 0
83
+ assert "code investigation agent" in prompt
84
+ assert "explaining a codebase" in prompt
85
+ assert "/test/project" in prompt
86
+
87
+ def test_browser_agent_prompt_generation(self, prompt_provider, prompt_context):
88
+ """Test that browser agent prompts can be generated."""
89
+ prompt = prompt_provider.get_system_prompt(agent_type=AgentType.BROWSER, context=prompt_context)
90
+
91
+ assert prompt is not None
92
+ assert len(prompt) > 0
93
+ assert "web browser agent" in prompt
94
+ assert "QA on a web application" in prompt
95
+ assert "URL Navigation Guidelines" in prompt
96
+
97
+ def test_cli_prompt_with_matching_prompt_extension(self, prompt_provider, prompt_context):
98
+ """CLI mode should render prompt extensions that target CLI mode."""
99
+ prompt = prompt_provider.get_system_prompt(
100
+ agent_type=AgentType.CODER,
101
+ mode=AgentMode.CLI,
102
+ prompt_extensions=[
103
+ PromptExtension(
104
+ id="cli-example",
105
+ title="CLI Extension",
106
+ markdown="Extra CLI context.",
107
+ agent_types=[AgentType.CODER],
108
+ modes=[AgentMode.CLI],
109
+ )
110
+ ],
111
+ context=prompt_context,
112
+ )
113
+
114
+ assert prompt is not None
115
+ assert "CLI Extension" in prompt
116
+ assert "Extra CLI context." in prompt
117
+
118
+ def test_prompt_filters_non_matching_prompt_extension(self, prompt_provider, prompt_context):
119
+ """Prompt extensions should only render for matching agent types and modes."""
120
+ prompt = prompt_provider.get_system_prompt(
121
+ agent_type=AgentType.CODER,
122
+ mode=AgentMode.CLI,
123
+ prompt_extensions=[
124
+ PromptExtension(
125
+ id="browser-only",
126
+ title="Browser Only",
127
+ markdown="This should not render.",
128
+ agent_types=[AgentType.BROWSER],
129
+ )
130
+ ],
131
+ context=prompt_context,
132
+ )
133
+
134
+ assert prompt is not None
135
+ assert "Browser Only" not in prompt
136
+ assert "This should not render." not in prompt
137
+
138
+ def test_cli_mode_prompt_includes_workspace_environment_variables(self, prompt_provider, prompt_context):
139
+ """Coder CLI mode should list workspace environment variable descriptions when provided."""
140
+ prompt_context.workspace_environment_variables = {
141
+ "STRIPE_API_KEY": "Stripe API key for billing",
142
+ }
143
+
144
+ prompt = prompt_provider.get_system_prompt(
145
+ agent_type=AgentType.CODER, mode=AgentMode.CLI, context=prompt_context
146
+ )
147
+
148
+ assert "STRIPE_API_KEY" in prompt
149
+ assert "Stripe API key for billing" in prompt
150
+
151
+ def test_host_template_dir_supplies_hosted_mode_prompt(self, tmp_path, prompt_context):
152
+ """Host template dirs can provide private hosted-mode prompts."""
153
+ agents_dir = tmp_path / "agents"
154
+ agents_dir.mkdir()
155
+ (agents_dir / "coder_code_mode.j2").write_text(
156
+ "Private {{ context.system_name }} prompt for {{ mode }} at {{ context.project_path }}",
157
+ encoding="utf-8",
158
+ )
159
+
160
+ prompt = PromptProvider(template_dirs=[tmp_path]).get_system_prompt(
161
+ agent_type=AgentType.CODER,
162
+ mode=AgentMode.CODE,
163
+ context=prompt_context,
164
+ )
165
+
166
+ assert prompt == "Private Kolega Code prompt for code at /test/project"
167
+
168
+ def test_host_template_dir_can_use_builtin_includes(self, tmp_path, prompt_context):
169
+ """Private templates can still include bundled generic snippets."""
170
+ agents_dir = tmp_path / "agents"
171
+ agents_dir.mkdir()
172
+ (agents_dir / "coder_vibe_mode.j2").write_text(
173
+ "{% include 'environment_variables/workspace_env_vars.md' %}",
174
+ encoding="utf-8",
175
+ )
176
+ prompt_context.workspace_environment_variables = {"PAYMENTS_REGION": "Region for payment processor"}
177
+
178
+ prompt = PromptProvider(template_dirs=[tmp_path]).get_system_prompt(
179
+ agent_type=AgentType.CODER,
180
+ mode=AgentMode.VIBE,
181
+ context=prompt_context,
182
+ )
183
+
184
+ assert "PAYMENTS_REGION" in prompt
185
+ assert "Region for payment processor" in prompt
186
+
187
+ def test_hosted_prompt_with_matching_prompt_extension(self, tmp_path, prompt_context):
188
+ """Private hosted prompts still receive filtered prompt extensions."""
189
+ agents_dir = tmp_path / "agents"
190
+ agents_dir.mkdir()
191
+ (agents_dir / "coder_fix_mode.j2").write_text(
192
+ "{% for extension in prompt_extensions %}{{ extension.title }}: {{ extension.markdown }}{% endfor %}",
193
+ encoding="utf-8",
194
+ )
195
+
196
+ prompt = PromptProvider(template_dirs=[tmp_path]).get_system_prompt(
197
+ agent_type=AgentType.CODER,
198
+ mode=AgentMode.FIX,
199
+ prompt_extensions=[
200
+ PromptExtension(
201
+ id="fix-example",
202
+ title="Fix Extension",
203
+ markdown="Extra fix context.",
204
+ agent_types=[AgentType.CODER],
205
+ modes=[AgentMode.FIX],
206
+ )
207
+ ],
208
+ context=prompt_context,
209
+ )
210
+
211
+ assert "Fix Extension: Extra fix context." in prompt
212
+
213
+ def test_prompt_with_template_slug_in_cli_mode(self, prompt_provider, prompt_context):
214
+ """Template guidance is still available to public CLI mode."""
215
+ prompt = prompt_provider.get_system_prompt(
216
+ agent_type=AgentType.CODER,
217
+ mode=AgentMode.CLI,
218
+ template_slug="mern-stack-template",
219
+ context=prompt_context,
220
+ )
221
+
222
+ assert prompt is not None
223
+ assert "Project Starter Template" in prompt
224
+
225
+ def test_minimal_context(self, prompt_provider):
226
+ """Test prompt generation with minimal context."""
227
+ prompt = prompt_provider.get_system_prompt(agent_type=AgentType.CODER, mode=AgentMode.CLI)
228
+
229
+ assert prompt is not None
230
+ assert "Kolega Code" in prompt
231
+
232
+ def test_templates_can_be_loaded(self, prompt_provider):
233
+ """Test that public templates load successfully from the template directory."""
234
+ prompt = prompt_provider.get_system_prompt(agent_type=AgentType.CODER, mode=AgentMode.CLI)
235
+ assert prompt is not None
236
+ assert len(prompt) > 0
237
+
238
+ def test_all_public_agent_types(self, prompt_provider, prompt_context):
239
+ """Test that all public agent types can generate prompts."""
240
+ for agent_type in AgentType:
241
+ mode = AgentMode.CLI if agent_type == AgentType.CODER else None
242
+ prompt = prompt_provider.get_system_prompt(agent_type=agent_type, mode=mode, context=prompt_context)
243
+
244
+ assert prompt is not None
245
+ assert len(prompt) > 0
246
+ if agent_type == AgentType.CODER:
247
+ assert "powerful AI coding assistant" in prompt
248
+ elif agent_type == AgentType.INVESTIGATION:
249
+ assert "code investigation agent" in prompt
250
+ elif agent_type == AgentType.BROWSER:
251
+ assert "web browser agent" in prompt
252
+
253
+
254
+ def test_public_package_does_not_contain_private_hosted_prompt_markers():
255
+ package_root = Path(__file__).parents[2]
256
+ forbidden_markers = [
257
+ "app.kolega.studio/kolega-error-reporter.js",
258
+ "kolega-memory-bank",
259
+ "Test Task Detection",
260
+ "Scope Boundary Management",
261
+ "You are operating in **fix mode**",
262
+ ]
263
+
264
+ for path in package_root.rglob("*"):
265
+ if not path.is_file() or path.suffix not in {".py", ".j2", ".md"}:
266
+ continue
267
+ if "__pycache__" in path.parts or path.name == "test_prompt_provider.py":
268
+ continue
269
+ text = path.read_text(encoding="utf-8")
270
+ for marker in forbidden_markers:
271
+ assert marker not in text, f"{marker!r} leaked into {path}"
@@ -0,0 +1,102 @@
1
+ """Tests for the first-class tool primitives (Tool, ToolRegistry, ToolPolicy)."""
2
+
3
+ import pytest
4
+
5
+ from kolega_code.llm.models import ToolDefinition
6
+ from kolega_code.tools import Tool, ToolPolicy, ToolRegistry
7
+
8
+
9
+ def make_tool(name: str, *, parallel_safe: bool = False, result: str = "ok") -> Tool:
10
+ async def handler(**inputs):
11
+ return f"{result}:{inputs.get('arg', '')}"
12
+
13
+ return Tool(
14
+ name=name,
15
+ definition=ToolDefinition(name=name, description=f"{name} tool", parameters=[]),
16
+ handler=handler,
17
+ parallel_safe=parallel_safe,
18
+ )
19
+
20
+
21
+ class TestToolRegistry:
22
+ def test_add_and_lookup(self):
23
+ registry = ToolRegistry().add(make_tool("read"), make_tool("write"))
24
+ assert "read" in registry
25
+ assert "missing" not in registry
26
+ assert registry.get("write").name == "write"
27
+ assert registry.names() == ["read", "write"]
28
+
29
+ def test_duplicate_registration_rejected(self):
30
+ registry = ToolRegistry().add(make_tool("read"))
31
+ with pytest.raises(ValueError, match="already registered"):
32
+ registry.add(make_tool("read"))
33
+
34
+ @pytest.mark.asyncio
35
+ async def test_call_dispatches_by_name(self):
36
+ registry = ToolRegistry().add(make_tool("read", result="contents"))
37
+ assert await registry.call("read", arg="x") == "contents:x"
38
+
39
+ @pytest.mark.asyncio
40
+ async def test_call_unknown_tool_raises(self):
41
+ with pytest.raises(KeyError):
42
+ await ToolRegistry().call("nope")
43
+
44
+ def test_select_applies_policy(self):
45
+ registry = ToolRegistry().add(make_tool("read"), make_tool("write"), make_tool("delete"))
46
+
47
+ excluded = registry.select(ToolPolicy(exclude=frozenset({"delete"})))
48
+ assert excluded.names() == ["read", "write"]
49
+
50
+ allowlisted = registry.select(ToolPolicy(include=frozenset({"write"})))
51
+ assert allowlisted.names() == ["write"]
52
+
53
+ def test_definitions_put_cache_checkpoint_on_last_only(self):
54
+ registry = ToolRegistry().add(make_tool("a"), make_tool("b"), make_tool("c"))
55
+
56
+ definitions = registry.definitions()
57
+ assert [d.cache_checkpoint for d in definitions] == [False, False, True]
58
+
59
+ # A subset view moves the checkpoint to its own last definition
60
+ subset = registry.select(ToolPolicy(exclude=frozenset({"c"})))
61
+ subset_definitions = subset.definitions()
62
+ assert [d.name for d in subset_definitions] == ["a", "b"]
63
+ assert [d.cache_checkpoint for d in subset_definitions] == [False, True]
64
+
65
+
66
+ class TestToolCollectionRegistry:
67
+ def test_registry_marks_parallel_safety_from_groups(self, tmp_path):
68
+ from unittest.mock import Mock
69
+
70
+ from kolega_code.agent.tools import ToolCollection
71
+
72
+ collection = ToolCollection(
73
+ tmp_path,
74
+ "ws",
75
+ "thread",
76
+ Mock(),
77
+ Mock(),
78
+ Mock(agent_name="test"),
79
+ )
80
+ registry = collection.registry()
81
+
82
+ assert registry.get("read_entire_file").parallel_safe
83
+ assert registry.get("search_codebase").parallel_safe
84
+ assert not registry.get("create_file").parallel_safe
85
+ assert not registry.get("run_command_tracked").parallel_safe
86
+
87
+ def test_get_tool_list_matches_registry_definitions(self, tmp_path):
88
+ from unittest.mock import Mock
89
+
90
+ from kolega_code.agent.tools import ToolCollection
91
+
92
+ collection = ToolCollection(
93
+ tmp_path,
94
+ "ws",
95
+ "thread",
96
+ Mock(),
97
+ Mock(),
98
+ Mock(agent_name="test"),
99
+ )
100
+ names_from_list = [d.name for d in collection.get_tool_list()]
101
+ registry_names = [tool.name for tool in collection.registry()]
102
+ assert names_from_list == registry_names