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