bone-agent 1.3.0

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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -0
  3. package/bin/npm-wrapper.js +235 -0
  4. package/bin/rg +0 -0
  5. package/bin/rg.exe +0 -0
  6. package/config.yaml.example +133 -0
  7. package/package.json +53 -0
  8. package/requirements.txt +9 -0
  9. package/src/__init__.py +11 -0
  10. package/src/core/__init__.py +1 -0
  11. package/src/core/agentic.py +1054 -0
  12. package/src/core/chat_manager.py +1552 -0
  13. package/src/core/config_manager.py +247 -0
  14. package/src/core/cron.py +527 -0
  15. package/src/core/cron_allowlist.py +118 -0
  16. package/src/core/memory.py +232 -0
  17. package/src/core/retry.py +71 -0
  18. package/src/core/sub_agent.py +326 -0
  19. package/src/core/tool_approval.py +220 -0
  20. package/src/core/tool_feedback.py +778 -0
  21. package/src/exceptions.py +79 -0
  22. package/src/llm/__init__.py +1 -0
  23. package/src/llm/client.py +171 -0
  24. package/src/llm/config.py +466 -0
  25. package/src/llm/prompts.py +735 -0
  26. package/src/llm/providers.py +417 -0
  27. package/src/llm/streaming.py +163 -0
  28. package/src/llm/token_tracker.py +368 -0
  29. package/src/tools/__init__.py +212 -0
  30. package/src/tools/constants.py +59 -0
  31. package/src/tools/create_file.py +136 -0
  32. package/src/tools/directory.py +389 -0
  33. package/src/tools/edit.py +543 -0
  34. package/src/tools/file_reader.py +322 -0
  35. package/src/tools/helpers/__init__.py +105 -0
  36. package/src/tools/helpers/base.py +550 -0
  37. package/src/tools/helpers/converters.py +44 -0
  38. package/src/tools/helpers/file_helpers.py +189 -0
  39. package/src/tools/helpers/formatters.py +411 -0
  40. package/src/tools/helpers/loader.py +231 -0
  41. package/src/tools/helpers/parallel_executor.py +231 -0
  42. package/src/tools/helpers/path_resolver.py +226 -0
  43. package/src/tools/helpers/plugin_manifest.py +156 -0
  44. package/src/tools/obsidian.py +96 -0
  45. package/src/tools/review_sub_agent.py +189 -0
  46. package/src/tools/rg_search.py +393 -0
  47. package/src/tools/search_plugins.py +109 -0
  48. package/src/tools/select_option.py +593 -0
  49. package/src/tools/shell.py +302 -0
  50. package/src/tools/sub_agent.py +139 -0
  51. package/src/tools/task_list.py +269 -0
  52. package/src/tools/web_search.py +61 -0
  53. package/src/ui/__init__.py +1 -0
  54. package/src/ui/banner.py +87 -0
  55. package/src/ui/commands.py +2694 -0
  56. package/src/ui/displays.py +213 -0
  57. package/src/ui/loader.py +284 -0
  58. package/src/ui/main.py +646 -0
  59. package/src/ui/prompt_utils.py +113 -0
  60. package/src/ui/setting_selector.py +590 -0
  61. package/src/ui/setup_wizard.py +294 -0
  62. package/src/ui/sub_agent_panel.py +234 -0
  63. package/src/ui/tool_confirmation.py +215 -0
  64. package/src/utils/__init__.py +1 -0
  65. package/src/utils/citation_parser.py +199 -0
  66. package/src/utils/editor.py +158 -0
  67. package/src/utils/gitignore_filter.py +149 -0
  68. package/src/utils/logger.py +254 -0
  69. package/src/utils/paths.py +30 -0
  70. package/src/utils/result_parsers.py +108 -0
  71. package/src/utils/safe_commands.py +243 -0
  72. package/src/utils/settings.py +174 -0
  73. package/src/utils/validation.py +191 -0
  74. package/src/utils/web_search.py +173 -0
@@ -0,0 +1,368 @@
1
+ """Token usage tracking for chat sessions."""
2
+
3
+ from llm.config import get_model_cost
4
+
5
+
6
+ def usage_with_cost(response: dict) -> dict:
7
+ """Extract usage dict from an LLM response, optionally including cost.
8
+
9
+ Copies 'usage' (which may contain 'cost' for OpenRouter-style responses) and
10
+ promotes a top-level 'cost' field (some providers) into the usage dict.
11
+ This ensures any upstream-reported cost is captured regardless of location.
12
+
13
+ Reduces repeated copy-and-merge boilerplate across call sites.
14
+
15
+ Args:
16
+ response: LLM response dict containing 'usage' (and optionally 'cost').
17
+
18
+ Returns:
19
+ dict with usage fields; empty dict if response has no 'usage'.
20
+ """
21
+ usage = dict(response.get("usage", {}))
22
+ # Top-level cost takes precedence (some providers place it here)
23
+ if "cost" in response:
24
+ usage["cost"] = response["cost"]
25
+ return usage
26
+
27
+
28
+ class TokenTracker:
29
+ """Tracks token usage across a chat session."""
30
+
31
+ def __init__(self):
32
+ self.total_prompt_tokens = 0 # Cumulative input tokens (never reset by compaction)
33
+ self.total_completion_tokens = 0 # Cumulative output tokens (never reset by compaction)
34
+ self.total_tokens = 0 # Cumulative total tokens (never reset by compaction)
35
+
36
+ # Conversation tokens: per-conversation billing (reset on /new)
37
+ self.conv_prompt_tokens = 0 # Current conversation input tokens
38
+ self.conv_completion_tokens = 0 # Current conversation output tokens
39
+ self.conv_total_tokens = 0 # Current conversation total tokens
40
+
41
+ # Context tokens: current conversation length (all messages in context)
42
+ self.current_context_tokens = 0 # Updated via set_context_tokens()
43
+
44
+ # Upstream-reported cost (e.g. OpenRouter's actual cost per request)
45
+ self.total_actual_cost = 0.0 # Cumulative upstream-reported cost (never reset by compaction)
46
+ self.conv_actual_cost = 0.0 # Per-conversation upstream-reported cost (reset on /new)
47
+
48
+ # Config-estimated cost (fallback when upstream cost is absent)
49
+ self.total_estimated_cost = 0.0 # Cumulative estimated cost (never reset by compaction)
50
+ self.conv_estimated_cost = 0.0 # Per-conversation estimated cost (reset on /new)
51
+
52
+ # Cache tokens: tracked when providers return cache breakdowns
53
+ # Only input tokens can be cached (no output caching in any known API)
54
+ self.total_cache_read_tokens = 0 # Cumulative input tokens read from cache
55
+ self.total_cache_creation_tokens = 0 # Cumulative input tokens written to cache
56
+ self.conv_cache_read_tokens = 0 # Per-conversation cache read tokens
57
+ self.conv_cache_creation_tokens = 0 # Per-conversation cache creation tokens
58
+ def add_usage(self, usage_data, model_name: str = ""):
59
+ """Add token usage from an API response.
60
+
61
+ Accepts either a full LLM response dict (non-streaming) or a pre-extracted
62
+ usage dict (streaming). Full responses are normalized internally via
63
+ usage_with_cost() to extract usage fields and promote top-level cost.
64
+
65
+ Cost is resolved internally:
66
+ 1. Upstream-reported cost (e.g. OpenRouter's response['usage']['cost']) — most accurate
67
+ 2. Config-based fallback (tokens × rates from MODEL_PRICES) — used when upstream cost is absent
68
+
69
+ Args:
70
+ usage_data: Full LLM response dict (with 'usage' key) or pre-extracted
71
+ usage dict (with 'prompt_tokens', 'completion_tokens').
72
+ May also contain 'cost' (upstream-reported actual cost).
73
+ model_name: Model name for config-based cost lookup (used as fallback).
74
+ """
75
+ if not usage_data or not isinstance(usage_data, dict):
76
+ return
77
+
78
+ # Normalize: full response dicts (non-streaming) have usage nested under
79
+ # a 'usage' key with cost possibly at the top level. Extract and merge.
80
+ # Pre-extracted usage dicts (streaming) pass through unchanged.
81
+ if "usage" in usage_data:
82
+ usage_data = usage_with_cost(usage_data)
83
+
84
+ # Update cumulative token counts (accumulated for billing, never reset by compaction)
85
+ prompt_tokens = usage_data.get('prompt_tokens', 0)
86
+ completion_tokens = usage_data.get('completion_tokens', 0)
87
+ self.total_prompt_tokens += prompt_tokens
88
+ self.total_completion_tokens += completion_tokens
89
+ self.total_tokens += prompt_tokens + completion_tokens
90
+
91
+ # Update conversation token counts (reset on /new)
92
+ self.conv_prompt_tokens += prompt_tokens
93
+ self.conv_completion_tokens += completion_tokens
94
+ self.conv_total_tokens += prompt_tokens + completion_tokens
95
+
96
+ # Extract cache tokens from provider responses (if available)
97
+ # Anthropic: cache_read_input_tokens, cache_creation_input_tokens
98
+ # OpenAI: prompt_tokens_details.cached_tokens
99
+ # Use explicit is-not-None checks to avoid treating 0 as falsy
100
+ cache_read = usage_data.get('cache_read_input_tokens')
101
+ if cache_read is None:
102
+ cache_read = usage_data.get('cached_tokens')
103
+ if cache_read is None:
104
+ details = usage_data.get('prompt_tokens_details')
105
+ cache_read = details.get('cached_tokens') if details else None
106
+ cache_read = cache_read or 0
107
+
108
+ cache_creation = usage_data.get('cache_creation_input_tokens', 0)
109
+ self.total_cache_read_tokens += cache_read
110
+ self.total_cache_creation_tokens += cache_creation
111
+ self.conv_cache_read_tokens += cache_read
112
+ self.conv_cache_creation_tokens += cache_creation
113
+
114
+ # Record cost: upstream-reported takes priority; compute from config as fallback
115
+ upstream_cost = usage_data.get('cost')
116
+ if upstream_cost is not None:
117
+ try:
118
+ self.add_actual_cost(float(upstream_cost))
119
+ except (ValueError, TypeError):
120
+ pass
121
+ else:
122
+ # Fallback: look up cost rates from config
123
+ cost_in, cost_out = get_model_cost(model_name)
124
+ if cost_in > 0 or cost_out > 0:
125
+ computed = self._calculate_cost(prompt_tokens, completion_tokens, cost_in, cost_out)
126
+ self.add_estimated_cost(computed['total_cost'])
127
+
128
+ def add_actual_cost(self, cost_usd: float):
129
+ """Add upstream-reported actual cost for a request.
130
+
131
+ Used when providers like OpenRouter return the exact cost in the response,
132
+ which is more accurate than estimating from token counts × static rates.
133
+
134
+ Args:
135
+ cost_usd: Actual cost in USD for a single request
136
+ """
137
+ self.total_actual_cost += cost_usd
138
+ self.conv_actual_cost += cost_usd
139
+
140
+ def add_estimated_cost(self, cost_usd: float):
141
+ """Add config-estimated cost for a request.
142
+
143
+ Used as a fallback when providers do not return cost in the response.
144
+ Estimated costs are tracked separately from upstream-reported actual costs
145
+ so they remain distinguishable.
146
+
147
+ Args:
148
+ cost_usd: Estimated cost in USD for a single request
149
+ """
150
+ self.total_estimated_cost += cost_usd
151
+ self.conv_estimated_cost += cost_usd
152
+
153
+ def has_actual_cost(self) -> bool:
154
+ """Whether any upstream-reported actual cost has been recorded."""
155
+ return self.total_actual_cost > 0.0
156
+
157
+ def has_estimated_cost(self) -> bool:
158
+ """Whether any config-estimated cost has been recorded."""
159
+ return self.total_estimated_cost > 0.0
160
+
161
+ def has_cost(self) -> bool:
162
+ """Whether any cost (actual or estimated) has been recorded."""
163
+ return self.total_actual_cost > 0.0 or self.total_estimated_cost > 0.0
164
+
165
+ def get_session_summary(self):
166
+ """Return formatted session usage summary string."""
167
+ parts = (
168
+ f"Session Input: [#5F9EA0]{self.current_context_tokens:,}[/#5F9EA0] | "
169
+ f"Session Total: [#5F9EA0]{self.conv_total_tokens:,}[/#5F9EA0]"
170
+ )
171
+ total_cost = self.total_actual_cost + self.total_estimated_cost
172
+ if total_cost > 0:
173
+ parts += f" | Cost: [green]${total_cost:.4f}[/green]"
174
+ return parts
175
+
176
+ def get_all_token_counts(self):
177
+ """Return all token counts as a dictionary for UI display.
178
+
179
+ Returns:
180
+ dict with keys: prompt_in, completion_out, total
181
+ """
182
+ return {
183
+ 'prompt_in': self.total_prompt_tokens,
184
+ 'completion_out': self.total_completion_tokens,
185
+ 'total': self.total_tokens
186
+ }
187
+
188
+ def reset(self, prompt_tokens=None, completion_tokens=None, total_tokens=None):
189
+ """Reset token counters to zero or to specified values.
190
+
191
+ Used by /clear to reset conversation context while preserving cumulative
192
+ billing costs across the session.
193
+
194
+ Args:
195
+ prompt_tokens: If provided, set total_prompt_tokens to this value
196
+ completion_tokens: If provided, set total_completion_tokens to this value
197
+ total_tokens: If provided, set total_tokens to this value
198
+ """
199
+ self.total_prompt_tokens = prompt_tokens if prompt_tokens is not None else 0
200
+ self.total_completion_tokens = completion_tokens if completion_tokens is not None else 0
201
+ if total_tokens is None:
202
+ self.total_tokens = self.total_prompt_tokens + self.total_completion_tokens
203
+ else:
204
+ self.total_tokens = total_tokens
205
+ self.current_context_tokens = 0 # Reset context tokens
206
+ self.total_cache_read_tokens = 0
207
+ self.total_cache_creation_tokens = 0
208
+ # Note: total_actual_cost and total_estimated_cost are preserved across resets (cumulative billing)
209
+
210
+ def reset_all(self):
211
+ """Full reset of all counters including cost accumulators.
212
+
213
+ Used on provider switch to clear stale cost state from the previous
214
+ provider. Unlike reset(), this zeros actual/estimated costs so the
215
+ new provider starts with a clean billing slate.
216
+ """
217
+ self.reset()
218
+ self.total_actual_cost = 0.0
219
+ self.total_estimated_cost = 0.0
220
+ self.total_cache_read_tokens = 0
221
+ self.total_cache_creation_tokens = 0
222
+
223
+ @staticmethod
224
+ def estimate_tokens(text, model=""):
225
+ """Estimate token count using tiktoken.
226
+
227
+ Args:
228
+ text: String to estimate tokens for
229
+ model: Optional model name for encoding selection (uses cl100k_base if empty)
230
+
231
+ Returns:
232
+ Estimated token count (int)
233
+ """
234
+ if not text:
235
+ return 0
236
+
237
+ try:
238
+ import tiktoken
239
+ try:
240
+ enc = tiktoken.encoding_for_model(model) if model else tiktoken.get_encoding("cl100k_base")
241
+ except Exception:
242
+ enc = tiktoken.get_encoding("cl100k_base")
243
+ return len(enc.encode(text))
244
+ except ImportError:
245
+ # Fallback to character-based approximation if tiktoken not available
246
+ return len(text) // 4
247
+
248
+ def set_context_tokens(self, token_count):
249
+ """Set the current context token count.
250
+
251
+ Args:
252
+ token_count: Actual token count of the current message list
253
+ """
254
+ self.current_context_tokens = token_count
255
+
256
+ @staticmethod
257
+ def _calculate_cost(prompt_tokens: int, completion_tokens: int, cost_in: float, cost_out: float) -> dict:
258
+ """Core cost formula: (tokens / 1M) * rate."""
259
+ input_cost = (prompt_tokens / 1_000_000) * cost_in
260
+ output_cost = (completion_tokens / 1_000_000) * cost_out
261
+ return {
262
+ 'input_cost': input_cost,
263
+ 'output_cost': output_cost,
264
+ 'total_cost': input_cost + output_cost,
265
+ }
266
+
267
+ def reset_conversation(self):
268
+ """Reset conversation token counters (called on /new).
269
+
270
+ Session totals (total_prompt_tokens, total_completion_tokens) are preserved.
271
+ """
272
+ self.conv_prompt_tokens = 0
273
+ self.conv_completion_tokens = 0
274
+ self.conv_total_tokens = 0
275
+ self.conv_actual_cost = 0.0
276
+ self.conv_estimated_cost = 0.0
277
+ self.conv_cache_read_tokens = 0
278
+ self.conv_cache_creation_tokens = 0
279
+
280
+ def get_usage_for_prompt(self, context_limit: int = 200_000) -> str:
281
+ """Get formatted usage information for inclusion in agent prompts.
282
+
283
+ This provides agents with awareness of their current context window
284
+ usage to help them work within context limits. Urgency is based on
285
+ actual context length (current_context_tokens), not cumulative billing.
286
+
287
+ Args:
288
+ context_limit: The context window limit to compare against (default: 200k)
289
+
290
+ Returns:
291
+ Formatted string with usage statistics and guidance
292
+ """
293
+ context_used = self.current_context_tokens
294
+ total_burned = self.total_tokens
295
+ remaining = context_limit - context_used
296
+ percentage = (context_used / context_limit) * 100
297
+
298
+ # Determine urgency level
299
+ if percentage >= 90:
300
+ urgency = "CRITICAL"
301
+ guidance = "You have nearly exhausted your token budget. Be extremely concise and limit exploration."
302
+ elif percentage >= 75:
303
+ urgency = "HIGH"
304
+ guidance = "You are approaching your token limit. Prioritize focused exploration over breadth."
305
+ elif percentage >= 50:
306
+ urgency = "MODERATE"
307
+ guidance = "You have used half your token budget. Be mindful of exploration scope."
308
+ else:
309
+ urgency = "LOW"
310
+ guidance = "Token usage is within normal bounds."
311
+
312
+ return (
313
+ f"## Token Usage Awareness\n\n"
314
+ f"**Status:** {urgency} | **Context:** {context_used:,} / {context_limit:,} ({percentage:.1f}%)\n"
315
+ f"**Remaining:** {remaining:,} tokens | **Session total burned:** {total_burned:,}\n\n"
316
+ f"**Guidance:** {guidance}\n\n"
317
+ f"**Note:** Context shows current conversation length; session total is cumulative across all LLM calls."
318
+ )
319
+
320
+ def get_context_summary(self) -> str:
321
+ """Get a brief summary of current context usage.
322
+
323
+ Returns:
324
+ Concise string with context and session totals
325
+ """
326
+ return (
327
+ f"Context: {self.current_context_tokens:,} tokens | "
328
+ f"Session total burned: {self.total_tokens:,} tokens"
329
+ )
330
+
331
+ def get_display_cost(self, model_name: str = "") -> float:
332
+ """Get the cost to display in UI (session-level).
333
+
334
+ Priority:
335
+ 1. Upstream-reported actual cost (most accurate, e.g. OpenRouter)
336
+ 2. Config-based fallback (tokens x rates from MODEL_PRICES)
337
+
338
+ Args:
339
+ model_name: Model name for config-based cost lookup (fallback).
340
+
341
+ Returns:
342
+ Total cost in USD, or 0.0 if neither source is available
343
+ """
344
+ # If we have upstream-reported cost, use it (most accurate)
345
+ if self.has_actual_cost():
346
+ return self.total_actual_cost + self.total_estimated_cost
347
+ # Fallback: full config-based recalculation for all tokens
348
+ cost_in, cost_out = get_model_cost(model_name)
349
+ if cost_in > 0 or cost_out > 0:
350
+ return self._calculate_cost(
351
+ self.total_prompt_tokens, self.total_completion_tokens,
352
+ cost_in, cost_out
353
+ )['total_cost']
354
+ return 0.0
355
+
356
+ def get_conversation_display_cost(self, cost_in: float, cost_out: float) -> float:
357
+ """Get the cost to display for conversation-level (reset on /new).
358
+
359
+ For callers that already have cost rates (e.g. config_manager), this
360
+ computes directly.
361
+
362
+ Returns:
363
+ Conversation cost in USD
364
+ """
365
+ return self._calculate_cost(
366
+ self.conv_prompt_tokens, self.conv_completion_tokens,
367
+ cost_in, cost_out
368
+ )['total_cost']
@@ -0,0 +1,212 @@
1
+ """Tool execution utilities.
2
+
3
+ This package provides command execution, file editing, and result formatting
4
+ capabilities for the bone-agent AI assistant.
5
+ """
6
+
7
+ import logging
8
+
9
+ _logger = logging.getLogger(__name__)
10
+
11
+ # Command execution (now in shell.py)
12
+ from .shell import (
13
+ confirm_tool,
14
+ run_shell_command,
15
+ )
16
+
17
+ # UI components
18
+ from ui.tool_confirmation import ToolConfirmationPanel
19
+
20
+ # File editing (now in edit.py)
21
+ from .edit import (
22
+ _resolve_repo_path,
23
+ preview_edit_file,
24
+ run_edit_file,
25
+ )
26
+
27
+ # Result formatting (now in helpers/)
28
+ from .helpers.formatters import (
29
+ format_tool_result,
30
+ format_file_result,
31
+ _build_diff,
32
+ _detect_newline,
33
+ )
34
+
35
+ # File operations
36
+ from .directory import list_directory
37
+ from .create_file import create_file
38
+ from .file_reader import read_file
39
+
40
+ # Constants
41
+ from . import constants
42
+
43
+ # Tool definitions
44
+ # Import tool modules to trigger @tool decorator registration
45
+ # These modules register themselves when imported
46
+ from . import file_reader
47
+ from . import directory
48
+ from . import create_file
49
+ from . import edit # edit.py now contains both core logic and @tool decorators
50
+ from . import rg_search
51
+ from . import shell # shell.py now contains both core logic and @tool decorators
52
+ from . import web_search
53
+ from . import sub_agent
54
+ # review_sub_agent is not an LLM tool — used as a /review slash command in ui.commands
55
+
56
+ from . import task_list
57
+ from . import select_option
58
+
59
+ # search_plugins — core meta-tool for plugin discovery
60
+ from . import search_plugins
61
+
62
+ # Obsidian tools — conditional registration (register() pattern, NOT @tool at import)
63
+ # Only imported and registered when vault is configured and enabled.
64
+ # This ensures zero token cost when no vault is linked.
65
+ try:
66
+ from utils.settings import obsidian_settings
67
+ if obsidian_settings.is_active():
68
+ from . import obsidian as _obsidian_mod
69
+ _obsidian_mod.register()
70
+ except Exception as e:
71
+ _logger.debug("Obsidian tools not loaded: %s", e)
72
+
73
+ # Tool schema exports (now in helpers/base.py, merged from definitions.py)
74
+ from .helpers.base import TOOLS
75
+
76
+ __all__ = [
77
+ # Command execution
78
+ 'confirm_tool',
79
+ 'run_shell_command',
80
+ # UI components
81
+ 'ToolConfirmationPanel',
82
+ # File editing
83
+ '_resolve_repo_path',
84
+ 'preview_edit_file',
85
+ 'run_edit_file',
86
+ # Formatters
87
+ 'format_tool_result',
88
+ 'format_file_result',
89
+ '_build_diff',
90
+ '_detect_newline',
91
+ # File operations
92
+ 'read_file',
93
+ 'list_directory',
94
+ 'create_file',
95
+ # Constants
96
+ 'constants',
97
+ # Tool definitions
98
+ 'TOOLS',
99
+ ]
100
+
101
+ # =============================================================================
102
+ # Backward compatibility: Re-export helpers at package level
103
+ # This allows imports like: from tools.base import tool
104
+ # =============================================================================
105
+ from .helpers import (
106
+ ToolDefinition,
107
+ ToolRegistry,
108
+ tool,
109
+ build_context,
110
+ get_tool_schemas,
111
+ get_terminal_policy,
112
+ TERMINAL_NONE,
113
+ TERMINAL_YIELD,
114
+ TERMINAL_STOP,
115
+ )
116
+
117
+ # Apply disabled tools from settings (after all tools are registered)
118
+ try:
119
+ from utils.settings import tool_settings
120
+ for tool_name in tool_settings.disabled_tools:
121
+ ToolRegistry.disable(tool_name)
122
+ except Exception as e:
123
+ _logger.debug("Failed to apply disabled tools: %s", e)
124
+
125
+ # Load plugin tools into the PluginManifest (not ToolRegistry).
126
+ # Plugin modules with @tool(tier="plugin") register into the manifest
127
+ # and are only activated in ToolRegistry on-demand via search_plugins.
128
+ try:
129
+ from .helpers.loader import load_plugin_tools
130
+ load_plugin_tools()
131
+ except Exception as e:
132
+ _logger.debug("Failed to load plugin tools: %s", e)
133
+
134
+ # Make base module available for backward compatibility
135
+ import sys
136
+ from types import ModuleType
137
+
138
+ # Create a synthetic 'base' module that re-exports from helpers
139
+ _base_module = ModuleType('tools.base')
140
+ _base_module.__dict__.update({
141
+ 'ToolDefinition': ToolDefinition,
142
+ 'ToolRegistry': ToolRegistry,
143
+ 'tool': tool,
144
+ 'build_context': build_context,
145
+ 'get_tool_schemas': get_tool_schemas,
146
+ 'TOOLS': TOOLS,
147
+ })
148
+ sys.modules['tools.base'] = _base_module
149
+
150
+ # Create synthetic modules for other helpers
151
+ _formatters_module = ModuleType('tools.formatters')
152
+ _formatters_module.__dict__.update({
153
+ 'format_tool_result': format_tool_result,
154
+ 'format_file_result': format_file_result,
155
+ '_build_diff': _build_diff,
156
+ '_detect_newline': _detect_newline,
157
+ })
158
+ sys.modules['tools.formatters'] = _formatters_module
159
+
160
+ _file_helpers_module = ModuleType('tools.file_helpers')
161
+ from .helpers.file_helpers import (
162
+ _is_reserved_windows_name,
163
+ GitignoreFilter,
164
+ )
165
+ _file_helpers_module.__dict__.update({
166
+ '_is_reserved_windows_name': _is_reserved_windows_name,
167
+ 'GitignoreFilter': GitignoreFilter,
168
+ })
169
+ sys.modules['tools.file_helpers'] = _file_helpers_module
170
+
171
+ # Path resolver module
172
+ _path_resolver_module = ModuleType('tools.path_resolver')
173
+ from .helpers.path_resolver import PathResolver
174
+ _path_resolver_module.__dict__.update({
175
+ 'PathResolver': PathResolver,
176
+ })
177
+ sys.modules['tools.path_resolver'] = _path_resolver_module
178
+
179
+ _converters_module = ModuleType('tools.converters')
180
+ from .helpers.converters import coerce_int, coerce_bool
181
+ _converters_module.__dict__.update({
182
+ 'coerce_int': coerce_int,
183
+ 'coerce_bool': coerce_bool,
184
+ })
185
+ sys.modules['tools.converters'] = _converters_module
186
+
187
+ _loader_module = ModuleType('tools.loader')
188
+ from .helpers.loader import (
189
+ discover_tools,
190
+ load_builtin_tools,
191
+ load_plugin_tools,
192
+ load_all_tools,
193
+ list_registered_tools,
194
+ )
195
+ _loader_module.__dict__.update({
196
+ 'discover_tools': discover_tools,
197
+ 'load_builtin_tools': load_builtin_tools,
198
+ 'load_plugin_tools': load_plugin_tools,
199
+ 'load_all_tools': load_all_tools,
200
+ 'list_registered_tools': list_registered_tools,
201
+ })
202
+ sys.modules['tools.loader'] = _loader_module
203
+
204
+ _parallel_executor_module = ModuleType('tools.parallel_executor')
205
+ from .helpers.parallel_executor import ToolCall, ToolResult, ParallelToolExecutor
206
+ _parallel_executor_module.__dict__.update({
207
+ 'ToolCall': ToolCall,
208
+ 'ToolResult': ToolResult,
209
+ 'ParallelToolExecutor': ParallelToolExecutor,
210
+ })
211
+ sys.modules['tools.parallel_executor'] = _parallel_executor_module
212
+
@@ -0,0 +1,59 @@
1
+ """Centralized constants for tools.
2
+
3
+ This module contains all magic numbers and configuration values used across
4
+ the tools infrastructure. Centralizing constants makes the code more
5
+ maintainable and self-documenting.
6
+ """
7
+
8
+ # ============================================================================
9
+ # Directory Listing Constants
10
+ # ============================================================================
11
+
12
+ # Total items that trigger truncation in directory listings
13
+ TRUNCATION_THRESHOLD = 100
14
+
15
+ # Maximum files to show per folder when truncating directory listings
16
+ MAX_FILES_PER_FOLDER = 10
17
+
18
+ # Hard upper limit for total items to collect in directory listings
19
+ # Prevents context explosion on very large directories
20
+ MAX_TOTAL_ITEMS = 500
21
+
22
+
23
+ # ============================================================================
24
+ # File Reading Constants
25
+ # ============================================================================
26
+
27
+ # Chunk size for streaming file reads (8KB)
28
+ # Balances memory usage with read performance
29
+ FILE_READ_CHUNK_SIZE = 8192
30
+
31
+ # Maximum buffer size for file reading (10MB)
32
+ # Handles pathological files with very long single lines
33
+ FILE_READ_MAX_BUFFER_SIZE = 10_000_000
34
+
35
+ # Maximum lines to show in file output formatting
36
+ # Prevents overwhelming context with excessive output
37
+ FORMATTER_MAX_LINES = 100
38
+
39
+
40
+ # ============================================================================
41
+ # Task List Constants
42
+ # ============================================================================
43
+
44
+ # Maximum number of tasks allowed in a task list
45
+ MAX_TASKS = 50
46
+
47
+ # Maximum length for individual task descriptions
48
+ MAX_TASK_LEN = 200
49
+
50
+ # Maximum length for task list titles
51
+ MAX_TASK_TITLE_LEN = 80
52
+
53
+
54
+ # ============================================================================
55
+ # UI/Display Constants
56
+ # ============================================================================
57
+
58
+ # Default terminal width fallback for non-TTY environments
59
+ DEFAULT_TERMINAL_WIDTH = 80