velune-cli 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +208 -0
  5. velune/cli/autocomplete.py +80 -0
  6. velune/cli/banner.py +60 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +175 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +228 -0
  11. velune/cli/commands/config.py +224 -0
  12. velune/cli/commands/daemon.py +88 -0
  13. velune/cli/commands/doctor.py +721 -0
  14. velune/cli/commands/init.py +170 -0
  15. velune/cli/commands/mcp.py +82 -0
  16. velune/cli/commands/memory.py +293 -0
  17. velune/cli/commands/models.py +683 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +270 -0
  20. velune/cli/commands/setup.py +184 -0
  21. velune/cli/commands/workspace.py +249 -0
  22. velune/cli/context.py +36 -0
  23. velune/cli/councilmodel_ui.py +199 -0
  24. velune/cli/display/council_view.py +254 -0
  25. velune/cli/display/memory_view.py +126 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +25 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +51 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +123 -0
  33. velune/cli/registry.py +80 -0
  34. velune/cli/rendering/__init__.py +5 -0
  35. velune/cli/rendering/error_panel.py +79 -0
  36. velune/cli/rendering/markdown.py +63 -0
  37. velune/cli/repl.py +1855 -0
  38. velune/cli/session_manager.py +71 -0
  39. velune/cli/slash_commands.py +37 -0
  40. velune/cli/theme.py +8 -0
  41. velune/cognition/__init__.py +23 -0
  42. velune/cognition/agents/__init__.py +7 -0
  43. velune/cognition/agents/coder.py +209 -0
  44. velune/cognition/agents/planner.py +156 -0
  45. velune/cognition/agents/reviewer.py +195 -0
  46. velune/cognition/arbitrator.py +220 -0
  47. velune/cognition/architecture.py +415 -0
  48. velune/cognition/budget.py +65 -0
  49. velune/cognition/council/__init__.py +47 -0
  50. velune/cognition/council/base.py +217 -0
  51. velune/cognition/council/challenger.py +74 -0
  52. velune/cognition/council/coder.py +79 -0
  53. velune/cognition/council/critic_agent.py +43 -0
  54. velune/cognition/council/critic_configs.py +111 -0
  55. velune/cognition/council/critics.py +41 -0
  56. velune/cognition/council/debate.py +46 -0
  57. velune/cognition/council/factory.py +140 -0
  58. velune/cognition/council/messages.py +56 -0
  59. velune/cognition/council/planner.py +124 -0
  60. velune/cognition/council/reviewer.py +74 -0
  61. velune/cognition/council/synthesizer.py +67 -0
  62. velune/cognition/council/tiers.py +188 -0
  63. velune/cognition/council_orchestrator.py +282 -0
  64. velune/cognition/firewall.py +354 -0
  65. velune/cognition/module.py +46 -0
  66. velune/cognition/orchestrator.py +1205 -0
  67. velune/cognition/personality.py +238 -0
  68. velune/cognition/state.py +104 -0
  69. velune/cognition/style_resolver.py +64 -0
  70. velune/cognition/verification.py +205 -0
  71. velune/context/__init__.py +28 -0
  72. velune/context/assembler.py +240 -0
  73. velune/context/budget.py +97 -0
  74. velune/context/extractive.py +95 -0
  75. velune/context/prompt_adaptation.py +480 -0
  76. velune/context/sections.py +99 -0
  77. velune/context/token_counter.py +134 -0
  78. velune/context/utilization.py +33 -0
  79. velune/context/window.py +63 -0
  80. velune/core/__init__.py +89 -0
  81. velune/core/background.py +5 -0
  82. velune/core/config/__init__.py +37 -0
  83. velune/core/errors/__init__.py +90 -0
  84. velune/core/errors/catalog.py +188 -0
  85. velune/core/errors/execution.py +31 -0
  86. velune/core/errors/memory.py +25 -0
  87. velune/core/errors/orchestration.py +31 -0
  88. velune/core/errors/provider.py +37 -0
  89. velune/core/event_loop.py +35 -0
  90. velune/core/logging.py +83 -0
  91. velune/core/paths.py +165 -0
  92. velune/core/runtime.py +113 -0
  93. velune/core/startup_profiler.py +56 -0
  94. velune/core/task_registry.py +117 -0
  95. velune/core/trace.py +83 -0
  96. velune/core/types/__init__.py +48 -0
  97. velune/core/types/agent.py +53 -0
  98. velune/core/types/context.py +42 -0
  99. velune/core/types/inference.py +38 -0
  100. velune/core/types/memory.py +42 -0
  101. velune/core/types/model.py +70 -0
  102. velune/core/types/provider.py +62 -0
  103. velune/core/types/repository.py +38 -0
  104. velune/core/types/task.py +61 -0
  105. velune/core/types/workspace.py +28 -0
  106. velune/daemon/client.py +13 -0
  107. velune/daemon/server.py +127 -0
  108. velune/daemon/transport.py +179 -0
  109. velune/events.py +204 -0
  110. velune/execution/__init__.py +22 -0
  111. velune/execution/benchmarker.py +315 -0
  112. velune/execution/cancellation.py +53 -0
  113. velune/execution/checkpointer.py +130 -0
  114. velune/execution/command_spec.py +165 -0
  115. velune/execution/diff_preview.py +197 -0
  116. velune/execution/executor.py +181 -0
  117. velune/execution/module.py +18 -0
  118. velune/execution/multi_diff.py +67 -0
  119. velune/execution/path_guard.py +74 -0
  120. velune/execution/planner.py +91 -0
  121. velune/execution/rollback.py +89 -0
  122. velune/execution/sandbox.py +268 -0
  123. velune/execution/validator.py +115 -0
  124. velune/hardware/__init__.py +1 -0
  125. velune/hardware/detector.py +192 -0
  126. velune/kernel/__init__.py +55 -0
  127. velune/kernel/bootstrap.py +125 -0
  128. velune/kernel/config.py +426 -0
  129. velune/kernel/entrypoint.py +78 -0
  130. velune/kernel/health.py +54 -0
  131. velune/kernel/lifecycle.py +143 -0
  132. velune/kernel/module.py +17 -0
  133. velune/kernel/modules.py +23 -0
  134. velune/kernel/registry.py +96 -0
  135. velune/kernel/schemas.py +28 -0
  136. velune/main.py +9 -0
  137. velune/mcp/__init__.py +9 -0
  138. velune/mcp/client.py +115 -0
  139. velune/mcp/config.py +19 -0
  140. velune/mcp/server.py +624 -0
  141. velune/memory/__init__.py +32 -0
  142. velune/memory/compaction.py +506 -0
  143. velune/memory/embedding_pipeline.py +241 -0
  144. velune/memory/lifecycle.py +680 -0
  145. velune/memory/module.py +218 -0
  146. velune/memory/prioritizer.py +67 -0
  147. velune/memory/storage/episodic_schema.sql +53 -0
  148. velune/memory/storage/lancedb_store.py +282 -0
  149. velune/memory/storage/sqlite_manager.py +369 -0
  150. velune/memory/storage/sqlite_pool.py +149 -0
  151. velune/memory/tiers/episodic.py +588 -0
  152. velune/memory/tiers/graph.py +378 -0
  153. velune/memory/tiers/lineage.py +416 -0
  154. velune/memory/tiers/semantic.py +475 -0
  155. velune/memory/tiers/working.py +168 -0
  156. velune/memory/vitality.py +132 -0
  157. velune/models/__init__.py +15 -0
  158. velune/models/family.py +76 -0
  159. velune/models/module.py +20 -0
  160. velune/models/probes.py +192 -0
  161. velune/models/profile_cache.py +84 -0
  162. velune/models/profiler.py +108 -0
  163. velune/models/registry.py +251 -0
  164. velune/models/scorer.py +233 -0
  165. velune/models/specializations.py +205 -0
  166. velune/orchestration/__init__.py +19 -0
  167. velune/orchestration/engine.py +239 -0
  168. velune/orchestration/module.py +15 -0
  169. velune/orchestration/role_assignments.py +82 -0
  170. velune/orchestration/schemas.py +98 -0
  171. velune/plugins/__init__.py +20 -0
  172. velune/plugins/hooks.py +50 -0
  173. velune/plugins/loader.py +161 -0
  174. velune/plugins/registry.py +56 -0
  175. velune/plugins/schemas.py +21 -0
  176. velune/providers/__init__.py +23 -0
  177. velune/providers/adapters/anthropic.py +257 -0
  178. velune/providers/adapters/fireworks.py +115 -0
  179. velune/providers/adapters/google.py +234 -0
  180. velune/providers/adapters/groq.py +151 -0
  181. velune/providers/adapters/huggingface.py +210 -0
  182. velune/providers/adapters/llamacpp.py +208 -0
  183. velune/providers/adapters/lmstudio.py +175 -0
  184. velune/providers/adapters/ollama.py +233 -0
  185. velune/providers/adapters/openai.py +213 -0
  186. velune/providers/adapters/openrouter.py +81 -0
  187. velune/providers/adapters/together.py +134 -0
  188. velune/providers/adapters/xai.py +60 -0
  189. velune/providers/base.py +86 -0
  190. velune/providers/benchmarker.py +138 -0
  191. velune/providers/discovery/__init__.py +33 -0
  192. velune/providers/discovery/anthropic.py +79 -0
  193. velune/providers/discovery/benchmarks.py +44 -0
  194. velune/providers/discovery/classifier.py +69 -0
  195. velune/providers/discovery/fireworks.py +95 -0
  196. velune/providers/discovery/gguf.py +88 -0
  197. velune/providers/discovery/google.py +95 -0
  198. velune/providers/discovery/gpu.py +117 -0
  199. velune/providers/discovery/groq.py +21 -0
  200. velune/providers/discovery/huggingface.py +67 -0
  201. velune/providers/discovery/lmstudio.py +80 -0
  202. velune/providers/discovery/ollama.py +162 -0
  203. velune/providers/discovery/openai.py +96 -0
  204. velune/providers/discovery/openrouter.py +113 -0
  205. velune/providers/discovery/scanner.py +115 -0
  206. velune/providers/discovery/together.py +114 -0
  207. velune/providers/discovery/xai.py +57 -0
  208. velune/providers/health.py +67 -0
  209. velune/providers/health_monitor.py +169 -0
  210. velune/providers/keystore.py +142 -0
  211. velune/providers/local_paths.py +49 -0
  212. velune/providers/local_resolver.py +229 -0
  213. velune/providers/module.py +51 -0
  214. velune/providers/ollama_manager.py +193 -0
  215. velune/providers/registry.py +220 -0
  216. velune/providers/router.py +255 -0
  217. velune/providers/task_classifier.py +288 -0
  218. velune/py.typed +0 -0
  219. velune/repository/__init__.py +33 -0
  220. velune/repository/analyzer.py +127 -0
  221. velune/repository/ast_parser.py +822 -0
  222. velune/repository/blast_radius.py +298 -0
  223. velune/repository/boundary_classifier.py +295 -0
  224. velune/repository/cognition.py +316 -0
  225. velune/repository/grapher.py +179 -0
  226. velune/repository/import_graph.py +263 -0
  227. velune/repository/incremental_indexer.py +275 -0
  228. velune/repository/index_state.py +96 -0
  229. velune/repository/indexer.py +243 -0
  230. velune/repository/module.py +17 -0
  231. velune/repository/parser.py +474 -0
  232. velune/repository/project_type.py +300 -0
  233. velune/repository/rename_journal.py +287 -0
  234. velune/repository/scanner.py +193 -0
  235. velune/repository/schemas.py +102 -0
  236. velune/repository/symbol_registry.py +365 -0
  237. velune/repository/tracker.py +252 -0
  238. velune/retrieval/__init__.py +27 -0
  239. velune/retrieval/cache.py +110 -0
  240. velune/retrieval/fast_path.py +391 -0
  241. velune/retrieval/graph.py +124 -0
  242. velune/retrieval/hybrid.py +271 -0
  243. velune/retrieval/keyword.py +131 -0
  244. velune/retrieval/module.py +26 -0
  245. velune/retrieval/pipeline.py +303 -0
  246. velune/retrieval/reranker.py +102 -0
  247. velune/retrieval/schemas.py +59 -0
  248. velune/retrieval/slow_path.py +364 -0
  249. velune/retrieval/vector.py +203 -0
  250. velune/telemetry/__init__.py +59 -0
  251. velune/telemetry/cognition.py +267 -0
  252. velune/telemetry/cost_estimator.py +92 -0
  253. velune/telemetry/debug.py +304 -0
  254. velune/telemetry/doctor.py +244 -0
  255. velune/telemetry/logging.py +286 -0
  256. velune/telemetry/spans.py +277 -0
  257. velune/telemetry/token_tracker.py +140 -0
  258. velune/telemetry/usage_tracker.py +340 -0
  259. velune/tools/__init__.py +41 -0
  260. velune/tools/base/registry.py +87 -0
  261. velune/tools/base/tool.py +63 -0
  262. velune/tools/code/navigate.py +116 -0
  263. velune/tools/code/search.py +123 -0
  264. velune/tools/filesystem/read.py +75 -0
  265. velune/tools/filesystem/search.py +136 -0
  266. velune/tools/filesystem/write.py +163 -0
  267. velune/tools/git/history.py +177 -0
  268. velune/tools/git/operations.py +122 -0
  269. velune/tools/git/state.py +121 -0
  270. velune/tools/module.py +81 -0
  271. velune/tools/terminal/execute.py +72 -0
  272. velune/tools/terminal/history.py +47 -0
  273. velune/tools/web/fetch.py +55 -0
  274. velune/tools/web/validator.py +122 -0
  275. velune_cli-0.9.0.dist-info/METADATA +518 -0
  276. velune_cli-0.9.0.dist-info/RECORD +279 -0
  277. velune_cli-0.9.0.dist-info/WHEEL +4 -0
  278. velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
  279. velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,340 @@
1
+ """Token and cost tracking for sessions.
2
+
3
+ Tracks:
4
+ - Token usage per model
5
+ - Estimated costs per session
6
+ - Cross-session analytics
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sqlite3
12
+ from dataclasses import dataclass
13
+ from datetime import datetime, timedelta
14
+ from pathlib import Path
15
+ from threading import Lock
16
+
17
+ import structlog
18
+
19
+ logger = structlog.get_logger()
20
+
21
+ # Cost estimates per 1M tokens (approximate)
22
+ DEFAULT_COSTS = {
23
+ "claude-opus": {"input": 15.0, "output": 75.0},
24
+ "claude-sonnet": {"input": 3.0, "output": 15.0},
25
+ "claude-haiku": {"input": 0.80, "output": 4.0},
26
+ "gpt-4": {"input": 30.0, "output": 60.0},
27
+ "gpt-3.5-turbo": {"input": 0.50, "output": 1.50},
28
+ }
29
+
30
+
31
+ @dataclass
32
+ class UsageRecord:
33
+ """Single record of model usage."""
34
+
35
+ session_id: str
36
+ timestamp: str
37
+ model: str
38
+ input_tokens: int
39
+ output_tokens: int
40
+ total_tokens: int
41
+ estimated_cost: float | None = None
42
+
43
+
44
+ @dataclass
45
+ class UsageSummary:
46
+ """Summary of usage for a session."""
47
+
48
+ session_id: str
49
+ total_tokens: int
50
+ total_cost: float | None
51
+ total_input_tokens: int
52
+ total_output_tokens: int
53
+ model_breakdown: dict[str, int] # {model: total_tokens}
54
+ record_count: int
55
+ start_time: str
56
+ end_time: str
57
+
58
+
59
+ class SessionUsageTracker:
60
+ """Tracks token and cost usage per session."""
61
+
62
+ def __init__(self, db_path: Path | None = None) -> None:
63
+ """Initialize tracker with SQLite database.
64
+
65
+ Args:
66
+ db_path: Path to SQLite database (default: ~/.velune/telemetry/usage.db)
67
+ """
68
+ if db_path is None:
69
+ db_path = Path.home() / ".velune" / "telemetry" / "usage.db"
70
+
71
+ db_path.parent.mkdir(parents=True, exist_ok=True)
72
+ self.db_path = db_path
73
+ self._lock = Lock()
74
+
75
+ # Initialize database
76
+ self._init_db()
77
+
78
+ def _init_db(self) -> None:
79
+ """Initialize SQLite schema."""
80
+ with sqlite3.connect(self.db_path) as conn:
81
+ conn.execute(
82
+ """
83
+ CREATE TABLE IF NOT EXISTS usage_records (
84
+ id INTEGER PRIMARY KEY,
85
+ session_id TEXT NOT NULL,
86
+ timestamp TEXT NOT NULL,
87
+ model TEXT NOT NULL,
88
+ input_tokens INTEGER NOT NULL,
89
+ output_tokens INTEGER NOT NULL,
90
+ total_tokens INTEGER NOT NULL,
91
+ estimated_cost REAL,
92
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
93
+ )
94
+ """
95
+ )
96
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_session_id ON usage_records(session_id)")
97
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_timestamp ON usage_records(timestamp)")
98
+ conn.commit()
99
+
100
+ def record_completion(
101
+ self,
102
+ session_id: str,
103
+ model: str,
104
+ input_tokens: int,
105
+ output_tokens: int,
106
+ ) -> None:
107
+ """Record a model completion.
108
+
109
+ Args:
110
+ session_id: Session identifier
111
+ model: Model name (e.g., "claude-opus")
112
+ input_tokens: Input tokens used
113
+ output_tokens: Output tokens used
114
+ """
115
+ total_tokens = input_tokens + output_tokens
116
+ estimated_cost = self._estimate_cost(model, input_tokens, output_tokens)
117
+
118
+ record = UsageRecord(
119
+ session_id=session_id,
120
+ timestamp=datetime.utcnow().isoformat(),
121
+ model=model,
122
+ input_tokens=input_tokens,
123
+ output_tokens=output_tokens,
124
+ total_tokens=total_tokens,
125
+ estimated_cost=estimated_cost,
126
+ )
127
+
128
+ with self._lock:
129
+ with sqlite3.connect(self.db_path) as conn:
130
+ conn.execute(
131
+ """
132
+ INSERT INTO usage_records
133
+ (session_id, timestamp, model, input_tokens, output_tokens, total_tokens, estimated_cost)
134
+ VALUES (?, ?, ?, ?, ?, ?, ?)
135
+ """,
136
+ (
137
+ record.session_id,
138
+ record.timestamp,
139
+ record.model,
140
+ record.input_tokens,
141
+ record.output_tokens,
142
+ record.total_tokens,
143
+ record.estimated_cost,
144
+ ),
145
+ )
146
+ conn.commit()
147
+
148
+ logger.debug(
149
+ "Usage recorded",
150
+ session_id=session_id,
151
+ model=model,
152
+ tokens=total_tokens,
153
+ cost=estimated_cost,
154
+ )
155
+
156
+ def get_session_total_tokens(self, session_id: str) -> int:
157
+ """Get total tokens used in a session."""
158
+ with sqlite3.connect(self.db_path) as conn:
159
+ cursor = conn.execute(
160
+ "SELECT SUM(total_tokens) FROM usage_records WHERE session_id = ?",
161
+ (session_id,),
162
+ )
163
+ result = cursor.fetchone()
164
+ return result[0] if result and result[0] else 0
165
+
166
+ def get_session_estimated_cost(self, session_id: str) -> float | None:
167
+ """Get estimated total cost for a session."""
168
+ with sqlite3.connect(self.db_path) as conn:
169
+ cursor = conn.execute(
170
+ "SELECT SUM(estimated_cost) FROM usage_records WHERE session_id = ?",
171
+ (session_id,),
172
+ )
173
+ result = cursor.fetchone()
174
+ return result[0] if result and result[0] else None
175
+
176
+ def get_session_summary(self, session_id: str) -> UsageSummary | None:
177
+ """Get complete summary for a session."""
178
+ with sqlite3.connect(self.db_path) as conn:
179
+ cursor = conn.execute(
180
+ """
181
+ SELECT
182
+ SUM(total_tokens) as total_tokens,
183
+ SUM(estimated_cost) as total_cost,
184
+ SUM(input_tokens) as input_tokens,
185
+ SUM(output_tokens) as output_tokens,
186
+ COUNT(*) as record_count,
187
+ MIN(timestamp) as start_time,
188
+ MAX(timestamp) as end_time
189
+ FROM usage_records
190
+ WHERE session_id = ?
191
+ """,
192
+ (session_id,),
193
+ )
194
+ row = cursor.fetchone()
195
+
196
+ if not row or row[0] is None:
197
+ return None
198
+
199
+ # Get model breakdown
200
+ cursor = conn.execute(
201
+ """
202
+ SELECT model, SUM(total_tokens)
203
+ FROM usage_records
204
+ WHERE session_id = ?
205
+ GROUP BY model
206
+ """,
207
+ (session_id,),
208
+ )
209
+ model_breakdown = {row[0]: row[1] for row in cursor.fetchall()}
210
+
211
+ return UsageSummary(
212
+ session_id=session_id,
213
+ total_tokens=row[0],
214
+ total_cost=row[1],
215
+ total_input_tokens=row[2],
216
+ total_output_tokens=row[3],
217
+ model_breakdown=model_breakdown,
218
+ record_count=row[4],
219
+ start_time=row[5],
220
+ end_time=row[6],
221
+ )
222
+
223
+ def get_recent_sessions(self, days: int = 7) -> list[UsageSummary]:
224
+ """Get summaries for sessions in the last N days."""
225
+ cutoff = datetime.utcnow() - timedelta(days=days)
226
+
227
+ with sqlite3.connect(self.db_path) as conn:
228
+ cursor = conn.execute(
229
+ """
230
+ SELECT DISTINCT session_id
231
+ FROM usage_records
232
+ WHERE timestamp > ?
233
+ ORDER BY timestamp DESC
234
+ """,
235
+ (cutoff.isoformat(),),
236
+ )
237
+
238
+ sessions = [row[0] for row in cursor.fetchall()]
239
+
240
+ summaries = []
241
+ for session_id in sessions:
242
+ summary = self.get_session_summary(session_id)
243
+ if summary:
244
+ summaries.append(summary)
245
+
246
+ return summaries
247
+
248
+ def get_stats_last_n_days(self, days: int = 7) -> dict[str, any]:
249
+ """Get aggregated stats for last N days."""
250
+ cutoff = datetime.utcnow() - timedelta(days=days)
251
+
252
+ with sqlite3.connect(self.db_path) as conn:
253
+ cursor = conn.execute(
254
+ """
255
+ SELECT
256
+ COUNT(DISTINCT session_id) as session_count,
257
+ SUM(total_tokens) as total_tokens,
258
+ SUM(estimated_cost) as total_cost,
259
+ COUNT(*) as completion_count
260
+ FROM usage_records
261
+ WHERE timestamp > ?
262
+ """,
263
+ (cutoff.isoformat(),),
264
+ )
265
+ row = cursor.fetchone()
266
+
267
+ cursor = conn.execute(
268
+ """
269
+ SELECT model, SUM(total_tokens)
270
+ FROM usage_records
271
+ WHERE timestamp > ?
272
+ GROUP BY model
273
+ ORDER BY SUM(total_tokens) DESC
274
+ LIMIT 1
275
+ """,
276
+ (cutoff.isoformat(),),
277
+ )
278
+ most_used = cursor.fetchone()
279
+
280
+ return {
281
+ "days": days,
282
+ "session_count": row[0] if row else 0,
283
+ "total_tokens": row[1] if row and row[1] else 0,
284
+ "total_cost": row[2] if row and row[2] else 0,
285
+ "completion_count": row[3] if row else 0,
286
+ "most_used_model": most_used[0] if most_used else None,
287
+ "most_used_model_tokens": most_used[1] if most_used else 0,
288
+ }
289
+
290
+ def _estimate_cost(
291
+ self,
292
+ model: str,
293
+ input_tokens: int,
294
+ output_tokens: int,
295
+ ) -> float | None:
296
+ """Estimate cost for a completion.
297
+
298
+ Uses DEFAULT_COSTS table. Returns None if model not found.
299
+ """
300
+ # Normalize model name
301
+ model_lower = model.lower()
302
+ for key in DEFAULT_COSTS:
303
+ if key.lower() in model_lower or model_lower in key.lower():
304
+ costs = DEFAULT_COSTS[key]
305
+ input_cost = (input_tokens / 1_000_000) * costs["input"]
306
+ output_cost = (output_tokens / 1_000_000) * costs["output"]
307
+ return input_cost + output_cost
308
+
309
+ # Unknown model
310
+ return None
311
+
312
+ def cleanup_old_records(self, days: int = 90) -> int:
313
+ """Delete records older than N days. Returns count deleted."""
314
+ cutoff = datetime.utcnow() - timedelta(days=days)
315
+
316
+ with self._lock:
317
+ with sqlite3.connect(self.db_path) as conn:
318
+ cursor = conn.execute(
319
+ "DELETE FROM usage_records WHERE timestamp < ?",
320
+ (cutoff.isoformat(),),
321
+ )
322
+ count = cursor.rowcount
323
+ conn.commit()
324
+
325
+ if count > 0:
326
+ logger.info("Cleaned up old usage records", count=count, days_old=days)
327
+
328
+ return count
329
+
330
+
331
+ # Global instance
332
+ _tracker: SessionUsageTracker | None = None
333
+
334
+
335
+ def get_tracker() -> SessionUsageTracker:
336
+ """Get or create global usage tracker instance."""
337
+ global _tracker
338
+ if _tracker is None:
339
+ _tracker = SessionUsageTracker()
340
+ return _tracker
@@ -0,0 +1,41 @@
1
+ """Tool system."""
2
+
3
+ from velune.tools.base.registry import ToolRegistry
4
+ from velune.tools.base.tool import BaseTool
5
+ from velune.tools.code.navigate import FindReferences, GoToDefinition
6
+ from velune.tools.code.search import SemanticCodeSearch, SymbolSearch
7
+ from velune.tools.filesystem.read import ReadDirectory, ReadFile
8
+ from velune.tools.filesystem.search import FindFiles, GrepFiles
9
+ from velune.tools.filesystem.write import CreateFile, DeleteFile, WriteFile
10
+ from velune.tools.git.history import GitBlame, GitDiff, GitLog
11
+ from velune.tools.git.operations import GitCheckout, GitCommit
12
+ from velune.tools.git.state import GitBranch, GitStatus
13
+ from velune.tools.terminal.execute import ExecuteCommand
14
+ from velune.tools.terminal.history import TerminalHistory
15
+ from velune.tools.web.fetch import WebFetch
16
+
17
+ __all__ = [
18
+ "BaseTool",
19
+ "ToolRegistry",
20
+ "ReadFile",
21
+ "ReadDirectory",
22
+ "WriteFile",
23
+ "CreateFile",
24
+ "DeleteFile",
25
+ "GrepFiles",
26
+ "FindFiles",
27
+ "GitLog",
28
+ "GitDiff",
29
+ "GitBlame",
30
+ "GitStatus",
31
+ "GitBranch",
32
+ "GitCommit",
33
+ "GitCheckout",
34
+ "ExecuteCommand",
35
+ "TerminalHistory",
36
+ "SemanticCodeSearch",
37
+ "SymbolSearch",
38
+ "GoToDefinition",
39
+ "FindReferences",
40
+ "WebFetch",
41
+ ]
@@ -0,0 +1,87 @@
1
+ """Tool registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from velune.tools.base.tool import BaseTool
6
+
7
+
8
+ class ToolRegistry:
9
+ """Registry for available tools."""
10
+
11
+ def __init__(self) -> None:
12
+ self._tools: dict[str, BaseTool] = {}
13
+ self._broken_tools: list[str] = []
14
+
15
+ def validate_tool(self, tool: BaseTool) -> bool:
16
+ """Validate a tool by calling get_name() and get_schema()."""
17
+ import logging
18
+
19
+ logger = logging.getLogger("velune")
20
+ try:
21
+ tool.get_name()
22
+ tool.get_schema()
23
+ return True
24
+ except Exception as e:
25
+ class_name = tool.__class__.__name__
26
+ logger.warning(
27
+ "Tool class %s failed validation: %s",
28
+ class_name,
29
+ str(e),
30
+ exc_info=True,
31
+ )
32
+ try:
33
+ broken_name = tool.get_name()
34
+ except Exception:
35
+ broken_name = class_name
36
+ if broken_name not in self._broken_tools:
37
+ self._broken_tools.append(broken_name)
38
+ return False
39
+
40
+ def list_broken_tools(self) -> list[str]:
41
+ """List all tool names that failed validation."""
42
+ return self._broken_tools
43
+
44
+ def register(self, tool: BaseTool, *, replace: bool = True) -> None:
45
+ """Register a tool."""
46
+ if not self.validate_tool(tool):
47
+ return
48
+
49
+ name = tool.get_name()
50
+ if not replace and name in self._tools:
51
+ raise ValueError(f"Tool already registered: {name}")
52
+ self._tools[name] = tool
53
+
54
+ def get(self, name: str) -> BaseTool | None:
55
+ """Get a tool by name."""
56
+ return self._tools.get(name)
57
+
58
+ def has(self, name: str) -> bool:
59
+ """Check if a tool is registered."""
60
+
61
+ return name in self._tools
62
+
63
+ def unregister(self, name: str) -> None:
64
+ """Remove a tool by name."""
65
+
66
+ self._tools.pop(name, None)
67
+
68
+ def list_tools(self) -> list[str]:
69
+ """List all registered tool names."""
70
+ return list(self._tools.keys())
71
+
72
+ def list_tool_schemas(self) -> list[dict[str, object]]:
73
+ """List schemas and capability metadata for all tools."""
74
+
75
+ schemas: list[dict[str, object]] = []
76
+ for tool in self._tools.values():
77
+ schemas.append(
78
+ {
79
+ "name": tool.get_name(),
80
+ "description": tool.get_description(),
81
+ "schema": tool.get_schema(),
82
+ "permissions": sorted(
83
+ permission.value for permission in tool.get_required_permissions()
84
+ ),
85
+ }
86
+ )
87
+ return schemas
@@ -0,0 +1,63 @@
1
+ """Base tool protocol and execution contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass, field
7
+ from enum import StrEnum
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ class ToolPermission(StrEnum):
13
+ """Permission boundaries enforced at tool execution time."""
14
+
15
+ FILESYSTEM_READ = "filesystem.read"
16
+ FILESYSTEM_WRITE = "filesystem.write"
17
+ GIT_READ = "git.read"
18
+ GIT_WRITE = "git.write"
19
+ TERMINAL_EXECUTE = "terminal.execute"
20
+ NETWORK_ACCESS = "network.access"
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class ToolCallContext:
25
+ """Execution context passed into policy-aware tool calls."""
26
+
27
+ run_id: str
28
+ actor: str
29
+ workspace: Path | None = None
30
+ permissions: set[ToolPermission] = field(default_factory=set)
31
+
32
+
33
+ class BaseTool(ABC):
34
+ """Abstract base class for tools."""
35
+
36
+ @abstractmethod
37
+ def get_name(self) -> str:
38
+ """Get the tool name."""
39
+ pass
40
+
41
+ @abstractmethod
42
+ def get_description(self) -> str:
43
+ """Get the tool description."""
44
+ pass
45
+
46
+ @abstractmethod
47
+ async def execute(self, **kwargs) -> Any:
48
+ """Execute the tool."""
49
+ pass
50
+
51
+ def get_schema(self) -> dict[str, Any]:
52
+ """Get the tool's parameter schema."""
53
+ return {}
54
+
55
+ def get_required_permissions(self) -> set[ToolPermission]:
56
+ """Permissions required to execute this tool."""
57
+
58
+ return set()
59
+
60
+ def validate_input(self, payload: dict[str, Any]) -> None:
61
+ """Validate tool input before execution."""
62
+
63
+ return None
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from velune.execution.path_guard import PathGuard
6
+ from velune.tools.base.tool import BaseTool
7
+
8
+
9
+ class GoToDefinition(BaseTool):
10
+ """Tool for navigating to symbol definitions."""
11
+
12
+ def __init__(self, workspace: Path | None = None) -> None:
13
+ self.workspace = Path(workspace).resolve() if workspace else Path.cwd().resolve()
14
+
15
+ def get_name(self) -> str:
16
+ return "go_to_definition"
17
+
18
+ def get_description(self) -> str:
19
+ return "Navigate to symbol definition"
20
+
21
+ async def execute(
22
+ self,
23
+ symbol_name: str,
24
+ file_path: str,
25
+ line: int,
26
+ ) -> dict | None:
27
+ """Go to symbol definition."""
28
+ from velune.repository.parser import ASTParser
29
+
30
+ guard = PathGuard(self.workspace)
31
+ path = guard.validate(file_path)
32
+
33
+ parser = ASTParser()
34
+
35
+ try:
36
+ with open(path, encoding="utf-8", errors="ignore") as f:
37
+ code = f.read()
38
+ except Exception:
39
+ return None
40
+
41
+ symbols, _ = parser.parse(path, code)
42
+ for symbol in symbols:
43
+ if symbol.name == symbol_name:
44
+ return {
45
+ "file": str(path),
46
+ "line": symbol.line_start,
47
+ "kind": symbol.kind.value if hasattr(symbol.kind, "value") else symbol.kind,
48
+ }
49
+
50
+ return None
51
+
52
+ def get_schema(self) -> dict:
53
+ return {
54
+ "type": "object",
55
+ "properties": {
56
+ "symbol_name": {
57
+ "type": "string",
58
+ "description": "Symbol name",
59
+ },
60
+ "file_path": {
61
+ "type": "string",
62
+ "description": "File containing the symbol reference",
63
+ },
64
+ "line": {
65
+ "type": "integer",
66
+ "description": "Line number of reference",
67
+ },
68
+ },
69
+ "required": ["symbol_name", "file_path", "line"],
70
+ }
71
+
72
+
73
+ class FindReferences(BaseTool):
74
+ """Tool for finding symbol references."""
75
+
76
+ def __init__(self, workspace: Path | None = None) -> None:
77
+ self.workspace = Path(workspace).resolve() if workspace else Path.cwd().resolve()
78
+
79
+ def get_name(self) -> str:
80
+ return "find_references"
81
+
82
+ def get_description(self) -> str:
83
+ return "Find all references to a symbol"
84
+
85
+ async def execute(
86
+ self,
87
+ symbol_name: str,
88
+ directory: str = ".",
89
+ ) -> list[dict]:
90
+ """Find references to a symbol."""
91
+ from velune.tools.filesystem.search import GrepFiles
92
+
93
+ grep = GrepFiles(workspace=self.workspace)
94
+ results = await grep.execute(
95
+ pattern=symbol_name,
96
+ directory=directory,
97
+ file_pattern="*.py",
98
+ )
99
+
100
+ return results
101
+
102
+ def get_schema(self) -> dict:
103
+ return {
104
+ "type": "object",
105
+ "properties": {
106
+ "symbol_name": {
107
+ "type": "string",
108
+ "description": "Symbol name to find references for",
109
+ },
110
+ "directory": {
111
+ "type": "string",
112
+ "description": "Directory to search in",
113
+ },
114
+ },
115
+ "required": ["symbol_name"],
116
+ }