bone-agent 1.3.3 → 2.0.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 (121) hide show
  1. package/bin/bone.js +39 -0
  2. package/package.json +25 -39
  3. package/LICENSE +0 -21
  4. package/README.md +0 -184
  5. package/bin/npm-wrapper.js +0 -235
  6. package/bin/rg +0 -0
  7. package/bin/rg.exe +0 -0
  8. package/config.yaml.example +0 -141
  9. package/prompts/main/ask_questions.md +0 -31
  10. package/prompts/main/batch_independent_calls.md +0 -5
  11. package/prompts/main/casual_interactions.md +0 -11
  12. package/prompts/main/code_references.md +0 -8
  13. package/prompts/main/communication_style.md +0 -12
  14. package/prompts/main/context_reliability.md +0 -12
  15. package/prompts/main/conversational_tool_calling.md +0 -15
  16. package/prompts/main/dream.md +0 -36
  17. package/prompts/main/editing_pattern.md +0 -13
  18. package/prompts/main/error_handling.md +0 -6
  19. package/prompts/main/exploration_pattern.md +0 -21
  20. package/prompts/main/intro.md +0 -1
  21. package/prompts/main/obsidian.md +0 -16
  22. package/prompts/main/obsidian_project.md +0 -79
  23. package/prompts/main/professional_objectivity.md +0 -3
  24. package/prompts/main/targeted_searching.md +0 -10
  25. package/prompts/main/task_lists_pattern.md +0 -8
  26. package/prompts/main/temp_folder.md +0 -9
  27. package/prompts/main/think_before_acting.md +0 -10
  28. package/prompts/main/tone_and_style.md +0 -4
  29. package/prompts/main/tool_preferences.md +0 -24
  30. package/prompts/main/trust_subagent_context.md +0 -21
  31. package/prompts/main/when_to_use_sub_agent.md +0 -7
  32. package/prompts/micro/ask_questions.md +0 -1
  33. package/prompts/micro/batch_independent_calls.md +0 -1
  34. package/prompts/micro/casual_interactions.md +0 -1
  35. package/prompts/micro/code_references.md +0 -1
  36. package/prompts/micro/communication_style.md +0 -1
  37. package/prompts/micro/context_reliability.md +0 -1
  38. package/prompts/micro/conversational_tool_calling.md +0 -1
  39. package/prompts/micro/editing_pattern.md +0 -1
  40. package/prompts/micro/error_handling.md +0 -1
  41. package/prompts/micro/exploration_pattern.md +0 -1
  42. package/prompts/micro/intro.md +0 -1
  43. package/prompts/micro/obsidian.md +0 -4
  44. package/prompts/micro/obsidian_project.md +0 -5
  45. package/prompts/micro/professional_objectivity.md +0 -1
  46. package/prompts/micro/targeted_searching.md +0 -1
  47. package/prompts/micro/task_lists_pattern.md +0 -1
  48. package/prompts/micro/temp_folder.md +0 -1
  49. package/prompts/micro/think_before_acting.md +0 -5
  50. package/prompts/micro/tone_and_style.md +0 -1
  51. package/prompts/micro/tool_preferences.md +0 -1
  52. package/prompts/micro/trust_subagent_context.md +0 -1
  53. package/prompts/micro/when_to_use_sub_agent.md +0 -1
  54. package/requirements.txt +0 -9
  55. package/src/__init__.py +0 -11
  56. package/src/core/__init__.py +0 -1
  57. package/src/core/agentic.py +0 -985
  58. package/src/core/chat_manager.py +0 -1564
  59. package/src/core/config_manager.py +0 -253
  60. package/src/core/cron.py +0 -582
  61. package/src/core/cron_allowlist.py +0 -118
  62. package/src/core/memory.py +0 -145
  63. package/src/core/retry.py +0 -71
  64. package/src/core/sub_agent.py +0 -326
  65. package/src/core/tool_approval.py +0 -220
  66. package/src/core/tool_feedback.py +0 -778
  67. package/src/exceptions.py +0 -79
  68. package/src/llm/__init__.py +0 -1
  69. package/src/llm/client.py +0 -171
  70. package/src/llm/config.py +0 -492
  71. package/src/llm/prompts.py +0 -489
  72. package/src/llm/providers.py +0 -436
  73. package/src/llm/streaming.py +0 -163
  74. package/src/llm/token_tracker.py +0 -384
  75. package/src/tools/__init__.py +0 -212
  76. package/src/tools/constants.py +0 -59
  77. package/src/tools/create_file.py +0 -136
  78. package/src/tools/directory.py +0 -389
  79. package/src/tools/edit.py +0 -545
  80. package/src/tools/file_reader.py +0 -322
  81. package/src/tools/helpers/__init__.py +0 -105
  82. package/src/tools/helpers/base.py +0 -550
  83. package/src/tools/helpers/converters.py +0 -44
  84. package/src/tools/helpers/file_helpers.py +0 -189
  85. package/src/tools/helpers/formatters.py +0 -411
  86. package/src/tools/helpers/loader.py +0 -231
  87. package/src/tools/helpers/parallel_executor.py +0 -231
  88. package/src/tools/helpers/path_resolver.py +0 -232
  89. package/src/tools/helpers/plugin_manifest.py +0 -156
  90. package/src/tools/obsidian.py +0 -96
  91. package/src/tools/review_sub_agent.py +0 -189
  92. package/src/tools/rg_search.py +0 -460
  93. package/src/tools/search_plugins.py +0 -109
  94. package/src/tools/select_option.py +0 -600
  95. package/src/tools/shell.py +0 -302
  96. package/src/tools/sub_agent.py +0 -139
  97. package/src/tools/task_list.py +0 -269
  98. package/src/tools/web_search.py +0 -61
  99. package/src/ui/__init__.py +0 -1
  100. package/src/ui/banner.py +0 -87
  101. package/src/ui/commands.py +0 -2809
  102. package/src/ui/displays.py +0 -214
  103. package/src/ui/loader.py +0 -284
  104. package/src/ui/main.py +0 -647
  105. package/src/ui/prompt_utils.py +0 -113
  106. package/src/ui/setting_selector.py +0 -590
  107. package/src/ui/setup_wizard.py +0 -294
  108. package/src/ui/sub_agent_panel.py +0 -234
  109. package/src/ui/tool_confirmation.py +0 -215
  110. package/src/utils/__init__.py +0 -1
  111. package/src/utils/citation_parser.py +0 -199
  112. package/src/utils/editor.py +0 -158
  113. package/src/utils/gitignore_filter.py +0 -149
  114. package/src/utils/logger.py +0 -254
  115. package/src/utils/paths.py +0 -30
  116. package/src/utils/result_parsers.py +0 -108
  117. package/src/utils/safe_commands.py +0 -243
  118. package/src/utils/settings.py +0 -191
  119. package/src/utils/user_message_logger.py +0 -120
  120. package/src/utils/validation.py +0 -191
  121. package/src/utils/web_search.py +0 -173
@@ -1,384 +0,0 @@
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
-
59
- # Active prompt variant (loaded from prompts/ directory)
60
- self.current_variant = "main"
61
-
62
- def add_usage(self, usage_data, model_name: str = ""):
63
- """Add token usage from an API response.
64
-
65
- Accepts either a full LLM response dict (non-streaming) or a pre-extracted
66
- usage dict (streaming). Full responses are normalized internally via
67
- usage_with_cost() to extract usage fields and promote top-level cost.
68
-
69
- Cost is resolved internally:
70
- 1. Upstream-reported cost (e.g. OpenRouter's response['usage']['cost']) — most accurate
71
- 2. Config-based fallback (tokens × rates from MODEL_PRICES) — used when upstream cost is absent
72
-
73
- Args:
74
- usage_data: Full LLM response dict (with 'usage' key) or pre-extracted
75
- usage dict (with 'prompt_tokens', 'completion_tokens').
76
- May also contain 'cost' (upstream-reported actual cost).
77
- model_name: Model name for config-based cost lookup (used as fallback).
78
- """
79
- if not usage_data or not isinstance(usage_data, dict):
80
- return
81
-
82
- # Normalize: full response dicts (non-streaming) have usage nested under
83
- # a 'usage' key with cost possibly at the top level. Extract and merge.
84
- # Pre-extracted usage dicts (streaming) pass through unchanged.
85
- if "usage" in usage_data:
86
- usage_data = usage_with_cost(usage_data)
87
-
88
- # Update cumulative token counts (accumulated for billing, never reset by compaction)
89
- prompt_tokens = usage_data.get('prompt_tokens', 0)
90
- completion_tokens = usage_data.get('completion_tokens', 0)
91
- self.total_prompt_tokens += prompt_tokens
92
- self.total_completion_tokens += completion_tokens
93
- self.total_tokens += prompt_tokens + completion_tokens
94
-
95
- # Update conversation token counts (reset on /new)
96
- self.conv_prompt_tokens += prompt_tokens
97
- self.conv_completion_tokens += completion_tokens
98
- self.conv_total_tokens += prompt_tokens + completion_tokens
99
-
100
- # Extract cache tokens from provider responses (if available)
101
- # Anthropic: cache_read_input_tokens, cache_creation_input_tokens
102
- # OpenAI: prompt_tokens_details.cached_tokens
103
- # Use explicit is-not-None checks to avoid treating 0 as falsy
104
- cache_read = usage_data.get('cache_read_input_tokens')
105
- if cache_read is None:
106
- cache_read = usage_data.get('cached_tokens')
107
- if cache_read is None:
108
- details = usage_data.get('prompt_tokens_details')
109
- cache_read = details.get('cached_tokens') if details else None
110
- cache_read = cache_read or 0
111
-
112
- cache_creation = usage_data.get('cache_creation_input_tokens', 0)
113
- self.total_cache_read_tokens += cache_read
114
- self.total_cache_creation_tokens += cache_creation
115
- self.conv_cache_read_tokens += cache_read
116
- self.conv_cache_creation_tokens += cache_creation
117
-
118
- # Record cost: upstream-reported takes priority; compute from config as fallback
119
- upstream_cost = usage_data.get('cost')
120
- if upstream_cost is not None:
121
- try:
122
- self.add_actual_cost(float(upstream_cost))
123
- except (ValueError, TypeError):
124
- pass
125
- else:
126
- # Fallback: look up cost rates from config
127
- cost_in, cost_out = get_model_cost(model_name)
128
- if cost_in > 0 or cost_out > 0:
129
- # Compute the billable (non-cache) input token count for cost
130
- # estimation. Providers normalize `prompt_tokens` differently:
131
- # - Anthropic handler: sums input + cache_read + cache_creation
132
- # - OpenAI: prompt_tokens natively includes cached_tokens
133
- # - Future providers: may exclude cache tokens from prompt_tokens
134
- # Use the explicit `input_tokens` field (Anthropic native,
135
- # non-cache portion) when available; otherwise subtract cache
136
- # tokens from prompt_tokens (assumes prompt_tokens includes
137
- # cache counts).
138
- base_prompt = usage_data.get('input_tokens')
139
- if base_prompt is None:
140
- base_prompt = max(0, prompt_tokens - cache_read - cache_creation)
141
- computed = self._calculate_cost(base_prompt, completion_tokens, cost_in, cost_out)
142
- self.add_estimated_cost(computed['total_cost'])
143
-
144
- def add_actual_cost(self, cost_usd: float):
145
- """Add upstream-reported actual cost for a request.
146
-
147
- Used when providers like OpenRouter return the exact cost in the response,
148
- which is more accurate than estimating from token counts × static rates.
149
-
150
- Args:
151
- cost_usd: Actual cost in USD for a single request
152
- """
153
- self.total_actual_cost += cost_usd
154
- self.conv_actual_cost += cost_usd
155
-
156
- def add_estimated_cost(self, cost_usd: float):
157
- """Add config-estimated cost for a request.
158
-
159
- Used as a fallback when providers do not return cost in the response.
160
- Estimated costs are tracked separately from upstream-reported actual costs
161
- so they remain distinguishable.
162
-
163
- Args:
164
- cost_usd: Estimated cost in USD for a single request
165
- """
166
- self.total_estimated_cost += cost_usd
167
- self.conv_estimated_cost += cost_usd
168
-
169
- def has_actual_cost(self) -> bool:
170
- """Whether any upstream-reported actual cost has been recorded."""
171
- return self.total_actual_cost > 0.0
172
-
173
- def has_estimated_cost(self) -> bool:
174
- """Whether any config-estimated cost has been recorded."""
175
- return self.total_estimated_cost > 0.0
176
-
177
- def has_cost(self) -> bool:
178
- """Whether any cost (actual or estimated) has been recorded."""
179
- return self.total_actual_cost > 0.0 or self.total_estimated_cost > 0.0
180
-
181
- def get_session_summary(self):
182
- """Return formatted session usage summary string."""
183
- parts = (
184
- f"Session Input: [#5F9EA0]{self.current_context_tokens:,}[/#5F9EA0] | "
185
- f"Session Total: [#5F9EA0]{self.conv_total_tokens:,}[/#5F9EA0]"
186
- )
187
- total_cost = self.total_actual_cost + self.total_estimated_cost
188
- if total_cost > 0:
189
- parts += f" | Cost: [green]${total_cost:.4f}[/green]"
190
- return parts
191
-
192
- def get_all_token_counts(self):
193
- """Return all token counts as a dictionary for UI display.
194
-
195
- Returns:
196
- dict with keys: prompt_in, completion_out, total
197
- """
198
- return {
199
- 'prompt_in': self.total_prompt_tokens,
200
- 'completion_out': self.total_completion_tokens,
201
- 'total': self.total_tokens
202
- }
203
-
204
- def reset(self, prompt_tokens=None, completion_tokens=None, total_tokens=None):
205
- """Reset token counters to zero or to specified values.
206
-
207
- Used by /clear to reset conversation context while preserving cumulative
208
- billing costs across the session.
209
-
210
- Args:
211
- prompt_tokens: If provided, set total_prompt_tokens to this value
212
- completion_tokens: If provided, set total_completion_tokens to this value
213
- total_tokens: If provided, set total_tokens to this value
214
- """
215
- self.total_prompt_tokens = prompt_tokens if prompt_tokens is not None else 0
216
- self.total_completion_tokens = completion_tokens if completion_tokens is not None else 0
217
- if total_tokens is None:
218
- self.total_tokens = self.total_prompt_tokens + self.total_completion_tokens
219
- else:
220
- self.total_tokens = total_tokens
221
- self.current_context_tokens = 0 # Reset context tokens
222
- self.total_cache_read_tokens = 0
223
- self.total_cache_creation_tokens = 0
224
- # Note: total_actual_cost and total_estimated_cost are preserved across resets (cumulative billing)
225
-
226
- def reset_all(self):
227
- """Full reset of all counters including cost accumulators.
228
-
229
- Used on provider switch to clear stale cost state from the previous
230
- provider. Unlike reset(), this zeros actual/estimated costs so the
231
- new provider starts with a clean billing slate.
232
- """
233
- self.reset()
234
- self.total_actual_cost = 0.0
235
- self.total_estimated_cost = 0.0
236
- self.total_cache_read_tokens = 0
237
- self.total_cache_creation_tokens = 0
238
-
239
- @staticmethod
240
- def estimate_tokens(text, model=""):
241
- """Estimate token count using tiktoken.
242
-
243
- Args:
244
- text: String to estimate tokens for
245
- model: Optional model name for encoding selection (uses cl100k_base if empty)
246
-
247
- Returns:
248
- Estimated token count (int)
249
- """
250
- if not text:
251
- return 0
252
-
253
- try:
254
- import tiktoken
255
- try:
256
- enc = tiktoken.encoding_for_model(model) if model else tiktoken.get_encoding("cl100k_base")
257
- except Exception:
258
- enc = tiktoken.get_encoding("cl100k_base")
259
- return len(enc.encode(text))
260
- except ImportError:
261
- # Fallback to character-based approximation if tiktoken not available
262
- return len(text) // 4
263
-
264
- def set_context_tokens(self, token_count):
265
- """Set the current context token count.
266
-
267
- Args:
268
- token_count: Actual token count of the current message list
269
- """
270
- self.current_context_tokens = token_count
271
-
272
- @staticmethod
273
- def _calculate_cost(prompt_tokens: int, completion_tokens: int, cost_in: float, cost_out: float) -> dict:
274
- """Core cost formula: (tokens / 1M) * rate."""
275
- input_cost = (prompt_tokens / 1_000_000) * cost_in
276
- output_cost = (completion_tokens / 1_000_000) * cost_out
277
- return {
278
- 'input_cost': input_cost,
279
- 'output_cost': output_cost,
280
- 'total_cost': input_cost + output_cost,
281
- }
282
-
283
- def reset_conversation(self):
284
- """Reset conversation token counters (called on /new).
285
-
286
- Session totals (total_prompt_tokens, total_completion_tokens) are preserved.
287
- """
288
- self.conv_prompt_tokens = 0
289
- self.conv_completion_tokens = 0
290
- self.conv_total_tokens = 0
291
- self.conv_actual_cost = 0.0
292
- self.conv_estimated_cost = 0.0
293
- self.conv_cache_read_tokens = 0
294
- self.conv_cache_creation_tokens = 0
295
-
296
- def get_usage_for_prompt(self, context_limit: int = 200_000) -> str:
297
- """Get formatted usage information for inclusion in agent prompts.
298
-
299
- This provides agents with awareness of their current context window
300
- usage to help them work within context limits. Urgency is based on
301
- actual context length (current_context_tokens), not cumulative billing.
302
-
303
- Args:
304
- context_limit: The context window limit to compare against (default: 200k)
305
-
306
- Returns:
307
- Formatted string with usage statistics and guidance
308
- """
309
- context_used = self.current_context_tokens
310
- total_burned = self.total_tokens
311
- remaining = context_limit - context_used
312
- percentage = (context_used / context_limit) * 100
313
-
314
- # Determine urgency level
315
- if percentage >= 90:
316
- urgency = "CRITICAL"
317
- guidance = "You have nearly exhausted your token budget. Be extremely concise and limit exploration."
318
- elif percentage >= 75:
319
- urgency = "HIGH"
320
- guidance = "You are approaching your token limit. Prioritize focused exploration over breadth."
321
- elif percentage >= 50:
322
- urgency = "MODERATE"
323
- guidance = "You have used half your token budget. Be mindful of exploration scope."
324
- else:
325
- urgency = "LOW"
326
- guidance = "Token usage is within normal bounds."
327
-
328
- return (
329
- f"## Token Usage Awareness\n\n"
330
- f"**Status:** {urgency} | **Context:** {context_used:,} / {context_limit:,} ({percentage:.1f}%)\n"
331
- f"**Remaining:** {remaining:,} tokens | **Session total burned:** {total_burned:,}\n\n"
332
- f"**Guidance:** {guidance}\n\n"
333
- f"**Note:** Context shows current conversation length; session total is cumulative across all LLM calls."
334
- )
335
-
336
- def get_context_summary(self) -> str:
337
- """Get a brief summary of current context usage.
338
-
339
- Returns:
340
- Concise string with context and session totals
341
- """
342
- return (
343
- f"Context: {self.current_context_tokens:,} tokens | "
344
- f"Session total burned: {self.total_tokens:,} tokens"
345
- )
346
-
347
- def get_display_cost(self, model_name: str = "") -> float:
348
- """Get the cost to display in UI (session-level).
349
-
350
- Priority:
351
- 1. Upstream-reported actual cost (most accurate, e.g. OpenRouter)
352
- 2. Config-based fallback (tokens x rates from MODEL_PRICES)
353
-
354
- Args:
355
- model_name: Model name for config-based cost lookup (fallback).
356
-
357
- Returns:
358
- Total cost in USD, or 0.0 if neither source is available
359
- """
360
- # If we have upstream-reported cost, use it (most accurate)
361
- if self.has_actual_cost():
362
- return self.total_actual_cost + self.total_estimated_cost
363
- # Fallback: full config-based recalculation for all tokens
364
- cost_in, cost_out = get_model_cost(model_name)
365
- if cost_in > 0 or cost_out > 0:
366
- return self._calculate_cost(
367
- self.total_prompt_tokens, self.total_completion_tokens,
368
- cost_in, cost_out
369
- )['total_cost']
370
- return 0.0
371
-
372
- def get_conversation_display_cost(self, cost_in: float, cost_out: float) -> float:
373
- """Get the cost to display for conversation-level (reset on /new).
374
-
375
- For callers that already have cost rates (e.g. config_manager), this
376
- computes directly.
377
-
378
- Returns:
379
- Conversation cost in USD
380
- """
381
- return self._calculate_cost(
382
- self.conv_prompt_tokens, self.conv_completion_tokens,
383
- cost_in, cost_out
384
- )['total_cost']
@@ -1,212 +0,0 @@
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
-
@@ -1,59 +0,0 @@
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