superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,515 @@
1
+ """
2
+ Enhanced Tool Framework - Production-Ready Tool System.
3
+
4
+ Builds upon the base tool system with:
5
+ - Tool result caching
6
+ - Retry logic with backoff
7
+ - Tool metrics and timing
8
+ - Tool validation
9
+ - Async-first design
10
+
11
+ Provides a robust foundation for tool execution.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import hashlib
18
+ import time
19
+ from abc import ABC, abstractmethod
20
+ from dataclasses import dataclass, field
21
+ from datetime import datetime, timedelta
22
+ from enum import Enum
23
+ from functools import wraps
24
+ from pathlib import Path
25
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar, Union
26
+ import json
27
+
28
+ from .base import Tool, ToolContext, ToolResult
29
+
30
+
31
+ T = TypeVar("T")
32
+
33
+
34
+ class ToolCategory(Enum):
35
+ """Categories of tools."""
36
+
37
+ FILE = "file" # File operations
38
+ SHELL = "shell" # Shell commands
39
+ SEARCH = "search" # Search operations
40
+ EDIT = "edit" # Code editing
41
+ NETWORK = "network" # Network requests
42
+ ANALYSIS = "analysis" # Code analysis
43
+ TEST = "test" # Testing tools
44
+ OTHER = "other"
45
+
46
+
47
+ @dataclass
48
+ class ToolMetrics:
49
+ """Metrics for tool execution."""
50
+
51
+ call_count: int = 0
52
+ success_count: int = 0
53
+ error_count: int = 0
54
+ total_duration_ms: float = 0.0
55
+ last_called: Optional[datetime] = None
56
+
57
+ @property
58
+ def avg_duration_ms(self) -> float:
59
+ """Average execution duration."""
60
+ if self.call_count == 0:
61
+ return 0.0
62
+ return self.total_duration_ms / self.call_count
63
+
64
+ @property
65
+ def success_rate(self) -> float:
66
+ """Success rate as percentage."""
67
+ if self.call_count == 0:
68
+ return 100.0
69
+ return (self.success_count / self.call_count) * 100
70
+
71
+
72
+ @dataclass
73
+ class CacheEntry:
74
+ """A cached tool result."""
75
+
76
+ result: ToolResult
77
+ created_at: datetime = field(default_factory=datetime.now)
78
+ hits: int = 0
79
+
80
+ def is_expired(self, ttl_seconds: float) -> bool:
81
+ """Check if cache entry is expired."""
82
+ age = (datetime.now() - self.created_at).total_seconds()
83
+ return age > ttl_seconds
84
+
85
+
86
+ class ToolCache:
87
+ """
88
+ Cache for tool results.
89
+
90
+ Caches results based on tool name and arguments hash.
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ max_size: int = 1000,
96
+ default_ttl: float = 300.0, # 5 minutes
97
+ ):
98
+ self.max_size = max_size
99
+ self.default_ttl = default_ttl
100
+ self._cache: Dict[str, CacheEntry] = {}
101
+ self._tool_ttls: Dict[str, float] = {}
102
+
103
+ def _make_key(self, tool_name: str, args: Dict[str, Any]) -> str:
104
+ """Create cache key from tool name and arguments."""
105
+ args_json = json.dumps(args, sort_keys=True, default=str)
106
+ args_hash = hashlib.md5(args_json.encode()).hexdigest()
107
+ return f"{tool_name}:{args_hash}"
108
+
109
+ def set_tool_ttl(self, tool_name: str, ttl: float) -> None:
110
+ """Set TTL for a specific tool."""
111
+ self._tool_ttls[tool_name] = ttl
112
+
113
+ def get(self, tool_name: str, args: Dict[str, Any]) -> Optional[ToolResult]:
114
+ """Get cached result."""
115
+ key = self._make_key(tool_name, args)
116
+ entry = self._cache.get(key)
117
+
118
+ if entry is None:
119
+ return None
120
+
121
+ ttl = self._tool_ttls.get(tool_name, self.default_ttl)
122
+ if entry.is_expired(ttl):
123
+ del self._cache[key]
124
+ return None
125
+
126
+ entry.hits += 1
127
+ return entry.result
128
+
129
+ def set(self, tool_name: str, args: Dict[str, Any], result: ToolResult) -> None:
130
+ """Cache a result."""
131
+ # Enforce size limit
132
+ if len(self._cache) >= self.max_size:
133
+ # Remove oldest entries
134
+ sorted_keys = sorted(
135
+ self._cache.keys(),
136
+ key=lambda k: self._cache[k].created_at,
137
+ )
138
+ for key in sorted_keys[: self.max_size // 4]:
139
+ del self._cache[key]
140
+
141
+ key = self._make_key(tool_name, args)
142
+ self._cache[key] = CacheEntry(result=result)
143
+
144
+ def invalidate(self, tool_name: str) -> int:
145
+ """Invalidate all cache entries for a tool."""
146
+ prefix = f"{tool_name}:"
147
+ keys_to_remove = [k for k in self._cache if k.startswith(prefix)]
148
+ for key in keys_to_remove:
149
+ del self._cache[key]
150
+ return len(keys_to_remove)
151
+
152
+ def clear(self) -> None:
153
+ """Clear all cache entries."""
154
+ self._cache.clear()
155
+
156
+ def stats(self) -> Dict[str, Any]:
157
+ """Get cache statistics."""
158
+ total_hits = sum(e.hits for e in self._cache.values())
159
+ return {
160
+ "size": len(self._cache),
161
+ "max_size": self.max_size,
162
+ "total_hits": total_hits,
163
+ }
164
+
165
+
166
+ class EnhancedTool(Tool):
167
+ """
168
+ Enhanced base class for tools.
169
+
170
+ Adds caching, metrics, and retry support to the base Tool class.
171
+ """
172
+
173
+ # Override in subclasses
174
+ category: ToolCategory = ToolCategory.OTHER
175
+ cacheable: bool = False
176
+ cache_ttl: float = 300.0 # 5 minutes
177
+ max_retries: int = 0
178
+ retry_delay: float = 1.0
179
+
180
+ def __init__(self):
181
+ self._metrics = ToolMetrics()
182
+
183
+ @property
184
+ def metrics(self) -> ToolMetrics:
185
+ """Get tool metrics."""
186
+ return self._metrics
187
+
188
+ async def execute_with_cache(
189
+ self,
190
+ args: Dict[str, Any],
191
+ ctx: ToolContext,
192
+ cache: Optional[ToolCache] = None,
193
+ ) -> ToolResult:
194
+ """Execute tool with caching support."""
195
+ # Check cache
196
+ if self.cacheable and cache:
197
+ cached = cache.get(self.name, args)
198
+ if cached:
199
+ return cached
200
+
201
+ # Execute
202
+ result = await self.execute(args, ctx)
203
+
204
+ # Cache result if successful
205
+ if self.cacheable and cache and result.success:
206
+ cache.set(self.name, args, result)
207
+
208
+ return result
209
+
210
+ async def execute_with_retry(
211
+ self,
212
+ args: Dict[str, Any],
213
+ ctx: ToolContext,
214
+ ) -> ToolResult:
215
+ """Execute tool with retry logic."""
216
+ last_error = None
217
+
218
+ for attempt in range(self.max_retries + 1):
219
+ try:
220
+ result = await self.execute(args, ctx)
221
+ if result.success:
222
+ return result
223
+ last_error = result.error
224
+ except Exception as e:
225
+ last_error = str(e)
226
+
227
+ if attempt < self.max_retries:
228
+ delay = self.retry_delay * (2**attempt) # Exponential backoff
229
+ await asyncio.sleep(delay)
230
+
231
+ return ToolResult(
232
+ success=False,
233
+ output="",
234
+ error=f"Failed after {self.max_retries + 1} attempts: {last_error}",
235
+ )
236
+
237
+ async def execute_with_metrics(
238
+ self,
239
+ args: Dict[str, Any],
240
+ ctx: ToolContext,
241
+ ) -> ToolResult:
242
+ """Execute tool and track metrics."""
243
+ start_time = time.monotonic()
244
+
245
+ self._metrics.call_count += 1
246
+ self._metrics.last_called = datetime.now()
247
+
248
+ try:
249
+ result = await self.execute(args, ctx)
250
+
251
+ if result.success:
252
+ self._metrics.success_count += 1
253
+ else:
254
+ self._metrics.error_count += 1
255
+
256
+ return result
257
+
258
+ except Exception as e:
259
+ self._metrics.error_count += 1
260
+ raise
261
+
262
+ finally:
263
+ duration_ms = (time.monotonic() - start_time) * 1000
264
+ self._metrics.total_duration_ms += duration_ms
265
+
266
+ def validate_args(self, args: Dict[str, Any]) -> List[str]:
267
+ """Validate tool arguments.
268
+
269
+ Returns list of validation errors (empty if valid).
270
+ Override in subclasses for custom validation.
271
+ """
272
+ errors = []
273
+
274
+ # Check required parameters
275
+ params = self.parameters
276
+ required = params.get("required", [])
277
+ properties = params.get("properties", {})
278
+
279
+ for param_name in required:
280
+ if param_name not in args:
281
+ errors.append(f"Missing required parameter: {param_name}")
282
+
283
+ # Type checking
284
+ for param_name, value in args.items():
285
+ if param_name in properties:
286
+ param_schema = properties[param_name]
287
+ expected_type = param_schema.get("type")
288
+
289
+ if expected_type == "string" and not isinstance(value, str):
290
+ errors.append(f"Parameter '{param_name}' must be a string")
291
+ elif expected_type == "integer" and not isinstance(value, int):
292
+ errors.append(f"Parameter '{param_name}' must be an integer")
293
+ elif expected_type == "boolean" and not isinstance(value, bool):
294
+ errors.append(f"Parameter '{param_name}' must be a boolean")
295
+ elif expected_type == "array" and not isinstance(value, list):
296
+ errors.append(f"Parameter '{param_name}' must be an array")
297
+
298
+ return errors
299
+
300
+ def to_openai_format_extended(self) -> Dict[str, Any]:
301
+ """Extended OpenAI format with metadata."""
302
+ base = self.to_openai_format()
303
+ base["metadata"] = {
304
+ "category": self.category.value,
305
+ "cacheable": self.cacheable,
306
+ "max_retries": self.max_retries,
307
+ }
308
+ return base
309
+
310
+
311
+ class EnhancedToolRegistry:
312
+ """
313
+ Enhanced tool registry with additional features.
314
+
315
+ Provides caching, metrics tracking, and tool management.
316
+ """
317
+
318
+ def __init__(
319
+ self,
320
+ enable_cache: bool = True,
321
+ cache_config: Optional[Dict[str, Any]] = None,
322
+ ):
323
+ self._tools: Dict[str, Tool] = {}
324
+ self._cache = ToolCache(**(cache_config or {})) if enable_cache else None
325
+ self._execution_order: List[str] = [] # Track execution order
326
+
327
+ def register(self, tool: Tool) -> None:
328
+ """Register a tool."""
329
+ self._tools[tool.name] = tool
330
+
331
+ def unregister(self, name: str) -> bool:
332
+ """Unregister a tool."""
333
+ if name in self._tools:
334
+ del self._tools[name]
335
+ return True
336
+ return False
337
+
338
+ def get(self, name: str) -> Optional[Tool]:
339
+ """Get a tool by name."""
340
+ return self._tools.get(name)
341
+
342
+ def list(self) -> List[Tool]:
343
+ """List all tools."""
344
+ return list(self._tools.values())
345
+
346
+ def list_by_category(self, category: ToolCategory) -> List[Tool]:
347
+ """List tools by category."""
348
+ return [
349
+ t
350
+ for t in self._tools.values()
351
+ if isinstance(t, EnhancedTool) and t.category == category
352
+ ]
353
+
354
+ async def execute(
355
+ self,
356
+ name: str,
357
+ args: Dict[str, Any],
358
+ ctx: ToolContext,
359
+ use_cache: bool = True,
360
+ use_retry: bool = True,
361
+ track_metrics: bool = True,
362
+ ) -> ToolResult:
363
+ """Execute a tool with all enhancements."""
364
+ tool = self.get(name)
365
+
366
+ if not tool:
367
+ return ToolResult(
368
+ success=False,
369
+ output="",
370
+ error=f"Tool not found: {name}",
371
+ )
372
+
373
+ # Track execution order
374
+ self._execution_order.append(name)
375
+
376
+ # Validate arguments
377
+ if isinstance(tool, EnhancedTool):
378
+ errors = tool.validate_args(args)
379
+ if errors:
380
+ return ToolResult(
381
+ success=False,
382
+ output="",
383
+ error=f"Validation errors: {'; '.join(errors)}",
384
+ )
385
+
386
+ # Execute with enhancements
387
+ if isinstance(tool, EnhancedTool):
388
+ if track_metrics:
389
+ if use_cache and self._cache:
390
+ result = await tool.execute_with_cache(args, ctx, self._cache)
391
+ elif use_retry and tool.max_retries > 0:
392
+ result = await tool.execute_with_retry(args, ctx)
393
+ else:
394
+ result = await tool.execute_with_metrics(args, ctx)
395
+ else:
396
+ result = await tool.execute(args, ctx)
397
+ else:
398
+ result = await tool.execute(args, ctx)
399
+
400
+ return result
401
+
402
+ def get_metrics(self) -> Dict[str, ToolMetrics]:
403
+ """Get metrics for all enhanced tools."""
404
+ return {
405
+ name: tool.metrics
406
+ for name, tool in self._tools.items()
407
+ if isinstance(tool, EnhancedTool)
408
+ }
409
+
410
+ def get_cache_stats(self) -> Optional[Dict[str, Any]]:
411
+ """Get cache statistics."""
412
+ return self._cache.stats() if self._cache else None
413
+
414
+ def invalidate_cache(self, tool_name: Optional[str] = None) -> int:
415
+ """Invalidate cache entries."""
416
+ if not self._cache:
417
+ return 0
418
+
419
+ if tool_name:
420
+ return self._cache.invalidate(tool_name)
421
+ else:
422
+ count = len(self._cache._cache)
423
+ self._cache.clear()
424
+ return count
425
+
426
+ def get_execution_history(self, limit: int = 100) -> List[str]:
427
+ """Get recent tool execution history."""
428
+ return self._execution_order[-limit:]
429
+
430
+ @classmethod
431
+ def default(cls) -> "EnhancedToolRegistry":
432
+ """Create registry with default tools."""
433
+ from .file_tools import ReadFileTool, WriteFileTool, ListDirectoryTool
434
+ from .edit_tools import EditFileTool, InsertTextTool
435
+ from .shell_tools import BashTool
436
+ from .search_tools import GrepTool, GlobTool
437
+
438
+ registry = cls()
439
+
440
+ # File operations
441
+ registry.register(ReadFileTool())
442
+ registry.register(WriteFileTool())
443
+ registry.register(ListDirectoryTool())
444
+
445
+ # Editing
446
+ registry.register(EditFileTool())
447
+ registry.register(InsertTextTool())
448
+
449
+ # Shell
450
+ registry.register(BashTool())
451
+
452
+ # Search
453
+ registry.register(GrepTool())
454
+ registry.register(GlobTool())
455
+
456
+ return registry
457
+
458
+
459
+ def cacheable(ttl: float = 300.0):
460
+ """Decorator to make a tool method cacheable."""
461
+
462
+ def decorator(func):
463
+ @wraps(func)
464
+ async def wrapper(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
465
+ # Check if we have a cache in context
466
+ cache = getattr(ctx, "_tool_cache", None)
467
+
468
+ if cache:
469
+ cached = cache.get(self.name, args)
470
+ if cached:
471
+ return cached
472
+
473
+ result = await func(self, args, ctx)
474
+
475
+ if cache and result.success:
476
+ cache.set(self.name, args, result)
477
+
478
+ return result
479
+
480
+ wrapper._cacheable = True
481
+ wrapper._cache_ttl = ttl
482
+ return wrapper
483
+
484
+ return decorator
485
+
486
+
487
+ def retry(max_retries: int = 3, delay: float = 1.0):
488
+ """Decorator to add retry logic to a tool method."""
489
+
490
+ def decorator(func):
491
+ @wraps(func)
492
+ async def wrapper(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
493
+ last_error = None
494
+
495
+ for attempt in range(max_retries + 1):
496
+ try:
497
+ result = await func(self, args, ctx)
498
+ if result.success:
499
+ return result
500
+ last_error = result.error
501
+ except Exception as e:
502
+ last_error = str(e)
503
+
504
+ if attempt < max_retries:
505
+ await asyncio.sleep(delay * (2**attempt))
506
+
507
+ return ToolResult(
508
+ success=False,
509
+ output="",
510
+ error=f"Failed after {max_retries + 1} attempts: {last_error}",
511
+ )
512
+
513
+ return wrapper
514
+
515
+ return decorator